mirror of
https://github.com/helenclx/leilukin-site.git
synced 2025-03-14 23:07:15 +00:00
521 lines
18 KiB
JavaScript
521 lines
18 KiB
JavaScript
/**
|
|
* 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;
|
|
|
|
//#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>
|
|
</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>
|
|
</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"></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 || ""}"
|
|
numChildren="${(comment.childrenIdxs || []).length}"
|
|
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;
|
|
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
|
|
? `<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 class="comment-id" role="img" aria-label="${headerText}">
|
|
<span aria-hidden="true" class="comment-id">#${this.commentId}</span>
|
|
</div>
|
|
${commenterNameEl}
|
|
<div class="comment-header-right">
|
|
<time id="${this.idKey}-timestamp"
|
|
datetime="${this.isoTimestamp}"
|
|
tabindex="-1"
|
|
>${displayTimestamp}</time>
|
|
<button id="${this.idKey}-show" class="show-comment">Show #${this.commentId}</button>
|
|
</div>
|
|
</div>
|
|
<blockquote>${this.commentText}</blockquote>
|
|
<div class="comment-footer">
|
|
<button id="${this.idKey}-reply">Reply to #${this.commentId}</button>
|
|
<button id="${this.idKey}-hide">Hide #${this.commentId}</button>
|
|
</div>
|
|
</article>
|
|
`;
|
|
|
|
//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);
|
|
}); |