Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IDE-style tooltips #209

Merged
merged 12 commits into from
May 10, 2024
2 changes: 1 addition & 1 deletion Assets/css/Main.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Assets/css/Main.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Assets/js/Main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Assets/js/Main.js.map

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ extension Unidoc.RenderFormat.Assets
/// To reduce cache churn, not all assets are versioned. For example, the fonts and
/// the favicon do not use the version numbers.
@inlinable public static
var version:MajorVersion { .v(26) }
var version:MajorVersion { .v(27) }
}
extension Unidoc.RenderFormat.Assets
{
Expand Down
4 changes: 4 additions & 0 deletions Sources/UnidocRender/Unidoc.VertexContext.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import HTML
import Symbols
import UnidocRecords

Expand All @@ -6,6 +7,7 @@ extension Unidoc
public
protocol VertexContext:AnyObject
{
associatedtype Tooltips:HTML.OutputStreamable
associatedtype Table:VertexContextTable

init(canonical:CanonicalVersion?,
Expand All @@ -15,6 +17,8 @@ extension Unidoc
vertices:Table)

var canonical:CanonicalVersion? { get }
var tooltips:Tooltips? { get }

/// Returns the metadata document for the principal volume of the associated page.
var volume:VolumeMetadata { get }
var media:PackageMedia? { get }
Expand Down
6 changes: 6 additions & 0 deletions Sources/UnidocUI/Endpoints/Unidoc.ExportEndpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ extension Unidoc.ExportEndpoint:Unidoc.VertexEndpoint, HTTP.ServerEndpoint
$0 ?= overview
$0 ?= details
}

$0[.div]
{
$0.style = "display: none;"
$0.id = "ss:tooltips"
} = context.tooltips
}

return .ok(.init(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,27 @@ extension Unidoc.IdentifiablePageContext

private
var uris:[Unidoc.Scalar: String]
private(set)
var used:[Unidoc.Scalar]

init(vertices:Table,
volumes:Unidoc.VolumeContext,
uris:[Unidoc.Scalar: String] = [:])
volumes:Unidoc.VolumeContext)
{
self.vertices = vertices
self.volumes = volumes
self.uris = uris
self.uris = [:]
self.used = []
}
}
}
extension Unidoc.IdentifiablePageContext.Cache
{
var tooltips:Unidoc.IdentifiablePageContext<Table>.Tooltips?
{
.init(vertices: self.vertices, uris: self.uris, list: self.used)
}
}
extension Unidoc.IdentifiablePageContext.Cache
{
mutating
func load(_ id:Unidoc.Scalar, by uri:(Unidoc.VolumeMetadata) -> URI?) -> Unidoc.LinkTarget?
Expand All @@ -38,6 +47,7 @@ extension Unidoc.IdentifiablePageContext.Cache
let volume:Unidoc.VolumeMetadata = self.volumes[id.edition],
let uri:URI = uri(volume)
{
self.used.append(id)
let target:String = "\(uri)"
$0 = target
return .init(location: target)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import HTML
import UnidocRecords

extension Unidoc.IdentifiablePageContext
{
@frozen public
struct Tooltips
{
private
let vertices:Table
private
let uris:[Unidoc.Scalar: String]
private
let list:[Unidoc.Scalar]

init?(vertices:Table,
uris:[Unidoc.Scalar: String],
list:[Unidoc.Scalar])
{
if list.isEmpty
{
return nil
}

self.vertices = vertices
self.uris = uris
self.list = list
}
}
}
extension Unidoc.IdentifiablePageContext.Tooltips:HTML.OutputStreamable
{
public static
func += (div:inout HTML.ContentEncoder, self:Self)
{
for id:Unidoc.Scalar in self.list
{
guard case (let vertex, principal: false)? = self.vertices[id],
let uri:String = self.uris[id]
else
{
continue
}

switch vertex
{
case .culture(let vertex):
div[.a, { $0.href = uri }]
{
$0[.pre, .code] = Unidoc.ImportSection.init(module: vertex.module.id)

$0 ?= vertex.overview?.markdown.safe
}

case .decl(let vertex):
div[.a, { $0.href = uri }]
{
$0[.pre, .code] = vertex.signature.expanded.bytecode.safe

$0 ?= vertex.overview?.markdown.safe
}

default:
continue
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ extension Unidoc.IdentifiablePageContext:Identifiable
}
extension Unidoc.IdentifiablePageContext:Unidoc.VertexContext
{
public final
var tooltips:Tooltips? { self.cache.tooltips }

public final
var volume:Unidoc.VolumeMetadata { self.cache.volumes.principal }

Expand Down
7 changes: 7 additions & 0 deletions Sources/UnidocUI/Page types/Unidoc.VertexPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,17 @@ extension Unidoc.VertexPage
}
$0[.div] { $0.class = "sidebar" } = sidebar.map { _ in "" }
}

body[.div, { $0.class = "app" }]
{
$0[.main, { $0.class = "content" }] { self.main(&$0, format: format) }
$0[.div] { $0.class = "sidebar" } = sidebar
}

body[.div]
{
$0.style = "display: none;"
$0.id = "ss:tooltips"
} = self.context.tooltips
}
}
3 changes: 2 additions & 1 deletion Stylesheets/Main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@import 'div.logo';
@import 'div.menu';
@import 'div.more';
@import 'div.tooltips';
@import 'dl';
@import 'figure';
@import 'form';
Expand Down Expand Up @@ -62,7 +63,7 @@ body
> *.app > *.content
{
flex: 0 1 48rem;

max-width: 48rem;
overflow-wrap: break-word;
}
> *.app > *.sidebar
Expand Down
47 changes: 47 additions & 0 deletions Stylesheets/_div.tooltips.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
div.tooltips
{
position: fixed;
z-index: 1;

> div
{
position: fixed;
display: block;

pointer-events: none;

opacity: 0;
margin-top: 0;
margin-left: -1rem;

transition: all 0.25s;

@include backdrop-blur;

padding: 0.5rem 1rem;
box-shadow: 0 1rem 3rem var(--blur-shadow);
border-radius: 0.5rem;
width: 30rem;

pre
{
margin: 0;
font-size: 90.625%;
}

p
{
font-size: 90.625%;
line-height: 1.4em;
}
}

> div.visible
{
opacity: 1;
margin-top: 0.5rem;

transition: opacity 0.25s;
transition: margin-top 0.15s;
}
}
80 changes: 80 additions & 0 deletions TypeScript/Sources/Main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function textfield(element: Element | null): boolean {
}
}

const tooltips: HTMLElement | null = document.getElementById('ss:tooltips');
const search: HTMLElement | null = document.getElementById('search');
const login: HTMLElement | null = document.getElementById('login');

Expand Down Expand Up @@ -124,6 +125,85 @@ if (login !== null) {
document.cookie = 'login_state=' + state + '; Path=/ ; SameSite=Lax ; Secure';
}

if (tooltips !== null) {
tooltips.remove();
// The tooltips `<div>` contains `<a>` elements only.
let cards: { [id: string] : HTMLSpanElement; } = {};
let frame: HTMLDivElement = document.createElement('div');

for (const anchor of tooltips.children) {
if (!(anchor instanceof HTMLAnchorElement)) {
continue;
}

// Cannot use `anchor.href`, we want the exact value of the `href` attribute.
const id: string | null = anchor.getAttribute("href")

if (id === null) {
continue;
}

// Change the tooltip into a `<div>` with `class="tooltip"`.
const tooltip: HTMLSpanElement = document.createElement('div');
tooltip.innerHTML = anchor.innerHTML;

cards[id] = tooltip;
frame.appendChild(tooltip);
}

// Inject the tooltips into every `<a>` element with the same `href` attribute.
// This should only be done within the `<main>` element.
const main: HTMLElement | null = document.querySelector('main');
if (main !== null) {
main.querySelectorAll('a').forEach((
anchor: HTMLAnchorElement,
key: number,
all: NodeListOf<Element>
) => {

// If the anchor is inside a card preview, the tooltip would be redundant.
if (anchor.parentElement?.tagName === 'CODE' &&
anchor.parentElement.classList.contains('decl')) {
return;
}
if (anchor.parentElement?.tagName === 'H3' &&
anchor.parentElement.classList.contains('module')) {
return;
}

const id: string | null = anchor.getAttribute("href")

if (id === null) {
return;
}
const tooltip: HTMLSpanElement | undefined = cards[id];

if (tooltip === undefined) {
return;
}

// When you hover over the anchor, show the tooltip by loading the (x, y) position
// of the anchor on the screen, and then adding the tooltip to the document as
// a fixed-position element.
anchor.addEventListener('mouseenter', (event: MouseEvent) => {
const r: DOMRect = anchor.getBoundingClientRect();

tooltip.style.left = r.x.toString() + 'px';
tooltip.style.top = r.bottom.toString() + 'px';

tooltip.classList.add('visible');
});
anchor.addEventListener('mouseleave', (event: MouseEvent) => {
tooltip.classList.remove('visible');
});
});

// Make the tooltips list visible; it was originally hidden to prevent FOUC.
frame.className = 'tooltips';
document.body.appendChild(frame);
}
}

document.querySelectorAll('form.sort-controls').forEach((
form: Element,
key: number,
Expand Down
Loading