| @@ -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({ | |||
| name: "dj-sexy-tooltip", | |||
| name: "dj-tooltip", | |||
| props: { | |||
| tooltip: { | |||
| type: String, | |||
| @@ -34,47 +80,15 @@ export default defineComponent({ | |||
| }, | |||
| }, | |||
| 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 () => <> | |||
| <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 />} | |||
| </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 DJTooltip from "@/DJTooltip.tsx"; | |||
| import DJTooltip, { setupTooltip } from "@/DJTooltip.tsx"; | |||
| import DJEmail from "@/DJEmail.tsx"; | |||
| export default defineComponent({ | |||
| name: "app-root", | |||
| setup() { | |||
| 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="shakeable"> | |||
| <div class={{ shakeable: true, shakeMe: shaking.value }}> | |||
| <div class="title_name"> | |||
| <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 tooltip="Easily the coolest guy out there."> | |||
| <span>DJ Ledda</span> | |||
| </DJTooltip> | |||
| <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> | |||
| </div> | |||
| <div class="main"> | |||
| @@ -71,6 +97,6 @@ export default defineComponent({ | |||
| <div id="tooltipCarrier"></div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| </>; | |||
| }, | |||
| }); | |||
| @@ -1,4 +1,4 @@ | |||
| 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 App from "@/home/App.tsx"; | |||
| import DJHomeRoot from "@/home/DJHomeRoot.tsx"; | |||
| export default function createApp() { | |||
| const app = createSSRApp(App); | |||
| const app = createSSRApp(DJHomeRoot); | |||
| return { app, router: null }; | |||
| } | |||
| @@ -42,6 +42,7 @@ export function css(strs: TemplateStringsArray, ...vals: string[]) { | |||
| return sheet; | |||
| } | |||
| /* | |||
| export class DJElement extends HTMLElement { | |||
| static styles: CSSStyleSheet; | |||
| @@ -57,3 +58,4 @@ export class DJElement extends HTMLElement { | |||
| 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; | |||
| display: block; | |||
| color: #333333; | |||
| text-decoration: none; | |||
| text-decorAtion: none; | |||
| transition: background-color 200ms; | |||
| } | |||
| @@ -187,3 +187,31 @@ a { | |||
| .tooltip-container { | |||
| 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; | |||
| } | |||
| } | |||