@@ -1,32 +1,78 @@ | |||||
import { watchEffect, watch, onMounted, type CSSProperties, defineComponent, ref } from "vue"; | |||||
import { nextTick, inject, provide, watch, type InjectionKey, onBeforeUnmount, watchEffect, onMounted, type Ref, type CSSProperties, defineComponent, ref } from "vue"; | |||||
import { h as djh } from "@/util.ts"; | |||||
const carrierStyle = { | |||||
opacity: "0", | |||||
display: "block", | |||||
pointerEvents: "none", | |||||
backgroundColor: "black", | |||||
border: "white solid 1px", | |||||
color: "white", | |||||
padding: "10px", | |||||
position: "absolute", | |||||
zIndex: "1", | |||||
overflow: "hidden", | |||||
height: "0", | |||||
width: "0", | |||||
transition: "opacity 200ms, height 200ms, width 200ms", | |||||
} satisfies CSSProperties; | |||||
type TooltipContext = { | |||||
show: (newText: string, x: number, y: number) => void, | |||||
hide: () => void, | |||||
}; | |||||
const textCarrierStyle = { | |||||
fontSize: '16px', | |||||
fontFamily: "Roboto, serif", | |||||
display: "block", | |||||
overflow: "hidden", | |||||
} satisfies CSSProperties; | |||||
const tooltipContext = Symbol('tooltip') as InjectionKey<TooltipContext>; | |||||
const defaultWidth = 350; | |||||
export function setupTooltip(options: { carrier: Ref<HTMLElement | null> }) { | |||||
const { carrier } = options; | |||||
watchEffect(() => { | |||||
if (carrier.value) { | |||||
carrier.value.classList.add('tooltip-carrier'); | |||||
carrier.value.appendChild(djh('div', { className: 'text-carrier' })); | |||||
} | |||||
}); | |||||
const listener = (pos: { x: number, y: number }) => { | |||||
if (carrier.value && getComputedStyle(carrier.value).opacity !== "0") { | |||||
if (pos.x + 15 + carrier.value.clientWidth <= document.body.scrollWidth) { | |||||
carrier.value.style.left = (pos.x + 15) + "px"; | |||||
} else { | |||||
carrier.value.style.left = (document.body.scrollWidth - carrier.value.clientWidth - 5) + "px"; | |||||
} | |||||
if (pos.y + carrier.value.clientHeight <= document.body.scrollHeight) { | |||||
carrier.value.style.top = pos.y + "px"; | |||||
} else { | |||||
carrier.value.style.top = (document.body.scrollHeight - carrier.value.clientHeight - 5) + "px"; | |||||
} | |||||
} | |||||
}; | |||||
const active = ref(false); | |||||
watch(active, async () => { | |||||
const tooltipCarrier = carrier.value; | |||||
if (tooltipCarrier) { | |||||
tooltipCarrier.style.opacity = active.value ? '1' : '0'; | |||||
tooltipCarrier.style.width = active.value ? '350px' : '0'; | |||||
await nextTick(); | |||||
if (active.value) { | |||||
if (tooltipCarrier.firstChild?.nodeType === Node.ELEMENT_NODE) { | |||||
const computedHeight = getComputedStyle(tooltipCarrier.firstChild as Element).height; | |||||
tooltipCarrier.style.height = computedHeight; | |||||
} | |||||
} else { | |||||
tooltipCarrier.style.height = '0'; | |||||
} | |||||
} | |||||
}); | |||||
onMounted(() => document.addEventListener("mousemove", (e) => listener({ x: e.pageX, y: e.pageY }))); | |||||
onBeforeUnmount(() => document.removeEventListener("mousemove", (e) => listener({ x: e.pageX, y: e.pageY }))); | |||||
const ctx: TooltipContext = { | |||||
show(tooltip, x, y) { | |||||
if (carrier.value) { | |||||
carrier.value.firstChild!.textContent = tooltip; | |||||
} | |||||
active.value = true; | |||||
listener({ x, y }); | |||||
}, | |||||
hide() { | |||||
active.value = false; | |||||
}, | |||||
}; | |||||
provide(tooltipContext, ctx); | |||||
} | |||||
export default defineComponent({ | export default defineComponent({ | ||||
name: "dj-sexy-tooltip", | |||||
name: "dj-tooltip", | |||||
props: { | props: { | ||||
tooltip: { | tooltip: { | ||||
type: String, | type: String, | ||||
@@ -34,47 +80,15 @@ export default defineComponent({ | |||||
}, | }, | ||||
}, | }, | ||||
setup(props, { slots, attrs }) { | setup(props, { slots, attrs }) { | ||||
const active = ref(false); | |||||
const carrier = ref<HTMLElement | null>(null); | |||||
const textCarrier = ref<HTMLElement | null>(null); | |||||
onMounted(() => { | |||||
document.addEventListener("mousemove", (event) => { | |||||
const pos = { x: event.pageX, y: event.pageY }; | |||||
if (carrier.value && getComputedStyle(carrier.value).opacity !== "0") { | |||||
if (pos.x + 15 + carrier.value.clientWidth <= document.body.scrollWidth) { | |||||
carrier.value.style.left = (pos.x + 15) + "px"; | |||||
} else { | |||||
carrier.value.style.left = (document.body.scrollWidth - carrier.value.clientWidth - 5) + "px"; | |||||
} | |||||
if (pos.y + carrier.value.clientHeight <= document.body.scrollHeight) { | |||||
carrier.value.style.top = pos.y + "px"; | |||||
} else { | |||||
carrier.value.style.top = (document.body.scrollHeight - carrier.value.clientHeight - 5) + "px"; | |||||
} | |||||
} | |||||
}); | |||||
}); | |||||
watchEffect(() => { | |||||
if (carrier.value) { | |||||
carrier.value.style.height = active.value ? '16px' : '0'; | |||||
carrier.value.style.opacity = active.value ? '1' : '0'; | |||||
carrier.value.style.width = active.value ? '350px' : '0'; | |||||
} | |||||
}); | |||||
const tooltip = inject(tooltipContext, () => { throw new Error('No tooltip context'); }, true); | |||||
return () => <> | return () => <> | ||||
<div class="tooltip-container" {...attrs} | |||||
onMouseenter={() => { active.value = true; }} | |||||
onMouseleave={() => { active.value = false; }} | |||||
> | |||||
<div class="tooltip-container" | |||||
{...attrs} | |||||
onMouseenter={(e) => tooltip.show(props.tooltip, e.pageX, e.pageY)} | |||||
onMouseleave={() => tooltip.hide()}> | |||||
{slots.default && <slots.default />} | {slots.default && <slots.default />} | ||||
</div> | </div> | ||||
<div style={carrierStyle} ref={carrier}> | |||||
<span style={textCarrierStyle}>{props.tooltip}</span> | |||||
</div> | |||||
</>; | </>; | ||||
}, | }, | ||||
}); | }); |
@@ -1,24 +1,50 @@ | |||||
import { defineComponent } from "vue"; | |||||
import { defineComponent, computed, ref, type Ref } from "vue"; | |||||
import useHead from "@/useHead.ts"; | import useHead from "@/useHead.ts"; | ||||
import DJTooltip from "@/DJTooltip.tsx"; | |||||
import DJTooltip, { setupTooltip } from "@/DJTooltip.tsx"; | |||||
import DJEmail from "@/DJEmail.tsx"; | import DJEmail from "@/DJEmail.tsx"; | ||||
export default defineComponent({ | export default defineComponent({ | ||||
name: "app-root", | name: "app-root", | ||||
setup() { | setup() { | ||||
useHead({ title: "DJ Ledda's Homepage" }); | useHead({ title: "DJ Ledda's Homepage" }); | ||||
return () => ( | |||||
const tooltipCarrier = ref<HTMLDivElement | null>(null); | |||||
setupTooltip({ carrier: tooltipCarrier }); | |||||
const dude1Spinning = ref(false); | |||||
const dude2Spinning = ref(false); | |||||
function toggleDude(event: MouseEvent, dudeRef: Ref<boolean>) { | |||||
const dude = event.target as HTMLImageElement; | |||||
if (dudeRef.value) { | |||||
dude.addEventListener("animationiteration", function listener() { | |||||
dudeRef.value = false; | |||||
dude.removeEventListener("animationiteration", listener as EventListenerOrEventListenerObject); | |||||
}); | |||||
} else { | |||||
dudeRef.value = true; | |||||
} | |||||
} | |||||
const shaking = computed(() => dude1Spinning.value || dude2Spinning.value); | |||||
return () => <> | |||||
<div ref={tooltipCarrier} class="tooltip-carrier" /> | |||||
<div class="supercontainer"> | <div class="supercontainer"> | ||||
<div class="shakeable"> | |||||
<div class={{ shakeable: true, shakeMe: shaking.value }}> | |||||
<div class="title_name"> | <div class="title_name"> | ||||
<DJTooltip tooltip="I wonder what he's listening to?"> | <DJTooltip tooltip="I wonder what he's listening to?"> | ||||
<img src="/home/img/dj.gif" alt="dj legt krasse Mucke auf" class="dude" /> | |||||
<img src="/home/img/dj.gif" alt="dj legt krasse Mucke auf" | |||||
class={{ dude: true, spinMe: dude1Spinning.value }} | |||||
onClick={ (e) => toggleDude(e, dude1Spinning)} /> | |||||
</DJTooltip> | </DJTooltip> | ||||
<DJTooltip tooltip="Easily the coolest guy out there."> | <DJTooltip tooltip="Easily the coolest guy out there."> | ||||
<span>DJ Ledda</span> | <span>DJ Ledda</span> | ||||
</DJTooltip> | </DJTooltip> | ||||
<DJTooltip tooltip="I once heard this guy played at revs."> | <DJTooltip tooltip="I once heard this guy played at revs."> | ||||
<img src="/home/img/dj.gif" alt="dj laying down some sick beats" class="dude" /> | |||||
<img src="/home/img/dj.gif" alt="dj laying down some sick beats" | |||||
class={{ dude: true, spinMe: dude2Spinning.value }} | |||||
onClick={ (e) => toggleDude(e, dude2Spinning) } /> | |||||
</DJTooltip> | </DJTooltip> | ||||
</div> | </div> | ||||
<div class="main"> | <div class="main"> | ||||
@@ -71,6 +97,6 @@ export default defineComponent({ | |||||
<div id="tooltipCarrier"></div> | <div id="tooltipCarrier"></div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
); | |||||
</>; | |||||
}, | }, | ||||
}); | }); |
@@ -1,4 +1,4 @@ | |||||
import { createSSRApp } from "vue"; | import { createSSRApp } from "vue"; | ||||
import App from "@/home/App.tsx"; | |||||
import DJHomeRoot from "@/home/DJHomeRoot.tsx"; | |||||
createSSRApp(App).mount("#app-root"); | |||||
createSSRApp(DJHomeRoot).mount("#app-root"); |
@@ -1,7 +1,7 @@ | |||||
import { createSSRApp } from "vue"; | import { createSSRApp } from "vue"; | ||||
import App from "@/home/App.tsx"; | |||||
import DJHomeRoot from "@/home/DJHomeRoot.tsx"; | |||||
export default function createApp() { | export default function createApp() { | ||||
const app = createSSRApp(App); | |||||
const app = createSSRApp(DJHomeRoot); | |||||
return { app, router: null }; | return { app, router: null }; | ||||
} | } |
@@ -42,6 +42,7 @@ export function css(strs: TemplateStringsArray, ...vals: string[]) { | |||||
return sheet; | return sheet; | ||||
} | } | ||||
/* | |||||
export class DJElement extends HTMLElement { | export class DJElement extends HTMLElement { | ||||
static styles: CSSStyleSheet; | static styles: CSSStyleSheet; | ||||
@@ -57,3 +58,4 @@ export class DJElement extends HTMLElement { | |||||
this.root.adoptedStyleSheets = statics.styles ? [statics.styles] : []; | this.root.adoptedStyleSheets = statics.styles ? [statics.styles] : []; | ||||
} | } | ||||
} | } | ||||
*/ |
@@ -1,130 +0,0 @@ | |||||
interface Point { | |||||
x: number; | |||||
y: number; | |||||
} | |||||
interface Size { | |||||
height: string; | |||||
width: string; | |||||
} | |||||
class SexyTooltip { | |||||
private static readonly carrierStyle: Partial<CSSStyleDeclaration> = { | |||||
opacity: "0", | |||||
display: "block", | |||||
pointerEvents: "none", | |||||
backgroundColor: "black", | |||||
border: "white solid 1px", | |||||
color: "white", | |||||
padding: "10px", | |||||
position: "absolute", | |||||
zIndex: "1", | |||||
overflow: "hidden", | |||||
height: "0", | |||||
width: "0", | |||||
transition: "opacity 200ms, height 200ms, width 200ms", | |||||
}; | |||||
private static readonly textCarrierStyle: Partial<CSSStyleDeclaration> = { | |||||
width: "350px", | |||||
display: "block", | |||||
overflow: "hidden", | |||||
}; | |||||
private static readonly defaultWidth = 350; | |||||
private readonly carrier: HTMLDivElement; | |||||
private readonly textCarrier: HTMLSpanElement; | |||||
private readonly elementsWithTooltips: Element[] = []; | |||||
private active: boolean = false; | |||||
constructor(carrier: HTMLDivElement) { | |||||
if (carrier.childNodes.length > 0) { | |||||
throw new Error("Incorrect setup for tooltip! Remove the child nodes from the tooltip div."); | |||||
} | |||||
this.carrier = carrier; | |||||
this.textCarrier = document.createElement("span"); | |||||
this.carrier.appendChild(this.textCarrier); | |||||
Object.assign(this.carrier.style, SexyTooltip.carrierStyle); | |||||
Object.assign(this.textCarrier.style, SexyTooltip.textCarrierStyle); | |||||
document.querySelectorAll(".tooltip").forEach((element) => { | |||||
if (element.nodeName === "SPAN") { | |||||
console.log(element.previousElementSibling); | |||||
this.elementsWithTooltips.push(element.previousElementSibling); | |||||
} | |||||
}); | |||||
document.addEventListener("mousemove", (event) => this.rerenderTooltip({ x: event.pageX, y: event.pageY })); | |||||
} | |||||
rerenderTooltip(mousePos: Point) { | |||||
const newText = this.getTooltipTextForHoveredElement(mousePos); | |||||
if (newText !== this.textCarrier.innerHTML) { | |||||
this.transitionTooltipSize(newText); | |||||
const tooltipHasText = newText !== ""; | |||||
if (tooltipHasText !== this.active) { | |||||
this.toggleActive(); | |||||
} | |||||
} | |||||
if (this.isStillVisible()) { | |||||
this.updatePosition(mousePos); | |||||
} | |||||
} | |||||
isStillVisible() { | |||||
return getComputedStyle(this.carrier).opacity !== "0"; | |||||
} | |||||
updatePosition(pos: Point) { | |||||
if (pos.x + 15 + this.carrier.clientWidth <= document.body.scrollWidth) { | |||||
this.carrier.style.left = (pos.x + 15) + "px"; | |||||
} else { | |||||
this.carrier.style.left = (document.body.scrollWidth - this.carrier.clientWidth - 5) + "px"; | |||||
} | |||||
if (pos.y + this.carrier.clientHeight <= document.body.scrollHeight) { | |||||
this.carrier.style.top = pos.y + "px"; | |||||
} else { | |||||
this.carrier.style.top = (document.body.scrollHeight - this.carrier.clientHeight - 5) + "px"; | |||||
} | |||||
} | |||||
toggleActive() { | |||||
if (this.active) { | |||||
this.active = false; | |||||
this.carrier.style.opacity = "0"; | |||||
this.carrier.style.padding = "0"; | |||||
} else { | |||||
this.active = true; | |||||
this.carrier.style.opacity = "1"; | |||||
this.carrier.style.padding = SexyTooltip.carrierStyle.padding; | |||||
} | |||||
} | |||||
transitionTooltipSize(text: string) { | |||||
const testDiv = document.createElement("div"); | |||||
testDiv.style.height = "auto"; | |||||
testDiv.style.width = SexyTooltip.defaultWidth + "px"; | |||||
testDiv.innerHTML = text; | |||||
document.body.appendChild(testDiv); | |||||
const calculatedHeight = testDiv.scrollHeight; | |||||
testDiv.style.display = "inline"; | |||||
testDiv.style.width = "auto"; | |||||
document.body.removeChild(testDiv); | |||||
const size = { height: calculatedHeight + "px", width: "350px" }; | |||||
this.carrier.style.height = size.height; | |||||
this.carrier.style.width = text === "" ? "0" : size.width; | |||||
this.textCarrier.innerHTML = text; | |||||
} | |||||
getTooltipTextForHoveredElement(mousePos: Point): string { | |||||
for (const elem of this.elementsWithTooltips) { | |||||
const boundingRect = elem.getBoundingClientRect(); | |||||
const inYRange = mousePos.y >= boundingRect.top + window.pageYOffset - 1 && | |||||
mousePos.y <= boundingRect.bottom + window.pageYOffset + 1; | |||||
const inXRange = mousePos.x >= boundingRect.left + window.pageXOffset - 1 && | |||||
mousePos.x <= boundingRect.right + window.pageXOffset + 1; | |||||
if (inYRange && inXRange) { | |||||
return (elem.nextElementSibling as HTMLSpanElement)?.innerText ?? ""; | |||||
} | |||||
} | |||||
return ""; | |||||
} | |||||
} | |||||
export default SexyTooltip; |
@@ -166,7 +166,7 @@ span.subjecttitle { | |||||
padding: 10px 20px 10px 20px; | padding: 10px 20px 10px 20px; | ||||
display: block; | display: block; | ||||
color: #333333; | color: #333333; | ||||
text-decoration: none; | |||||
text-decorAtion: none; | |||||
transition: background-color 200ms; | transition: background-color 200ms; | ||||
} | } | ||||
@@ -187,3 +187,31 @@ a { | |||||
.tooltip-container { | .tooltip-container { | ||||
display: inline-block; | display: inline-block; | ||||
} | } | ||||
.tooltip-carrier { | |||||
opacity: 0; | |||||
display: block; | |||||
pointer-events: none; | |||||
background-color: black; | |||||
border: white solid 1px; | |||||
color: white; | |||||
padding: 10px; | |||||
position: absolute; | |||||
z-index: 1; | |||||
overflow: hidden; | |||||
height: 0; | |||||
width: 0; | |||||
position: absolute; | |||||
transition: opacity 200ms, height 200ms, width 200ms; | |||||
.text-carrier { | |||||
position: absolute; | |||||
width: 350px; | |||||
font-size: 16px; | |||||
font-family: "Roboto", serif; | |||||
display: block; | |||||
overflow: hidden; | |||||
} | |||||
} | |||||