Filter option in File Embed widget?

Question: Is there a simpler way to add filter options?

Link to site: https://test-writing.squarespace.com/330?noredirect

Password: abc

I want to create Filter options in Filter Embed widget, something like this

I don’t see an option to add Filter so currently I edit File name, use format like this

File 01 ##English, Natural, Free

then use JS code like this to convert strings after ## to filter options

<script>
setTimeout(() => {
    const languages = new Set();
    const types = new Set();
    
    const galleryItems = document.querySelectorAll('.eapp-file-embed-item-component');
    
    galleryItems.forEach(item => {
        const nameElement = item.querySelector('.eapp-file-embed-item-name');
        if (nameElement) {
            const fullName = nameElement.textContent.trim();
            const hashIndex = fullName.indexOf('##');
            
            if (hashIndex !== -1) {
                const beforeHash = fullName.substring(0, hashIndex).trim();
                const afterHash = fullName.substring(hashIndex + 2).trim();
                
                nameElement.textContent = beforeHash;
                
                const parts = afterHash.split(',').map(part => part.trim());
                if (parts.length >= 2) {
                    const language = parts[0];
                    const type = parts[1];
                    
                    languages.add(language);
                    types.add(type);
                    
                    item.setAttribute('data-language', language);
                    item.setAttribute('data-type', type);
                    
                    if (afterHash.includes('Member')) {
                        const wrapper = item.querySelector('.eapp-file-embed-item-wrapper');
                        if (wrapper) {
                            const loginNotice = document.createElement('div');
                            loginNotice.className = 'login-notice';
                            loginNotice.textContent = 'Login to download';
                            loginNotice.style.cssText = 'color: #666; font-size: 12px; margin-top: 4px;';
                            wrapper.appendChild(loginNotice);
                        }
                    }
                }
            }
        }
    });
    
    const titleElement = document.querySelector('.eapp-file-embed-title-component');
    if (titleElement) {
        const filterContainer = document.createElement('div');
        filterContainer.className = 'gallery-filter-bar';
        filterContainer.style.cssText = 'margin: 20px 0; text-align: center;';
        
        const languageWrapper = document.createElement('div');
        languageWrapper.style.cssText = 'display: inline-block; margin: 0 15px;';
        
        const languageLabel = document.createElement('label');
        languageLabel.textContent = 'Language:';
        languageLabel.style.cssText = 'display: block; margin-bottom: 5px; font-weight: bold;';
        
        const languageSelect = document.createElement('select');
        languageSelect.style.cssText = 'padding: 8px 12px; border: 1px solid #ccc; border-radius: 5px; background: white;';
        
        const allLanguagesOption = document.createElement('option');
        allLanguagesOption.value = '';
        allLanguagesOption.textContent = 'All Languages';
        languageSelect.appendChild(allLanguagesOption);
        
        languages.forEach(lang => {
            const option = document.createElement('option');
            option.value = lang;
            option.textContent = lang;
            languageSelect.appendChild(option);
        });
        
        const typeWrapper = document.createElement('div');
        typeWrapper.style.cssText = 'display: inline-block; margin: 0 15px;';
        
        const typeLabel = document.createElement('label');
        typeLabel.textContent = 'Type:';
        typeLabel.style.cssText = 'display: block; margin-bottom: 5px; font-weight: bold;';
        
        const typeSelect = document.createElement('select');
        typeSelect.style.cssText = 'padding: 8px 12px; border: 1px solid #ccc; border-radius: 5px; background: white;';
        
        const allTypesOption = document.createElement('option');
        allTypesOption.value = '';
        allTypesOption.textContent = 'All Types';
        typeSelect.appendChild(allTypesOption);
        
        types.forEach(type => {
            const option = document.createElement('option');
            option.value = type;
            option.textContent = type;
            typeSelect.appendChild(option);
        });
        
        function filterItems() {
            const selectedLanguage = languageSelect.value;
            const selectedType = typeSelect.value;
            
            galleryItems.forEach(item => {
                const itemLanguage = item.getAttribute('data-language') || '';
                const itemType = item.getAttribute('data-type') || '';
                
                const languageMatch = !selectedLanguage || itemLanguage === selectedLanguage;
                const typeMatch = !selectedType || itemType === selectedType;
                
                if (languageMatch && typeMatch) {
                    item.style.display = '';
                } else {
                    item.style.display = 'none';
                }
            });
        }
        
        languageSelect.addEventListener('change', filterItems);
        typeSelect.addEventListener('change', filterItems);
        
        languageWrapper.appendChild(languageLabel);
        languageWrapper.appendChild(languageSelect);
        
        typeWrapper.appendChild(typeLabel);
        typeWrapper.appendChild(typeSelect);
        
        filterContainer.appendChild(languageWrapper);
        filterContainer.appendChild(typeWrapper);
        
        titleElement.parentNode.insertBefore(filterContainer, titleElement.nextSibling);
    }
}, 3000);
</script>

Thank you!

2 Likes

Hi there, @tuanphan :waving_hand:

Nice workaround, by the way! At the moment, there is no other solution to add filters to the widget, but we have this idea on the Wishlist. Feel free to upvote it :slightly_smiling_face: - Add filters like in Calendar widget

1 Like

Thank you.

Besides filling filter text in File Name, do you have any other suggestions?

2 Likes

Do I get it right that you need a different solution to get rid of the filter text that appears for a second during the page load?

1 Like

I mean besides filling in text ## English, Natural, Free in File Name, is there any other good position to fill it? Thank you

2 Likes

Would it be fine for you if this part (##English, Natural, Free) won’t appear on the page load at all?

1 Like

Do you have code for this? Thank you :joy:

I intend to write a guide with this (PDF Filter) for Squarespace site. Questions about this feature don’t come up much on the Squarespace Forum, but I thought it was worth writing about.

2 Likes

Yep, we can adjust your script to fix 2 existing flaws:

  1. Completely hide ## English, Natural, Free on the page load

  2. Fix the file order after applying filters.

I’ve passed your request on to the dev team and will let you know once the solution is ready :wink:

2 Likes

Thank you!

2 Likes

Hi there, @tuanphan :waving_hand:

Please try to use this script in the Custom JS field instead:

const widgetId = "";

const widgetEl = document.querySelector(`.elfsight-app-${widgetId}`);

const LANGUAGE_FILTER_NAME = "language";
const TYPE_FILTER_NAME = "type";
const MEMBERSHIP_FILTER_NAME = "membership";

const DEFAULT_LANGUAGE_FILTER = "All Languages";
const DEFAULT_TYPE_FILTER = "All Types";
const DEFAULT_MEMBERSHIP_FILTER = "All members";

const currentFilters = {
  language: DEFAULT_LANGUAGE_FILTER,
  type: DEFAULT_TYPE_FILTER,
  membership: DEFAULT_MEMBERSHIP_FILTER,
};

const defaultFilters = [
  DEFAULT_LANGUAGE_FILTER,
  DEFAULT_TYPE_FILTER,
  MEMBERSHIP_FILTER_NAME,
];

const filters = [LANGUAGE_FILTER_NAME, TYPE_FILTER_NAME];

const filterSets = {
  [LANGUAGE_FILTER_NAME]: new Set([DEFAULT_LANGUAGE_FILTER]),
  [TYPE_FILTER_NAME]: new Set([DEFAULT_TYPE_FILTER]),
};

let filesStorage = [];

const widgetObserver = new MutationObserver(() => {
  const files = widgetEl.querySelectorAll(".eapp-file-embed-grid-item");

  if (!files) return;

  filesStorage = Array.from(files);

  handleFiles();
  createFilterBar();
});

function getFilterAtributeName(name) {
  return `data-${name}`;
}

function handleLinks(file, href, label) {
  const links = file.querySelectorAll("a");

  links.forEach((link) => {
    link.href = href;
    link.setAttribute("aria-label", label || "");
    link.removeAttribute("download");
  });
}

function handleLanguage(file, language) {
  if (!language) return;

  filterSets[LANGUAGE_FILTER_NAME].add(language);
  file.setAttribute(getFilterAtributeName(LANGUAGE_FILTER_NAME), language);
}

function handleType(file, type) {
  if (!type) return;

  filterSets[TYPE_FILTER_NAME].add(type);
  file.setAttribute(getFilterAtributeName(TYPE_FILTER_NAME), type);
}

function handleMembership(file, membership) {
  if (!membership) return;

  if (membership === "Member") {
    handleLinks(file, "/login", "Login to download");

    const mainLink = file.querySelector("[class*='Item__Link'");

    mainLink.classList.add("login-notice");
    mainLink.textContent = "Login to download";
  }
}

const fileHandlers = [handleLanguage, handleType, handleMembership];

function handleFile(file) {
  const elNameContainer = file.querySelector(".eapp-file-embed-item-name");

  if (!elNameContainer) return;

  const elNameOrigin = elNameContainer.textContent;

  const [elName, rawTags] = elNameOrigin.split("##").map((part) => part.trim());

  if (!rawTags) return;

  const tags = rawTags.split(",").map((tag) => tag.trim());

  tags.forEach((tag, i) => fileHandlers[i](file, tag));

  elNameContainer.textContent = elName;
}

function handleFiles() {
  widgetEl.style.visibility = "hidden";

  filesStorage.forEach((item) => handleFile(item));

  widgetEl.style.visibility = "";

  widgetObserver.disconnect();
}

function handleSelectFilter(e) {
  const { name, value } = e.target;

  currentFilters[name] = value;

  renderFiles(currentFilters);
}

function createSelect(name, labelText, options, selectHandler) {
  const wrapper = document.createElement("div");
  const label = document.createElement("label");
  const select = document.createElement("select");

  wrapper.className = "select-wrapper";
  label.textContent = labelText;
  select.setAttribute("name", name);

  options.forEach((optionText) => {
    const option = document.createElement("option");
    option.textContent = optionText;
    option.value = optionText;
    select.appendChild(option);
  });

  select.addEventListener("change", selectHandler);

  label.appendChild(select);
  wrapper.appendChild(label);

  return wrapper;
}

function handleFilterNameToLabel(name) {
  if (!name) return "";
  return name.charAt(0).toUpperCase() + name.slice(1) + ":";
}

function createFilterBar() {
  const titleElement = widgetEl.querySelector(
    ".eapp-file-embed-title-component"
  );

  if (!titleElement) return;
  const filterContainer = document.createElement("div");
  filterContainer.className = "gallery-filter-bar";

  const filtersSelects = filters.map((filter) =>
    createSelect(
      filter,
      handleFilterNameToLabel(filter),
      [...filterSets[filter]],
      handleSelectFilter
    )
  );

  filtersSelects.forEach((filterSelect) =>
    filterContainer.appendChild(filterSelect)
  );

  titleElement.parentNode.insertBefore(
    filterContainer,
    titleElement.nextSibling
  );
}

function getFilterFiles(filtersValues, files) {
  const filteredFiles = files.filter((file) => {
    const filtersResults = filters.filter((filter) => {
      if (defaultFilters.includes(filtersValues[filter])) {
        return true;
      }

      const fileFilterValue = file.getAttribute(getFilterAtributeName(filter));

      return fileFilterValue === filtersValues[filter];
    });

    return filters.length === filtersResults.length;
  });

  return filteredFiles;
}

function renderFiles(filesFiters) {
  const filteredFiles = getFilterFiles(filesFiters, filesStorage);

  const filesContainer = document.querySelector(
    ".eapp-file-embed-grid-component"
  );

  filesContainer.innerHTML = "";

  if (filteredFiles.length === 0) {
    const p = document.createElement("p");

    p.className = "empty-files-list";
    p.textContent = "No files available for these filter values";

    filesContainer.appendChild(p);

    return;
  }

  filteredFiles.forEach((file) => filesContainer.appendChild(file));
}

widgetEl.style.visibility = "hidden";

widgetObserver.observe(widgetEl, { childList: true });

And this code should be added to the Custom CSS field:

.eapp-file-embed-item-blockLink {
  height: auto;
}

.eapp-file-embed-item-link,
.login-notice {
  color: #666;
  font-size: 12px;
}

.login-notice:hover {
  text-decoration: underline;
}

.gallery-filter-bar {
  margin: 20px 0;
  text-align: center;
}

.select-wrapper {
  display: inline-block;
  margin: 0 15px;
}

.select-wrapper label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

.select-wrapper select {
  padding: 8px 12px;
  border: 1px solid #ccc;
  border-radius: 5px;
  background: white;
}

.empty-files-list {
  color: #666;
  width: 100%;
  text-align: center;
}

Give it a try and let me know if you like the result :slightly_smiling_face:

Thank you.

I will try it then write a guide for Squarespace Users :grin:

2 Likes