@evcc/icons
Version:
A collection of evcc icons for vehicles, meters, chargers, smartswitches, and heating systems with web components and SVG registry.
233 lines • 7.6 kB
JavaScript
// Configuration constants
const CONFIG = {
DEFAULT_ACCENT_COLOR: "#4eb84b",
DEFAULT_OUTLINE_COLOR: "#000",
INTERSECTION_ROOT_MARGIN: "50px",
INTERSECTION_THRESHOLD: 0.1,
};
// Simple cache for loaded icons
const iconCache = new Map();
export class EvccIcon extends HTMLElement {
static get observedAttributes() {
return ["type", "name", "accent-color", "outline-color", "size"];
}
constructor() {
super();
this._loading = false;
this._intersectionObserver = null;
this._isInViewport = false;
this._loadAttempted = false;
this._loadingPromise = null;
this._currentKey = null;
this._type = null;
this._name = null;
this.attachShadow({ mode: "open" });
this._setupIntersectionObserver();
}
connectedCallback() {
this._updateAttributes();
this.render();
this._startObserving();
}
disconnectedCallback() {
this._stopObserving();
}
attributeChangedCallback() {
this._updateAttributes();
if (!this._loading) {
this.render();
}
}
_updateAttributes() {
const type = this.getAttribute("type");
const name = this.getAttribute("name");
const newKey = type && name ? `${type}/${name}` : null;
// Reset loading state if the icon key has changed
if (newKey !== this._currentKey) {
this._currentKey = newKey;
this._loadAttempted = false;
this._loadingPromise = null;
}
this._type = type;
this._name = name;
}
get _currentIconKey() {
return this._type && this._name ? `${this._type}/${this._name}` : null;
}
_setupIntersectionObserver() {
// Only create observer if IntersectionObserver is supported
if (typeof IntersectionObserver !== "undefined") {
this._intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.target === this) {
this._isInViewport = entry.isIntersecting;
if (this._isInViewport && !this._loadAttempted) {
this._loadIcon();
}
}
});
}, {
// Load when the icon is 50px away from entering the viewport
rootMargin: CONFIG.INTERSECTION_ROOT_MARGIN,
// Trigger when at least 10% of the element is visible
threshold: CONFIG.INTERSECTION_THRESHOLD,
});
}
}
_startObserving() {
if (this._intersectionObserver) {
this._intersectionObserver.observe(this);
}
}
_stopObserving() {
if (this._intersectionObserver) {
this._intersectionObserver.unobserve(this);
}
}
_getSizeStyles() {
const size = this.getAttribute("size");
if (size) {
return `width: ${/^\d+$/.test(size) ? `${size}px` : size};`;
}
return "";
}
_getBaseHostStyles() {
return `
:host {
display: inline-block;
aspect-ratio: 1;
${this._getSizeStyles()}
}`;
}
_render(className, ariaLabel) {
this.shadowRoot.innerHTML = `
<style>
${this._getBaseHostStyles()}
.${className} {
width: 100%;
height: 100%;
background: white;
border-radius: 4px;
}
</style>
<div class="${className}"></div>
`;
this.setAttribute("aria-label", ariaLabel);
}
async render() {
if (!this._type || !this._name) {
this._renderError("Both type and name attributes are required");
return;
}
const key = this._currentIconKey;
// Check if already loaded from cache
if (iconCache.has(key)) {
this._renderIcon(key);
return;
}
// If IntersectionObserver is not supported, load immediately
if (typeof IntersectionObserver === "undefined") {
await this._loadIcon();
return;
}
// For lazy loading, check if in viewport
if (this._isInViewport && !this._loadAttempted) {
await this._loadIcon();
}
else {
this._renderPlaceholder();
}
}
async _loadIcon() {
if (this._loadAttempted || this._loadingPromise) {
return this._loadingPromise || Promise.resolve();
}
this._loadAttempted = true;
this._loading = true;
const key = this._currentIconKey;
// Show loading state
this._renderLoading();
this._loadingPromise = (async () => {
try {
// Import the registry
const registry = (await import("./svg-registry.js")).default;
// Try to find the specific icon first
let iconLoader = registry[key];
let attemptedKey = key;
if (!iconLoader) {
const genericKey = `${this._type}/generic.ext`;
iconLoader = registry[genericKey];
attemptedKey = genericKey;
}
if (!iconLoader) {
this._renderError(`Icon not found: ${key} (and no generic fallback available)`);
return;
}
const iconModule = await iconLoader();
const svgString = iconModule.default;
if (svgString) {
// Cache the icon content under the requested key
iconCache.set(key, svgString);
this._renderIcon(key);
}
else {
this._renderError(`Failed to load icon: ${attemptedKey}`);
}
}
catch (error) {
console.error(`Error loading icon ${key}:`, error);
this._renderError(`Error loading icon: ${key}`);
}
finally {
this._loading = false;
}
})();
return this._loadingPromise;
}
_renderIcon(key) {
const svgContent = iconCache.get(key);
if (!svgContent) {
this._renderError(`Icon not loaded: ${key}`);
return;
}
const accentColor = this.getAttribute("accent-color") || CONFIG.DEFAULT_ACCENT_COLOR;
const outlineColor = this.getAttribute("outline-color") || CONFIG.DEFAULT_OUTLINE_COLOR;
this.shadowRoot.innerHTML = `
<style>
${this._getBaseHostStyles()}
:host {
--evcc-accent-color: ${accentColor};
--evcc-outline-color: ${outlineColor};
}
svg {
width: 100%;
height: 100%;
}
</style>
${svgContent}
`;
// Set aria-label for accessibility
this.setAttribute("aria-label", `${this._type} ${this._name}`);
this.setAttribute("role", "img");
}
_renderPlaceholder() {
this._render("placeholder", "Loading icon");
}
_renderLoading() {
this._render("loading", "Loading icon");
}
_renderError(message) {
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
font-size: 0.8em;
color: #d32f2f;
}
</style>
<span>${message}</span>
`;
}
}
export default EvccIcon;
//# sourceMappingURL=evcc-icon.js.map