diff --git a/dist/booster-pack.min.mjs b/dist/booster-pack.min.mjs new file mode 100644 index 0000000..a59ad04 --- /dev/null +++ b/dist/booster-pack.min.mjs @@ -0,0 +1,207 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: !0, configurable: !0, writable: !0, value }) : obj[key] = value; +var __publicField = (obj, key, value) => (__defNormalProp(obj, typeof key != "symbol" ? key + "" : key, value), value); +class Booster { + constructor(element = "", options = {}) { + __publicField(this, "mounted", !1); + __publicField(this, "elm", null); + __publicField(this, "target", null); + __publicField(this, "_state", {}); + this._options = options || {}, element && (this.elm = element); + } + get options() { + return this._options; + } + set options(defaults) { + let options = {}; + if (this.elm) { + let mount = document.querySelector(this.elm); + if (mount) { + let optionsFromAttribute = mount.dataset.options; + optionsFromAttribute && (options = JSON.parse(optionsFromAttribute)), mount = null; + } + } + this._options = { + ...this._options, + ...defaults, + ...options + }; + } + mount() { + } + unmount() { + } + refresh() { + this.unmount(), this.mount(); + } + get state() { + return console.warn("You should not get state manually. Use getState() instead."), this._state; + } + set state(state) { + console.warn("You should not change state manually. Use setState() instead."), this._state = state; + } + setState(scope = "local", changes) { + let stateChanges = {}, stateRef = this._state; + scope === "global" ? stateRef = Booster._globalState : scope === "component" && (Booster._globalState.hasOwnProperty(this.constructor.name) || (Booster._globalState[this.constructor.name] = {}), stateRef = Booster._globalState[this.constructor.name]), Object.keys(changes).forEach((key) => { + Array.isArray(changes[key]) ? stateRef[key] != null && Array.isArray(stateRef[key]) && stateRef[key].length === changes[key].length ? changes[key].some((item, index) => stateRef[key][index] !== item ? (stateChanges[key] = changes[key], stateRef[key] = stateChanges[key], !0) : !1) : (stateChanges[key] = changes[key], stateRef[key] = stateChanges[key]) : typeof changes[key] == "object" ? (stateRef[key] != null && typeof stateRef[key] == "object" ? (stateChanges[key] = {}, Object.keys(changes[key]).forEach((subkey) => { + stateRef[key][subkey] !== changes[key][subkey] && (stateChanges[key][subkey] = changes[key][subkey]); + })) : stateChanges[key] = changes[key], stateRef[key] = { + ...stateRef[key], + ...stateChanges[key] + }) : stateRef !== changes[key] && (stateChanges[key] = changes[key], stateRef[key] = changes[key]); + }), Object.keys(stateChanges).forEach((key) => { + Array.isArray(changes[key]) ? stateChanges[key].length === 0 && delete stateChanges[key] : typeof changes[key] == "object" && Object.keys(stateChanges[key]).length === 0 && delete stateChanges[key]; + }), stateRef = null, this.stateChange(stateChanges); + } + stateChange(changes) { + } + getState(scope = "local", defaults = {}) { + let stateRef = this._state; + return scope === "global" ? stateRef = Booster._globalState : scope === "component" && (Booster._globalState.hasOwnProperty(this.constructor.name) ? stateRef = Booster._globalState[this.constructor.name] : stateRef = {}), { + ...defaults, + ...stateRef + }; + } + destroyState(scope = "local") { + scope === "global" ? Booster._globalState = {} : scope === "component" ? Booster._globalState.hasOwnProperty(this.constructor.name) && (Booster._globalState[this.constructor.name] = {}) : this._state = {}; + } + css(urls) { + return Promise.all(urls.map(this._loadCSS)); + } + _loadCSS(href) { + return new Promise((resolve) => { + if (Booster._sheets.includes(href)) + return resolve(); + Booster._sheets.push(href); + let link = document.createElement("link"); + link.type = "text/css", link.rel = "stylesheet", link.onload = resolve, link.setAttribute("href", href), document.head.appendChild(link); + }); + } +} +Object.defineProperty(Booster, "_sheets", { + value: [], + writable: !0 +}); +Object.defineProperty(Booster, "_globalState", { + value: {}, + writable: !0 +}); +const event = (requirement) => new Promise((resolve) => { + let topic; + if (requirement.indexOf("(") !== -1) { + const topicStart = requirement.indexOf("(") + 1; + topic = requirement.slice(topicStart, -1); + } + topic ? document.body.addEventListener(topic, () => { + resolve(); + }, { once: !0 }) : resolve(); +}), idle = () => new Promise((resolve) => { + "requestIdleCallback" in window ? window.requestIdleCallback(resolve) : setTimeout(resolve, 200); +}), media = (requirement) => new Promise((resolve) => { + const queryStart = requirement.indexOf("("), query = requirement.slice(queryStart), mediaQuery = window.matchMedia(query); + mediaQuery.matches ? resolve() : mediaQuery.addEventListener("change", resolve, { once: !0 }); +}), visible = (selector = null, requirement) => selector ? new Promise((resolve) => { + let rootMargin = "0px 0px 0px 0px"; + if (requirement.indexOf("(") !== -1) { + const rootMarginStart = requirement.indexOf("(") + 1; + rootMargin = requirement.slice(rootMarginStart, -1); + } + const observer = new IntersectionObserver((entries) => { + entries[0].isIntersecting && (observer.disconnect(), resolve()); + }, { rootMargin }); + let elm = document.querySelector(selector); + elm ? observer.observe(elm) : resolve(); +}) : Promise.resolve(!0); +function loadStrategies(strategy, selector) { + let promises = []; + if (strategy) { + let requirements = strategy.split("|").map((requirement) => requirement.trim()).filter((requirement) => requirement !== "immediate").filter((requirement) => requirement !== "eager"); + for (let requirement of requirements) { + if (requirement.startsWith("event")) { + promises.push( + event(requirement) + ); + continue; + } + if (requirement === "idle") { + promises.push( + idle() + ); + continue; + } + if (requirement.startsWith("media")) { + promises.push( + media(requirement) + ); + continue; + } + requirement.startsWith("visible") && promises.push( + visible(selector, requirement) + ); + } + } + return promises; +} +class BoosterFactory extends Booster { + constructor() { + super(); + __publicField(this, "loaded", []); + __publicField(this, "config", {}); + this.config = { + origin: location.origin, + basePath: "scripts/boosts" + }; + let configMeta = document.querySelector('meta[name="booster-config"]') ?? null; + configMeta && (this.config = { + ...this.config, + ...JSON.parse(configMeta.content) + }), this.config.basePath = this.config.basePath.replace(/^\/|\/$/g, ""), this.mount(); + } + mount() { + let targetId = htmx.config.currentTargetId ?? "main", target = document.getElementById(targetId); + if (target) { + let components = target.querySelectorAll("[data-booster]"); + for (let el of components) + this.lazyload(el); + target = null, components = null; + } + } + unmount() { + let targetId = htmx.config.currentTargetId ?? "main", target = document.getElementById(targetId); + if (target) { + for (let i = this.loaded.length - 1; i >= 0; i--) { + let inTarget = target.querySelector(this.loaded[i].selector), inDocument = document.querySelector(this.loaded[i].selector); + (inTarget || !inDocument) && (this.loaded[i].instance.unmount(), this.loaded.splice(i, 1)); + } + target = null; + } + } + /** + * Import a component on demand, optionally using a loading strategy + * + * @param el + */ + lazyload(el) { + let component = el.dataset.booster, version = el.dataset.version ?? "1", strategy = el.dataset.load ?? null, selector = el.getAttribute("id") ? "#" + el.getAttribute("id") : '[data-booster="' + component + '"]', promises = loadStrategies(strategy, selector); + Promise.all(promises).then(() => { + import( + /* @vite-ignore */ + `${this.config.origin}/${this.config.basePath}/${component}.js?v=${version}` + ).then( + (lazyComponent) => { + let instance = new lazyComponent.default(selector); + instance.mounted = !0, this.loaded.push({ + name: component, + selector, + instance + }); + } + ); + }); + } +} +export { + Booster, + BoosterFactory, + loadStrategies +}; diff --git a/lib/ext/booster-pack.mjs b/lib/ext/booster-pack.mjs new file mode 100644 index 0000000..31e5164 --- /dev/null +++ b/lib/ext/booster-pack.mjs @@ -0,0 +1,10 @@ +/** + * Booster Pack extension - module exports + * + * @author Mark Croxton, Hallmark Design + */ + +import Booster from '../booster.js'; +import BoosterFactory from '../boosterFactory.js'; +import { loadStrategies } from '../loadStrategies.js'; +export { Booster, BoosterFactory, loadStrategies }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e1fa220..d3bc99e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "htmx-booster-pack", - "version": "1.0.5", + "version": "1.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "htmx-booster-pack", - "version": "1.0.5", + "version": "1.0.6", "license": "BSD 2-Clause", "devDependencies": { "@rollup/plugin-inject": "^5.0.5", diff --git a/package.json b/package.json index 88bae90..6862970 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "name": "htmx-booster-pack", - "version": "1.0.5", + "version": "1.0.6", "description": "Minimal component framework for htmx", "type": "module", "exports": { ".": { "import": "./dist/booster.min.js" + }, + "./booster-pack": { + "import": "./dist/booster-pack.min.mjs" } }, "files": [ @@ -13,7 +16,7 @@ ], "scripts": { "dev": "vite", - "build": "vite build", + "build": "vite build && vite build --config vite.config.pack.js --emptyOutDir=false", "preview": "vite preview" }, "repository": { diff --git a/vite.config.pack.js b/vite.config.pack.js new file mode 100644 index 0000000..de294a8 --- /dev/null +++ b/vite.config.pack.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; + +export default defineConfig(({}) => { + return { + esbuild: { + minifyIdentifiers: false + }, + build: { + lib: { + entry: { + "booster-pack": "./lib/ext/booster-pack.mjs" + }, + formats: ["es"], + fileName: (format, name) => `${name}.min.mjs` + }, + minify: true + } + } +}); \ No newline at end of file