@trimble-oss/moduswebcomponents
Version:
Modus Web Components is a modern, accessible UI library built with Stencil JS that provides reusable web components following Trimble's Modus design system. This updated version focuses on improved flexibility, enhanced theming options, comprehensive cust
331 lines (323 loc) • 10.9 kB
JavaScript
;
var index = require('./index-CtEGR9Z7.js');
const appendToMap = (map, propName, value) => {
let refs = map.get(propName);
if (!refs) {
refs = [];
map.set(propName, refs);
}
if (!refs.some((ref) => ref.deref() === value)) {
refs.push(new WeakRef(value));
}
};
const debounce = (fn, ms) => {
let timeoutId;
return (...args) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
timeoutId = 0;
fn(...args);
}, ms);
};
};
/**
* Check if a possible element isConnected.
* The property might not be there, so we check for it.
*
* We want it to return true if isConnected is not a property,
* otherwise we would remove these elements and would not update.
*
* Better leak in Edge than to be useless.
*/
const isConnected = (maybeElement) => !('isConnected' in maybeElement) || maybeElement.isConnected;
const cleanupElements = debounce((map) => {
for (let key of map.keys()) {
const refs = map.get(key).filter((ref) => {
const elm = ref.deref();
return elm && isConnected(elm);
});
map.set(key, refs);
}
}, 2_000);
const core = index.StencilCore;
const forceUpdate = core.forceUpdate;
const getRenderingRef = core.getRenderingRef;
const stencilSubscription = () => {
if (typeof getRenderingRef !== 'function' || typeof forceUpdate !== 'function') {
// If we are not in a stencil project, we do nothing.
// This function is not really exported by @stencil/core.
return {};
}
const ensureForceUpdate = forceUpdate;
const ensureGetRenderingRef = getRenderingRef;
const elmsToUpdate = new Map();
return {
dispose: () => elmsToUpdate.clear(),
get: (propName) => {
const elm = ensureGetRenderingRef();
if (elm) {
appendToMap(elmsToUpdate, propName, elm);
}
},
set: (propName) => {
const refs = elmsToUpdate.get(propName);
if (refs) {
const nextRefs = refs.filter((ref) => {
const elm = ref.deref();
if (!elm)
return false;
return ensureForceUpdate(elm);
});
elmsToUpdate.set(propName, nextRefs);
}
cleanupElements(elmsToUpdate);
},
reset: () => {
elmsToUpdate.forEach((refs) => {
refs.forEach((ref) => {
const elm = ref.deref();
if (elm)
ensureForceUpdate(elm);
});
});
cleanupElements(elmsToUpdate);
},
};
};
const unwrap = (val) => (typeof val === 'function' ? val() : val);
const createObservableMap = (defaultState, shouldUpdate = (a, b) => a !== b) => {
const resolveDefaultState = () => (unwrap(defaultState) ?? {});
const initialState = resolveDefaultState();
let states = new Map(Object.entries(initialState));
const proxyAvailable = typeof Proxy !== 'undefined';
const plainState = proxyAvailable ? null : {};
const handlers = {
dispose: [],
get: [],
set: [],
reset: [],
};
// Track onChange listeners to enable removeListener functionality
const changeListeners = new Map();
const reset = () => {
// When resetting the state, the default state may be a function - unwrap it to invoke it.
// otherwise, the state won't be properly reset
states = new Map(Object.entries(resolveDefaultState()));
if (!proxyAvailable) {
syncPlainStateKeys();
}
handlers.reset.forEach((cb) => cb());
};
const dispose = () => {
// Call first dispose as resetting the state would
// cause less updates ;)
handlers.dispose.forEach((cb) => cb());
reset();
};
const get = (propName) => {
handlers.get.forEach((cb) => cb(propName));
return states.get(propName);
};
const set = (propName, value) => {
const oldValue = states.get(propName);
if (shouldUpdate(value, oldValue, propName)) {
states.set(propName, value);
if (!proxyAvailable) {
ensurePlainProperty(propName);
}
handlers.set.forEach((cb) => cb(propName, value, oldValue));
}
};
const state = (proxyAvailable
? new Proxy(initialState, {
get(_, propName) {
return get(propName);
},
ownKeys(_) {
return Array.from(states.keys());
},
getOwnPropertyDescriptor() {
return {
enumerable: true,
configurable: true,
};
},
has(_, propName) {
return states.has(propName);
},
set(_, propName, value) {
set(propName, value);
return true;
},
})
: (() => {
syncPlainStateKeys();
return plainState;
})());
const on = (eventName, callback) => {
handlers[eventName].push(callback);
return () => {
removeFromArray(handlers[eventName], callback);
};
};
const onChange = (propName, cb) => {
const setHandler = (key, newValue) => {
if (key === propName) {
cb(newValue);
}
};
const resetHandler = () => {
const snapshot = resolveDefaultState();
cb(snapshot[propName]);
};
// Register the handlers
const unSet = on('set', setHandler);
const unReset = on('reset', resetHandler);
// Track the relationship between the user callback and internal handlers
changeListeners.set(cb, { setHandler, resetHandler, propName });
return () => {
unSet();
unReset();
changeListeners.delete(cb);
};
};
const use = (...subscriptions) => {
const unsubs = subscriptions.reduce((unsubs, subscription) => {
if (subscription.set) {
unsubs.push(on('set', subscription.set));
}
if (subscription.get) {
unsubs.push(on('get', subscription.get));
}
if (subscription.reset) {
unsubs.push(on('reset', subscription.reset));
}
if (subscription.dispose) {
unsubs.push(on('dispose', subscription.dispose));
}
return unsubs;
}, []);
return () => unsubs.forEach((unsub) => unsub());
};
const forceUpdate = (key) => {
const oldValue = states.get(key);
handlers.set.forEach((cb) => cb(key, oldValue, oldValue));
};
const removeListener = (propName, listener) => {
const listenerInfo = changeListeners.get(listener);
if (listenerInfo && listenerInfo.propName === propName) {
// Remove the specific handlers that were created for this listener
removeFromArray(handlers.set, listenerInfo.setHandler);
removeFromArray(handlers.reset, listenerInfo.resetHandler);
changeListeners.delete(listener);
}
};
function ensurePlainProperty(key) {
if (proxyAvailable || !plainState) {
return;
}
if (Object.prototype.hasOwnProperty.call(plainState, key)) {
return;
}
Object.defineProperty(plainState, key, {
configurable: true,
enumerable: true,
get() {
return get(key);
},
set(value) {
set(key, value);
},
});
}
function syncPlainStateKeys() {
if (proxyAvailable || !plainState) {
return;
}
const knownKeys = new Set(states.keys());
for (const key of Object.keys(plainState)) {
if (!knownKeys.has(key)) {
delete plainState[key];
}
}
for (const key of knownKeys) {
ensurePlainProperty(key);
}
}
return {
state,
get,
set,
on,
onChange,
use,
dispose,
reset,
forceUpdate,
removeListener,
};
};
const removeFromArray = (array, item) => {
const index = array.indexOf(item);
if (index >= 0) {
array[index] = array[array.length - 1];
array.length--;
}
};
const createStore = (defaultState, shouldUpdate) => {
const map = createObservableMap(defaultState, shouldUpdate);
map.use(stencilSubscription());
return map;
};
// eslint-disable-next-line @typescript-eslint/unbound-method
const { state, onChange, reset, dispose } = createStore({
mode: 'light',
theme: 'modus-modern',
});
const themeStore = {
state,
onChange,
setTheme: (theme) => (state.theme = theme),
setMode: (mode) => (state.mode = mode),
reset,
dispose,
};
const getStoredMode = () => {
try {
const stored = localStorage.getItem('modus-theme-config');
return stored ? JSON.parse(stored).mode : null;
}
catch (error) {
console.warn('Failed to parse theme config from localStorage:', error);
return null;
}
};
const getSystemPreferredMode = () => {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return prefersDark ? 'dark' : 'light';
};
// Initialize theme store with system preferences and stored values
const initializeThemeStore = (initialConfig) => {
var _a, _b, _c;
// Set theme with default fallback
state.theme = (_a = initialConfig === null || initialConfig === void 0 ? void 0 : initialConfig.theme) !== null && _a !== void 0 ? _a : 'modus-modern';
// Mode resolution with priority: explicit config > stored preference > system preference
state.mode =
(_c = (_b = initialConfig === null || initialConfig === void 0 ? void 0 : initialConfig.mode) !== null && _b !== void 0 ? _b : getStoredMode()) !== null && _c !== void 0 ? _c : getSystemPreferredMode();
};
// Watch for system theme changes
const watchSystemTheme = () => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e) => {
if (!localStorage.getItem('preferred-mode')) {
state.mode = e.matches ? 'dark' : 'light';
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
};
exports.initializeThemeStore = initializeThemeStore;
exports.themeStore = themeStore;
exports.watchSystemTheme = watchSystemTheme;