2026-05-24 11:47:20 +02:00
|
|
|
/** @typedef { import('types/HovercardResponse.js').HovercardResponse} HovercardResponse */
|
|
|
|
|
|
|
|
|
|
export default function hovercard(){
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @type {boolean}
|
|
|
|
|
*/
|
|
|
|
|
start: false,
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @type {HovercardResponse}
|
|
|
|
|
*/
|
|
|
|
|
data: null,
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @type {boolean}
|
|
|
|
|
*/
|
|
|
|
|
loading: false,
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @type {any}
|
|
|
|
|
*/
|
|
|
|
|
error: false,
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @type {HTMLElement|null}
|
|
|
|
|
*/
|
|
|
|
|
anchorEl: null,
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @type {number}
|
|
|
|
|
*/
|
|
|
|
|
x: 0,
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @type {number}
|
|
|
|
|
*/
|
|
|
|
|
y: 0,
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {HTMLElement} anchorEl
|
|
|
|
|
* @param {string} fetchUrl
|
|
|
|
|
* @return {Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
async open(anchorEl, fetchUrl){
|
|
|
|
|
|
|
|
|
|
if( this.start && this.anchorEl === anchorEl ){
|
|
|
|
|
// this.close();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.start = true;
|
|
|
|
|
this.anchorEl = anchorEl;
|
|
|
|
|
this.data = null;
|
|
|
|
|
this.loading = true;
|
|
|
|
|
this.error = false;
|
|
|
|
|
this.updatePosition(anchorEl);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const RESPONSE = await fetch(fetchUrl);
|
|
|
|
|
if( !RESPONSE.ok )
|
|
|
|
|
throw new Error(RESPONSE.status);
|
|
|
|
|
|
|
|
|
|
let json = await RESPONSE.json();
|
|
|
|
|
if( !json.user )
|
|
|
|
|
throw new Error(RESPONSE.status);
|
|
|
|
|
|
|
|
|
|
this.data = json.user;
|
|
|
|
|
|
|
|
|
|
Alpine.nextTick(() => {
|
|
|
|
|
const card = document.querySelector('.hovercard');
|
|
|
|
|
if (card) window.refreshIcons(card);
|
2026-07-01 11:51:30 +02:00
|
|
|
this.updatePosition(this.anchorEl);
|
2026-05-24 11:47:20 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} catch( error ){
|
|
|
|
|
this.error = true;
|
|
|
|
|
} finally {
|
|
|
|
|
this.loading = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update Hovercard position
|
|
|
|
|
* @param {HTMLElement} anchorEl
|
|
|
|
|
*/
|
|
|
|
|
updatePosition(anchorEl){
|
|
|
|
|
const RECT = anchorEl.getBoundingClientRect();
|
|
|
|
|
const SCROLL_X = window.scrollX;
|
|
|
|
|
const SCROLL_Y = window.scrollY;
|
2026-07-01 11:51:30 +02:00
|
|
|
const VIEWPORT_WIDTH = window.innerWidth;
|
|
|
|
|
const VIEWPORT_HEIGHT = window.innerHeight;
|
2026-05-24 11:47:20 +02:00
|
|
|
|
2026-07-01 11:51:30 +02:00
|
|
|
const CARD = document.querySelector('.hovercard');
|
|
|
|
|
const WIDTH = CARD?.offsetWidth || 280;
|
|
|
|
|
const HEIGHT = CARD?.offsetHeight || 320;
|
|
|
|
|
|
|
|
|
|
let x = RECT.right + SCROLL_X + 8;
|
2026-05-24 11:47:20 +02:00
|
|
|
let y = RECT.bottom + SCROLL_Y + 8;
|
|
|
|
|
|
2026-07-01 11:51:30 +02:00
|
|
|
if( x + WIDTH > VIEWPORT_WIDTH - 8 && RECT.left + SCROLL_X - WIDTH - 8 >= 8 ){
|
|
|
|
|
x = RECT.left + SCROLL_X - WIDTH - 8;
|
|
|
|
|
} else {
|
|
|
|
|
x = Math.max(8, Math.min(x, VIEWPORT_WIDTH - WIDTH - 8));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if( y + HEIGHT > VIEWPORT_HEIGHT + SCROLL_Y - 8 && RECT.top + SCROLL_Y - HEIGHT - 8 >= 8 ){
|
|
|
|
|
y = RECT.top + SCROLL_Y - HEIGHT - 8;
|
|
|
|
|
} else {
|
|
|
|
|
y = Math.max(8, Math.min(y, VIEWPORT_HEIGHT + SCROLL_Y - HEIGHT - 8));
|
2026-05-24 11:47:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.x = x;
|
|
|
|
|
this.y = y;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Close the hovercard.
|
|
|
|
|
*/
|
|
|
|
|
close(){
|
|
|
|
|
this.start = false;
|
|
|
|
|
this.data = null;
|
|
|
|
|
this.anchorEl = null;
|
|
|
|
|
this.error = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|