update ticle

This commit is contained in:
Lera Elvoé 2022-06-21 17:07:23 +03:00
parent 6478f54689
commit fe8aa24ca8

View File

@ -1,405 +1,248 @@
<!DOCTYPE html>
<html>
<head>
<title>Parcel Sandbox</title>
<title>Tickle</title>
<meta charset="UTF-8" />
<style></style>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Lobster&family=Nunito+Sans:ital,wght@0,400;0,700;1,400&display=swap"
rel="stylesheet"
/>
<style>
:root {
--accent: hotpink;
--background: #fdfdfd;
font-family: "Nunito Sans", sans-serif;
}
body,
html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.trigger {
display: none;
}
.left-nav > * {
position: fixed;
top: 0;
left: 0;
}
.left-nav > .menu {
padding: 1em;
transform: translateX(-100%);
display: flex;
flex-direction: column;
top: 0;
bottom: 0;
transition: all 0.3s ease-out;
gap: 1em;
}
.left-nav > .menu a {
text-decoration: none;
background: var(--background);
color: var(--accent);
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
padding: 0.2em 0.4em;
}
.left-nav > .menu a:hover {
background: var(--accent);
color: var(--background);
}
.left-nav > .trigger:checked ~ .menu {
transform: translateX(0);
transition-duration: 0.1s;
}
.burger {
width: 2em;
height: 2em;
display: flex;
justify-content: center;
align-content: center;
text-align: center;
vertical-align: middle;
line-height: 2em;
border-radius: 0.2em;
box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
transition: background 0.5s ease;
}
.left-nav > .trigger:checked ~ .burger {
color: transparent;
}
.left-nav > .trigger:checked ~ .burger,
#Loading {
top: 0;
right: 0;
width: 100%;
height: 100%;
border-radius: 0;
background: rgba(17, 17, 17, 0.2);
}
#Loading {
position: absolute;
text-align: center;
align-items: center;
justify-content: center;
font-size: 8rem;
color: var(--accent);
opacity: 0;
transition: opacity 0.3s ease-in, height 0s linear 0.3s;
height: 0;
display: flex;
overflow: hidden;
}
.is-loading #Loading {
opacity: 1;
height: 100%;
transition: opacity 1s ease-in, height 0 linear;
}
#Loading::after {
content: "❤";
animation: beat 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
}
@keyframes beat {
0% {
transform: scale(0.95);
}
5% {
transform: scale(1.1);
}
39% {
transform: scale(0.85);
}
45% {
transform: scale(1);
}
60% {
transform: scale(0.95);
}
100% {
transform: scale(0.9);
}
}
#Body {
padding: 3em;
max-width: 52em;
margin: 0 auto;
}
#Body h1,
#Body h2,
#Body h3,
#Body h4 {
color: var(--accent);
font-family: "Lobster", serif;
}
#Body a {
color: var(--background);
background-color: var(--accent);
text-decoration: none;
padding: 0.1em 0.4em;
}
</style>
</head>
<body>
<template id="my-link">
<style>
:host {
--background-regular: hsla(196, 61%, 58%, 0.75);
--background-active: red;
text-decoration: none;
color: #18272f;
font-weight: 700;
cursor: pointer;
position: relative;
display: flex;
}
:host span {
width: 100%;
height: 100%;
}
:host::before {
content: "";
background-color: var(--background-regular);
position: absolute;
left: 0;
bottom: 3px;
width: 100%;
height: 8px;
z-index: -1;
transition: all 0.3s ease-in-out;
}
:host(:hover)::before {
bottom: 0;
height: 100%;
}
:host([active])::before {
background-color: var(--background-active);
}
</style>
<span><slot /></span>
</template>
<template id="my-menu">
<style>
:host ul,
:host li {
list-style: none;
padding: 0;
margin: 0;
}
:host nav {
display: flex;
flex-direction: column;
}
</style>
<nav>
<slot />
</nav>
</template>
<my-menu id="menu">
<my-link main href="d">Home</my-link>
<h2>Articles</h2>
</my-menu>
<div id="App"></div>
<script>
//@ts-check
/**********************************************************************
*
* UTILITIES
*
* A few common methods to use in the project
*
*********************************************************************/
const Signal = () => {
const listeners = new Set();
return {
remove: listeners.delete.bind(listeners),
add(/** @type {(arg:any)=>void} */ listener) {
listeners.add(listener);
return listeners.delete.bind(listeners, listener);
},
emit(/** @type {any} */ data) {
listeners.forEach((l) => l(data));
},
};
};
const getText = (/** @type {string} */ file) =>
fetch(`./${file}`)
.then((response) => response.text())
.catch((err) => {
console.error(`could not find file "${file}"`);
throw err;
});
const parseMarkdown = (/** @type {string} */ text) =>
text
// lists
.replace(
/^\s*\n((?:\*\s.+\s*\n)+)([^\*])/gm,
(_, bullets, next) =>
`<ul>${bullets.replace(
/^\*\s(.+)/gm,
"<li>$1</li>"
)}\n</ul>\n\n${next}`
)
.replace(
/^\s*\n((?:\d\..+\s*\n)+)([^\*])/gm,
(_, bullets, next) =>
`<ol>${bullets.replace(
/^\d\.\s(.+)/gm,
"<li>$1</li>"
)}\n</ol>\n\n${next}`
)
// blockquotes
.replace(/^\>(.+)/gm, "<blockquote>$1</blockquote>")
// headers
.replace(/(#+)(.+)/g, (_, { length: l }, t) => `<h${l}>${t}</h${l}>`)
.replace(/^(.+)\n\=+/gm, "<h1>$1</h1>")
.replace(/^(.+)\n\-+/gm, "<h2>$1</h2>")
//images
.replace(/\!\[([^\]]+)\]\(([^\)]+)\)/g, '<img src="$2" alt="$1" />')
//links
.replace(
/[\[]{1}([^\]]+)[\]]{1}[\(]{1}([^\)\"]+)(\"(.+)\")?[\)]{1}/g,
'<a href="$2" title="$4">$1</a>'
)
//font styles
.replace(/[\*\_]{2}([^\*\_]+)[\*\_]{2}/g, "<strong>$1</strong>")
.replace(/[\*\_]{1}([^\*\_]+)[\*\_]{1}/g, "<em>$1</em>")
.replace(/[\~]{2}([^\~]+)[\~]{2}/g, "<del>$1</del>")
//pre
.replace(/^\s*\n\`\`\`(([^\s]+))?/gm, '<pre class="$2">')
.replace(/^\`\`\`\s*\n/gm, "</pre>\n\n")
//code
.replace(/[\`]{1}([^\`]+)[\`]{1}/g, "<code>$1</code>")
//p
.replace(/^\s*(\n)?(.+)/gm, (m) => {
return /\<(\/)?(h\d|ul|ol|li|blockquote|pre|img)/.test(m)
? m
: "<p>" + m + "</p>";
})
//strip p from pre
.replace(/(\<pre.+\>)\s*\n\<p\>(.+)\<\/p\>/gm, "$1$2")
.trim();
const getMarkdown = (/** @type {string} */ file) =>
getText(file).then(parseMarkdown);
<nav class="left-nav">
<input id="main-nav" type="checkbox" class="trigger" />
<label for="main-nav" class="burger">&#8801;</label>
<div id="Menu" class="menu"></div>
</nav>
<header id="Menu"></header>
<main id="Body"></main>
<div id="Loading"></div>
<script type="module">
/**
* markdown parser. Remove if you don't use markdown
*/
// @ts-ignore
import { micromark } from "https://esm.sh/micromark@3?bundle";
/**
* useful to check for transitions while developing styles, if the loading screen disappears too fast
*
* @param {string} tag
* @param {Record<string, any>} props
* @param {string|Node[]} children
* @returns
*/
const el = (tag = "div", props = {}, children = []) => {
const node = document.createElement(tag);
Object.keys(props).forEach((key) => {
node.setAttribute(key, props[key]);
});
if (typeof children == "string") {
children = [document.createTextNode(children)];
const wait = (val) => new Promise((ok) => setTimeout(ok, 1, val));
/**
* The elements we will need
*/
const [Menu, Body, Loading] = ["Menu", "Body", "Loading"].map((id) =>
document.getElementById(id)
);
/**
* cache the original title to append it to the page title
*/
const mainTitle = document.title;
const startLoading = () => document.body.classList.add("is-loading");
const stopLoading = () => document.body.classList.remove("is-loading");
const getCurrentPage = () =>
window.location.hash[1] === "/" ? window.location.hash.slice(2) : "";
const onHashChange = (evt) => {
const path = getCurrentPage();
if (!path) {
return false;
}
children.forEach((child) => node.appendChild(child));
return node;
};
const makeTitelize = (alwaysLowCaps = [], alwaysUpperCaps = []) => {
const specials = [...alwaysLowCaps, ...alwaysUpperCaps].reduce(
(result, word) =>
result.set(new RegExp("\\b" + word + "\\b", "gi"), word),
/** @type {Map<RegExp, string>}*/ (new Map())
);
const titelize = (/** @type {string} */ text) => {
text = text
.replace(/_-\//g, " ")
.replace(/\.\w+$/, "")
.replace(/\s+/, " ")
.split(" ")
.map((word) =>
word.length > 1
? word[0].toUpperCase() + word.slice(1).toLowerCase()
: word
)
.join(" ");
for (const [key, value] of specials) {
text = text.replace(key, value);
}
return text;
};
return titelize;
};
const titelize = makeTitelize(["the", "a"], ["TV", "ID", "AI"]);
const Router = (() => {
const onRouteChange = Signal();
let route = "";
const set = (/** @type {string} */ newRoute) => {
if (newRoute === route) {
return false;
}
window.location.hash = newRoute;
route = newRoute;
onRouteChange.emit(route);
return true;
};
const get = () => window.location.hash.slice(1).replace(/\//gi, "/");
const is = (href) => href === get();
window.addEventListener("popstate", () => set(get()));
return { set, get, is, onRouteChange };
})();
const getTemplateClone = (/** @type {string} */ id) => {
const templateModel = /** @type {HTMLTemplateElement} */ (
document.getElementById(id)
);
const template = /** @type {HTMLElement} */ (
templateModel.content.cloneNode(true)
);
return template;
};
/**********************************************************************
* WEB COMPONENTS
*
* Sources:
* https://web.dev/custom-elements-best-practices/
* https://googlechromelabs.github.io/howto-components/
*
* A set of neat components to use in the page
*
*********************************************************************/
class CustomElement extends HTMLElement {
/** @type {ShadowRoot} */
shadow = this.attachShadow({ mode: "closed" });
/**
* A user may set a property on an instance of an element, before its prototype has been connected to this class.
* Will check for any instance properties and run them through the proper class setters.
* @param {string} prop
*/
_syncProperty(prop) {
if (this.hasOwnProperty(prop)) {
let value = this[prop];
delete this[prop];
this[prop] = value;
}
}
}
class MyLink extends CustomElement {
constructor() {
super();
this.shadow.append(getTemplateClone("my-link"));
this.shadow.addEventListener("click", this._onClick.bind(this));
Router.onRouteChange.add(this.updateActive.bind(this));
}
static get observedAttributes() {
return ["href", "active", "main"];
}
_onClick() {
if (this.href) {
Router.set(this.href);
}
}
attributeChangedCallback(property, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
this[property] = newValue;
}
updateActive() {
if (Router.is(this.href)) {
this.setAttribute("active", "");
} else {
this.removeAttribute("active");
}
}
set href(/** @type {string}*/ value) {
this.setAttribute("href", value);
this.updateActive();
}
get href() {
return this.getAttribute("href");
}
set main(/** @type {boolean}*/ value) {
if (value) {
this.setAttribute("main", "");
} else {
this.removeAttribute("main");
}
}
get main() {
return this.hasAttribute("main");
}
connectedCallback() {
["active", "main"].forEach((prop) => this._syncProperty(prop));
this.updateActive();
if (this.getAttribute("main")) {
console.log("sdfsdff");
}
}
}
customElements.define("my-link", MyLink);
class MyMenu extends CustomElement {
_handled = new Set();
constructor() {
super();
this.shadow.append(getTemplateClone("my-menu"));
const slot = this.shadow.querySelector("slot");
slot.addEventListener("slotchange", (event) => {
for (const child of slot.assignedElements()) {
if (this._handled.has(child) || !(child instanceof MyLink)) {
continue;
}
this._handled.add(child);
// TODO: pre-fetch
//console.log("new child: ", child);
}
startLoading();
return fetch(`./${path}`)
.then((response) => response.text())
.then(wait)
.then((text) => {
const [, title] = text.match(/^(#\s\w+)/) ||
text.match(/(.*?)\n===+/m) || [, path];
document.title = `${title} | ${mainTitle}`;
Body.innerHTML = micromark(text);
stopLoading();
});
}
}
customElements.define("my-menu", MyMenu);
};
/**********************************************************************
* MARKDOWN PARSING
*********************************************************************/
window.addEventListener("hashchange", onHashChange);
const load = (/** @type {string} */ file) =>
getMarkdown(file).then((md) => {
app.innerHTML = md;
});
/**
* Loads the article list, parses it, creates the menu items
*/
const start = () => {
startLoading();
fetch("./files.txt")
.then((response) => response.text())
.then((lines) => {
Menu.innerHTML = lines
.split(`\n`)
.map((line, index) => {
const today = new Date().toISOString().split("T")[0];
const result = line.match(
/(?<name>.+)\.(?<ext>\w{2,3})(?:\s(?<date>[\d-]+)?(?<title>.+))?/
);
if (!result) {
console.log(`could not parse line ${index}: [${line}]`);
return null;
}
const {
groups: { name, ext, date = today, title = name },
} = result;
const href = `/${name}.${ext}`;
if (!getCurrentPage()) {
window.location.hash = `#${href}`;
}
return { name, href, date, title };
})
.filter(Boolean)
.sort(({ date: a }, { date: b }) => a - b)
.map(
({ href, title }) => `<a data-link href="#${href}">${title}</a>`
)
.join(`\n`);
/**********************************************************************
* BOOSTRAPPING
*********************************************************************/
getText("files.txt").then((lines) => {
lines
.split(`\n`)
.map((line) => {
const [file, maybeDate, ...rest] = line.split(/\s/);
const href = file.trim();
let date = maybeDate ? new Date(maybeDate) : new Date();
if (isNaN(date.getTime())) {
date = new Date();
rest.unshift(maybeDate);
}
const textContent = rest.length
? rest.join(" ").trim()
: titelize(file);
return { href, date, textContent };
})
.sort(({ date: a }, { date: b }) => a.getTime() - b.getTime())
.forEach(({ href, date, textContent }) => {
const link = /** @type {MyLink} */ el(
"my-link",
{ href },
textContent
);
document.getElementById("menu").appendChild(link);
onHashChange();
});
});
};
const app = document.getElementById("App");
const BLOCKQUOTE = Symbol("blockquote");
const PARAGRAPH = Symbol("paragraph");
const LIST = Symbol("list");
Router.onRouteChange.add((route) => load(route));
start();
</script>
</body>
</html>