magic! now it works on web kit

This commit is contained in:
liquidex 2024-03-03 21:23:37 +01:00
parent 33416e8963
commit 3a4eb87ca0
17 changed files with 164 additions and 116 deletions

View file

@ -527,7 +527,8 @@ where
escape_html(&mut self.writer, &branch.attributes.id)?; escape_html(&mut self.writer, &branch.attributes.id)?;
self.writer.write_str("\">")?; self.writer.write_str("\">")?;
} }
self.writer.write_str("<img is=\"th-emoji\" title=\":")?; self.writer
.write_str("<img data-cast=\"emoji\" title=\":")?;
escape_html(&mut self.writer, name)?; escape_html(&mut self.writer, name)?;
self.writer.write_str(":\" src=\"")?; self.writer.write_str(":\" src=\"")?;
escape_html(&mut self.writer, &self.config.site)?; escape_html(&mut self.writer, &self.config.site)?;

View file

@ -45,9 +45,9 @@ pub fn branch_to_html(
} }
let component = if let Content::Link(_) = branch.attributes.content { let component = if let Content::Link(_) = branch.attributes.content {
"th-b-linked" "b-linked"
} else { } else {
"th-b" "b"
}; };
let linked_branch = if let Content::Link(link) = &branch.attributes.content { let linked_branch = if let Content::Link(link) = &branch.attributes.content {
@ -64,7 +64,7 @@ pub fn branch_to_html(
write!( write!(
s, s,
"<li is=\"{component}\" class=\"{class}\" id=\"{}\"{linked_branch}{do_not_persist}>", "<li data-cast=\"{component}\" class=\"{class}\" id=\"{}\"{linked_branch}{do_not_persist}>",
EscapeAttribute(&branch.html_id) EscapeAttribute(&branch.html_id)
) )
.unwrap(); .unwrap();

View file

@ -107,7 +107,7 @@ pub enum Content {
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
pub struct Classes { pub struct Classes {
/// Classes to append to the branch itself (<li is="th-b">). /// Classes to append to the branch itself (<li data-cast="b">).
#[serde(default)] #[serde(default)]
pub branch: String, pub branch: String,

View file

@ -8,6 +8,7 @@
"tairu/*": [ "tairu/*": [
"./components/tairu/*" "./components/tairu/*"
] ]
} },
"target": "ES2020",
}, },
} }

View file

@ -447,7 +447,7 @@ h1.page-title {
} }
/* Style the `new` link on the homepage */ /* Style the `new` link on the homepage */
a[is="th-new"] { a[data-cast~="new"] {
flex-shrink: 0; flex-shrink: 0;
color: var(--text-color); color: var(--text-color);
opacity: 50%; opacity: 50%;
@ -509,7 +509,7 @@ footer {
/* Style emojis to be readable */ /* Style emojis to be readable */
img[is="th-emoji"] { img[data-cast~="emoji"] {
max-width: 1.5em; max-width: 1.5em;
max-height: 1.5em; max-height: 1.5em;
vertical-align: bottom; vertical-align: bottom;
@ -777,7 +777,7 @@ th-literate-program[data-mode="output"] {
/* Style settings sections */ /* Style settings sections */
section[is="th-settings"] { section[data-cast~="settings"] {
/* Don't display settings when JavaScript is disabled. /* Don't display settings when JavaScript is disabled.
JS overrides this value on the element itself. */ JS overrides this value on the element itself. */
display: none; display: none;

View file

@ -64,7 +64,7 @@ section {
} }
} }
section[is="th-settings"] { section.settings {
& h3 { & h3 {
display: inline; display: inline;
} }

View file

@ -1,10 +1,13 @@
// Emoji zoom-in functionality. // Emoji zoom-in functionality.
import { addSpell } from "treehouse/spells.js";
class EmojiTooltip extends HTMLElement { class EmojiTooltip extends HTMLElement {
constructor(emoji, { onClosed }) { constructor(emoji, element, { onClosed }) {
super(); super();
this.emoji = emoji; this.emoji = emoji;
this.emojiElement = element;
this.onClosed = onClosed; this.onClosed = onClosed;
} }
@ -12,12 +15,12 @@ class EmojiTooltip extends HTMLElement {
this.role = "tooltip"; this.role = "tooltip";
this.image = new Image(); this.image = new Image();
this.image.src = this.emoji.src; this.image.src = this.emojiElement.src;
this.description = document.createElement("p"); this.description = document.createElement("p");
this.description.textContent = `${this.emoji.emojiName}`; this.description.textContent = `${this.emoji.emojiName}`;
let emojiBoundingBox = this.emoji.getBoundingClientRect(); let emojiBoundingBox = this.emojiElement.getBoundingClientRect();
this.style.left = `${emojiBoundingBox.left + emojiBoundingBox.width / 2}px`; this.style.left = `${emojiBoundingBox.left + emojiBoundingBox.width / 2}px`;
this.style.top = `calc(${emojiBoundingBox.top}px + 1.5em)`; this.style.top = `calc(${emojiBoundingBox.top}px + 1.5em)`;
@ -68,8 +71,8 @@ class EmojiTooltips extends HTMLElement {
this.abortController.abort(); this.abortController.abort();
} }
openTooltip(emoji) { openTooltip(emoji, element) {
let tooltip = new EmojiTooltip(emoji, { let tooltip = new EmojiTooltip(emoji, element, {
onClosed: () => { onClosed: () => {
this.removeChild(tooltip); this.removeChild(tooltip);
this.tooltips.delete(tooltip); this.tooltips.delete(tooltip);
@ -95,21 +98,21 @@ class EmojiTooltips extends HTMLElement {
customElements.define("th-emoji-tooltips", EmojiTooltips); customElements.define("th-emoji-tooltips", EmojiTooltips);
class Emoji extends HTMLImageElement { class Emoji {
connectedCallback() { constructor(element) {
this.emojiName = this.title; this.emojiName = element.title;
// title makes the browser add a tooltip. We replace browser tooltips with our own, // title makes the browser add a tooltip. We replace browser tooltips with our own,
// so remove the title. // so remove the title.
this.title = ""; element.title = "";
this.addEventListener("mouseenter", () => this.openTooltip()); element.addEventListener("mouseenter", () => this.openTooltip(element));
this.addEventListener("mouseleave", () => this.closeTooltip()); element.addEventListener("mouseleave", () => this.closeTooltip());
this.addEventListener("scroll", () => this.closeTooltip()); element.addEventListener("scroll", () => this.closeTooltip());
} }
openTooltip() { openTooltip(element) {
this.tooltip = emojiTooltips.openTooltip(this); this.tooltip = emojiTooltips.openTooltip(this, element);
} }
closeTooltip() { closeTooltip() {
@ -118,4 +121,4 @@ class Emoji extends HTMLImageElement {
} }
} }
customElements.define("th-emoji", Emoji, { extends: "img" }); addSpell("emoji", Emoji);

View file

@ -1,6 +1,7 @@
// news.js because new.js makes the TypeScript language server flip out. // news.js because new.js makes the TypeScript language server flip out.
// Likely because `new` is a keyword, but also, what the fuck. // Likely because `new` is a keyword, but also, what the fuck.
import { addSpell, spell } from "treehouse/spells.js";
import { getSettingValue } from "treehouse/settings.js"; import { getSettingValue } from "treehouse/settings.js";
import { Branch } from "treehouse/tree.js"; import { Branch } from "treehouse/tree.js";
@ -15,15 +16,17 @@ function saveSeenStates() {
} }
function markAsRead(branch) { function markAsRead(branch) {
if (!seenStates.has(branch.namedID) && seenCount > 0) { let branchData = spell(branch, Branch);
if (!seenStates.has(branchData.namedID) && seenCount > 0) {
let badge = document.createElement("span"); let badge = document.createElement("span");
badge.classList.add("badge", "red", "before-content"); badge.classList.add("badge", "red", "before-content");
badge.textContent = "new"; badge.textContent = "new";
branch.branchContent.firstChild.insertBefore(badge, branch.branchContent.firstChild.firstChild); branchData.branchContent.firstChild.insertBefore(badge, branchData.branchContent.firstChild.firstChild);
} }
seenStates.add(branch.namedID); seenStates.add(branchData.namedID);
} }
export function initNewsPage() { export function initNewsPage() {
@ -43,8 +46,8 @@ export function markAllAsUnread() {
localStorage.removeItem(seenStatesKey); localStorage.removeItem(seenStatesKey);
} }
class New extends HTMLAnchorElement { addSpell("new", class New {
connectedCallback() { constructor(element) {
// Do not show the badge to people who have never seen any news. // Do not show the badge to people who have never seen any news.
// It's just annoying in that case. // It's just annoying in that case.
// In case you do not wish to see the badge anymore, go to the news page and uncheck the // In case you do not wish to see the badge anymore, go to the news page and uncheck the
@ -54,17 +57,15 @@ class New extends HTMLAnchorElement {
if (userSawNews && userWantsToSeeNews && unseenCount > 0) { if (userSawNews && userWantsToSeeNews && unseenCount > 0) {
this.newText = document.createElement("span"); this.newText = document.createElement("span");
this.newText.classList.add("new-text"); this.newText.classList.add("new-text");
this.newText.textContent = this.textContent; this.newText.textContent = element.textContent;
this.textContent = ""; element.textContent = "";
this.appendChild(this.newText); element.appendChild(this.newText);
this.badge = document.createElement("span"); this.badge = document.createElement("span");
this.badge.classList.add("badge", "red"); this.badge.classList.add("badge", "red");
this.badge.textContent = unseenCount.toString(); this.badge.textContent = unseenCount.toString();
this.appendChild(this.badge); element.appendChild(this.badge);
this.classList.add("has-news"); element.classList.add("has-news");
} }
} }
} });
customElements.define("th-new", New, { extends: "a" });

View file

@ -1,3 +1,5 @@
import { addSpell } from "treehouse/spells.js";
const settingsKey = "treehouse.settings"; const settingsKey = "treehouse.settings";
const settings = JSON.parse(localStorage.getItem(settingsKey)) || {}; const settings = JSON.parse(localStorage.getItem(settingsKey)) || {};
@ -13,23 +15,15 @@ export function getSettingValue(setting) {
return settings[setting] ?? defaultSettingValues[setting]; return settings[setting] ?? defaultSettingValues[setting];
} }
class SettingCheckbox extends HTMLInputElement { class SettingCheckbox {
connectedCallback() { constructor(element) {
this.checked = getSettingValue(this.id); element.checked = getSettingValue(element.id);
this.addEventListener("change", () => { element.addEventListener("change", () => {
settings[this.id] = this.checked; settings[element.id] = element.checked;
saveSettings(); saveSettings();
}); });
} }
} }
customElements.define("th-setting-checkbox", SettingCheckbox, { extends: "input" }); addSpell("setting-checkbox", SettingCheckbox);
class Settings extends HTMLElement {
connectedCallback() {
this.style.display = "block";
}
}
customElements.define("th-settings", Settings, { extends: "section" });

85
static/js/spells.js Normal file
View file

@ -0,0 +1,85 @@
// Spells are a simplistic, composition-based replacement for the is="" attribute, which is not
// supported on Webkit due to Apple's engineering being a bunch of obstinate idiots who explicitly
// choose not to follow web standards.
// Limitations:
// - The data-cast attribute cannot be added dynamically.
// - There is no disconnectedCallback. Only a constructor which initializes a spell.
let spells = new Map();
let elementsWithUnknownSpells = new Map();
const sSpells = Symbol("spells");
function castSpellOnElement(spellName, element) {
element[sSpells] ??= new Map();
if (!elementsWithUnknownSpells.has(spellName)) {
elementsWithUnknownSpells.set(spellName, new Set());
}
let Spell = spells.get(spellName);
if (Spell != null && !element[sSpells].has(Spell)) {
element[sSpells].set(Spell, new Spell(element));
elementsWithUnknownSpells.get(spellName).delete(element);
} else {
elementsWithUnknownSpells.get(spellName).add(element);
}
}
function applySpells(elements) {
for (let element of elements) {
if (element instanceof Element) {
let spellListString = element.getAttribute("data-cast");
if (spellListString != null) {
let spellList = spellListString.split(' ');
for (let spellName of spellList) {
castSpellOnElement(spellName, element);
}
}
}
}
}
export function addSpell(name, spell) {
spells.set(name, spell);
let elementsWithThisSpell = elementsWithUnknownSpells.get(name);
if (elementsWithThisSpell != null) {
for (let element of elementsWithThisSpell) {
castSpellOnElement(name, element);
}
}
}
// Returns a spell's data. Gotchas: the spell needs to already be on the element.
// Therefore, if this is used from within a spell, the requested spell must have already been
// applied by this point.
// Someday I may change this to an async function that resumes whenever the spell is available to
// iron over this limitation. But today is not that day.
export function spell(element, spell) {
return element[sSpells].get(spell);
}
// Apply spells to elements which have them and have been loaded so far.
let loadedSoFar = document.querySelectorAll("[data-cast]");
applySpells(loadedSoFar);
// For all other elements, add a mutation observer that will handle them.
let mutationObserver = new MutationObserver(records => {
for (let record of records) {
let mutatedNodes = new Set();
// NOTE: Added nodes may contain children which also need to be processed.
// Collect those that have [data-cast] on them and apply spells to them.
for (let addedNode of record.addedNodes) {
addedNode.querySelectorAll("[data-cast]").forEach(element => mutatedNodes.add(element));
}
applySpells(mutatedNodes);
}
});
mutationObserver.observe(document, { subtree: true, childList: true });
// ------------ Common spells ------------
// js makes things visible only when JavaScript is enabled.
addSpell("js", function (element) {
element.style.display = "block";
});

View file

@ -1,21 +0,0 @@
// Detect if we can have crucial functionality (ie. custom elements call constructors).
// This doesn't seem to happen in Epiphany, and also other Webkit-based browsers.
let works = false;
class WebkitMoment extends HTMLLIElement {
constructor() {
super();
works = true;
}
}
customElements.define("th-webkit-moment", WebkitMoment, { extends: "li" });
let willItWorkOrWillItNot = document.createElement("div");
willItWorkOrWillItNot.innerHTML = `<li is="th-webkit-moment"></li>`;
// If my takeoff fails
// tell my mother I'm sorry
let box = document.getElementById("webkit-makes-me-go-insane");
if (!works) {
box.style = "display: block";
}

View file

@ -1,5 +1,6 @@
// This is definitely not a three.js ripoff. // This is definitely not a three.js ripoff.
import { addSpell } from "treehouse/spells.js";
import { navigationMap } from "/navmap.js"; import { navigationMap } from "/navmap.js";
import * as ulid from "treehouse/ulid.js"; import * as ulid from "treehouse/ulid.js";
@ -17,18 +18,20 @@ function branchIsOpen(branchID) {
return branchState[branchID]; return branchState[branchID];
} }
export class Branch extends HTMLLIElement { export class Branch {
static branchesByNamedID = new Map(); static branchesByNamedID = new Map();
static onAdded = []; static onAdded = [];
connectedCallback() { constructor(element) {
this.isLeaf = this.classList.contains("leaf"); this.element = element;
this.details = this.childNodes[0]; this.isLeaf = element.classList.contains("leaf");
this.details = element.childNodes[0];
this.innerUL = this.details.childNodes[1]; this.innerUL = this.details.childNodes[1];
if (this.isLeaf) { if (this.isLeaf) {
this.contentContainer = this.childNodes[0]; this.contentContainer = element.childNodes[0];
} else { } else {
this.contentContainer = this.details.childNodes[0]; this.contentContainer = this.details.childNodes[0];
} }
@ -36,19 +39,19 @@ export class Branch extends HTMLLIElement {
this.branchContent = this.contentContainer.childNodes[1]; this.branchContent = this.contentContainer.childNodes[1];
this.buttonBar = this.contentContainer.childNodes[2]; this.buttonBar = this.contentContainer.childNodes[2];
let doPersist = !this.hasAttribute("data-th-do-not-persist"); let doPersist = !element.hasAttribute("data-th-do-not-persist");
let isOpen = branchIsOpen(this.id); let isOpen = branchIsOpen(element.id);
if (doPersist && isOpen !== undefined) { if (doPersist && isOpen !== undefined) {
this.details.open = isOpen; this.details.open = isOpen;
} }
if (!this.isLeaf) { if (!this.isLeaf) {
this.details.addEventListener("toggle", _ => { this.details.addEventListener("toggle", _ => {
saveBranchIsOpen(this.id, this.details.open); saveBranchIsOpen(element.id, this.details.open);
}); });
} }
this.namedID = this.id.split(':')[1]; this.namedID = element.id.split(':')[1];
Branch.branchesByNamedID.set(this.namedID, this); Branch.branchesByNamedID.set(this.namedID, element);
if (ulid.isCanonicalUlid(this.namedID)) { if (ulid.isCanonicalUlid(this.namedID)) {
let timestamp = ulid.getTimestamp(this.namedID); let timestamp = ulid.getTimestamp(this.namedID);
@ -59,22 +62,22 @@ export class Branch extends HTMLLIElement {
} }
for (let callback of Branch.onAdded) { for (let callback of Branch.onAdded) {
callback(this); callback(element, this);
} }
} }
} }
customElements.define("th-b", Branch, { extends: "li" }); addSpell("b", Branch);
/* Linked branches */ /* Linked branches */
class LinkedBranch extends Branch { class LinkedBranch extends Branch {
static byLink = new Map(); static byLink = new Map();
connectedCallback() { constructor(element) {
super.connectedCallback(); super(element);
this.linkedTree = this.getAttribute("data-th-link"); this.linkedTree = element.getAttribute("data-th-link");
LinkedBranch.byLink.set(this.linkedTree, this); LinkedBranch.byLink.set(this.linkedTree, this);
this.loadingText = document.createElement("p"); this.loadingText = document.createElement("p");
@ -115,11 +118,10 @@ class LinkedBranch extends Branch {
let styles = main.getElementsByTagName("link"); let styles = main.getElementsByTagName("link");
let scripts = main.getElementsByTagName("script"); let scripts = main.getElementsByTagName("script");
this.loadingText.remove(); this.loadingText.remove();
this.innerUL.innerHTML = ul.innerHTML; this.innerUL.innerHTML = ul.innerHTML;
this.append(...styles); this.element.append(...styles);
for (let script of scripts) { for (let script of scripts) {
// No need to await for the import because we don't use the resulting module. // No need to await for the import because we don't use the resulting module.
// Just fire and forger 💀 // Just fire and forger 💀
@ -139,7 +141,7 @@ class LinkedBranch extends Branch {
} }
} }
customElements.define("th-b-linked", LinkedBranch, { extends: "li" }); addSpell("b-linked", LinkedBranch);
/* Fragment navigation */ /* Fragment navigation */

View file

@ -7,12 +7,3 @@ document.addEventListener("click", event => {
event.preventDefault(); event.preventDefault();
} }
}) })
// Certain words don't make sense if scripts are disabled.
class YesScript extends HTMLElement {
connectedCallback() {
this.classList.add("yes-indeed");
}
}
customElements.define("th-yesscript", YesScript);

View file

@ -11,13 +11,12 @@
{{!-- For /index, include a "new" link that goes to the curated news feed page. --}} {{!-- For /index, include a "new" link that goes to the curated news feed page. --}}
{{#if (eq page.tree_path "index")}} {{#if (eq page.tree_path "index")}}
<a href="{{ config.site }}/treehouse/new.html" is="th-new">new</a> <a href="{{ config.site }}/treehouse/new.html" data-cast="new">new</a>
{{/if}} {{/if}}
{{/ components/_nav.hbs }} {{/ components/_nav.hbs }}
{{> components/_noscript.hbs }} {{> components/_noscript.hbs }}
{{> components/_webkit.hbs }}
<section> <section>
<p>welcome!</p> <p>welcome!</p>
@ -33,7 +32,7 @@
</p> </p>
</section> </section>
<section is="th-settings"> <section class="settings" data-cast="js">
<details> <details>
<summary> <summary>
settings settings
@ -41,7 +40,7 @@
<section> <section>
<p>if you find the newsfeed annoying, you can customize some aspects of it.</p> <p>if you find the newsfeed annoying, you can customize some aspects of it.</p>
<p> <p>
<input type="checkbox" is="th-setting-checkbox" id="showNewPostIndicator"> <input type="checkbox" data-cast="setting-checkbox" id="showNewPostIndicator">
<label for="showNewPostIndicator">show the <span class="badge red">1</span> badge on the homepage <label for="showNewPostIndicator">show the <span class="badge red">1</span> badge on the homepage
for for
new posts you haven't read yet</label> new posts you haven't read yet</label>

View file

@ -11,13 +11,12 @@
{{!-- For /index, include a "new" link that goes to the curated news feed page. --}} {{!-- For /index, include a "new" link that goes to the curated news feed page. --}}
{{#if (eq page.tree_path "index")}} {{#if (eq page.tree_path "index")}}
<a href="{{ config.site }}/treehouse/new.html" is="th-new">new</a> <a href="{{ config.site }}/treehouse/new.html" data-cast="new">new</a>
{{/if}} {{/if}}
{{/ components/_nav.hbs }} {{/ components/_nav.hbs }}
{{> components/_noscript.hbs }} {{> components/_noscript.hbs }}
{{> components/_webkit.hbs }}
{{!-- {{!--
NOTE: ~ because components/_tree.hbss must not include any extra indentation, because it may NOTE: ~ because components/_tree.hbss must not include any extra indentation, because it may

View file

@ -20,12 +20,12 @@
const TREEHOUSE_NEWS_COUNT = {{ len feeds.news.branches }}; const TREEHOUSE_NEWS_COUNT = {{ len feeds.news.branches }};
</script> </script>
<script type="module" src="{{ config.site }}/navmap.js"></script> <script type="module" src="{{ config.site }}/navmap.js"></script>
<script type="module" src="{{ config.site }}/static/js/spells.js"></script>
<script type="module" src="{{ config.site }}/static/js/ulid.js"></script> <script type="module" src="{{ config.site }}/static/js/ulid.js"></script>
<script type="module" src="{{ config.site }}/static/js/usability.js"></script> <script type="module" src="{{ config.site }}/static/js/usability.js"></script>
<script type="module" src="{{ config.site }}/static/js/settings.js"></script> <script type="module" src="{{ config.site }}/static/js/settings.js"></script>
<script type="module" src="{{ config.site }}/static/js/tree.js"></script> <script type="module" src="{{ config.site }}/static/js/tree.js"></script>
<script type="module" src="{{ config.site }}/static/js/emoji.js"></script> <script type="module" src="{{ config.site }}/static/js/emoji.js"></script>
<script type="module" src="{{ config.site }}/static/js/thanks-webkit.js"></script>
<script type="module" src="{{ config.site }}/static/js/news.js"></script> <script type="module" src="{{ config.site }}/static/js/news.js"></script>
<meta property="og:site_name" content="{{ config.user.title }}"> <meta property="og:site_name" content="{{ config.user.title }}">

View file

@ -1,7 +0,0 @@
<div id="webkit-makes-me-go-insane" class="noscript" role="note">
<p>hey! looks like you're using a weird or otherwise quirky web browser. this basically means, the website will
not work for you correctly. I might fix it in the future but I have very limited time to work on this
website and so don't have an estimate on when that might happen.</p>
<p>in the meantime I suggest switching to <a href="https://firefox.com">something more modern.</a></p>
<p>sorry for the inconvenience!</p>
</div>