All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.last-icon.js Maven / Gradle / Ivy

The newest version!
const JSDELIVR = "https://cdn.jsdelivr.net/";
const CACHE = {};

/**
 * @typedef IconSet
 * @property {String} alias Short two letters alias
 * @property {Function} svgPath The svg path
 * @property {Boolean} [fixFill] Does this set needs fixing fill:currentColor ?
 * @property {String} [useStroke] Add stroke to svg
 * @property {String} [defaultStroke] Default stroke to use (if supports stroke)
 * @property {String} [defaultType] Default type to use (when there are multiple types)
 * @property {Object.} [prefixes] Types to prefixes
 * @property {Function} [fontClass] Font class
 * @property {Boolean} [opticalFont] Is an optical font?
 * @property {String} [name] Full name (injected automatically)
 */

/**
 * @typedef Options
 * @property {Boolean} debug Should we output messages to console
 * @property {Boolean} lazy Load icons lazily
 * @property {Object} replaceName Transparently replace icons with other values
 * @property {Array} fonts Icon sets using font icons rather than svg
 * @property {String} defaultSet Default icon set
 * @property {Object.} sets Available iconsets
 */
const options = {
  debug: false,
  lazy: true,
  replaceName: {},
  fonts: [],
  defaultSet: "tabler",
  defaultStroke: 2,
  sets: {
    bootstrap: {
      alias: "bs",
      svgPath: () => JSDELIVR + "npm/bootstrap-icons@1/icons/{icon}.svg",
    },
    boxicons: {
      alias: "bx",
      // types: ["solid", "regular", "logos"],
      defaultType: "solid",
      svgPath: () => JSDELIVR + "npm/boxicons@2/svg/{type}/{prefix}-{icon}.svg",
      fixFill: true,
      fontClass: () => "bx {prefix}-{icon}",
      prefixes: {
        solid: "bxs",
        regular: "bx",
        logos: "bxl",
      },
    },
    bytesize: {
      alias: "by",
      svgPath: () => JSDELIVR + "npm/bytesize-icons@1/dist/icons/{icon}.svg",
      useStroke: true,
    },
    cssgg: {
      alias: "gg",
      svgPath: () => JSDELIVR + "npm/css.gg@2/icons/svg/{icon}.svg",
    },
    emojicc: {
      alias: "em",
      svgPath: () => JSDELIVR + "npm/emoji-cc@1/svg/{icon}.svg",
    },
    eos: {
      alias: "eo",
      // types: ["solid", "outlined", "animated"],
      defaultType: "solid",
      svgPath: () => JSDELIVR + "gh/lekoala/eos-icons-mirror/{type}/{icon}.svg",
      fixFill: true,
    },
    feather: {
      alias: "ft",
      svgPath: () => JSDELIVR + "npm/feather-icons@4/dist/icons/{icon}.svg",
    },
    flags: {
      alias: "fl",
      // types: ["4x3", "1x1"],
      defaultType: "4x3",
      svgPath: () => JSDELIVR + "npm/flag-svg-collection@1/flags/{type}/{icon}.svg",
    },
    fontawesome: {
      alias: "fa",
      // types: ["solid", "regular", "brands", "light", "duotone"],
      defaultType: "solid",
      svgPath: () => JSDELIVR + "npm/@fortawesome/fontawesome-free@5/svgs/{type}/{icon}.svg",
      fixFill: true,
      fontClass: () => "{prefix} fa-{icon}",
      prefixes: {
        solid: "fas",
        regular: "far",
        light: "fal",
        duotone: "fad",
        brands: "fab",
      },
    },
    iconoir: {
      alias: "in",
      svgPath: () => JSDELIVR + "gh/lucaburgio/iconoir/icons/{icon}.svg",
      fontClass: () => "iconoir-{icon}",
      useStroke: true,
    },
    iconpark: {
      alias: "ip",
      types: [], // see full list here https://github.com/bytedance/IconPark/tree/master/source
      svgPath: () => JSDELIVR + "gh/bytedance/IconPark/source/{type}/{icon}.svg",
      useStroke: true,
    },
    lucide: {
      alias: "lu",
      svgPath: () => JSDELIVR + "npm/lucide-static/icons/{icon}.svg",
    },
    material: {
      alias: "mi",
      // types: ["filled", "outlined", "round", "sharp", "two-tone"],
      defaultType: "filled",
      svgPath: () => JSDELIVR + "npm/@material-design-icons/svg/{type}/{icon}.svg",
      fontClass: (type) => {
        if (type === "filled") {
          return "material-icons";
        }
        return "material-icons-{type}";
      },
    },
    phosphor: {
      alias: "ph",
      // types: ["regular", "bold", "duotone", "fill", "light", "thin"],
      defaultType: "regular",
      svgPath: (type) => {
        if (type === "regular") {
          return JSDELIVR + "npm/@phosphor-icons/core@2/assets/{type}/{icon}.svg";
        }
        return JSDELIVR + "npm/@phosphor-icons/core@2/assets/{type}/{icon}-{type}.svg";
      },
      fontClass: (type) => {
        if (type === "regular") {
          return "ph ph-{icon}";
        }
        return "ph-{type} ph-{icon}";
      },
    },
    supertiny: {
      alias: "st",
      svgPath: () => JSDELIVR + "npm/super-tiny-icons/images/svg/{icon}.svg",
    },
    symbols: {
      alias: "ms",
      // types: ["outlined", "rounded", "sharp"],
      defaultType: "outlined",
      svgPath: () => JSDELIVR + "npm/@material-symbols/[email protected]/{type}/{icon}.svg",
      fixFill: true,
      fontClass: () => "material-symbols-{type}",
      opticalFont: true,
    },
    tabler: {
      alias: "ti",
      svgPath: () => JSDELIVR + "npm/@tabler/icons@2/icons/{icon}.svg",
      useStroke: true,
      fontClass: () => "ti ti-{icon}",
    },
  },
};

/**
 * @var {IntersectionObserver}
 */
const observer = new window.IntersectionObserver((entries, observerRef) => {
  entries.forEach(async (entry) => {
    if (entry.isIntersecting) {
      observerRef.unobserve(entry.target);
      entry.target.init();
    }
  });
});

/**
 * @param {string} value
 * @param {string} iconName
 * @param {IconSet} iconSet
 * @param {string} iconType
 * @return {string}
 */
function replacePlaceholders(value, iconName, iconSet, iconType) {
  value = value.replace("{icon}", iconName);
  if (iconType) {
    value = value.replaceAll("{type}", iconType);
  } else {
    // Maybe we want to remove the type like in material icons
    value = value.replace("-{type}", "");
  }
  if (iconSet.prefixes && iconSet.prefixes[iconType]) {
    value = value.replace("{prefix}", iconSet.prefixes[iconType]);
  }
  return value;
}

function log(message) {
  if (options.debug) {
    console.log(`[l-i] ${message}`);
  }
}

/**
 * @param {string} iconName
 * @param {IconSet} iconSet
 * @param {string} iconType
 * @return {Promise}
 */
function getIconSvg(iconName, iconSet, iconType) {
  let iconUrl = iconSet.svgPath(iconType);
  if (!iconUrl) {
    throw Error(`Icon set ${iconSet} does not exists`);
  }
  const cacheKey = `${iconSet.name}-${iconName}-${iconType || "base"}`;
  iconUrl = replacePlaceholders(iconUrl, iconName, iconSet, iconType);

  // If we have it in cache
  if (iconUrl && CACHE[cacheKey]) {
    log(`Fetching ${cacheKey} from cache`);
    return CACHE[cacheKey];
  }

  // Or resolve
  log(`Fetching ${cacheKey} from url ${iconUrl}`);
  CACHE[cacheKey] = fetch(iconUrl).then(function (response) {
    if (response.ok) {
      return response.text();
    }
    throw Error(response.status);
  });
  return CACHE[cacheKey];
}

/**
 * @param {LastIcon} inst
 * @param {string} iconName
 * @param {IconSet} iconSet
 * @param {string} iconType
 */
function refreshIcon(inst, iconName, iconSet, iconType) {
  // Replace name
  if (options.replaceName[iconName]) {
    iconName = options.replaceName[iconName];
  }
  // Set default type if any
  if (!iconType && iconSet.defaultType) {
    iconType = iconSet.defaultType;
  }

  // Use font (if not using a specific stroke)
  if (options.fonts.includes(iconSet.name) && !inst.hasAttribute("stroke")) {
    log(`Using font for ${iconName}`);
    let iconClass = iconSet.fontClass(iconType);
    let nameAsClass = iconClass.includes("{icon}");
    iconClass = replacePlaceholders(iconClass, iconName, iconSet, iconType);
    if (nameAsClass) {
      inst.innerHTML = ``;
    } else {
      inst.innerHTML = `${iconName}`;
    }
    if (inst.stroke && iconSet.opticalFont) {
      inst.style.setProperty("--weight", inst.stroke * 100);
    }
    return; // Return early
  }

  getIconSvg(iconName, iconSet, iconType)
    .then((iconData) => {
      // Strip class attribute as it may be affected by css
      if (iconData.includes("class=")) {
        iconData = iconData.replace(/ class="([a-z- ]*)"/g, "");
      }
      // Add and/or fix stroke
      if (inst.stroke || iconSet.useStroke) {
        iconData = iconData.replace(/stroke-width="([0-9\.]*)"/g, `stroke-width="${inst.stroke}"`);
      }
      // Fix fill to currentColor
      if (iconSet.fixFill) {
        iconData = iconData.replace(/(/, '$1 fill="currentColor">');
      }
      // If we have some html, pass it along (useful for svg anim)
      if (inst.defaultHTML) {
        iconData = iconData.replace("", `${inst.defaultHTML}`);
      }
      inst.innerHTML = iconData;
    })
    .catch((error) => {
      inst.innerHTML = "⚠️";
      console.error(`Failed to load icon ${iconName} (error ${error})`);
    });
}

/**
 * @param {HTMLElement} element
 * @returns {Boolean}
 */
function isInViewport(element) {
  const rect = element.getBoundingClientRect();
  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  );
}

/**
 * Performs a deep merge of objects and returns new object. Does not modify
 * objects (immutable) and merges arrays via concatenation.
 *
 * @param {...object} objects - Objects to merge
 * @returns {object} New object with merged key/values
 */
function mergeDeep(...objects) {
  const isObject = (obj) => obj && typeof obj === "object";
  const isArray = Array.isArray;

  return objects.reduce((prev, obj) => {
    Object.keys(obj).forEach((key) => {
      const pVal = prev[key];
      const oVal = obj[key];

      if (isArray(pVal) && isArray(oVal)) {
        prev[key] = pVal.concat(...oVal);
      } else if (isObject(pVal) && isObject(oVal)) {
        prev[key] = mergeDeep(pVal, oVal);
      } else {
        prev[key] = oVal;
      }
    });

    return prev;
  }, {});
}

let aliases = {};
function processIconSets() {
  for (const [key, set] of Object.entries(options.sets)) {
    // List aliases for easy retrieval
    aliases[set.alias] = key;
    // Include full name in iconset definition
    set.name = key;
  }
}
processIconSets();

class LastIcon extends HTMLElement {
  /**
   * @param {object} opts
   * @returns {Options} The updated option object
   */
  static configure(opts = {}) {
    for (const k in opts) {
      if (typeof options[k] === "undefined") {
        console.error(`Invalid option key ${k}`);
        return;
      }
      if (Array.isArray(opts[k])) {
        options[k] = options[k].concat(opts[k]);
      } else if (typeof opts[k] === "object") {
        options[k] = mergeDeep(options[k], opts[k]);
      } else {
        options[k] = opts[k];
      }
    }
    processIconSets();
    // Log after we had the opportunity to change debug flag
    log("configuring options");
    return options;
  }

  /**
   * @return {String|null}
   */
  get type() {
    return this.getAttribute("type") || null;
  }

  /**
   * @return {String}
   */
  get set() {
    let v = this.getAttribute("set");
    return aliases[v] || options.defaultSet;
  }

  /**
   * @return {IconSet}
   */
  get iconSet() {
    return options.sets[this.set];
  }

  /**
   * @return {Number}
   */
  get stroke() {
    return this.getAttribute("stroke") || options.defaultStroke;
  }

  static get observedAttributes() {
    return ["name", "stroke", "size", "set", "type"];
  }

  connectedCallback() {
    // innerHTML is not available because not parsed yet
    // setTimeout also allows whenDefined to kick in before init
    setTimeout(() => {
      if (options.lazy && !isInViewport(this)) {
        // observer will call init when element is visible
        observer.observe(this);
      } else {
        // init directly
        this.init();
      }
    });
  }

  init() {
    // Store default content as we will inject it back later
    this.defaultHTML = this.innerHTML;
    this.loadIcon();
  }

  loadIcon() {
    const name = this.getAttribute("name");
    if (!name) {
      return;
    }
    const iconSet = this.iconSet;
    // Clear icon
    this.innerHTML = "";
    // Useful for customizing size in css
    const size = this.getAttribute("size");
    if (size) {
      this.setSize(size);
    }
    refreshIcon(this, name, iconSet, this.type);
  }

  setSize(size) {
    this.style.setProperty("--size", `${size}px`);
    if (this.iconSet.opticalFont) {
      this.style.setProperty("--opsz", size);
    }
  }

  attributeChangedCallback(attr, oldVal, newVal) {
    // Wait until properly loaded for the first time
    if (typeof this.defaultHTML !== "string") {
      return;
    }
    log(`Attr ${attr} changed from ${oldVal} to ${newVal}`);
    if (attr === "size") {
      this.setSize(newVal);
    } else if (newVal) {
      log("attribute changed");
      this.loadIcon();
    }
  }
}

customElements.define("l-i", LastIcon);




© 2015 - 2025 Weber Informatics LLC | Privacy Policy