2025-08-12 17:17:30 -03:00
|
|
|
"use strict";
|
|
|
|
|
|
2025-10-16 15:02:34 -03:00
|
|
|
/**
|
|
|
|
|
* @param {string} str
|
|
|
|
|
* @returns {string}
|
|
|
|
|
*/
|
|
|
|
|
function hashString(str) {
|
|
|
|
|
return Array.from(str).reduce(
|
|
|
|
|
(s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0,
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class IPUBBackground extends HTMLElement {
|
|
|
|
|
static elementName = "ipub-background";
|
|
|
|
|
static observedAttributes = ["sticky", "fade", "id"];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
static #observer = (() => {
|
|
|
|
|
/** @type {Map<string, IPUBBackground>} */
|
|
|
|
|
const instancesOnScreen = new Map();
|
|
|
|
|
|
|
|
|
|
document.addEventListener("scroll", () => {
|
|
|
|
|
for (const [_, instance] of instancesOnScreen) {
|
|
|
|
|
const perc = getPercentageInView(
|
|
|
|
|
instance.querySelector("img") || instance,
|
|
|
|
|
);
|
|
|
|
|
instance.fade(perc);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return new IntersectionObserver((entries) =>
|
|
|
|
|
entries.forEach((e) => {
|
|
|
|
|
let instance = e.target.parentElement;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
instance.tagName.toLowerCase() !==
|
|
|
|
|
IPUBBackground.elementName.toLowerCase()
|
|
|
|
|
) {
|
|
|
|
|
instance = instance.parentElement;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
instance.tagName.toLowerCase() !==
|
|
|
|
|
IPUBBackground.elementName.toLowerCase()
|
|
|
|
|
) {
|
|
|
|
|
console.error(
|
|
|
|
|
"IPUBBackground: malformed <ipub-background> element",
|
|
|
|
|
e.target,
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (e.intersectionRatio > 0 && instance.id) {
|
|
|
|
|
instancesOnScreen.set(instance.id, instance);
|
|
|
|
|
} else if (instance.id) {
|
|
|
|
|
instancesOnScreen.delete(instance.id);
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
attributeChangedCallback(name, _oldValue, _newValue) {
|
|
|
|
|
console.debug("IPUBBackground: attribute changed", name);
|
|
|
|
|
switch (name) {
|
|
|
|
|
case "id": {
|
|
|
|
|
if (!this.id) {
|
|
|
|
|
console.warn(
|
|
|
|
|
`IPUBBackground: no ID specified, assigning one based on innerHTML`,
|
|
|
|
|
this,
|
|
|
|
|
);
|
|
|
|
|
this.id = hashString(this.innerHTML);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case "fade": {
|
|
|
|
|
const image = this.querySelector("img");
|
|
|
|
|
if (image) {
|
|
|
|
|
console.debug(
|
|
|
|
|
`IPUBBackground: ipub-background#${this.id} to observer`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (this.hasAttribute("fade")) {
|
|
|
|
|
IPUBBackground.#observer.observe(image);
|
|
|
|
|
} else {
|
|
|
|
|
IPUBBackground.#observer.unobserve(image);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const perc = getPercentageInView(image);
|
|
|
|
|
if (perc > 0) {
|
|
|
|
|
this.fade(perc);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {number} perc
|
|
|
|
|
* @throws {Error}
|
|
|
|
|
* @returns {void | Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
fade(perc) {
|
|
|
|
|
console.debug(`IPUBBackground: ${this.id} is ${perc} on screen`);
|
|
|
|
|
|
|
|
|
|
if (!this.style.getPropertyValue("--ipub-fade")) {
|
|
|
|
|
this.style.setProperty("--ipub-fade", `${perc}%`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentPerc = this.style.getPropertyValue("--ipub-fade");
|
|
|
|
|
if (currentPerc === "100%" && perc >= 100) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (perc % 10 === 0) {
|
|
|
|
|
this.style.setProperty("--ipub-fade", `${perc}%`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-12 17:17:30 -03:00
|
|
|
globalThis.addEventListener("load", () => {
|
|
|
|
|
console.log("IPUB SCRIPT LOADED");
|
|
|
|
|
|
2025-10-16 15:02:34 -03:00
|
|
|
customElements.define(IPUBBackground.elementName, IPUBBackground);
|
|
|
|
|
|
2025-08-12 17:17:30 -03:00
|
|
|
/** @type {Map<string, Element>} */
|
|
|
|
|
const onScreenMap = new Map();
|
|
|
|
|
|
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
|
|
|
entries.forEach((e) => {
|
|
|
|
|
if (e.intersectionRatio > 0) {
|
2025-10-16 15:02:34 -03:00
|
|
|
// console.debug(
|
|
|
|
|
// `IntersectionObserver: adding element #${e.target.id} to onScreenMap`,
|
|
|
|
|
// );
|
2025-08-12 17:17:30 -03:00
|
|
|
onScreenMap.set(e.target.id, e.target);
|
|
|
|
|
} else {
|
2025-10-16 15:02:34 -03:00
|
|
|
// console.debug(
|
|
|
|
|
// `IntersectionObserver: removing element #${e.target.id} to onScreenMap`,
|
|
|
|
|
// );
|
2025-08-12 17:17:30 -03:00
|
|
|
onScreenMap.delete(e.target.id);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const element of document.querySelectorAll(
|
|
|
|
|
`[data-ipub-trigger="on-screen"]`,
|
|
|
|
|
)) {
|
|
|
|
|
observer.observe(element);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener("scroll", async () => {
|
|
|
|
|
for (const [id, element] of onScreenMap) {
|
|
|
|
|
const perc = getPercentageInView(element);
|
2025-10-16 15:02:34 -03:00
|
|
|
// console.debug(`Element #${id} is now ${perc}% on screen`);
|
2025-08-12 17:17:30 -03:00
|
|
|
|
|
|
|
|
const played = element.getAttribute("data-ipub-trigger-played") == "true";
|
|
|
|
|
|
|
|
|
|
if (perc >= 100 && !played) {
|
|
|
|
|
await playIpubElement(element);
|
|
|
|
|
element.setAttribute("data-ipub-trigger-played", "true");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Element} element
|
|
|
|
|
*/
|
|
|
|
|
async function playIpubElement(element) {
|
|
|
|
|
switch (element.tagName) {
|
|
|
|
|
case "audio": {
|
|
|
|
|
/** @type {HTMLAudioElement} */
|
|
|
|
|
const audio = element;
|
|
|
|
|
|
2025-10-16 15:02:34 -03:00
|
|
|
// await audio.play();
|
2025-08-12 17:17:30 -03:00
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Element} element
|
|
|
|
|
* @returns {number}
|
|
|
|
|
*/
|
|
|
|
|
function getPercentageInView(element) {
|
|
|
|
|
const viewTop = globalThis.pageYOffset;
|
|
|
|
|
const viewBottom = viewTop + globalThis.innerHeight;
|
|
|
|
|
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
const elementTop = rect.y + viewTop;
|
|
|
|
|
const elementBottom = rect.y + rect.height + viewTop;
|
|
|
|
|
|
|
|
|
|
if (viewTop > elementBottom || viewBottom < elementTop) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
(viewTop < elementTop && viewBottom > elementBottom) ||
|
|
|
|
|
(elementTop < viewTop && elementBottom > viewBottom)
|
|
|
|
|
) {
|
|
|
|
|
return 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let inView = rect.height;
|
|
|
|
|
|
|
|
|
|
if (elementTop < viewTop) {
|
|
|
|
|
inView = rect.height - (globalThis.pageYOffset - elementTop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (elementBottom > viewBottom) {
|
|
|
|
|
inView = inView - (elementBottom - viewBottom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Math.round((inView / globalThis.innerHeight) * 100);
|
|
|
|
|
}
|