npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

filter-widget-cartum

v1.0.36

Published

JavaScript widget for rendering catalog filters on the homepage of Cartum IO and Horoshop platforms

Readme

FilterWidget.js

JavaScript widget for rendering catalog filters on the homepage of Cartum IO and Horoshop e-commerce sites. Learn more on GitHub or install via npm.


Installation and Setup

  1. In your platform’s admin panel (Site → Design → Design Editor), make sure to place a “Brands” block—this is what the widget replaces by default.
  2. Insert the following code before </body>:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/filterWidget.umd.js"></script>
<script>
  document.addEventListener('DOMContentLoaded', () => {
    const widget = new FilterWidget({
      runOn          : 'home',                             // 'home' | 'all' | [ '/path1', '/path2' ]
      catalogUrl     : '/kontaktni-linzy/',                  // URL to fetch catalog filters from
      sourceSelectors: [
        'section.filter.__listScroll .filter-list ul.filter-lv1'
      ],
      targetSelector : 'section.frontBrands.__grayscale ul.frontBrands-list',                                 // render filters into different blocks
      hideOutOfStock : true,                                 // hide options with zero items
      labelMap       : {                                     // override displayed labels
        '1 день'   : 'Одноденні лінзи',
        '1 місяць' : 'Місячні лінзи'
      },
      imageMap       : {                                     // brand logo URLs
        'CooperVision': '/images/brands/CooperVision.png',
        'Alcon'       : '/images/brands/Alcon.png'
      },
      autoExpand     : false,                                // false = show only one row + expander button; true = show all items
      expanderText   : 'Показать ещё',
      title          : 'Бренды'
    });
    widget.init();
  });
</script>

Also, using the production-ready variant without inline comments:

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/filterWidget.umd.js"></script>
<script>
  document.addEventListener('DOMContentLoaded', () => {
    const widget = new FilterWidget({
      runOn: 'home',
      catalogUrl: '/kontaktni-linzy/',
      sourceSelectors: [
        'section.filter.__listScroll .filter-list ul.filter-lv1'
      ],
      groups: [ // або targetSelector : 'section.frontBrands.__grayscale ul.frontBrands-list',
        {
          // General filters (not branded)
          targetSelector: 'section.frontBrands.__grayscale ul.frontBrands-list',
          match: opt => !/\/filter\/brand=/.test(opt.url),
          title: 'Типи'
        },
        {
          // Only branded filters
          targetSelector: 'section.banners.banners--block.banners--gaps-none .banner-image',
          match: opt => /\/filter\/brand=/.test(opt.url),
          title: 'Бренди'
        }
        // You can display any elements taken by the parser in any places and style them
        // see Extending and Customization - Example 1: Cloning filters into a custom menu for more details
      ],
      hideOutOfStock: true,
      labelMap: {
        '1 день'            : 'Одноденні лінзи',
        '1 місяць'          : 'Місячні лінзи',
        '2 тижні'           : 'Двотижневі лінзи',
        'Світло-блакитне'   : 'Світло-блакитні лінзи',
        'Гідрогель'         : 'Гідрогель',
        'Силікон-гідрогель' : 'Силікон-гідрогель',
        'CooperVision'      : 'CooperVision',
        'Alcon'             : 'Alcon',
        'Bausch & Lomb'     : 'Bausch & Lomb',
        'Johnson & Johnson' : 'Johnson & Johnson',
        'Гнучкий'           : 'Гнучкі лінзи',
        'Денний'            : 'Денні лінзи',
        'Пролонгований'     : 'Пролонговані лінзи'
      },
      // Add a formatter only if custom logic is needed.
      // See the "Extending and Customization" section for examples.
      imageMap: {
        'CooperVision'      : '/content/images/47/137x120l75nn0/coopervision-80176384117891.webp?884',
        'Alcon'             : '/content/images/48/120x120l75nn0/alcon-46644902566954.webp',
        'Bausch & Lomb'     : '/content/images/49/120x120l75nn0/bausch-lomb-23791386782850.webp',
        'Johnson & Johnson' : '/content/images/50/120x120l75nn0/johnson-johnson-99333620756049.jpg'
      },
      autoExpand: true,
      expanderText: 'Розгорнути'
    });
    widget.init();
  });
</script>

Configuration Options

| Option | Type | Description | | ----------------- | ----------------------- | ---------------------------------------------------------------------------------------------- | | runOn | string | string[] | Where to run the widget: 'home', 'all', or specific paths. | | catalogUrl | string | URL of the catalog page to fetch filters from. | | sourceSelectors | string[] | CSS selectors targeting original filter lists. | | targetSelector | string | CSS selector of the <ul> to replace with rendered filters. | | groups | Array | Advanced: array of {targetSelector, match, title?} objects to render options into multiple blocks. | | hideOutOfStock | boolean | true to omit zero-count options. | | labelMap | Record<string,string> | Custom label overrides. | | labelFormatter | (option) ⇒ string | Function returning label per option; overrides labelMap. | | imageMap | Record<string,string> | Mapping option names to logo image URLs. | | autoExpand | boolean | false = render a single collapsed row + expander button; true = render all items expanded. | | expanderText | string | Text of the “Show more” button when autoExpand is false. | | title | string | Optional heading text before the rendered list. Empty to omit. | | titleTag | string | Wrapper tag name for the heading. Default 'span'. | | titleClass | string | CSS class for the heading wrapper. | | brandLast | boolean | true to render brand filters in a separate second list after general filters. | | clone | boolean | true to duplicate items instead of removing them from the source lists. | | insertMode | string | 'replace' (default) to overwrite the target or 'append' to keep its contents. | | afterSelector | string | When appending, insert new lists after the element matching this selector. | | markup | object | Customize classes and tags: {listClass, itemTag, itemClass, linkTag, linkClass, imgClass, labelTag, labelClass} |


Styling (example CSS)

.frontBrands .frontBrands-list {
  margin-bottom: 8px !important;
}
.filter-block {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 8px;
  padding: 12px 16px;
  background: #f5f5f5;
  text-decoration: none;
  border-radius: 8px;
  transition: background 0.3s;
}
.filter-block:hover {
  background: #e0e0e0;
}
.filter-block__img {
  width: 100px;
  height: 100px;
  object-fit: fill;
  object-position: center center;
  filter: grayscale(100%);
  transition: filter 0.3s;
}
.filter-block:hover .filter-block__img {
  filter: none;
}
.filter-block__label {
  font-size: 14px;
  color: #333;
}
.frontBrands-list.__collapsed {
  max-height: 120px;
  overflow: hidden;
}
.frontBrands-list.__expanded {
  max-height: none;
  overflow: visible;
}
.frontBrands-expander a {
  display: inline-block;
  margin: 8px;
  cursor: pointer;
}

Extending and Customization

  • Add more selectors to sourceSelectors to capture additional groups.
  • Extend imageMap/labelMap or implement labelFormatter for custom labels.
  • You can obtain filter cover URLs by right-clicking the cover image in Products → References → Brands and selecting “Copy image address”.
  • For advanced logic, create an instance with new FilterWidget(config) and call init() in your own scripts after post-processing the rendered lists.

Example 1: Cloning filters into a custom menu

With the default clone behaviour enabled, you can duplicate selected options and place them into any list while keeping them in their original block. The snippet below adds three cloned items to an existing menu:

<script>
  document.addEventListener("DOMContentLoaded", () => {
    FilterWidget.init({
      catalogUrl: "/kontaktni-linzy/",
      sourceSelectors: [
        "section.filter.__listScroll .filter-list ul.filter-lv1"
      ],
      groups: [
        {
          targetSelector: ".products-menu__container",
          match: opt => ["1 день", "Ні", "Гнучкий"].includes(opt.name),
          insertMode: 'append',
          afterSelector: 'li',
          markup: {
            itemTag: 'li',
            itemClass: 'products-menu__item j-submenu-item'
          }
        }
      ],
      labelMap: {
        "1 день": "Однодневные",
        "Ні": "Без UV-фільтра",
        "Гнучкий": "Гибкие"
      }
    });
  });
</script>

Resulting markup inside .products-menu__container:

<ul class="products-menu__container">
  <li class="products-menu__item j-submenu-item">…</li>
  <li class="frontBrands-i"><a href="/kontaktni-linzy/filter/rezhimZamni=1/" class="frontBrands-a filter-block"><span class="filter-block__label">Однодневные</span></a></li>
  <li class="frontBrands-i"><a href="/kontaktni-linzy/filter/uvFltr=no/" class="frontBrands-a filter-block"><span class="filter-block__label">Без UV-фільтра</span></a></li>
  <li class="frontBrands-i"><a href="/kontaktni-linzy/filter/rezhimNosnnja=1/" class="frontBrands-a filter-block"><span class="filter-block__label">Гибкие</span></a></li>
  <li class="products-menu__item j-submenu-item">…</li>
</ul>

Example 2: Combining labelMap and labelFormatter

You can override individual names via labelMap and still apply conditional transformations in labelFormatter.

<script>
  document.addEventListener('DOMContentLoaded', () => {
    FilterWidget.init({
      catalogUrl: '/kontaktni-linzy/',
      labelMap: {
        '1 день': 'Daily lenses',
        '1 місяць': 'Monthly lenses'
      },
      labelFormatter(opt) {
        const u = opt.url;
        if (u.includes('uvFltr=')) {
          return opt.name === 'Ні' ? 'Without UV filter' : 'With UV filter';
        }
        return this.labelMap[opt.name] || opt.name;
      }
    });
  });
</script>

Example 3: Shortening long names

<script>
  document.addEventListener('DOMContentLoaded', () => {
    FilterWidget.init({
      catalogUrl: '/kontaktni-linzy/',
      labelMap: {
        'Johnson & Johnson': 'J&J'
      },
      labelFormatter(opt) {
        const mapped = this.labelMap[opt.name];
        if (mapped) return mapped;
        return opt.name.length > 12 ? opt.name.slice(0, 12) + '…' : opt.name;
      }
    });
  });
</script>

Example 4: Prefixing every label

<script>
  document.addEventListener('DOMContentLoaded', () => {
    FilterWidget.init({
      catalogUrl: '/kontaktni-linzy/',
      labelMap: {
        'Гнучкий': 'Flexible',
        'Денний': 'Daily wear'
      },
      labelFormatter(opt) {
        return '★ ' + (this.labelMap[opt.name] || opt.name);
      }
    });
  });
</script>

Example 5: Horizontal layout for brand items

To make the brand list fill the row in four columns only inside its parent block (section.frontBrands), use flex layout tied to that container. Other lists remain unaffected:

/* four items per row with gaps */
section.frontBrands .frontBrands-list {
  display: flex;
  flex-wrap: wrap;
}
section.frontBrands .frontBrands-i {
  box-sizing: border-box;
  flex: 0 0 calc(25% - 16px);
}

Security

  • Only uses textContent, createElement and URL APIs—no innerHTML, preventing XSS.
  • Links are given rel="nofollow".
  • Errors are caught so the widget won’t break your page.
  • All parameters validated on init.