diff --git a/src/demo/index.php b/src/demo/index.php index 657e03e..8ac5ba5 100644 --- a/src/demo/index.php +++ b/src/demo/index.php @@ -49,7 +49,7 @@
- +

Options

@@ -131,6 +131,8 @@ + +
@@ -152,7 +154,7 @@ - @@ -163,7 +165,7 @@ - diff --git a/src/demo/js/script.js b/src/demo/js/script.js index 4946ed3..d61faa1 100644 --- a/src/demo/js/script.js +++ b/src/demo/js/script.js @@ -1,5 +1,5 @@ const preview = { - // default values + // default values for Readme Typing SVG parameters defaults: { font: "monospace", weight: "400", @@ -18,6 +18,12 @@ const preview = { random: "false", separator: ";", }, + // input field initial values that differ from the defaults + overrides: { + font: "Fira Code", + pause: "1000", + width: "435", + }, // dummy text for default line values dummyText: [ "The five boxing wizards jump quickly", @@ -30,11 +36,11 @@ const preview = { ], /** - * Update the preview image and markdown + * Get the current parameters from the form + * @returns {object} The current parameters */ - update() { - const copyButtons = document.querySelectorAll(".copy-button"); - // get parameter values from all .param elements + getParams() { + // get all parameters from the .param fields const params = Array.from(document.querySelectorAll(".param:not([data-index])")).reduce((acc, next) => { // copy accumulator into local object let obj = acc; @@ -72,14 +78,20 @@ const preview = { } } params.lines = mergeLines(lineInputs, params.separator); - // function to URI encode string but keep semicolons as ';' and spaces as '+' - const encode = (str) => { - return encodeURIComponent(str).replace(/%3B/g, ";").replace(/%20/g, "+"); - }; + return params; + }, + + /** + * Update the preview image and markdown + */ + update() { + const copyButtons = document.querySelectorAll(".copy-button"); + // get parameter values + const params = this.getParams(); // convert parameters to query string const query = Object.keys(params) .filter((key) => params[key] !== this.defaults[key]) // skip if default value - .map((key) => encode(key) + "=" + encode(params[key])) // encode keys and values + .map((key) => this.customEncode(key) + "=" + this.customEncode(params[key])) // encode keys and values .join("&"); // join lines with '&' delimiter // generate links and markdown const imageURL = `${window.location.origin}?${query}`; @@ -102,44 +114,58 @@ const preview = { htmlElement.innerText = html; // disable copy button if no lines are filled in copyButtons.forEach((el) => (el.disabled = !params.lines.length)); + // update URL to match parameters + this.openPermalink(); }, /** - * Add a new line to the input fields + * Encode a string for use in a URL but keep semicolons as ';' and spaces as '+' + * @param {string} str The string to encode + * @returns The encoded string + */ + customEncode(str) { + return encodeURIComponent(str).replace(/%3B/g, ";").replace(/%20/g, "+"); + }, + + /** + * Add new line input fields + * @param {number} count The number of lines to add * @returns {false} Always returns false to prevent form submission */ - addLine() { - const parent = document.querySelector(".lines"); - const index = parent.querySelectorAll("input").length + 1; - // label - const label = document.createElement("label"); - label.innerText = `Line ${index}`; - label.setAttribute("for", `line-${index}`); - label.dataset.index = index; - // line input box - const input = document.createElement("input"); - input.className = "param"; - input.type = "text"; - input.id = `line-${index}`; - input.name = `line-${index}`; - input.placeholder = "Enter text here"; - input.value = this.dummyText[(index - 1) % this.dummyText.length]; - input.dataset.index = index; - // removal button - const deleteButton = document.createElement("button"); - deleteButton.className = "delete-line btn"; - deleteButton.setAttribute("onclick", "return preview.removeLine(this.dataset.index);"); - deleteButton.innerHTML = - ' '; - deleteButton.dataset.index = index; + addLines(count) { + for (let i = 0; i < count; i++) { + const parent = document.querySelector(".lines"); + const index = parent.querySelectorAll("input").length + 1; + // label + const label = document.createElement("label"); + label.innerText = `Line ${index}`; + label.setAttribute("for", `line-${index}`); + label.dataset.index = index; + // line input box + const input = document.createElement("input"); + input.className = "param"; + input.type = "text"; + input.id = `line-${index}`; + input.name = `line-${index}`; + input.placeholder = "Enter text here"; + input.value = this.dummyText[(index - 1) % this.dummyText.length]; + input.dataset.index = index; + // removal button + const deleteButton = document.createElement("button"); + deleteButton.className = "delete-line btn"; + deleteButton.setAttribute("onclick", "return preview.removeLine(this.dataset.index);"); + deleteButton.innerHTML = + ' '; + deleteButton.dataset.index = index; - // add elements - parent.appendChild(label); - parent.appendChild(input); - parent.appendChild(deleteButton); + // add elements + parent.appendChild(label); + parent.appendChild(input); + parent.appendChild(deleteButton); - // disable button if only 1 - parent.querySelector(".delete-line.btn").disabled = index == 1; + // disable button if only 1 + parent.querySelector(".delete-line.btn").disabled = index == 1; + } // update and exit this.update(); @@ -147,7 +173,7 @@ const preview = { }, /** - * Remove a line from the input fields + * Remove a line input field * @param {number} index The index of the line to remove * @returns {false} Always returns false to prevent form submission */ @@ -192,19 +218,14 @@ const preview = { }, /** - * Reset all input fields to default values + * Reset all input fields to their initial values * @returns {false} Always returns false to prevent form submission */ reset() { - const overrides = { - font: "Fira Code", - pause: "1000", - width: "435", - }; // reset all inputs const inputs = document.querySelectorAll(".param"); inputs.forEach((input) => { - let value = overrides[input.name] || this.defaults[input.name]; + let value = this.overrides[input.name] || this.defaults[input.name]; if (value) { if (["color", "background"].includes(input.name)) { input.jscolor.fromString(value); @@ -214,20 +235,89 @@ const preview = { } }); }, + + /** + * Get the current parameters in a permalink format + * @returns {string} The permalink URL + */ + getPermalink() { + // get parameters from form + const params = this.getParams(); + // convert parameters to query string + const defaultInputs = { ...this.defaults, ...this.overrides }; + defaultInputs.lines = this.dummyText[0]; + const query = Object.keys(params) + .filter((key) => params[key] !== defaultInputs[key]) // skip if default value + .map((key) => this.customEncode(key) + "=" + this.customEncode(params[key])) // encode keys and values + .join("&"); // join lines with '&' delimiter + // return permalink + return `${window.location.origin}${window.location.pathname}` + (query ? `?${query}` : ""); + }, + + /** + * Save the current parameters to the URL + */ + openPermalink() { + window.history.replaceState({}, "", this.getPermalink()); + }, + + /** + * Restore the last saved parameters from the URL + */ + restore() { + // get parameters from URL + const urlParams = new URLSearchParams(window.location.search); + const params = { ...this.defaults, ...this.overrides, ...Object.fromEntries(urlParams) }; + // set all parameters + const inputs = document.querySelectorAll(".param"); + inputs.forEach((input) => { + let value = params[input.name]; + if (value) { + if (["color", "background"].includes(input.name)) { + input.jscolor.fromString(value); + } else { + input.value = value; + } + } + }); + // add lines + const lines = params.lines || this.dummyText[0]; + const lineInputs = lines.split(params.separator); + this.addLines(lineInputs.length); + lineInputs.forEach((line, index) => { + document.querySelector(`#line-${index + 1}`).value = line; + }); + }, }; const clipboard = { /** - * Copy the text from a code block to the clipboard - * @param {HTMLElement} el The element that was clicked + * Copy text to the clipboard + * @param {HTMLElement} btn The element that was clicked + * @param {String} text The text to copy */ - copy(el) { - const textToCopy = el.parentElement.querySelector("code").innerText; - navigator.clipboard.writeText(textToCopy).then(() => { + copy(btn, text) { + navigator.clipboard.writeText(text).then(() => { // set tooltip text - el.title = "Copied!"; + btn.title = "Copied!"; }); }, + + /** + * Copy the text from a code block to the clipboard + * @param {HTMLElement} btn The element that was clicked + */ + copyCode(btn) { + this.copy(btn, btn.parentElement.querySelector("code").innerText); + }, + + /** + * Copy the permalink to the clipboard + * @param {HTMLElement} btn The element that was clicked + */ + copyPermalink(btn) { + this.copy(btn, preview.getPermalink()); + }, }; const tooltip = { @@ -255,9 +345,8 @@ document.querySelector(".show-border input").addEventListener("change", function window.addEventListener( "load", () => { - // add first line - preview.addLine(); - preview.update(); + preview.restore(); // restore parameters + preview.update(); // update preview }, false );