magic! now it works on web kit
This commit is contained in:
parent
33416e8963
commit
3a4eb87ca0
17 changed files with 164 additions and 116 deletions
|
@ -1,10 +1,13 @@
|
|||
// Emoji zoom-in functionality.
|
||||
|
||||
import { addSpell } from "treehouse/spells.js";
|
||||
|
||||
class EmojiTooltip extends HTMLElement {
|
||||
constructor(emoji, { onClosed }) {
|
||||
constructor(emoji, element, { onClosed }) {
|
||||
super();
|
||||
|
||||
this.emoji = emoji;
|
||||
this.emojiElement = element;
|
||||
this.onClosed = onClosed;
|
||||
}
|
||||
|
||||
|
@ -12,12 +15,12 @@ class EmojiTooltip extends HTMLElement {
|
|||
this.role = "tooltip";
|
||||
|
||||
this.image = new Image();
|
||||
this.image.src = this.emoji.src;
|
||||
this.image.src = this.emojiElement.src;
|
||||
|
||||
this.description = document.createElement("p");
|
||||
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.top = `calc(${emojiBoundingBox.top}px + 1.5em)`;
|
||||
|
||||
|
@ -68,8 +71,8 @@ class EmojiTooltips extends HTMLElement {
|
|||
this.abortController.abort();
|
||||
}
|
||||
|
||||
openTooltip(emoji) {
|
||||
let tooltip = new EmojiTooltip(emoji, {
|
||||
openTooltip(emoji, element) {
|
||||
let tooltip = new EmojiTooltip(emoji, element, {
|
||||
onClosed: () => {
|
||||
this.removeChild(tooltip);
|
||||
this.tooltips.delete(tooltip);
|
||||
|
@ -95,21 +98,21 @@ class EmojiTooltips extends HTMLElement {
|
|||
|
||||
customElements.define("th-emoji-tooltips", EmojiTooltips);
|
||||
|
||||
class Emoji extends HTMLImageElement {
|
||||
connectedCallback() {
|
||||
this.emojiName = this.title;
|
||||
class Emoji {
|
||||
constructor(element) {
|
||||
this.emojiName = element.title;
|
||||
|
||||
// title makes the browser add a tooltip. We replace browser tooltips with our own,
|
||||
// so remove the title.
|
||||
this.title = "";
|
||||
element.title = "";
|
||||
|
||||
this.addEventListener("mouseenter", () => this.openTooltip());
|
||||
this.addEventListener("mouseleave", () => this.closeTooltip());
|
||||
this.addEventListener("scroll", () => this.closeTooltip());
|
||||
element.addEventListener("mouseenter", () => this.openTooltip(element));
|
||||
element.addEventListener("mouseleave", () => this.closeTooltip());
|
||||
element.addEventListener("scroll", () => this.closeTooltip());
|
||||
}
|
||||
|
||||
openTooltip() {
|
||||
this.tooltip = emojiTooltips.openTooltip(this);
|
||||
openTooltip(element) {
|
||||
this.tooltip = emojiTooltips.openTooltip(this, element);
|
||||
}
|
||||
|
||||
closeTooltip() {
|
||||
|
@ -118,4 +121,4 @@ class Emoji extends HTMLImageElement {
|
|||
}
|
||||
}
|
||||
|
||||
customElements.define("th-emoji", Emoji, { extends: "img" });
|
||||
addSpell("emoji", Emoji);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// news.js because new.js makes the TypeScript language server flip out.
|
||||
// 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 { Branch } from "treehouse/tree.js";
|
||||
|
||||
|
@ -15,15 +16,17 @@ function saveSeenStates() {
|
|||
}
|
||||
|
||||
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");
|
||||
badge.classList.add("badge", "red", "before-content");
|
||||
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() {
|
||||
|
@ -43,8 +46,8 @@ export function markAllAsUnread() {
|
|||
localStorage.removeItem(seenStatesKey);
|
||||
}
|
||||
|
||||
class New extends HTMLAnchorElement {
|
||||
connectedCallback() {
|
||||
addSpell("new", class New {
|
||||
constructor(element) {
|
||||
// Do not show the badge to people who have never seen any news.
|
||||
// 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
|
||||
|
@ -54,17 +57,15 @@ class New extends HTMLAnchorElement {
|
|||
if (userSawNews && userWantsToSeeNews && unseenCount > 0) {
|
||||
this.newText = document.createElement("span");
|
||||
this.newText.classList.add("new-text");
|
||||
this.newText.textContent = this.textContent;
|
||||
this.textContent = "";
|
||||
this.appendChild(this.newText);
|
||||
this.newText.textContent = element.textContent;
|
||||
element.textContent = "";
|
||||
element.appendChild(this.newText);
|
||||
|
||||
this.badge = document.createElement("span");
|
||||
this.badge.classList.add("badge", "red");
|
||||
this.badge.textContent = unseenCount.toString();
|
||||
this.appendChild(this.badge);
|
||||
this.classList.add("has-news");
|
||||
element.appendChild(this.badge);
|
||||
element.classList.add("has-news");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("th-new", New, { extends: "a" });
|
||||
});
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { addSpell } from "treehouse/spells.js";
|
||||
|
||||
const settingsKey = "treehouse.settings";
|
||||
const settings = JSON.parse(localStorage.getItem(settingsKey)) || {};
|
||||
|
||||
|
@ -13,23 +15,15 @@ export function getSettingValue(setting) {
|
|||
return settings[setting] ?? defaultSettingValues[setting];
|
||||
}
|
||||
|
||||
class SettingCheckbox extends HTMLInputElement {
|
||||
connectedCallback() {
|
||||
this.checked = getSettingValue(this.id);
|
||||
class SettingCheckbox {
|
||||
constructor(element) {
|
||||
element.checked = getSettingValue(element.id);
|
||||
|
||||
this.addEventListener("change", () => {
|
||||
settings[this.id] = this.checked;
|
||||
element.addEventListener("change", () => {
|
||||
settings[element.id] = element.checked;
|
||||
saveSettings();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("th-setting-checkbox", SettingCheckbox, { extends: "input" });
|
||||
|
||||
class Settings extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("th-settings", Settings, { extends: "section" });
|
||||
addSpell("setting-checkbox", SettingCheckbox);
|
||||
|
|
85
static/js/spells.js
Normal file
85
static/js/spells.js
Normal 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";
|
||||
});
|
|
@ -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";
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
// This is definitely not a three.js ripoff.
|
||||
|
||||
import { addSpell } from "treehouse/spells.js";
|
||||
import { navigationMap } from "/navmap.js";
|
||||
import * as ulid from "treehouse/ulid.js";
|
||||
|
||||
|
@ -17,18 +18,20 @@ function branchIsOpen(branchID) {
|
|||
return branchState[branchID];
|
||||
}
|
||||
|
||||
export class Branch extends HTMLLIElement {
|
||||
export class Branch {
|
||||
static branchesByNamedID = new Map();
|
||||
static onAdded = [];
|
||||
|
||||
connectedCallback() {
|
||||
this.isLeaf = this.classList.contains("leaf");
|
||||
constructor(element) {
|
||||
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];
|
||||
|
||||
if (this.isLeaf) {
|
||||
this.contentContainer = this.childNodes[0];
|
||||
this.contentContainer = element.childNodes[0];
|
||||
} else {
|
||||
this.contentContainer = this.details.childNodes[0];
|
||||
}
|
||||
|
@ -36,19 +39,19 @@ export class Branch extends HTMLLIElement {
|
|||
this.branchContent = this.contentContainer.childNodes[1];
|
||||
this.buttonBar = this.contentContainer.childNodes[2];
|
||||
|
||||
let doPersist = !this.hasAttribute("data-th-do-not-persist");
|
||||
let isOpen = branchIsOpen(this.id);
|
||||
let doPersist = !element.hasAttribute("data-th-do-not-persist");
|
||||
let isOpen = branchIsOpen(element.id);
|
||||
if (doPersist && isOpen !== undefined) {
|
||||
this.details.open = isOpen;
|
||||
}
|
||||
if (!this.isLeaf) {
|
||||
this.details.addEventListener("toggle", _ => {
|
||||
saveBranchIsOpen(this.id, this.details.open);
|
||||
saveBranchIsOpen(element.id, this.details.open);
|
||||
});
|
||||
}
|
||||
|
||||
this.namedID = this.id.split(':')[1];
|
||||
Branch.branchesByNamedID.set(this.namedID, this);
|
||||
this.namedID = element.id.split(':')[1];
|
||||
Branch.branchesByNamedID.set(this.namedID, element);
|
||||
|
||||
if (ulid.isCanonicalUlid(this.namedID)) {
|
||||
let timestamp = ulid.getTimestamp(this.namedID);
|
||||
|
@ -59,22 +62,22 @@ export class Branch extends HTMLLIElement {
|
|||
}
|
||||
|
||||
for (let callback of Branch.onAdded) {
|
||||
callback(this);
|
||||
callback(element, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("th-b", Branch, { extends: "li" });
|
||||
addSpell("b", Branch);
|
||||
|
||||
/* Linked branches */
|
||||
|
||||
class LinkedBranch extends Branch {
|
||||
static byLink = new Map();
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
constructor(element) {
|
||||
super(element);
|
||||
|
||||
this.linkedTree = this.getAttribute("data-th-link");
|
||||
this.linkedTree = element.getAttribute("data-th-link");
|
||||
LinkedBranch.byLink.set(this.linkedTree, this);
|
||||
|
||||
this.loadingText = document.createElement("p");
|
||||
|
@ -115,11 +118,10 @@ class LinkedBranch extends Branch {
|
|||
let styles = main.getElementsByTagName("link");
|
||||
let scripts = main.getElementsByTagName("script");
|
||||
|
||||
|
||||
this.loadingText.remove();
|
||||
this.innerUL.innerHTML = ul.innerHTML;
|
||||
|
||||
this.append(...styles);
|
||||
this.element.append(...styles);
|
||||
for (let script of scripts) {
|
||||
// No need to await for the import because we don't use the resulting module.
|
||||
// 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 */
|
||||
|
||||
|
|
|
@ -7,12 +7,3 @@ document.addEventListener("click", event => {
|
|||
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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue