@agnos-ui/core
Version:
Framework-agnostic headless component library.
527 lines (526 loc) • 17.3 kB
JavaScript
;
const esmEnv = require("esm-env");
const tansu = require("@amadeus-it-group/tansu");
const utils_func = require("./utils/func.cjs");
const importantSuffixRegExp = /\s*!important$/;
const withoutImportant = (value) => value?.replace(importantSuffixRegExp, "") ?? "";
const toKebabCase = (str) => str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
const getStyleKeyFromDirectProp = (str) => {
if (typeof str !== "string" || str.startsWith("--")) {
return void 0;
}
return toKebabCase(str);
};
const getStyleKeyFromMethods = (str) => {
if (!str.startsWith("--") && str !== toKebabCase(str)) {
return void 0;
}
return str;
};
const ssrHTMLElementStyle = /* @__PURE__ */ Symbol("style");
class SSRStyle {
// all styles in kebab-case, including custom properties:
[ssrHTMLElementStyle] = {};
constructor() {
return new Proxy(this, {
get: (target, prop) => {
const styleKey = prop in target ? void 0 : getStyleKeyFromDirectProp(prop);
if (styleKey) {
return withoutImportant(target[ssrHTMLElementStyle][styleKey]);
} else {
return target[prop];
}
},
set: (target, prop, value) => {
const styleKey = prop in target ? void 0 : getStyleKeyFromDirectProp(prop);
if (styleKey) {
target.setProperty(styleKey, value);
} else {
target[prop] = value;
}
return true;
}
});
}
setProperty(property, value, priority) {
if (!value) {
this.removeProperty(property);
return;
}
const key = getStyleKeyFromMethods(property);
if (!key || priority !== "important" && priority !== "" && priority != null || importantSuffixRegExp.test(value)) {
return;
}
this[ssrHTMLElementStyle][key] = value + (priority ? " !important" : "");
}
removeProperty(property) {
const key = getStyleKeyFromMethods(property);
if (!key) {
return "";
}
const style = this[ssrHTMLElementStyle];
const value = style[key];
delete style[key];
return withoutImportant(value);
}
}
const ssrHTMLElementAttributesAndStyle = /* @__PURE__ */ Symbol("attributesAndStyle");
const spaceRegExp = /\s+/;
const ssrHTMLElement = () => {
const attributes = {};
const style = new SSRStyle();
let classNames = /* @__PURE__ */ new Set();
const toggleClass = (className, force = !classNames.has(className)) => {
if (force) {
classNames.add(className);
} else {
classNames.delete(className);
}
return !!force;
};
const toggleAll = (force) => (...classNames2) => classNames2.forEach((className) => toggleClass(className, force));
return {
style,
classList: {
add: toggleAll(true),
remove: toggleAll(false),
toggle: toggleClass
},
setAttribute(name, value) {
if (name === "class") {
classNames = new Set(value.trim().split(spaceRegExp));
} else if (name === "style") {
throw new Error('setAttribute("style",...) is not implemented in ssrHTMLElement. Use the style property instead.');
} else {
attributes[name] = value;
}
},
removeAttribute(name) {
if (name === "class") {
classNames = /* @__PURE__ */ new Set();
} else {
delete attributes[name];
}
},
[ssrHTMLElementAttributesAndStyle]() {
return { attributes: { ...attributes }, classNames: [...classNames], style: { ...style[ssrHTMLElementStyle] } };
}
};
};
const cssEscapeStyleName = (styleName) => styleName.replace(/([^\w-])/gi, "\\$1");
const cssTextFromObject = (style) => Object.entries(style).filter(([, value]) => !!value).map(([name, value]) => `${cssEscapeStyleName(name)}: ${value};`).join("");
function r(e) {
var t, f, n = "";
if ("string" == typeof e || "number" == typeof e) n += e;
else if ("object" == typeof e) if (Array.isArray(e)) {
var o = e.length;
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
} else for (f in e) e[f] && (n && (n += " "), n += f);
return n;
}
function clsx() {
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
return n;
}
const isBrowserHTMLElement = esmEnv.BROWSER ? (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
((element) => {
const contentWindow = element?.ownerDocument?.defaultView ?? window;
return element instanceof contentWindow.HTMLElement;
})
) : (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
((element) => false)
);
const browserDirective = esmEnv.BROWSER ? (directive) => (node, args) => {
if (isBrowserHTMLElement(node)) {
return directive(node, args);
}
} : (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
((directive) => utils_func.noop)
);
const bindDirective = (directive, directiveArg$) => (element) => {
let firstTime = true;
let instance;
const unsubscribe = directiveArg$.subscribe((value) => {
if (firstTime) {
firstTime = false;
instance = directive(element, value);
} else {
instance?.update?.(value);
}
});
return {
destroy() {
instance?.destroy?.();
unsubscribe();
}
};
};
const noArg = tansu.readable(void 0);
const bindDirectiveNoArg = (directive) => bindDirective(directive, noArg);
const mapDirectiveArg = (directive, fn) => (node, arg) => {
const instance = directive(node, fn(arg));
return {
update: (arg2) => {
instance?.update?.(fn(arg2));
},
destroy: () => instance?.destroy?.()
};
};
const directiveSubscribe = (store, asyncUnsubscribe = true) => () => {
const unsubscribe = store.subscribe(utils_func.noop);
return {
destroy: async () => {
if (asyncUnsubscribe) {
await Promise.resolve();
}
unsubscribe();
}
};
};
const directiveUpdate = (update) => (_element, arg) => {
update(arg);
return {
update
};
};
const equalOption = { equal: Object.is };
const registrationArray = () => {
const elements$ = tansu.writable([], equalOption);
return tansu.asReadable(elements$, {
/**
* Add the given element to the array.
* @param element - Element to be added to the array.
* @returns A function to remove the element from the array.
*/
register: (element) => {
let removed = false;
elements$.update((currentElements) => [...currentElements, element]);
return () => {
if (!removed) {
removed = true;
elements$.update((currentElements) => {
const index = currentElements.indexOf(element);
if (index > -1) {
const copy = [...currentElements];
copy.splice(index, 1);
return copy;
}
return currentElements;
});
}
};
}
});
};
const createConditionalStoreArrayDirective = () => {
const elements$ = registrationArray();
return {
elements$: tansu.asReadable(elements$),
directive: (element, enabled = true) => {
let destroyElements;
const update = (newEnabled = true) => {
if (!!newEnabled != !!destroyElements) {
if (newEnabled) {
destroyElements = elements$.register(element);
} else {
destroyElements?.();
destroyElements = void 0;
}
}
};
update(enabled);
return {
destroy: () => update(false),
update
};
}
};
};
const createStoreArrayDirective = () => {
const { directive, elements$ } = createConditionalStoreArrayDirective();
return {
directive: bindDirectiveNoArg(directive),
elements$
};
};
const createBrowserStoreArrayDirective = () => {
const { directive, elements$ } = createStoreArrayDirective();
return { directive: browserDirective(directive), elements$ };
};
const createConditionalBrowserStoreArrayDirective = () => {
const { directive, elements$ } = createConditionalStoreArrayDirective();
return { directive: browserDirective(directive), elements$ };
};
const createStoreDirective = () => {
const element$ = tansu.writable(null, equalOption);
return {
element$: tansu.asReadable(element$),
directive: (element) => {
let valid = false;
element$.update((currentElement) => {
if (currentElement) {
console.error("The directive cannot be used on multiple elements.", currentElement, element);
return currentElement;
}
valid = true;
return element;
});
return valid ? {
destroy() {
element$.update((currentElement) => element === currentElement ? null : currentElement);
}
} : void 0;
}
};
};
const createBrowserStoreDirective = () => {
const { directive, element$ } = createStoreDirective();
return { directive: browserDirective(directive), element$ };
};
const conditionalDirective = (directive, condition) => (element, arg) => {
const instance$ = tansu.derived(
condition,
(enabled, set) => {
const instance = enabled ? directive(element, arg) : void 0;
set(instance);
return () => instance?.destroy?.();
},
void 0
);
const destroy = instance$.subscribe(() => {
});
return {
update(newArg) {
arg = newArg;
instance$()?.update?.(newArg);
},
destroy
};
};
const mergeDirectives = (...args) => (element, arg) => {
const instances = tansu.batch(() => args.map((directive) => directive(element, arg)));
return {
update(arg2) {
tansu.batch(() => instances.forEach((instance) => instance?.update?.(arg2)));
},
destroy() {
tansu.batch(() => instances.reverse().forEach((instance) => instance?.destroy?.()));
}
};
};
const multiDirective = (element, directives) => {
const instances = [];
const update = (directives2) => tansu.batch(() => {
directives2.forEach((directiveWithArg, index) => {
const [directive, arg] = Array.isArray(directiveWithArg) ? directiveWithArg : [directiveWithArg, void 0];
const oldInstance = instances[index];
if (oldInstance) {
if (oldInstance.directive === directive) {
if (oldInstance.arg !== arg) {
oldInstance.instance?.update?.(arg);
oldInstance.arg = arg;
}
return;
}
oldInstance.instance?.destroy?.();
}
const instance = directive(element, arg);
instances[index] = { directive, instance, arg };
});
const extraInstances = instances.splice(directives2.length);
extraInstances.reverse().forEach(({ instance }) => instance?.destroy?.());
});
update(directives);
return {
update,
destroy: () => update([])
};
};
const createAttributesDirective = (propsFn) => (node, args) => {
const unsubscribers = [];
const args$ = tansu.writable(args);
const { events, attributes, styles, classNames } = propsFn(args$);
if (isBrowserHTMLElement(node)) {
for (const [type, event] of Object.entries(events ?? {})) {
if (typeof event === "function") {
unsubscribers.push(addEvent(node, type, event));
} else {
unsubscribers.push(addEvent(node, type, event.handler, event.options));
}
}
}
for (const [attributeName, value] of Object.entries(attributes ?? {})) {
if (value != null) {
unsubscribers.push(bindAttribute(node, attributeName, value));
}
}
for (const [styleName, value] of Object.entries(styles ?? {})) {
if (value) {
unsubscribers.push(bindStyle(node, styleName, value));
}
}
for (const [className, value] of Object.entries(classNames ?? {})) {
unsubscribers.push(bindClassName(node, className, value));
}
return {
update: (args2) => args$.set(args2),
destroy: () => unsubscribers.forEach((fn) => fn())
};
};
const attributesData = (...directives) => {
const instances = [];
try {
const element = ssrHTMLElement();
for (const directive of directives) {
instances.push(Array.isArray(directive) ? directive[0](element, directive[1]) : directive(element));
}
return element[ssrHTMLElementAttributesAndStyle]();
} finally {
instances.forEach((instance) => instance?.destroy?.());
}
};
const classDirective = createAttributesDirective((className$) => ({
attributes: { class: tansu.computed(() => clsx(className$())) }
}));
function directiveAttributes(...directives) {
const { attributes, classNames, style } = attributesData(...directives);
if (classNames.length) {
attributes["class"] = classNames.join(" ");
}
const stringStyle = cssTextFromObject(style);
if (stringStyle.length) {
attributes["style"] = stringStyle;
}
return attributes;
}
const ssrAttributes = esmEnv.BROWSER ? () => ({}) : directiveAttributes;
const computeCommonAncestor = (elements) => {
const length = elements.length;
if (length === 0) return null;
let ancestor = elements[0];
for (let i = 1; i < length && ancestor; i++) {
const element = elements[i];
while (ancestor) {
if (ancestor === element) {
break;
}
const comparison = ancestor.compareDocumentPosition(element);
if (comparison & Node.DOCUMENT_POSITION_CONTAINED_BY) {
break;
} else if (comparison & Node.DOCUMENT_POSITION_CONTAINS) {
ancestor = element;
break;
} else if (comparison & Node.DOCUMENT_POSITION_DISCONNECTED) {
return null;
}
ancestor = ancestor.parentElement;
}
}
return ancestor;
};
const reflow = esmEnv.BROWSER ? (element = document.body) => {
if (isBrowserHTMLElement(element)) {
element.getBoundingClientRect();
}
} : utils_func.noop;
const addClasses = (element, classes) => {
if (classes && classes.length > 0) {
element.classList.add(...classes);
}
};
const removeClasses = (element, classes) => {
if (classes && classes.length > 0) {
element.classList.remove(...classes);
}
};
function addEvent(element, type, fn, options) {
element.addEventListener(type, fn, options);
return function() {
element.removeEventListener(type, fn, options);
};
}
let idCount = 0;
const generateId = () => `auId-${idCount++}`;
const notEmpty = (value) => value != null && value !== false;
function classNamesSubscribe(node, classNames$) {
let currentClassNames = /* @__PURE__ */ new Set();
return classNames$.subscribe((newClassName) => {
const classNames = new Set(`${newClassName ?? ""}`.split(" "));
classNames.delete("");
const classList = node.classList;
for (const className of currentClassNames) {
if (!classNames.has(className)) {
classList.remove(className);
}
}
if (classNames.size > 0) {
classList.add(...classNames);
}
currentClassNames = classNames;
});
}
function attributeSubscribe(node, attributeName, value$) {
return value$.subscribe((value) => {
if (notEmpty(value)) {
node.setAttribute(attributeName, "" + (value === true ? attributeName : value));
} else {
node.removeAttribute(attributeName);
}
});
}
function bindAttribute(node, attributeName, value$) {
const isClass = attributeName === "class";
return isClass ? classNamesSubscribe(node, value$) : attributeSubscribe(node, attributeName, value$);
}
const isCustomProperty = (styleName) => styleName.startsWith("--");
function bindStyle(node, styleName, value$) {
return value$.subscribe((value) => {
const style = node.style;
value = "" + (notEmpty(value) ? value : "");
if (isCustomProperty(styleName)) {
style.setProperty(styleName, value);
} else {
style[styleName] = value;
}
});
}
function bindClassName(node, className, value$) {
const unsubscribe = value$.subscribe((isPresent) => {
node.classList.toggle(className, isPresent);
});
return () => {
unsubscribe();
node.classList.remove(className);
};
}
exports.addClasses = addClasses;
exports.addEvent = addEvent;
exports.attributesData = attributesData;
exports.bindDirective = bindDirective;
exports.bindDirectiveNoArg = bindDirectiveNoArg;
exports.browserDirective = browserDirective;
exports.classDirective = classDirective;
exports.computeCommonAncestor = computeCommonAncestor;
exports.conditionalDirective = conditionalDirective;
exports.createAttributesDirective = createAttributesDirective;
exports.createBrowserStoreArrayDirective = createBrowserStoreArrayDirective;
exports.createBrowserStoreDirective = createBrowserStoreDirective;
exports.createConditionalBrowserStoreArrayDirective = createConditionalBrowserStoreArrayDirective;
exports.createConditionalStoreArrayDirective = createConditionalStoreArrayDirective;
exports.createStoreArrayDirective = createStoreArrayDirective;
exports.createStoreDirective = createStoreDirective;
exports.directiveAttributes = directiveAttributes;
exports.directiveSubscribe = directiveSubscribe;
exports.directiveUpdate = directiveUpdate;
exports.generateId = generateId;
exports.isBrowserHTMLElement = isBrowserHTMLElement;
exports.mapDirectiveArg = mapDirectiveArg;
exports.mergeDirectives = mergeDirectives;
exports.multiDirective = multiDirective;
exports.reflow = reflow;
exports.registrationArray = registrationArray;
exports.removeClasses = removeClasses;
exports.ssrAttributes = ssrAttributes;