holy-loader
Version:
Holy Loader is a lightweight, customizable top-loading progress bar component for React applications.
499 lines (493 loc) • 19.6 kB
JavaScript
"use strict";
"use client";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.tsx
var src_exports = {};
__export(src_exports, {
default: () => src_default,
startHolyLoader: () => startHolyLoader,
stopHolyLoader: () => stopHolyLoader
});
module.exports = __toCommonJS(src_exports);
var React = __toESM(require("react"), 1);
// src/constants.ts
var DEFAULTS = {
color: "#59a2ff",
initialPosition: 0.08,
height: 4,
easing: "ease",
speed: 200,
zIndex: 2147483647,
showSpinner: false,
boxShadow: void 0,
ignoreSearchParams: false,
dir: "ltr"
};
var START_HOLY_EVENT = "holy-progress-start";
var STOP_HOLY_EVENT = "holy-progress-stop";
// src/utils.ts
var toAbsoluteURL = /* @__PURE__ */ __name((url) => {
return new URL(url, window.location.href).href;
}, "toAbsoluteURL");
var isSamePageAnchor = /* @__PURE__ */ __name((currentUrl, newUrl) => {
const current = new URL(toAbsoluteURL(currentUrl));
const next = new URL(toAbsoluteURL(newUrl));
return current.href.split("#")[0] === next.href.split("#")[0];
}, "isSamePageAnchor");
var isSameHost = /* @__PURE__ */ __name((currentUrl, newUrl) => {
const current = new URL(toAbsoluteURL(currentUrl));
const next = new URL(toAbsoluteURL(newUrl));
return current.hostname.replace(/^www\./, "") === next.hostname.replace(/^www\./, "");
}, "isSameHost");
var paramsAreEqual = /* @__PURE__ */ __name((params1, params2) => Array.from(params1).every(
([key, value]) => params2.has(key) && params2.get(key) === value
), "paramsAreEqual");
var hasSameQueryParameters = /* @__PURE__ */ __name((currentUrl, newUrl) => {
const current = new URL(toAbsoluteURL(currentUrl));
const next = newUrl instanceof URL ? newUrl : new URL(toAbsoluteURL(newUrl));
const currentParams = new URLSearchParams(current.search);
const nextParams = new URLSearchParams(next.search);
return paramsAreEqual(currentParams, nextParams) && paramsAreEqual(nextParams, currentParams);
}, "hasSameQueryParameters");
var isSamePathname = /* @__PURE__ */ __name((currentUrl, newUrl) => {
const current = new URL(toAbsoluteURL(currentUrl));
const next = newUrl instanceof URL ? newUrl : new URL(toAbsoluteURL(newUrl));
return current.pathname === next.pathname;
}, "isSamePathname");
var clamp = /* @__PURE__ */ __name((n, min, max) => Math.max(min, Math.min(n, max)), "clamp");
var queue = /* @__PURE__ */ (() => {
const pending = [];
const next = /* @__PURE__ */ __name(() => {
const fn = pending.shift();
if (fn !== void 0) {
fn(next);
}
}, "next");
return (fn) => {
pending.push(fn);
if (pending.length === 1) {
next();
}
};
})();
var repaintElement = /* @__PURE__ */ __name((obj) => {
void obj.offsetWidth;
return obj;
}, "repaintElement");
// src/HolyProgress.ts
var _HolyProgress = class _HolyProgress {
/**
* Create a HolyProgress instance.
* @param {Partial<HolyProgressProps>} [customSettings] - Optional custom settings to override defaults.
*/
constructor(customSettings) {
/**
* Sets the progress to a specific value.
* @private
* @param {number} n - The new progress value (0 to 1).
* @returns {HolyProgress} The current instance for chaining methods.
*/
this.setTo = /* @__PURE__ */ __name((n) => {
const isStarted = typeof this.progressN === "number";
n = clamp(n, this.settings.initialPosition, 1);
this.progressN = n === 1 ? null : n;
const progressBar = this.getOrCreateBar(!isStarted);
if (!progressBar) {
return this;
}
repaintElement(progressBar);
queue((next) => {
if (!this.bar) {
return;
}
Object.assign(this.bar.style, this.barPositionCSS(n), {
transition: `all ${this.settings.speed}ms ${this.settings.easing}`
});
if (n === 1) {
progressBar.style.transition = "none";
progressBar.style.opacity = "1";
repaintElement(progressBar);
setTimeout(() => {
progressBar.style.transition = `all ${this.settings.speed}ms linear`;
progressBar.style.opacity = "0";
setTimeout(() => {
this.removeBarFromDOM();
next();
}, this.settings.speed);
this.removeSpinnerFromDOM();
}, this.settings.speed);
} else {
setTimeout(next, this.settings.speed);
}
});
return this;
}, "setTo");
/**
* Converts a progress value (0 to 1) into a percentage representation.
* Used for calculating the visual width of the progress bar.
* @private
* @param {number} n - The progress value to convert.
* @returns {number} The percentage representation of the progress value.
*/
this.toBarPercentage = /* @__PURE__ */ __name((n) => this.settings.dir === "ltr" ? (-1 + n) * 100 : (1 - n) * 100, "toBarPercentage");
/**
* Initiates the progress bar's movement. If already started, it continues from the current position.
* Automatically handles automatic incrementation ('trickle') if enabled.
* @public
* @returns {HolyProgress} The current instance for chaining methods.
*/
this.start = /* @__PURE__ */ __name(() => {
if (this.progressN === null) {
this.setTo(0);
this.startTrickle();
if (this.settings.showSpinner === true) {
this.createSpinner();
}
}
return this;
}, "start");
/**
* Performs automatic incrementation of the progress bar.
* This function is recursive and continues to increment the progress at intervals defined by `speed`.
* @private
*/
this.startTrickle = /* @__PURE__ */ __name(() => {
const run = /* @__PURE__ */ __name(() => {
if (this.progressN === null) return;
this.incrementStatus();
setTimeout(run, this.settings.speed);
}, "run");
setTimeout(run, this.settings.speed);
}, "startTrickle");
/**
* Completes the progress, moving it to 100%
* @public
* @returns {HolyProgress} The current instance for chaining methods.
*/
this.complete = /* @__PURE__ */ __name(() => this.setTo(1), "complete");
/**
* Calculates an increment value based on the current status of the progress.
* This is used to determine the amount of progress to add during automatic incrementation.
* @private
* @param {number} status - The current progress status.
* @returns {number} The calculated increment value.
*/
this.calculateIncrement = /* @__PURE__ */ __name((status) => {
const base = 0.1;
const scale = 5;
return base * Math.exp(-scale * status);
}, "calculateIncrement");
/**
* Increments the progress bar by a specified amount, or by an amount determined by `calculateIncrement` if not specified.
* @private
* @param {number} [amount] - The amount to increment the progress bar.
* @returns {HolyProgress} The current instance for chaining methods.
*/
this.incrementStatus = /* @__PURE__ */ __name((amount) => {
if (this.progressN === null) {
return this.start();
}
if (this.progressN > 1) {
return this;
}
if (typeof amount !== "number") {
amount = this.calculateIncrement(this.progressN);
}
this.progressN = clamp(this.progressN + amount, 0, 0.994);
return this.setTo(this.progressN);
}, "incrementStatus");
/**
* Creates and initializes a new progress bar element in the DOM.
* It sets up the necessary styles and appends the element to the document body.
* @private
* @param {boolean} fromStart - Indicates if the bar is created from the start position.
* @returns {HTMLElement | null} The created progress bar element, or null if creation fails.
*/
this.createBar = /* @__PURE__ */ __name((fromStart) => {
var _a, _b;
const barContainer = document.createElement("div");
barContainer.id = "holy-progress";
barContainer.style.pointerEvents = "none";
barContainer.style.position = "fixed";
barContainer.style.zIndex = this.settings.zIndex.toString();
barContainer.innerHTML = '<div class="bar" role="bar"></div>';
this.bar = barContainer.querySelector(
'[role="bar"]'
);
if (!this.bar) {
return null;
}
const percentage = this.toBarPercentage(
fromStart ? 0 : (_a = this.progressN) != null ? _a : 0
);
this.bar.style.background = this.settings.color;
if (typeof this.settings.height === "number") {
this.bar.style.height = `${this.settings.height}px`;
} else {
this.bar.style.height = this.settings.height;
}
this.bar.style.position = "fixed";
this.bar.style.width = "100%";
this.bar.style.top = "0";
this.bar.style.left = "0";
this.bar.style.transition = "all 0 linear";
this.bar.style.transform = `translate3d(${percentage}%,0,0)`;
this.bar.style.boxShadow = (_b = this.settings.boxShadow) != null ? _b : "";
document.body.appendChild(barContainer);
return barContainer;
}, "createBar");
/**
* Creates and initializes a new spinner element in the DOM.
* It sets up the necessary styles and appends the element to the document body.
* @private
* @returns {void}
*/
this.createSpinner = /* @__PURE__ */ __name(() => {
if (document.getElementById("holy-progress-spinner") !== null) {
return;
}
const spinner = document.createElement("div");
spinner.id = "holy-progress-spinner";
spinner.style.pointerEvents = "none";
spinner.style.display = "block";
spinner.style.position = "fixed";
spinner.style.top = "15px";
spinner.style.right = "15px";
spinner.style.width = "18px";
spinner.style.height = "18px";
spinner.style.boxSizing = "border-box";
spinner.style.border = "solid 2px transparent";
spinner.style.borderTopColor = this.settings.color;
spinner.style.borderLeftColor = this.settings.color;
spinner.style.borderRadius = "50%";
spinner.style.animation = "holy-progress-spinner 400ms linear infinite";
const keyframes = `
holy-progress-spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
const style = document.createElement("style");
style.innerHTML = keyframes;
spinner.appendChild(style);
document.body.appendChild(spinner);
}, "createSpinner");
this.getOrCreateBar = /* @__PURE__ */ __name((fromStart) => {
var _a;
return (_a = document.getElementById("holy-progress")) != null ? _a : this.createBar(fromStart);
}, "getOrCreateBar");
this.removeBarFromDOM = /* @__PURE__ */ __name(() => {
var _a;
return (_a = document.getElementById("holy-progress")) == null ? void 0 : _a.remove();
}, "removeBarFromDOM");
this.removeSpinnerFromDOM = /* @__PURE__ */ __name(() => {
var _a;
return (_a = document.getElementById("holy-progress-spinner")) == null ? void 0 : _a.remove();
}, "removeSpinnerFromDOM");
/**
* Determines the most suitable CSS positioning strategy based on browser capabilities.
* Checks for transform properties with vendor prefixes and standard un-prefixed properties.
* @private
* @returns {TransformStrategy} - The optimal CSS positioning strategy ('translate3d', 'translate', or 'margin').
*/
this.getTransformStrategy = /* @__PURE__ */ __name(() => {
const style = document.body.style;
const prefixes = ["Webkit", "Moz", "ms", "O", ""];
let transformProp = "";
for (let i = 0; i < prefixes.length; i++) {
if (`${prefixes[i]}Transform` in style) {
transformProp = prefixes[i];
break;
}
}
if (transformProp !== "" && `${transformProp}Perspective` in style) {
return "translate3d";
}
if (transformProp !== "") {
return "translate";
}
return "margin";
}, "getTransformStrategy");
/**
* Generates the CSS for the progress bar position based on the detected positioning strategy.
* Dynamically sets the transform or margin-left properties for the bar's position.
* @private
* @param {number} n - Position value of the bar, as a number between 0 and 1.
* @returns {Object} - CSS styles for the progress bar.
*/
this.barPositionCSS = /* @__PURE__ */ __name((n) => {
const transformStrategy = this.getTransformStrategy();
const barPosition = `${this.toBarPercentage(n)}%`;
if (transformStrategy === "translate3d") {
return { transform: `translate3d(${barPosition},0,0)` };
}
if (transformStrategy === "translate") {
return { transform: `translate(${barPosition},0)` };
}
return { marginLeft: barPosition };
}, "barPositionCSS");
this.settings = __spreadValues(__spreadValues({}, DEFAULTS), customSettings);
this.progressN = null;
this.bar = null;
}
};
__name(_HolyProgress, "HolyProgress");
var HolyProgress = _HolyProgress;
// src/index.tsx
var startHolyLoader = /* @__PURE__ */ __name(() => {
document.dispatchEvent(new Event(START_HOLY_EVENT));
}, "startHolyLoader");
var stopHolyLoader = /* @__PURE__ */ __name(() => {
document.dispatchEvent(new Event(STOP_HOLY_EVENT));
}, "stopHolyLoader");
var HolyLoader = /* @__PURE__ */ __name(({
color = DEFAULTS.color,
initialPosition = DEFAULTS.initialPosition,
height = DEFAULTS.height,
easing = DEFAULTS.easing,
speed = DEFAULTS.speed,
zIndex = DEFAULTS.zIndex,
boxShadow = DEFAULTS.boxShadow,
showSpinner = DEFAULTS.showSpinner,
ignoreSearchParams = DEFAULTS.ignoreSearchParams,
dir = DEFAULTS.dir
}) => {
const holyProgressRef = React.useRef(null);
React.useEffect(() => {
const startProgress = /* @__PURE__ */ __name(() => {
if (holyProgressRef.current === null) {
return;
}
try {
holyProgressRef.current.start();
} catch (error) {
}
}, "startProgress");
const stopProgress = /* @__PURE__ */ __name(() => {
if (holyProgressRef.current === null) {
return;
}
try {
holyProgressRef.current.complete();
} catch (error) {
}
}, "stopProgress");
let isHistoryPatched = false;
const stopProgressOnHistoryUpdate = /* @__PURE__ */ __name(() => {
if (isHistoryPatched) {
return;
}
const originalPushState = history.pushState.bind(history);
history.pushState = (...args) => {
const url = args[2];
if (url && isSamePathname(window.location.href, url) && (ignoreSearchParams || hasSameQueryParameters(window.location.href, url))) {
originalPushState(...args);
return;
}
stopProgress();
originalPushState(...args);
};
const originalReplaceState = history.replaceState.bind(history);
history.replaceState = (...args) => {
const url = args[2];
if (url && isSamePathname(window.location.href, url) && (ignoreSearchParams || hasSameQueryParameters(window.location.href, url))) {
originalReplaceState(...args);
return;
}
stopProgress();
originalReplaceState(...args);
};
isHistoryPatched = true;
}, "stopProgressOnHistoryUpdate");
const handleClick = /* @__PURE__ */ __name((event) => {
try {
const target = event.target;
const anchor = target.closest("a");
const anchorOpensExternally = (anchor == null ? void 0 : anchor.target) && anchor.target !== "_self";
if (anchor === null || anchorOpensExternally || anchor.hasAttribute("download") || event.ctrlKey || event.metaKey || // Skip if URL points to a different domain
!isSameHost(window.location.href, anchor.href) || // Skip if URL is a same-page anchor (href="#", href="#top", etc.).
isSamePageAnchor(window.location.href, anchor.href) || // Skip if URL uses a non-http/https protocol (mailto:, tel:, etc.).
!toAbsoluteURL(anchor.href).startsWith("http") || // Skip if the URL is the same as the current page
isSamePathname(window.location.href, anchor.href) && (ignoreSearchParams || hasSameQueryParameters(window.location.href, anchor.href))) {
return;
}
startProgress();
} catch (error) {
stopProgress();
}
}, "handleClick");
try {
if (holyProgressRef.current === null) {
holyProgressRef.current = new HolyProgress({
color,
height,
initialPosition,
easing,
speed,
zIndex,
boxShadow,
showSpinner,
dir
});
}
document.addEventListener("click", handleClick);
document.addEventListener(START_HOLY_EVENT, startProgress);
document.addEventListener(STOP_HOLY_EVENT, stopProgress);
stopProgressOnHistoryUpdate();
} catch (error) {
}
return () => {
document.removeEventListener("click", handleClick);
document.removeEventListener(START_HOLY_EVENT, startProgress);
document.removeEventListener(STOP_HOLY_EVENT, stopProgress);
};
}, [holyProgressRef]);
return null;
}, "HolyLoader");
var src_default = HolyLoader;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
startHolyLoader,
stopHolyLoader
});