make the 🔗 button copy branch links to clipboard
to accomplish this, I generalised emoji tooltips to a shared Tooltip class. in the long run I'd like to transform all existing `title=""` tooltips into these for stylistic consistency with the rest of the website, but this is good enough for now. I also ended up cleaning up some old code from before the /b rework.
This commit is contained in:
parent
c537eb844f
commit
74baa61122
8 changed files with 208 additions and 138 deletions
|
@ -57,13 +57,13 @@ pub fn branch_to_html(
|
||||||
|
|
||||||
let linked_branch = if let Content::ResolvedLink(file_id) = &branch.attributes.content {
|
let linked_branch = if let Content::ResolvedLink(file_id) = &branch.attributes.content {
|
||||||
let path = treehouse.tree_path(*file_id).expect(".tree file expected");
|
let path = treehouse.tree_path(*file_id).expect(".tree file expected");
|
||||||
format!(" data-th-link=\"{}\"", EscapeHtml(path.as_str()))
|
format!(" th-link=\"{}\"", EscapeHtml(path.as_str()))
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let do_not_persist = if branch.attributes.do_not_persist {
|
let do_not_persist = if branch.attributes.do_not_persist {
|
||||||
" data-th-do-not-persist=\"\""
|
" th-do-not-persist"
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
|
@ -165,7 +165,7 @@ pub fn branch_to_html(
|
||||||
} else {
|
} else {
|
||||||
write!(
|
write!(
|
||||||
s,
|
s,
|
||||||
"<a class=\"icon icon-permalink\" href=\"/b?{}\" title=\"permalink\"></a>",
|
"<a th-p class=\"icon icon-permalink\" href=\"/b?{}\" title=\"permalink\"></a>",
|
||||||
EscapeAttribute(&branch.named_id)
|
EscapeAttribute(&branch.named_id)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -611,21 +611,26 @@ img[data-cast~="emoji"] {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* And also style emoji tooltips. */
|
/* Tooltips */
|
||||||
|
|
||||||
th-emoji-tooltip {
|
th-overlays {
|
||||||
display: flex;
|
position: absolute;
|
||||||
flex-direction: column;
|
left: 0;
|
||||||
align-items: center;
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
th-tooltip {
|
||||||
|
display: block;
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
transform: translateX(-50%) translateY(-10%) scale(0.8);
|
|
||||||
width: max-content;
|
width: max-content;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
background-color: var(--background-color-tooltip);
|
background-color: var(--background-color-tooltip);
|
||||||
padding: 0.8rem;
|
padding: 0.4rem 0.8rem;
|
||||||
margin-top: 0.8rem;
|
|
||||||
border-radius: 0.6rem;
|
border-radius: 0.6rem;
|
||||||
|
|
||||||
transition:
|
transition:
|
||||||
|
@ -635,28 +640,52 @@ th-emoji-tooltip {
|
||||||
opacity: 0%;
|
opacity: 0%;
|
||||||
filter: blur(0.3rem);
|
filter: blur(0.3rem);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
|
||||||
|
|
||||||
th-emoji-tooltip.transitioned-in {
|
font-size: 0.9em;
|
||||||
opacity: 100%;
|
|
||||||
filter: blur(0);
|
&[th-side="bottom"] {
|
||||||
|
transform: translateX(-50%) translateY(-10%) scale(0.8);
|
||||||
|
|
||||||
|
&.transitioned-in {
|
||||||
transform: translateX(-50%) scale(1);
|
transform: translateX(-50%) scale(1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
th-emoji-tooltip img {
|
&[th-side="left"] {
|
||||||
|
transform: translateX(-90%) translateY(-50%) scale(0.8);
|
||||||
|
|
||||||
|
&.transitioned-in {
|
||||||
|
transform: translateX(-100%) translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th-tooltip.transitioned-in {
|
||||||
|
opacity: 100%;
|
||||||
|
filter: blur(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
th-tooltip.tooltip-emoji {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 0.8rem;
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
|
||||||
|
& > img {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 7.2rem;
|
max-width: 7.2rem;
|
||||||
max-height: 7.2rem;
|
max-height: 7.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
th-emoji-tooltip p {
|
& > p {
|
||||||
--recursive-wght: 550;
|
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 0.9em;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-top: 0.6rem;
|
padding-top: 0.6rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.th-emoji-unknown {
|
.th-emoji-unknown {
|
||||||
text-decoration: 0.1rem underline var(--error-color);
|
text-decoration: 0.1rem underline var(--error-color);
|
||||||
|
|
|
@ -239,7 +239,7 @@ th-bc {
|
||||||
.tree details:not([open])>summary>th-bc>:last-child,
|
.tree details:not([open])>summary>th-bc>:last-child,
|
||||||
/* NOTE: Linked branches have a slightly different structure (extra <noscript> tag) and therefore
|
/* NOTE: Linked branches have a slightly different structure (extra <noscript> tag) and therefore
|
||||||
:last-child does not work. */
|
:last-child does not work. */
|
||||||
.tree li[data-th-link]>details:not([open])>summary>th-bc>:nth-last-child(2) {
|
.tree li[th-link]>details:not([open])>summary>th-bc>:nth-last-child(2) {
|
||||||
&::after {
|
&::after {
|
||||||
content: "\00A0";
|
content: "\00A0";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -1,103 +1,21 @@
|
||||||
// Emoji zoom-in functionality.
|
// Emoji zoom-in functionality.
|
||||||
|
|
||||||
import { addSpell } from "treehouse/spells.js";
|
import { addSpell } from "treehouse/spells.js";
|
||||||
|
import { attachTooltip, Tooltip } from "treehouse/overlay.js";
|
||||||
|
|
||||||
class EmojiTooltip extends HTMLElement {
|
function createEmojiTooltip(emoji, element) {
|
||||||
constructor(emoji, element, { onClosed }) {
|
let tooltip = new Tooltip(element, "bottom");
|
||||||
super();
|
tooltip.classList.add("tooltip-emoji");
|
||||||
|
|
||||||
this.emoji = emoji;
|
let img = tooltip.appendChild(new Image());
|
||||||
this.emojiElement = element;
|
img.src = element.src;
|
||||||
this.onClosed = onClosed;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
let description = tooltip.appendChild(document.createElement("p"));
|
||||||
this.role = "tooltip";
|
description.textContent = emoji.emojiName;
|
||||||
|
|
||||||
this.image = new Image();
|
|
||||||
this.image.src = this.emojiElement.src;
|
|
||||||
|
|
||||||
this.description = document.createElement("p");
|
|
||||||
this.description.textContent = `${this.emoji.emojiName}`;
|
|
||||||
|
|
||||||
let emojiBoundingBox = this.emojiElement.getBoundingClientRect();
|
|
||||||
this.style.left = `${emojiBoundingBox.left + emojiBoundingBox.width / 2}px`;
|
|
||||||
this.style.top = `calc(${emojiBoundingBox.top}px + 1.5em)`;
|
|
||||||
|
|
||||||
this.fullyOpaque = false;
|
|
||||||
this.addEventListener("transitionend", event => {
|
|
||||||
if (event.propertyName == "opacity") {
|
|
||||||
this.fullyOpaque = !this.fullyOpaque;
|
|
||||||
if (!this.fullyOpaque) {
|
|
||||||
this.onClosed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Timeout is zero because we just want to execute this later, to be definitely sure
|
|
||||||
// the transition plays out.
|
|
||||||
setTimeout(() => this.classList.add("transitioned-in"), 0);
|
|
||||||
|
|
||||||
this.appendChild(this.image);
|
|
||||||
this.appendChild(this.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.classList.remove("transitioned-in");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("th-emoji-tooltip", EmojiTooltip);
|
|
||||||
|
|
||||||
let emojiTooltips = null;
|
|
||||||
|
|
||||||
class EmojiTooltips extends HTMLElement {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.tooltips = new Set();
|
|
||||||
this.abortController = new AbortController();
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
emojiTooltips = this;
|
|
||||||
|
|
||||||
addEventListener(
|
|
||||||
"wheel",
|
|
||||||
event => emojiTooltips.closeTooltips(event),
|
|
||||||
{ signal: this.abortController.signal },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
this.abortController.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
openTooltip(emoji, element) {
|
|
||||||
let tooltip = new EmojiTooltip(emoji, element, {
|
|
||||||
onClosed: () => {
|
|
||||||
this.removeChild(tooltip);
|
|
||||||
this.tooltips.delete(tooltip);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.appendChild(tooltip);
|
|
||||||
this.tooltips.add(tooltip);
|
|
||||||
|
|
||||||
return tooltip;
|
return tooltip;
|
||||||
}
|
}
|
||||||
|
|
||||||
closeTooltip(tooltip) {
|
|
||||||
tooltip.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeTooltips() {
|
|
||||||
for (let tooltip of this.tooltips) {
|
|
||||||
tooltip.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("th-emoji-tooltips", EmojiTooltips);
|
|
||||||
|
|
||||||
class Emoji {
|
class Emoji {
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
this.emojiName = element.title;
|
this.emojiName = element.title;
|
||||||
|
@ -106,18 +24,7 @@ class Emoji {
|
||||||
// so remove the title.
|
// so remove the title.
|
||||||
element.title = "";
|
element.title = "";
|
||||||
|
|
||||||
element.addEventListener("mouseenter", () => this.openTooltip(element));
|
attachTooltip(element, () => createEmojiTooltip(this, element)).showOnHover();
|
||||||
element.addEventListener("mouseleave", () => this.closeTooltip());
|
|
||||||
element.addEventListener("scroll", () => this.closeTooltip());
|
|
||||||
}
|
|
||||||
|
|
||||||
openTooltip(element) {
|
|
||||||
this.tooltip = emojiTooltips.openTooltip(this, element);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeTooltip() {
|
|
||||||
emojiTooltips.closeTooltip(this.tooltip);
|
|
||||||
this.tooltip = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
116
static/js/overlay.js
Normal file
116
static/js/overlay.js
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
export class Overlay extends HTMLElement {}
|
||||||
|
|
||||||
|
/** @type Overlays */
|
||||||
|
export let overlays = null;
|
||||||
|
|
||||||
|
export class Overlays extends HTMLElement {
|
||||||
|
overlays = new Set();
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
overlays = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
overlays = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
open(overlay) {
|
||||||
|
this.appendChild(overlay);
|
||||||
|
this.overlays.add(overlay);
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
close(overlay) {
|
||||||
|
this.removeChild(overlay);
|
||||||
|
this.overlays.delete(overlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("th-overlays", Overlays);
|
||||||
|
|
||||||
|
export class Tooltip extends Overlay {
|
||||||
|
constructor(element, side) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.element = element;
|
||||||
|
this.side = side;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.role = "tooltip";
|
||||||
|
this.setAttribute("th-side", this.side);
|
||||||
|
|
||||||
|
let bb = this.element.getBoundingClientRect();
|
||||||
|
switch (this.side) {
|
||||||
|
// NOTE: The elements are positioned directly at (width / 2) or (height / 2), because
|
||||||
|
// they are transformed to the centre over on the CSS side.
|
||||||
|
|
||||||
|
case "bottom":
|
||||||
|
this.style.left = `${bb.left + bb.width / 2}px`;
|
||||||
|
this.style.top = `${bb.bottom}px`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "left":
|
||||||
|
this.style.left = `${bb.left}px`;
|
||||||
|
this.style.top = `${bb.top + bb.height / 2}px`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.error(`th-tooltip: unknown attachment side ${this.side}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addEventListener("transitionend", (event) => {
|
||||||
|
if (event.propertyName == "opacity") {
|
||||||
|
let style = getComputedStyle(this);
|
||||||
|
if (style.opacity < 0.01) {
|
||||||
|
this.dispatchEvent(new Event(".close"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Timeout is zero because we just want to execute this later, to be definitely sure
|
||||||
|
// the transition plays out.
|
||||||
|
setTimeout(() => this.classList.add("transitioned-in"), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.classList.remove("transitioned-in");
|
||||||
|
|
||||||
|
// NOTE: In case there is no transition, we may need to trigger the close event immediately.
|
||||||
|
let style = getComputedStyle(this);
|
||||||
|
if (style.opacity < 0.01) {
|
||||||
|
this.dispatchEvent(new Event(".close"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("th-tooltip", Tooltip);
|
||||||
|
|
||||||
|
export function attachTooltip(element, makeTooltip) {
|
||||||
|
let show = () => {
|
||||||
|
let tooltip = overlays.open(makeTooltip(element));
|
||||||
|
let abortController = new AbortController();
|
||||||
|
|
||||||
|
tooltip.addEventListener(".close", () => {
|
||||||
|
overlays.close(tooltip);
|
||||||
|
abortController.abort();
|
||||||
|
console.log("closing tooltip");
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("wheel", () => tooltip.close(), {
|
||||||
|
signal: abortController.signal,
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
element.addEventListener("mouseleave", () => tooltip.close(), {
|
||||||
|
signal: abortController.signal,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
show,
|
||||||
|
showOnHover() {
|
||||||
|
element.addEventListener("mouseenter", show);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { addSpell } from "treehouse/spells.js";
|
import { addSpell } from "treehouse/spells.js";
|
||||||
import * as ulid from "treehouse/ulid.js";
|
import * as ulid from "treehouse/ulid.js";
|
||||||
|
import { attachTooltip, Tooltip } from "treehouse/overlay.js";
|
||||||
|
|
||||||
/* Branch persistence */
|
/* Branch persistence */
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ export class Branch {
|
||||||
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 = !element.hasAttribute("data-th-do-not-persist");
|
let doPersist = !element.hasAttribute("th-do-not-persist");
|
||||||
let isOpen = branchIsOpen(element.id);
|
let isOpen = branchIsOpen(element.id);
|
||||||
if (doPersist && isOpen !== undefined) {
|
if (doPersist && isOpen !== undefined) {
|
||||||
this.details.open = isOpen;
|
this.details.open = isOpen;
|
||||||
|
@ -52,6 +53,22 @@ export class Branch {
|
||||||
this.namedID = element.id.replace(/^b-/, "");
|
this.namedID = element.id.replace(/^b-/, "");
|
||||||
Branch.branchesByNamedID.set(this.namedID, element);
|
Branch.branchesByNamedID.set(this.namedID, element);
|
||||||
|
|
||||||
|
let permalinkButton = this.buttonBar.querySelector("a[th-p]");
|
||||||
|
if (permalinkButton != null) {
|
||||||
|
permalinkButton.title = "copy permalink";
|
||||||
|
permalinkButton.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault(); // do not navigate the link
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
new URL(permalinkButton.href, window.location).toString(),
|
||||||
|
);
|
||||||
|
attachTooltip(permalinkButton, () => {
|
||||||
|
let tooltip = new Tooltip(permalinkButton, "left");
|
||||||
|
tooltip.append("permalink copied to clipboard!");
|
||||||
|
return tooltip;
|
||||||
|
}).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (ulid.isCanonicalUlid(this.namedID)) {
|
if (ulid.isCanonicalUlid(this.namedID)) {
|
||||||
let timestamp = ulid.getTimestamp(this.namedID);
|
let timestamp = ulid.getTimestamp(this.namedID);
|
||||||
let date = document.createElement("span");
|
let date = document.createElement("span");
|
||||||
|
@ -76,7 +93,7 @@ class LinkedBranch extends Branch {
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
super(element);
|
super(element);
|
||||||
|
|
||||||
this.linkedTree = element.getAttribute("data-th-link");
|
this.linkedTree = element.getAttribute("th-link");
|
||||||
LinkedBranch.byLink.set(this.linkedTree, this);
|
LinkedBranch.byLink.set(this.linkedTree, this);
|
||||||
|
|
||||||
this.loadingText = document.createElement("p");
|
this.loadingText = document.createElement("p");
|
||||||
|
@ -135,7 +152,7 @@ class LinkedBranch extends Branch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadTree() {
|
loadTree(_why) {
|
||||||
if (!this.loading) {
|
if (!this.loading) {
|
||||||
this.loading = this.loadTreePromise();
|
this.loading = this.loadTreePromise();
|
||||||
}
|
}
|
||||||
|
@ -157,16 +174,16 @@ function expandDetailsRecursively(element) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentlyHighlightedBranch() {
|
function getCurrentlyHighlightedBranch() {
|
||||||
if (window.location.pathname == "/b" && window.location.search.length > 0) {
|
if (window.location.hash.length > 0) {
|
||||||
let shortID = window.location.search.substring(1);
|
|
||||||
return Branch.branchesByNamedID.get(shortID).id;
|
|
||||||
} else {
|
|
||||||
return window.location.hash.substring(1);
|
return window.location.hash.substring(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function highlightCurrentBranch() {
|
async function highlightCurrentBranch() {
|
||||||
let branch = document.getElementById(getCurrentlyHighlightedBranch());
|
let branchId = getCurrentlyHighlightedBranch();
|
||||||
|
if (branchId == null) return;
|
||||||
|
|
||||||
|
let branch = document.getElementById(branchId);
|
||||||
if (branch != null) {
|
if (branch != null) {
|
||||||
branch.scrollIntoView();
|
branch.scrollIntoView();
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
{{> components/_footer.hbs }}
|
{{> components/_footer.hbs }}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<th-emoji-tooltips></th-emoji-tooltips>
|
<th-overlays></th-overlays>
|
||||||
<th-command-line></th-command-line>
|
<th-command-line></th-command-line>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ clever to do while browser vendors figure that out, we'll just have to do a cach
|
||||||
import "treehouse/tree.js";
|
import "treehouse/tree.js";
|
||||||
import "treehouse/emoji.js";
|
import "treehouse/emoji.js";
|
||||||
import "treehouse/command-line.js";
|
import "treehouse/command-line.js";
|
||||||
|
import "treehouse/overlay.js";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<meta property="og:site_name" content="{{ config.user.title }}">
|
<meta property="og:site_name" content="{{ config.user.title }}">
|
||||||
|
|
Loading…
Reference in a new issue