From 79e9a79b021d5fa51c272aca8443194a76dc8bf4 Mon Sep 17 00:00:00 2001 From: Helen Chong <119173961+helenclx@users.noreply.github.com> Date: Sat, 25 May 2024 00:03:05 +0800 Subject: [PATCH] Set up GroundedWren's guestbook --- src/assets/css/comments.css | 254 ++++++++++++ src/assets/css/svgIconControl.css | 21 + src/assets/js/comments.js | 470 +++++++++++++++++++++++ src/assets/js/googleSheetsReaderGizmo.js | 179 +++++++++ src/assets/js/svgIconControl.js | 432 +++++++++++++++++++++ src/pages/guestbook.njk | 104 +---- 6 files changed, 1372 insertions(+), 88 deletions(-) create mode 100644 src/assets/css/comments.css create mode 100644 src/assets/css/svgIconControl.css create mode 100644 src/assets/js/comments.js create mode 100644 src/assets/js/googleSheetsReaderGizmo.js create mode 100644 src/assets/js/svgIconControl.js diff --git a/src/assets/css/comments.css b/src/assets/css/comments.css new file mode 100644 index 00000000..69a40176 --- /dev/null +++ b/src/assets/css/comments.css @@ -0,0 +1,254 @@ +/** +* Author: Vera Konigin +* Site: https://groundedwren.neocities.org +* Contact: vera@groundedwren.com +* +* File Description: Styles for the guestbook control +* CSS variables come from https://groundedwren.neocities.org/styles/gwBoilerPlatePersonalization.css +*/ +:root { + --icon-color: var(--clr-body-txt); +} + +gw-comment-form, gw-comment-list { + width: 100%; +} + +.comments-region { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.8rem; +} + +.comment-form input { + width: 135px; +} + +.comments-container { + display: flex; + flex-direction: column; + align-items: stretch; + max-width: 100%; + gap: 0.4em; +} + +.comment-box-container { + margin-top: 0.4em; + margin-left: auto; + margin-right: auto; + width: fit-content; +} + +.comment-form-title { + display: flex; + justify-content: center; + margin-bottom: 0.4em; + font-size: 1.25em; +} + +.comment-article { + border-left: 2px solid var(--border-color); + padding: 0.5em 0; + background-color: var(--content-color); + display: flex; + flex-direction: column; +} + +.comment-article blockquote { + max-width: unset !important; + overflow-wrap: break-word; +} + +.comment-article > button { + max-width: fit-content; + height: 30px; +} + +.comment-header { + display: grid; + grid-template-columns: 0fr 1fr 1fr; + gap: 5px; + align-items: baseline; +} + +.comment-article .comment-article { + margin-left: 0.9em; + margin-top: 0.9em; + padding-right: 0; +} + +.comment-id, .comment-timestamp { + font-size: 0.85em; + font-weight: lighter; + font-style: italic; + word-break: keep-all; +} + +.comment-timestamp { + text-align: end; +} + +.commenter-name { + font-size: 1.1em; + font-weight: bold; +} + +form { + border: 1px solid; + padding: 0.4em; + border-color: var(--general-border-color); + width: 100%; + background-color: var(--content-color); +} + +form.transparent-form { + border: none; + padding: 0; + width: auto; + background-color: inherit; +} + +.transparent-fieldset { + margin: 0; + padding: 0; + border: 0; +} + +form > h1, form > h2, form > h3, form > h4, form > h5, form > h6 { + margin: 0; + text-align: center; +} + +.transparent-fieldset > legend { + display: none !important; +} + +.form-footer { + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.form-footer > * { + margin: 3px; + min-height: 30px; +} + +.input-line > label { + text-align: left; + width: 150px; + min-width: 100px; +} + +.input-line * { + display: inline-block; +} + +.input-line { + margin-bottom: 5px; +} + +.input-flex { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; +} + +.input-flex-line { + display: flex; + flex-direction: row; + width: 100%; + justify-content: flex-start; + align-items: center; + margin-bottom: 5px; +} + +.input-horizontal-flex { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-evenly; + align-items: center; + row-gap: 4px; +} + +.input-horizontal-flex > .input-flex-line, .input-horizontal-flex > .input-vertical-line { + width: auto; + margin-right: 10px; +} + +.input-vertical { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + margin-left: 5px; + margin-right: 5px; +} + +.input-vertical-line { + display: flex; + flex-direction: column; + width: 100%; + justify-content: flex-start; + align-items: flex-start; + margin-bottom: 6px; +} + +.input-vertical-line > label, .input-vertical > label { + padding-bottom: 2px; +} + +.input-grid { + display: grid; + grid-template-columns: minmax(75px, 1fr) minmax(75px, auto); + grid-auto-rows: minmax(25px, auto); + grid-column-gap: 5px; + grid-row-gap: 2px; + align-items: baseline; +} + +.input-grid > .full-line { + grid-column: 1 / -1; +} + +.input-line label, .input-flex-line label, .input-vertical-line label, .input-grid label { + cursor: pointer; +} + +.input-line > button, .input-flex-line > button, .input-vertical-line > button, .input-grid button { + height: 30px; +} + +.input-flex-line > label { + text-align: left; + flex-grow: 1; + padding-right: 10px; +} + +.sr-only { + position: absolute; + left: -99999999px; +} + +.inline-banner-wrapper { + display: flex; +} + +.inline-banner-wrapper > .inline-banner { + width: 100%; +} + +.inline-banner { + margin: 10px; + border: 1px solid var(-border-color); + background-color: var(--banner-color); + color: var(--text-color); + padding: 10px; + word-break: break-word; + display: flex; + align-items: center; + gap: 5px; +} \ No newline at end of file diff --git a/src/assets/css/svgIconControl.css b/src/assets/css/svgIconControl.css new file mode 100644 index 00000000..abc701c6 --- /dev/null +++ b/src/assets/css/svgIconControl.css @@ -0,0 +1,21 @@ +/** + * Author: Vera Konigin + * Site: https://groundedwren.neocities.org + * Contact: vera@groundedwren.com + * + * File Description: Styles for the SVG Icon web component + * CSS variables come from https://groundedwren.neocities.org/styles/gwBoilerPlatePersonalization.css + */ + + .icon { + fill: var(--icon-color); + width: 16px; + height: 16px; +} + +gw-icon { + display: inline-flex; + flex-direction: row; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/src/assets/js/comments.js b/src/assets/js/comments.js new file mode 100644 index 00000000..8bd07236 --- /dev/null +++ b/src/assets/js/comments.js @@ -0,0 +1,470 @@ +/** + * 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 = ` +
+ ${this.titleText} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ + Comments are manually approved +
+ +
+ `; + + //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 message 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 = ` +
+ + 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; + 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 + ? `${this.commenterName}` + : `${this.commenterName}`; + + //Markup + this.innerHTML = ` +
+
+
+ + ${headerText} +
+ ${commenterNameEl} + +
+
${this.commentText}
+ +
+ `; + + //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); +}); \ No newline at end of file diff --git a/src/assets/js/googleSheetsReaderGizmo.js b/src/assets/js/googleSheetsReaderGizmo.js new file mode 100644 index 00000000..318aac18 --- /dev/null +++ b/src/assets/js/googleSheetsReaderGizmo.js @@ -0,0 +1,179 @@ +/** + * Author: Vera Konigin + * Site: https://groundedwren.neocities.org + * Contact: vera@groundedwren.com + * + * File Description: Gizmo for reading from Google Sheets. + * Neocities editor users will see a lot of linter errors in this file, but none of them are real errors. The linter just doesn't understand some modern JS. + */ + + /** + * 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.Gizmos", function(ns) { + /** + * A class to read from one page in a google sheet document + */ + ns.GoogleSheetsReader = class GoogleSheetsReader + { + //setResponse is intended for React - it's how google responds to our GET. Fortunately valid JSON is inside this call, so we can just parse it out. + static #RESPONSE_PREFIX = "setResponse("; + static #RESPONSE_SUFFIX = ");"; + + //Here we can define any custom types based on column label. "Timestamp" is intended for ISO 8601 format date/time strings. + static #CUSTOM_LABEL_TYPES = { + "Timestamp": "timestamp" + }; + + spreadsheetId; //The ID of the spreadsheet. This is the part just after /d/ in the docs.google.com URL + sheetName; //The name of the particular sheet we're after + + #sheetURL; //Composed request URL + + loadPromise = null; //A promise created when loading begins and which resolves when data has finished loading. + tableJSON = null; //Raw JS Object version of the returned JSON. + rowData = null; //Parsed row data + colData = null; //A shortcut to the the column data in tableJSON + colIndex = null; //An index from column label to its metadata, plus array position + + /** + * Constructs a GoogleSheetsReader object + * spreadsheetId is the part of the docs.google.com URL just after /d/ + * sheetName is the name of the particular page + */ + constructor(spreadsheetId, sheetName) + { + this.spreadsheetId = spreadsheetId; + this.sheetName = sheetName; + this.#sheetURL = `https://docs.google.com/spreadsheets/d/${spreadsheetId}/gviz/tq?sheet=${sheetName}`; + } + + /** + * Loads and parses sheet data via HTTP GET + * Returns null on success, and an error string on failure. + */ + async loadData() + { + this.loadPromise = this.#loadData(); + return this.loadPromise; + } + + async #loadData() + { + this.tableJSON = null; + this.rowData = null; + this.colData = null; + this.colIndex = null; + + const response = await fetch(this.#sheetURL); + if (response.ok) + { + return response.text().then((unparsedData) => + { + //This is parsing out the valid JSON from the React method they gave us + const targetData = unparsedData.split( + GoogleSheetsReader.#RESPONSE_PREFIX + )[1].split( + GoogleSheetsReader.#RESPONSE_SUFFIX + )[0]; + + this.tableJSON = GoogleSheetsReader.#applyCustomLabelTypes(JSON.parse(targetData).table); + this.rowData = GoogleSheetsReader.#parseAllRows(this.tableJSON); + this.colIndex = GoogleSheetsReader.#indexColumns(this.tableJSON); + this.colData = this.tableJSON.cols; + + return null; + }); + } + else + { + return response.statusText || response.status; + } + } + + /** + * Overrides any google-returned column data types with custom ones based on label + */ + static #applyCustomLabelTypes(tableJSON) + { + tableJSON.cols.forEach(col => { + if(this.#CUSTOM_LABEL_TYPES[col.label]) + { + col.type = this.#CUSTOM_LABEL_TYPES[col.label] + }; + }); + return tableJSON; + } + + static #parseAllRows(tableJSON) + { + const rowDataArray = []; + for(let i = 0; i < tableJSON.rows.length; i++) + { + rowDataArray.push(this.#parseRow(i, tableJSON)); + } + return rowDataArray; + } + + static #parseRow(rowIdx, tableJSON) + { + const rowData = {}; + const cells = tableJSON.rows[rowIdx].c; + + for(let i = 0; i < cells.length; i++) + { + rowData[tableJSON.cols[i].label] = this.#parseCellType(cells[i], tableJSON.cols[i].type); + } + return rowData; + } + + /** + * Parses a cell based on its type. Further custom types will need their own parsing added here. + */ + static #parseCellType(cellData, cellType) + { + switch (cellType) + { + case "string": + return cellData ? cellData.v : cellData; + case "number": + return cellData ? cellData.v : cellData; + case "datetime": + case "date": + return (cellData && cellData.v) ? eval("new " + cellData.v) : null; + case "timestamp": + const cellTimestamp = new Date(cellData ? cellData.v : ""); + return isNaN(cellTimestamp) ? null : cellTimestamp; + default: + return cellData; + } + } + + static #indexColumns(tableJSON) + { + const colIndex = {}; + for(let i = 0; i < tableJSON.cols.length; i++) + { + const colData = tableJSON.cols[i]; + colIndex[colData.label] = {...colData, index: i}; + } + return colIndex; + } + }; +}); \ No newline at end of file diff --git a/src/assets/js/svgIconControl.js b/src/assets/js/svgIconControl.js new file mode 100644 index 00000000..546b3897 --- /dev/null +++ b/src/assets/js/svgIconControl.js @@ -0,0 +1,432 @@ +/** + * Author: Vera Konigin + * Site: https://groundedwren.neocities.org + * Contact: vera@groundedwren.com + * + * File Description: A web component to site SVG icons easily + */ + +/** + * 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.SVGLib", function(ns) { + const XML_NAMESPACE = "http://www.w3.org/2000/svg"; + + /** + * https://www.w3.org/TR/SVG/eltindex.html + */ + ns.ElementTypes = { + svg: "svg", //viewBox, preserveAspectRatio, zoomAndPan, transform - https://www.w3.org/TR/SVG/struct.html#SVGElement + circle: "circle", //cx, cy, r - https://www.w3.org/TR/SVG/shapes.html#CircleElement + linearGradient: "linearGradient", //x1, y1, x2, y2, gradientUnits, gradientTransform, spreadMethod, href - https://www.w3.org/TR/SVG/pservers.html#LinearGradientElement + stop: "stop", //offset, stop-color https://www.w3.org/TR/SVG/pservers.html#StopElement + defs: "defs", //https://www.w3.org/TR/SVG/struct.html#DefsElement + rect: "rect", //x, y, width, height, rx, ry - https://www.w3.org/TR/SVG/shapes.html#RectElement + text: "text", //x, y, dominant-baseline, text-anchor, fill - https://www.w3.org/TR/SVG/text.html#TextElement + a: "a", //href, target - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/a + path: "path", //d, pathLength - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path + title: "title", // - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title + desc: "desc", // - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc + foreignObject: "foreignObject", // - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject + }; + + /** + * Shortcut to create an SVG element and append it to a parent + * @param parent the parent element + * ... see ns.createElement + */ + function createChildElement(parent, elementType, attributes, innerHTML) + { + var childEl = createElement(elementType, attributes, innerHTML); + parent.appendChild(childEl); + return childEl; + } + ns.createChildElement = createChildElement; + + /** + * Creates an SVG element to spec + * @param elementType type of SVG element (see ns.ElementTypes) + * @param attributes object of attributes and values + * @param innerHTML inner HTML + */ + function createElement(elementType, attributes, innerHTML) + { + var el = document.createElementNS(XML_NAMESPACE, elementType); + Object.keys(attributes || {}).forEach((attr) => el.setAttributeNS(null, attr, attributes[attr])); + el.innerHTML = innerHTML || null; + return el; + } + ns.createElement = createElement; + + //#region Icons + const ICON_CITATION = ""; + const ICON_CLASS = "icon"; + + /** + * Icon SVG path definitions + */ + ns.Icons = { + "circle-info": { + title: "info", + viewBox: "0 0 512 512", + d: "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z" + }, + "play": { + title: "play", + viewBox: "0 0 384 512", + d: "M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z" + }, + "backward-step": { + title: "backward step", + viewBox: "0 0 320 512", + d: "M267.5 440.6c9.5 7.9 22.8 9.7 34.1 4.4s18.4-16.6 18.4-29V96c0-12.4-7.2-23.7-18.4-29s-24.5-3.6-34.1 4.4l-192 160L64 241V96c0-17.7-14.3-32-32-32S0 78.3 0 96V416c0 17.7 14.3 32 32 32s32-14.3 32-32V271l11.5 9.6 192 160z" + }, + "forward-step": { + title: "forward step", + viewBox: "0 0 320 512", + d: "M52.5 440.6c-9.5 7.9-22.8 9.7-34.1 4.4S0 428.4 0 416V96C0 83.6 7.2 72.3 18.4 67s24.5-3.6 34.1 4.4l192 160L256 241V96c0-17.7 14.3-32 32-32s32 14.3 32 32V416c0 17.7-14.3 32-32 32s-32-14.3-32-32V271l-11.5 9.6-192 160z" + }, + "pause": { + title: "pause", + viewBox: "0 0 320 512", + d: "M48 64C21.5 64 0 85.5 0 112V400c0 26.5 21.5 48 48 48H80c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H48zm192 0c-26.5 0-48 21.5-48 48V400c0 26.5 21.5 48 48 48h32c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H240z" + }, + "triangle-exclamation": { + title: "warning", + viewBox: "0 0 512 512", + d: "M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z" + }, + "laptop-code": { + title: "coding", + viewBox: "0 0 640 512", + d: "M64 96c0-35.3 28.7-64 64-64H512c35.3 0 64 28.7 64 64V352H512V96H128V352H64V96zM0 403.2C0 392.6 8.6 384 19.2 384H620.8c10.6 0 19.2 8.6 19.2 19.2c0 42.4-34.4 76.8-76.8 76.8H76.8C34.4 480 0 445.6 0 403.2zM281 209l-31 31 31 31c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-48-48c-9.4-9.4-9.4-24.6 0-33.9l48-48c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9zM393 175l48 48c9.4 9.4 9.4 24.6 0 33.9l-48 48c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l31-31-31-31c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0z" + }, + "guitar": { + title: "guitar", + viewBox: "0 0 512 512", + d: "M465 7c-9.4-9.4-24.6-9.4-33.9 0L383 55c-2.4 2.4-4.3 5.3-5.5 8.5l-15.4 41-77.5 77.6c-45.1-29.4-99.3-30.2-131 1.6c-11 11-18 24.6-21.4 39.6c-3.7 16.6-19.1 30.7-36.1 31.6c-25.6 1.3-49.3 10.7-67.3 28.6C-16 328.4-7.6 409.4 47.5 464.5s136.1 63.5 180.9 18.7c17.9-17.9 27.4-41.7 28.6-67.3c.9-17 15-32.3 31.6-36.1c15-3.4 28.6-10.5 39.6-21.4c31.8-31.8 31-85.9 1.6-131l77.6-77.6 41-15.4c3.2-1.2 6.1-3.1 8.5-5.5l48-48c9.4-9.4 9.4-24.6 0-33.9L465 7zM208 256a48 48 0 1 1 0 96 48 48 0 1 1 0-96z" + }, + "pen-fancy": { + title: "pen", + viewBox: "0 0 512 512", + d: "M373.5 27.1C388.5 9.9 410.2 0 433 0c43.6 0 79 35.4 79 79c0 22.8-9.9 44.6-27.1 59.6L277.7 319l-10.3-10.3-64-64L193 234.3 373.5 27.1zM170.3 256.9l10.4 10.4 64 64 10.4 10.4-19.2 83.4c-3.9 17.1-16.9 30.7-33.8 35.4L24.4 510.3l95.4-95.4c2.6 .7 5.4 1.1 8.3 1.1c17.7 0 32-14.3 32-32s-14.3-32-32-32s-32 14.3-32 32c0 2.9 .4 5.6 1.1 8.3L1.7 487.6 51.5 310c4.7-16.9 18.3-29.9 35.4-33.8l83.4-19.2z" + }, + "glasses": { + title: "glasses", + viewBox: "0 0 576 512", + d: "M118.6 80c-11.5 0-21.4 7.9-24 19.1L57 260.3c20.5-6.2 48.3-12.3 78.7-12.3c32.3 0 61.8 6.9 82.8 13.5c10.6 3.3 19.3 6.7 25.4 9.2c3.1 1.3 5.5 2.4 7.3 3.2c.9 .4 1.6 .7 2.1 1l.6 .3 .2 .1 .1 0 0 0 0 0s0 0-6.3 12.7h0l6.3-12.7c5.8 2.9 10.4 7.3 13.5 12.7h40.6c3.1-5.3 7.7-9.8 13.5-12.7l6.3 12.7h0c-6.3-12.7-6.3-12.7-6.3-12.7l0 0 0 0 .1 0 .2-.1 .6-.3c.5-.2 1.2-.6 2.1-1c1.8-.8 4.2-1.9 7.3-3.2c6.1-2.6 14.8-5.9 25.4-9.2c21-6.6 50.4-13.5 82.8-13.5c30.4 0 58.2 6.1 78.7 12.3L481.4 99.1c-2.6-11.2-12.6-19.1-24-19.1c-3.1 0-6.2 .6-9.2 1.8L416.9 94.3c-12.3 4.9-26.3-1.1-31.2-13.4s1.1-26.3 13.4-31.2l31.3-12.5c8.6-3.4 17.7-5.2 27-5.2c33.8 0 63.1 23.3 70.8 56.2l43.9 188c1.7 7.3 2.9 14.7 3.5 22.1c.3 1.9 .5 3.8 .5 5.7v6.7V352v16c0 61.9-50.1 112-112 112H419.7c-59.4 0-108.5-46.4-111.8-105.8L306.6 352H269.4l-1.2 22.2C264.9 433.6 215.8 480 156.3 480H112C50.1 480 0 429.9 0 368V352 310.7 304c0-1.9 .2-3.8 .5-5.7c.6-7.4 1.8-14.8 3.5-22.1l43.9-188C55.5 55.3 84.8 32 118.6 32c9.2 0 18.4 1.8 27 5.2l31.3 12.5c12.3 4.9 18.3 18.9 13.4 31.2s-18.9 18.3-31.2 13.4L127.8 81.8c-2.9-1.2-6-1.8-9.2-1.8zM64 325.4V368c0 26.5 21.5 48 48 48h44.3c25.5 0 46.5-19.9 47.9-45.3l2.5-45.6c-2.3-.8-4.9-1.7-7.5-2.5c-17.2-5.4-39.9-10.5-63.6-10.5c-23.7 0-46.2 5.1-63.2 10.5c-3.1 1-5.9 1.9-8.5 2.9zM512 368V325.4c-2.6-.9-5.5-1.9-8.5-2.9c-17-5.4-39.5-10.5-63.2-10.5c-23.7 0-46.4 5.1-63.6 10.5c-2.7 .8-5.2 1.7-7.5 2.5l2.5 45.6c1.4 25.4 22.5 45.3 47.9 45.3H464c26.5 0 48-21.5 48-48z" + }, + "clock": { + title: "glasses", + viewBox: "0 0 512 512", + d: "M256 0a256 256 0 1 1 0 512A256 256 0 1 1 256 0zM232 120V256c0 8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2V120c0-13.3-10.7-24-24-24s-24 10.7-24 24z" + }, + "envelope": { + title: "envelope", + viewBox: "0 0 512 512", + d: "M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z" + }, + "discord": { + title: "discord", + viewBox: "0 0 640 512", + d: "M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z" + }, + "plus": { + title: "plus", + viewBox: "0 0 448 512", + d: "M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32V224H48c-17.7 0-32 14.3-32 32s14.3 32 32 32H192V432c0 17.7 14.3 32 32 32s32-14.3 32-32V288H400c17.7 0 32-14.3 32-32s-14.3-32-32-32H256V80z" + }, + "circle-check": { + title: "done", + viewBox: "0 0 512 512", + d: "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z" + }, + "delete-left": { + title: "delete", + viewBox: "0 0 576 512", + d: "M576 128c0-35.3-28.7-64-64-64H205.3c-17 0-33.3 6.7-45.3 18.7L9.4 233.4c-6 6-9.4 14.1-9.4 22.6s3.4 16.6 9.4 22.6L160 429.3c12 12 28.3 18.7 45.3 18.7H512c35.3 0 64-28.7 64-64V128zM271 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z" + }, + "link": { + title: "link", + viewBox: "0 0 640 512", + d: "M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z" + }, + "xmark": { + title: "close", + viewBox: "0 0 384 512", + d: "M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z" + }, + "trash": { + title: "trash", + viewBox: "0 0 448 512", + d: "M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z" + }, + "thumbtack": { + title: "trash", + viewBox: "0 0 384 512", + d: "M32 32C32 14.3 46.3 0 64 0H320c17.7 0 32 14.3 32 32s-14.3 32-32 32H290.5l11.4 148.2c36.7 19.9 65.7 53.2 79.5 94.7l1 3c3.3 9.8 1.6 20.5-4.4 28.8s-15.7 13.3-26 13.3H32c-10.3 0-19.9-4.9-26-13.3s-7.7-19.1-4.4-28.8l1-3c13.8-41.5 42.8-74.8 79.5-94.7L93.5 64H64C46.3 64 32 49.7 32 32zM160 384h64v96c0 17.7-14.3 32-32 32s-32-14.3-32-32V384z" + }, + "pen-to-square": { + title: "edit", + viewBox: "0 0 512 512", + d: "M471.6 21.7c-21.9-21.9-57.3-21.9-79.2 0L362.3 51.7l97.9 97.9 30.1-30.1c21.9-21.9 21.9-57.3 0-79.2L471.6 21.7zm-299.2 220c-6.1 6.1-10.8 13.6-13.5 21.9l-29.6 88.8c-2.9 8.6-.6 18.1 5.8 24.6s15.9 8.7 24.6 5.8l88.8-29.6c8.2-2.7 15.7-7.4 21.9-13.5L437.7 172.3 339.7 74.3 172.4 241.7zM96 64C43 64 0 107 0 160V416c0 53 43 96 96 96H352c53 0 96-43 96-96V320c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H96z" + }, + "book": { + title: "book", + viewBox: "0 0 448 512", + d: "M96 0C43 0 0 43 0 96V416c0 53 43 96 96 96H384h32c17.7 0 32-14.3 32-32s-14.3-32-32-32V384c17.7 0 32-14.3 32-32V32c0-17.7-14.3-32-32-32H384 96zm0 384H352v64H96c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16zm16 48H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16s7.2-16 16-16z" + }, + "gamepad": { + title: "game", + viewBox: "0 0 640 512", + d: "M192 64C86 64 0 150 0 256S86 448 192 448H448c106 0 192-86 192-192s-86-192-192-192H192zM496 168a40 40 0 1 1 0 80 40 40 0 1 1 0-80zM392 304a40 40 0 1 1 80 0 40 40 0 1 1 -80 0zM168 200c0-13.3 10.7-24 24-24s24 10.7 24 24v32h32c13.3 0 24 10.7 24 24s-10.7 24-24 24H216v32c0 13.3-10.7 24-24 24s-24-10.7-24-24V280H136c-13.3 0-24-10.7-24-24s10.7-24 24-24h32V200z" + }, + "hammer": { + title: "game", + viewBox: "0 0 576 512", + d: "M413.5 237.5c-28.2 4.8-58.2-3.6-80-25.4l-38.1-38.1C280.4 159 272 138.8 272 117.6V105.5L192.3 62c-5.3-2.9-8.6-8.6-8.3-14.7s3.9-11.5 9.5-14l47.2-21C259.1 4.2 279 0 299.2 0h18.1c36.7 0 72 14 98.7 39.1l44.6 42c24.2 22.8 33.2 55.7 26.6 86L503 183l8-8c9.4-9.4 24.6-9.4 33.9 0l24 24c9.4 9.4 9.4 24.6 0 33.9l-88 88c-9.4 9.4-24.6 9.4-33.9 0l-24-24c-9.4-9.4-9.4-24.6 0-33.9l8-8-17.5-17.5zM27.4 377.1L260.9 182.6c3.5 4.9 7.5 9.6 11.8 14l38.1 38.1c6 6 12.4 11.2 19.2 15.7L134.9 484.6c-14.5 17.4-36 27.4-58.6 27.4C34.1 512 0 477.8 0 435.7c0-22.6 10.1-44.1 27.4-58.6z" + }, + "location-dot": { + title: "location", + viewBox: "0 0 384 512", + d: "M215.7 499.2C267 435 384 279.4 384 192C384 86 298 0 192 0S0 86 0 192c0 87.4 117 243 168.3 307.2c12.3 15.3 35.1 15.3 47.4 0zM192 128a64 64 0 1 1 0 128 64 64 0 1 1 0-128z" + }, + "box-open": { + title: "box", + viewBox: "0 0 640 512", + d: "M58.9 42.1c3-6.1 9.6-9.6 16.3-8.7L320 64 564.8 33.4c6.7-.8 13.3 2.7 16.3 8.7l41.7 83.4c9 17.9-.6 39.6-19.8 45.1L439.6 217.3c-13.9 4-28.8-1.9-36.2-14.3L320 64 236.6 203c-7.4 12.4-22.3 18.3-36.2 14.3L37.1 170.6c-19.3-5.5-28.8-27.2-19.8-45.1L58.9 42.1zM321.1 128l54.9 91.4c14.9 24.8 44.6 36.6 72.5 28.6L576 211.6v167c0 22-15 41.2-36.4 46.6l-204.1 51c-10.2 2.6-20.9 2.6-31 0l-204.1-51C79 419.7 64 400.5 64 378.5v-167L191.6 248c27.8 8 57.6-3.8 72.5-28.6L318.9 128h2.2z" + }, + "calendar": { + title: "box", + viewBox: "0 0 448 512", + d: "M128 0c17.7 0 32 14.3 32 32V64H288V32c0-17.7 14.3-32 32-32s32 14.3 32 32V64h48c26.5 0 48 21.5 48 48v48H0V112C0 85.5 21.5 64 48 64H96V32c0-17.7 14.3-32 32-32zM0 192H448V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V192zm64 80v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm128 0v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H208c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H336zM64 400v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H208zm112 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H336c-8.8 0-16 7.2-16 16z" + }, + "people-group": { + title: "people", + viewBox: "0 0 640 512", + d: "M72 88a56 56 0 1 1 112 0A56 56 0 1 1 72 88zM64 245.7C54 256.9 48 271.8 48 288s6 31.1 16 42.3V245.7zm144.4-49.3C178.7 222.7 160 261.2 160 304c0 34.3 12 65.8 32 90.5V416c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V389.2C26.2 371.2 0 332.7 0 288c0-61.9 50.1-112 112-112h32c24 0 46.2 7.5 64.4 20.3zM448 416V394.5c20-24.7 32-56.2 32-90.5c0-42.8-18.7-81.3-48.4-107.7C449.8 183.5 472 176 496 176h32c61.9 0 112 50.1 112 112c0 44.7-26.2 83.2-64 101.2V416c0 17.7-14.3 32-32 32H480c-17.7 0-32-14.3-32-32zm8-328a56 56 0 1 1 112 0A56 56 0 1 1 456 88zM576 245.7v84.7c10-11.3 16-26.1 16-42.3s-6-31.1-16-42.3zM320 32a64 64 0 1 1 0 128 64 64 0 1 1 0-128zM240 304c0 16.2 6 31 16 42.3V261.7c-10 11.3-16 26.1-16 42.3zm144-42.3v84.7c10-11.3 16-26.1 16-42.3s-6-31.1-16-42.3zM448 304c0 44.7-26.2 83.2-64 101.2V448c0 17.7-14.3 32-32 32H288c-17.7 0-32-14.3-32-32V405.2c-37.8-18-64-56.5-64-101.2c0-61.9 50.1-112 112-112h32c61.9 0 112 50.1 112 112z" + }, + "comments": { + title: "comments", + viewBox: "0 0 640 512", + d: "M208 352c114.9 0 208-78.8 208-176S322.9 0 208 0S0 78.8 0 176c0 38.6 14.7 74.3 39.6 103.4c-3.5 9.4-8.7 17.7-14.2 24.7c-4.8 6.2-9.7 11-13.3 14.3c-1.8 1.6-3.3 2.9-4.3 3.7c-.5 .4-.9 .7-1.1 .8l-.2 .2 0 0 0 0C1 327.2-1.4 334.4 .8 340.9S9.1 352 16 352c21.8 0 43.8-5.6 62.1-12.5c9.2-3.5 17.8-7.4 25.3-11.4C134.1 343.3 169.8 352 208 352zM448 176c0 112.3-99.1 196.9-216.5 207C255.8 457.4 336.4 512 432 512c38.2 0 73.9-8.7 104.7-23.9c7.5 4 16 7.9 25.2 11.4c18.3 6.9 40.3 12.5 62.1 12.5c6.9 0 13.1-4.5 15.2-11.1c2.1-6.6-.2-13.8-5.8-17.9l0 0 0 0-.2-.2c-.2-.2-.6-.4-1.1-.8c-1-.8-2.5-2-4.3-3.7c-3.6-3.3-8.5-8.1-13.3-14.3c-5.5-7-10.7-15.4-14.2-24.7c24.9-29 39.6-64.7 39.6-103.4c0-92.8-84.9-168.9-192.6-175.5c.4 5.1 .6 10.3 .6 15.5z" + }, + "clipboard-check": { + title: "clipboard", + viewBox: "0 0 384 512", + d: "M192 0c-41.8 0-77.4 26.7-90.5 64H64C28.7 64 0 92.7 0 128V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64H282.5C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM305 273L177 401c-9.4 9.4-24.6 9.4-33.9 0L79 337c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L271 239c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z" + }, + "user": { + title: "clipboard", + viewBox: "0 0 448 512", + d: "M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z" + }, + "door-open": { + title: "open door", + viewBox: "0 0 576 512", + d: "M320 32c0-9.9-4.5-19.2-12.3-25.2S289.8-1.4 280.2 1l-179.9 45C79 51.3 64 70.5 64 92.5V448H32c-17.7 0-32 14.3-32 32s14.3 32 32 32H96 288h32V480 32zM256 256c0 17.7-10.7 32-24 32s-24-14.3-24-32s10.7-32 24-32s24 14.3 24 32zm96-128h96V480c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32s-14.3-32-32-32H512V128c0-35.3-28.7-64-64-64H352v64z" + }, + "scroll": { + title: "scroll", + viewBox: "0 0 576 512", + d: "M0 80v48c0 17.7 14.3 32 32 32H48 96V80c0-26.5-21.5-48-48-48S0 53.5 0 80zM112 32c10 13.4 16 30 16 48V384c0 35.3 28.7 64 64 64s64-28.7 64-64v-5.3c0-32.4 26.3-58.7 58.7-58.7H480V128c0-53-43-96-96-96H112zM464 480c61.9 0 112-50.1 112-112c0-8.8-7.2-16-16-16H314.7c-14.7 0-26.7 11.9-26.7 26.7V384c0 53-43 96-96 96H368h96z" + }, + "star": { + title: "star", + viewBox: "0 0 576 512", + d: "M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z" + }, + "boxes-stacked": { + title: "stacked boxes", + viewBox: "0 0 576 512", + d: "M248 0H208c-26.5 0-48 21.5-48 48V160c0 35.3 28.7 64 64 64H352c35.3 0 64-28.7 64-64V48c0-26.5-21.5-48-48-48H328V80c0 8.8-7.2 16-16 16H264c-8.8 0-16-7.2-16-16V0zM64 256c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H224c35.3 0 64-28.7 64-64V320c0-35.3-28.7-64-64-64H184v80c0 8.8-7.2 16-16 16H120c-8.8 0-16-7.2-16-16V256H64zM352 512H512c35.3 0 64-28.7 64-64V320c0-35.3-28.7-64-64-64H472v80c0 8.8-7.2 16-16 16H408c-8.8 0-16-7.2-16-16V256H352c-15 0-28.8 5.1-39.7 13.8c4.9 10.4 7.7 22 7.7 34.2V464c0 12.2-2.8 23.8-7.7 34.2C323.2 506.9 337 512 352 512z" + }, + "person-running": { + title: "person running", + viewBox: "0 0 448 512", + d: "M320 48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM125.7 175.5c9.9-9.9 23.4-15.5 37.5-15.5c1.9 0 3.8 .1 5.6 .3L137.6 254c-9.3 28 1.7 58.8 26.8 74.5l86.2 53.9-25.4 88.8c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l28.7-100.4c5.9-20.6-2.6-42.6-20.7-53.9L238 299l30.9-82.4 5.1 12.3C289 264.7 323.9 288 362.7 288H384c17.7 0 32-14.3 32-32s-14.3-32-32-32H362.7c-12.9 0-24.6-7.8-29.5-19.7l-6.3-15c-14.6-35.1-44.1-61.9-80.5-73.1l-48.7-15c-11.1-3.4-22.7-5.2-34.4-5.2c-31 0-60.8 12.3-82.7 34.3L57.4 153.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l23.1-23.1zM91.2 352H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h69.6c19 0 36.2-11.2 43.9-28.5L157 361.6l-9.5-6c-17.5-10.9-30.5-26.8-37.9-44.9L91.2 352z" + }, + "handshake": { + title: "handshake", + viewBox: "0 0 640 512", + d: "M323.4 85.2l-96.8 78.4c-16.1 13-19.2 36.4-7 53.1c12.9 17.8 38 21.3 55.3 7.8l99.3-77.2c7-5.4 17-4.2 22.5 2.8s4.2 17-2.8 22.5l-20.9 16.2L550.2 352H592c26.5 0 48-21.5 48-48V176c0-26.5-21.5-48-48-48H516h-4-.7l-3.9-2.5L434.8 79c-15.3-9.8-33.2-15-51.4-15c-21.8 0-43 7.5-60 21.2zm22.8 124.4l-51.7 40.2C263 274.4 217.3 268 193.7 235.6c-22.2-30.5-16.6-73.1 12.7-96.8l83.2-67.3c-11.6-4.9-24.1-7.4-36.8-7.4C234 64 215.7 69.6 200 80l-72 48H48c-26.5 0-48 21.5-48 48V304c0 26.5 21.5 48 48 48H156.2l91.4 83.4c19.6 17.9 49.9 16.5 67.8-3.1c5.5-6.1 9.2-13.2 11.1-20.6l17 15.6c19.5 17.9 49.9 16.6 67.8-2.9c4.5-4.9 7.8-10.6 9.9-16.5c19.4 13 45.8 10.3 62.1-7.5c17.9-19.5 16.6-49.9-2.9-67.8l-134.2-123z" + }, + "reply": { + title: "reply", + viewBox: "0 0 512 512", + d: "M205 34.8c11.5 5.1 19 16.6 19 29.2v64H336c97.2 0 176 78.8 176 176c0 113.3-81.5 163.9-100.2 174.1c-2.5 1.4-5.3 1.9-8.1 1.9c-10.9 0-19.7-8.9-19.7-19.7c0-7.5 4.3-14.4 9.8-19.5c9.4-8.8 22.2-26.4 22.2-56.7c0-53-43-96-96-96H224v64c0 12.6-7.4 24.1-19 29.2s-25 3-34.4-5.4l-160-144C3.9 225.7 0 217.1 0 208s3.9-17.7 10.6-23.8l160-144c9.4-8.5 22.9-10.6 34.4-5.4z" + }, + "d20": { + title: "d20", + viewBox: "0 0 512 512", + d: "M48.7 125.8l53.2 31.9c7.8 4.7 17.8 2 22.2-5.9L201.6 12.1c3-5.4-.9-12.1-7.1-12.1c-1.6 0-3.2 .5-4.6 1.4L47.9 98.8c-9.6 6.6-9.2 20.9 .8 26.9zM16 171.7V295.3c0 8 10.4 11 14.7 4.4l60-92c5-7.6 2.6-17.8-5.2-22.5L40.2 158C29.6 151.6 16 159.3 16 171.7zM310.4 12.1l77.6 139.6c4.4 7.9 14.5 10.6 22.2 5.9l53.2-31.9c10-6 10.4-20.3 .8-26.9L322.1 1.4c-1.4-.9-3-1.4-4.6-1.4c-6.2 0-10.1 6.7-7.1 12.1zM496 171.7c0-12.4-13.6-20.1-24.2-13.7l-45.3 27.2c-7.8 4.7-10.1 14.9-5.2 22.5l60 92c4.3 6.7 14.7 3.6 14.7-4.4V171.7zm-49.3 246L286.1 436.6c-8.1 .9-14.1 7.8-14.1 15.9v52.8c0 3.7 3 6.8 6.8 6.8c.8 0 1.6-.1 2.4-.4l172.7-64c6.1-2.2 10.1-8 10.1-14.5c0-9.3-8.1-16.5-17.3-15.4zM233.2 512c3.7 0 6.8-3 6.8-6.8V452.6c0-8.1-6.1-14.9-14.1-15.9l-160.6-19c-9.2-1.1-17.3 6.1-17.3 15.4c0 6.5 4 12.3 10.1 14.5l172.7 64c.8 .3 1.6 .4 2.4 .4zM41.7 382.9l170.9 20.2c7.8 .9 13.4-7.5 9.5-14.3l-85.7-150c-5.9-10.4-20.7-10.8-27.3-.8L30.2 358.2c-6.5 9.9-.3 23.3 11.5 24.7zm439.6-24.8L402.9 238.1c-6.5-10-21.4-9.6-27.3 .8L290.2 388.5c-3.9 6.8 1.6 15.2 9.5 14.3l170.1-20c11.8-1.4 18-14.7 11.5-24.6zm-216.9 11l78.4-137.2c6.1-10.7-1.6-23.9-13.9-23.9H183.1c-12.3 0-20 13.3-13.9 23.9l78.4 137.2c3.7 6.4 13 6.4 16.7 0zM174.4 176H337.6c12.2 0 19.9-13.1 14-23.8l-80-144c-2.8-5.1-8.2-8.2-14-8.2h-3.2c-5.8 0-11.2 3.2-14 8.2l-80 144c-5.9 10.7 1.8 23.8 14 23.8z" + }, + "screwdriver-wrench": { + title: "settings", + viewBox: "0 0 512 512", + d: "M78.6 5C69.1-2.4 55.6-1.5 47 7L7 47c-8.5 8.5-9.4 22-2.1 31.6l80 104c4.5 5.9 11.6 9.4 19 9.4h54.1l109 109c-14.7 29-10 65.4 14.3 89.6l112 112c12.5 12.5 32.8 12.5 45.3 0l64-64c12.5-12.5 12.5-32.8 0-45.3l-112-112c-24.2-24.2-60.6-29-89.6-14.3l-109-109V104c0-7.5-3.5-14.5-9.4-19L78.6 5zM19.9 396.1C7.2 408.8 0 426.1 0 444.1C0 481.6 30.4 512 67.9 512c18 0 35.3-7.2 48-19.9L233.7 374.3c-7.8-20.9-9-43.6-3.6-65.1l-61.7-61.7L19.9 396.1zM512 144c0-10.5-1.1-20.7-3.2-30.5c-2.4-11.2-16.1-14.1-24.2-6l-63.9 63.9c-3 3-7.1 4.7-11.3 4.7H352c-8.8 0-16-7.2-16-16V102.6c0-4.2 1.7-8.3 4.7-11.3l63.9-63.9c8.1-8.1 5.2-21.8-6-24.2C388.7 1.1 378.5 0 368 0C288.5 0 224 64.5 224 144l0 .8 85.3 85.3c36-9.1 75.8 .5 104 28.7L429 274.5c49-23 83-72.8 83-130.5zM56 432a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z" + }, + "dumbbell": { + title: "dumbbell", + viewBox: "0 0 640 512", + d: "M96 64c0-17.7 14.3-32 32-32h32c17.7 0 32 14.3 32 32V224v64V448c0 17.7-14.3 32-32 32H128c-17.7 0-32-14.3-32-32V384H64c-17.7 0-32-14.3-32-32V288c-17.7 0-32-14.3-32-32s14.3-32 32-32V160c0-17.7 14.3-32 32-32H96V64zm448 0v64h32c17.7 0 32 14.3 32 32v64c17.7 0 32 14.3 32 32s-14.3 32-32 32v64c0 17.7-14.3 32-32 32H544v64c0 17.7-14.3 32-32 32H480c-17.7 0-32-14.3-32-32V288 224 64c0-17.7 14.3-32 32-32h32c17.7 0 32 14.3 32 32zM416 224v64H224V224H416z" + }, + "chevron-down": { + title: "chevron down", + viewBox: "0 0 512 512", + d: "M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z" + }, + "chevron-up": { + title: "chevron up", + viewBox: "0 0 512 512", + d: "M233.4 105.4c12.5-12.5 32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 173.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l192-192z" + }, + "chevron-left": { + title: "chevron left", + viewBox: "0 0 320 512", + d: "M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" + }, + "chevron-right": { + title: "chevron right", + viewBox: "0 0 320 512", + d: "M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" + }, + }; + + ns.createIcon = function createIcon(iconDef, title, desc, classes, style) + { + var iconEl = createElement( + ns.ElementTypes.svg, + { + "viewBox": iconDef.viewBox, + "class": `${ICON_CLASS} ${classes}`, + "role": "img", + "style": style || "" + }, + ICON_CITATION + ); + + createChildElement( + iconEl, + ns.ElementTypes.title, + {}, + title || iconDef.title + ); + if (desc) + { + createChildElement( + iconEl, + ns.ElementTypes.desc, + {}, + desc + ); + } + + createChildElement( + iconEl, + ns.ElementTypes.path, + { + "d": iconDef.d + } + ); + + return iconEl; + }; + //#endregion +}); + +registerNamespace("GW.Controls", function(ns) { + ns.IconEl = class IconEl extends HTMLElement + { + //#region staticProperties + static observedAttributes = []; + static instanceCount = 0; + static instanceMap = {}; + //#endregion + + //#region instance properties + instanceId; + iconObj; + titleText; + isInitialized; + + //#region element properties + //#endregion + //#endregion + + constructor() + { + super(); + this.instanceId = IconEl.instanceCount++; + IconEl.instanceMap[this.instanceId] = this; + } + + get idKey() + { + return `gw-icon-${this.instanceId}`; + } + + //#region HTMLElement implementation + connectedCallback() + { + if (this.isInitialized) { return; } + + this.iconObj = ns.SVGLib.Icons[this.getAttribute("iconKey")]; + + this.titleText = this.getAttribute("title"); + if (this.hasAttribute("titleId")) + { + const titleEl = document.getElementById(this.getAttribute("titleId")); + this.titleText = titleEl.innerText; + titleEl.remove(); + } + + this.iconDescription = this.getAttribute("description"); + this.iconClasses = this.getAttribute("iconClasses"); + this.iconStyle = this.getAttribute("iconStyle"); + + this.renderContent(); + this.isInitialized = true; + } + //#endregion + + renderContent() + { + if (!this.iconObj) + { + debugger; + return; + } + + //Markup + this.appendChild( + ns.SVGLib.createIcon( + this.iconObj, + this.titleText, + this.iconDescription, + this.iconClasses, + this.iconStyle, + ) + ); + + //element properties + } + }; + customElements.define("gw-icon", ns.IconEl); +}); \ No newline at end of file diff --git a/src/pages/guestbook.njk b/src/pages/guestbook.njk index 5825171d..d2faf1d6 100644 --- a/src/pages/guestbook.njk +++ b/src/pages/guestbook.njk @@ -16,93 +16,21 @@ eleventyComputed:

Archive of my previous guestbook hosted on 123Guestbook

-

Leave A Message

+ -
-
- - -
+ -
- - -
- -
- - -
- -
- - -
- - -
- -
-

Messages

-
-
- - - - \ No newline at end of file + + + + + \ No newline at end of file