mirror of
https://github.com/home-assistant/home-assistant.io.git
synced 2025-07-13 12:26:50 +00:00
Filters for integration page (#33644)
This commit is contained in:
parent
5ca73a8476
commit
c9aec73ad6
@ -197,4 +197,4 @@ RUBY VERSION
|
|||||||
ruby 2.6.2p47
|
ruby 2.6.2p47
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.3
|
2.5.3
|
@ -5,22 +5,156 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: .005em;
|
letter-spacing: .005em;
|
||||||
color: $gray;
|
color: $gray;
|
||||||
|
padding-top: 8px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-search {
|
.component-search {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
min-height: 80px;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
background-color: #fefefe;
|
background-color: #fefefe;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
border-color: #7c7c7c #c3c3c3 #ddd;
|
border-color: #7c7c7c #c3c3c3 #ddd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-filters {
|
||||||
|
@media only screen and (min-width: $desk-start) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-filter-radio {
|
||||||
|
display: flex;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
grid-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
width: fit-content;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: $blue;
|
||||||
|
color: white;
|
||||||
|
border-radius: 24px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
padding-left: 16px;
|
||||||
|
display: block;
|
||||||
|
margin-left: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
border-radius: 100%;
|
||||||
|
border-color: transparent;
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22%231abcf2%22%20transform%3D%22rotate(90)%22%3E%3Ccircle%20cx%3D%228%22%20cy%3D%228%22%20r%3D%223%22%2F%3E%3C%2Fsvg%3E");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px 16px 4px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-filter.integration-filter-select {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: 2px;
|
||||||
|
right: 8px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 5px solid transparent;
|
||||||
|
border-right: 5px solid transparent;
|
||||||
|
border-top: 5px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ha_category {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
color: #222;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align-last: center;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
border: 2px solid $primary-color;
|
||||||
|
border-top-right-radius: 0px;
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
border-top: 0;
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
padding: 5px 5px 2px;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-title {
|
||||||
|
background: $primary-color;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-size: 18px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-filter {
|
||||||
|
margin: 10px 5px 0px 0px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
min-width: 100px;
|
||||||
|
background-color: $primary-color;
|
||||||
|
border-radius: 28px;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
line-height: 18px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
iconify-icon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
font-family: $sans-serif;
|
font-family: $sans-serif;
|
||||||
border: 0;
|
border: 0;
|
||||||
@ -44,6 +178,10 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.alert {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.option-card {
|
.option-card {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
@ -147,7 +285,7 @@ a.option-card:hover {
|
|||||||
img {
|
img {
|
||||||
max-width: 48px;
|
max-width: 48px;
|
||||||
max-height: 48px;
|
max-height: 48px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@ -201,7 +339,7 @@ a.option-card:hover {
|
|||||||
}
|
}
|
||||||
.category_select {
|
.category_select {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.category_list {
|
.category_list {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -330,4 +468,4 @@ a.option-card:hover {
|
|||||||
to {
|
to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -23,68 +23,38 @@ regenerate: false
|
|||||||
{%- assign components = site.integrations | sort: 'title' -%}
|
{%- assign components = site.integrations | sort: 'title' -%}
|
||||||
{%- assign components_by_version = site.integrations | group_components_by_release -%}
|
{%- assign components_by_version = site.integrations | group_components_by_release -%}
|
||||||
{%- assign categories = components | map: 'ha_category' | join: ',' | downcase | split: ',' | uniq | sort -%}
|
{%- assign categories = components | map: 'ha_category' | join: ',' | downcase | split: ',' | uniq | sort -%}
|
||||||
|
{%- assign iot_classes = "Local Push,Local Polling,Cloud Push,Cloud Polling,Assumed State,Calculated,Configurable" |
|
||||||
|
join: ',' | split: ',' -%}
|
||||||
|
{% assign quality_scales =
|
||||||
|
"icon:🏆,name:platinum|icon:🥇,name:gold|icon:🥈,name:silver|icon:🥉,name:bronze|icon:🏠,name:internal|icon:💾,name:legacy"
|
||||||
|
| split: "|"
|
||||||
|
%}
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="grid__item one-fifth lap-one-whole palm-one-whole">
|
|
||||||
|
|
||||||
<div class="filter-button-group">
|
<div class="grid__item six-sixths lap-one-whole palm-one-whole">
|
||||||
<div class="all">
|
<fieldset class="integration-filter integration-filter-radio">
|
||||||
<a href='#all' class="btn">All <span class="count">{{tot}}</span></a>
|
<div class="filter-button">
|
||||||
|
<input type="radio" id="brandsAll" name="brands" value="all" data-id="brands" checked="checked" />
|
||||||
|
<label for="brandsAll">All</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-button">
|
||||||
<div class="featured">
|
<input type="radio" id="brandsFeatured" name="brands" value="featured" data-id="brands" />
|
||||||
<a href='#featured' class="btn">Featured</a>
|
<label for="brandsFeatured">Featured</label>
|
||||||
<a href='#works-with-home-assistant' class="btn">Partner brands</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-button">
|
||||||
<div class="version_select">
|
<input type="radio" id="brandsPartners" name="brands" value="partners" data-id="brands" />
|
||||||
<label>By release</label>
|
<label for="brandsPartners">Partners</label>
|
||||||
<select id="versions" name="versions">
|
|
||||||
<option value="#"></option>
|
|
||||||
{%- for group in components_by_version -%}
|
|
||||||
<optgroup label="{{ group.label }} ({{group.new_components_count}})">
|
|
||||||
{%- for version in group.versions -%}
|
|
||||||
<option value="#version/{{ version.label }}">{{ version.label }} ({{ version.new_components_count }})
|
|
||||||
</option>
|
|
||||||
{%- endfor -%}
|
|
||||||
</optgroup>
|
|
||||||
{%- endfor -%}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="category_list">
|
<div class="grid-filters">
|
||||||
<label>By Categories</label>
|
<div class="grid__item one-quarter lap-one-whole palm-one-whole">
|
||||||
{%- for category in categories -%}
|
<div class="integration-filter integration-filter-select">
|
||||||
{%- assign category_name = "" -%}
|
<h3 class="filter-title">Category</h3>
|
||||||
{%- assign components_count = 0 -%}
|
<select class="ha_category" name="category" data-id="cat">
|
||||||
{%- for comp in components -%}
|
<option value="#">All</option>
|
||||||
{%- assign comp_categories = comp.ha_category | join: ',' | downcase -%}
|
|
||||||
{%- if comp_categories contains category -%}
|
|
||||||
{%- if category_name == "" -%}
|
|
||||||
{%- for cat in comp.ha_category -%}
|
|
||||||
{%- assign lower_cat = cat | downcase -%}
|
|
||||||
{%- if lower_cat == category -%}
|
|
||||||
{%- assign category_name = cat -%}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- assign components_count = components_count | plus: 1 -%}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- if category != 'other' and components_count != 0 -%}
|
|
||||||
{%- if category_name == "" -%}
|
|
||||||
{%- assign category_name = category | capitalize -%}
|
|
||||||
{%- endif -%}
|
|
||||||
<a href='#{{ category_name | slugify }}' class="btn" onclick="document.querySelector('.page-content').scrollTop = 0">{{ category_name }} <span class="count">{{ components_count }}</span></a>
|
|
||||||
{%- endif -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
<a href='#other' class="btn" onclick="document.querySelector('.page-content').scrollTop = 0">Other <span class="count">{{ components | where: 'ha_category', 'Other' | size }}</span></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="category_select">
|
|
||||||
<label>By Categories</label>
|
|
||||||
<select id="categories" name="categories">
|
|
||||||
<option value="#"></option>
|
|
||||||
{%- for category in categories -%}
|
{%- for category in categories -%}
|
||||||
{%- assign category_name = "" -%}
|
{%- assign category_name = "" -%}
|
||||||
{%- assign components_count = 0 -%}
|
{%- assign components_count = 0 -%}
|
||||||
@ -106,154 +76,341 @@ regenerate: false
|
|||||||
{%- if category_name == "" -%}
|
{%- if category_name == "" -%}
|
||||||
{%- assign category_name = category | capitalize -%}
|
{%- assign category_name = category | capitalize -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<option value="#{{ category_name | slugify }}">{{ category_name }} ({{ components_count }})
|
<option value='{{ category | slugify }}'>{{ category_name }}</option>
|
||||||
</option>
|
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
<option value="#other">Other ({{ components | where: 'ha_category', 'Other' | size }})
|
|
||||||
</option>
|
<option value='other'>Other</option>
|
||||||
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid__item one-quarter lap-one-whole palm-one-whole">
|
||||||
|
<div class="integration-filter integration-filter-select">
|
||||||
|
<h3 class="filter-title">Version</h3>
|
||||||
|
<select class="ha_category" name="versions" data-id="version">
|
||||||
|
<option value="#">All</option>
|
||||||
|
{%- for group in components_by_version -%}
|
||||||
|
<optgroup label="{{ group.label }} ({{group.new_components_count}})">
|
||||||
|
{%- for version in group.versions -%}
|
||||||
|
<option value="{{ version.label }}">{{ version.label }}</option>
|
||||||
|
{%- endfor -%}
|
||||||
|
</optgroup>
|
||||||
|
{%- endfor -%}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid__item one-quarter lap-one-whole palm-one-whole">
|
||||||
|
<div class="integration-filter integration-filter-select">
|
||||||
|
<h3 class="filter-title">IoT Class</h3>
|
||||||
|
<select class="ha_category" name="iot_classes" data-id="iot_class">
|
||||||
|
<option value="#">All</option>
|
||||||
|
{%- for iot_class in iot_classes -%}
|
||||||
|
{%- assign iot_class_count = components | where: 'ha_iot_class', iot_class | size -%}
|
||||||
|
{%- if iot_class_count != 0 -%}
|
||||||
|
<option value="{{ iot_class | slugify }}">{{ iot_class }}</option>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid__item one-quarter lap-one-whole palm-one-whole">
|
||||||
|
<div class="integration-filter integration-filter-select">
|
||||||
|
<h3 class="filter-title">Quality Scale</h3>
|
||||||
|
<select class="ha_category" name="quality_scales" data-id="quality_scale">
|
||||||
|
<option value="#">All</option>
|
||||||
|
{%- for quality_scale in quality_scales -%}
|
||||||
|
{% assign quality_scale_data = quality_scale | split: "," %}
|
||||||
|
{% assign quality_icon = quality_scale_data[0] | split: ":" | last %}
|
||||||
|
{% assign quality_name = quality_scale_data[1] | split: ":" | last %}
|
||||||
|
{%- assign quality_scale_count = components | where: 'ha_quality_scale', quality_name | size -%}
|
||||||
|
{%- if quality_scale_count != 0 -%}
|
||||||
|
<option value="{{ quality_name | slugify }}">{{quality_icon}} {{ quality_name | capitalize }}</option>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid__item four-fifths lap-one-whole palm-one-whole">
|
|
||||||
|
<div class="grid__item six-sixths lap-one-whole palm-one-whole">
|
||||||
<div class="component-search">
|
<div class="component-search">
|
||||||
<form onsubmit="event.preventDefault(); return false">
|
<form onsubmit="event.preventDefault(); return false">
|
||||||
<input type="text" name="search" id="search" class="search text-input" placeholder="Search integrations..." autofocus />
|
<input type="text" name="search" id="search" data-id="search" class="search text-input"
|
||||||
|
placeholder="Search integrations..." autofocus />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="grid__item six-sixths lap-one-whole palm-one-whole">
|
||||||
<div class="hass-option-cards" id="componentContainer"> </div>
|
<div class="hass-option-cards" id="componentContainer"> </div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
// This object contains all components we have
|
// This object contains all components we have
|
||||||
const allComponents = [
|
const integrations = [
|
||||||
{%- for component in components -%}
|
{%- for component in components -%}
|
||||||
{%- if component.ha_category -%}
|
{%- if component.ha_category -%}
|
||||||
{%- assign sliced_version = component.ha_release | split: '.' -%}
|
{%- assign sliced_version = component.ha_release | split: '.' -%}
|
||||||
{%- assign minor_version = sliced_version[1]|plus: 0 -%}
|
{%- assign minor_version = sliced_version[1] | plus: 0 -%}
|
||||||
{%- assign major_version = sliced_version[0]|plus: 0 -%}
|
{%- assign major_version = sliced_version[0] | plus: 0 -%}
|
||||||
{% assign categories = "" | split: ',' %}
|
{%- assign categories = "" | split: ',' -%}
|
||||||
{%- for ha_category in component.ha_category -%}
|
{%- for ha_category in component.ha_category -%}
|
||||||
{% capture category %}"{{ ha_category | slugify | downcase }}"{% endcapture %}
|
{% capture category %} "{{ ha_category | slugify | downcase }}"{% endcapture %}
|
||||||
{% assign categories = categories | push: category %}
|
{% assign categories = categories | push: category %}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{url:"{{ component.url }}", title:"{{component.title}}", cat: [{{categories|join: ","}}], featured: {% if component.featured %}true{% else %}false{% endif %}, v: "{{major_version}}.{{minor_version}}", logo: "{{component.logo}}", domain: "{{component.ha_domain}}", ha_brand: "{{component.ha_brand}}", wwha: {% if component.works_with %}true{% else %}false{% endif %}},
|
{
|
||||||
{% endif -%}
|
url: "{{ component.url }}",
|
||||||
{%- endfor -%}
|
title: "{{component.title}}",
|
||||||
false
|
cat: [{{ categories| join: ","}}],
|
||||||
];
|
featured: {{ component.featured }},
|
||||||
allComponents.pop(); // remove placeholder element at the end
|
version: "{{major_version}}.{{minor_version}}",
|
||||||
|
logo: "{{component.logo}}",
|
||||||
|
domain: "{{component.ha_domain}}",
|
||||||
|
ha_brand: "{{component.ha_brand}}",
|
||||||
|
wwha: {% if component.works_with %} true{% else %} false{% endif %},
|
||||||
|
iot_class: "{{component.ha_iot_class | slugify }}",
|
||||||
|
quality_scale: "{{component.ha_quality_scale}}"
|
||||||
|
},
|
||||||
|
{% endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
false
|
||||||
|
];
|
||||||
|
integrations.pop(); // remove placeholder element at the end
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
(function () {
|
(function () {
|
||||||
const SEARCH_PREFIX = '#search/';
|
let filteredComponents = {};
|
||||||
const VERSION_PREFIX = '#version/';
|
const baseHashRegex = /(^.*)(?=\?)/;
|
||||||
|
const integrationSelectFilterElements = [...document.querySelectorAll('.integration-filter select')];
|
||||||
|
const integrationRadioFilterElements = [...document.querySelectorAll('.integration-filter input[name="brands"]')];
|
||||||
const searchInputEl = document.querySelector('.component-search input');
|
const searchInputEl = document.querySelector('.component-search input');
|
||||||
const componentContainerEl = document.querySelector('#componentContainer');
|
const componentContainerEl = document.querySelector('#componentContainer');
|
||||||
|
let searchInputDirty = false;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
let queryParams = new URLSearchParams(document.location.search);
|
||||||
// do the lowerCase transformation once
|
// do the lowerCase transformation once
|
||||||
for (let component of allComponents) {
|
for (let i = 0; i < integrations.length; i++) {
|
||||||
const title = component.title.toLowerCase();
|
const title = integrations[i].title.toLowerCase();
|
||||||
const domain = component.domain;
|
const domain = integrations[i].domain;
|
||||||
const titleNormalized = title
|
const iot_class = integrations[i].iot_class;
|
||||||
.normalize("NFD")
|
const quality_scale = integrations[i].quality_scale;
|
||||||
.replace(/[\u0300-\u036f]/g, "");
|
const title_normalized = title.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
||||||
const titleDedashed = title.replace(/[-_]/g, " ");
|
const title_dedashed = title.replace(/[-_]/g, " ");
|
||||||
const titleNormalizedDedashed = titleNormalized.replace(/[-_]/g, " ");
|
const title_normalized_dedashed = title_normalized.replace(/[-_]/g, " ");
|
||||||
|
|
||||||
component.titleLC = title;
|
integrations[i].titleLC = title;
|
||||||
component.search = `${title} ${titleNormalized} ${titleDedashed} ${titleNormalizedDedashed} ${domain}`;
|
integrations[i].search = `${title} ${title_normalized} ${title_dedashed} ${title_normalized_dedashed} ${domain} ${iot_class} ${quality_scale}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort the components alphabetically
|
// sort the components alphabetically
|
||||||
allComponents.sort((a, b) => a.titleLC.localeCompare(b.titleLC));
|
integrations.sort(function (a, b) {
|
||||||
|
return a.titleLC.localeCompare(b.titleLC);
|
||||||
|
});
|
||||||
|
|
||||||
if (location.hash !== '' && location.hash.indexOf(SEARCH_PREFIX) === 0) {
|
//add these to the window for dynamic grabbing in matchFilterElementToQuery()
|
||||||
// set default value in search from URL
|
window.$catFilter = document.querySelector('.integration-filter select[data-id="cat"]');
|
||||||
const search = decodeURIComponent(location.hash).substring(SEARCH_PREFIX.length);
|
window.$versionFilter = document.querySelector('.integration-filter select[data-id="version"]');
|
||||||
searchInputEl.value = search;
|
window.$iot_classFilter = document.querySelector('.integration-filter select[data-id="iot_class"]');
|
||||||
|
window.$quality_scaleFilter = document.querySelector('.integration-filter select[data-id="quality_scale"]');
|
||||||
|
window.$searchFilter = document.querySelector('.component-search input');
|
||||||
|
window.$brandsFilter = [...document.querySelectorAll('.integration-filter input[name="brands"]')]
|
||||||
|
|
||||||
|
//listen for changes in the hash / user clicking back or forward button in browser
|
||||||
|
window.addEventListener('hashchange', applyFilter);
|
||||||
|
window.addEventListener('popstate', applyFilter);
|
||||||
|
|
||||||
|
//on initial page load, if there's no active filters set the default to featured
|
||||||
|
let query = buildQueryFromURL();
|
||||||
|
|
||||||
|
if (query !== null) {
|
||||||
|
if (Object.keys(query).length === 0) {
|
||||||
|
setQueryURL('brands', 'featured');
|
||||||
|
} else {
|
||||||
|
if (query['brands'] !== 'featured') {
|
||||||
|
searchInputDirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
applyFilter();
|
||||||
|
|
||||||
// add focus to the search field
|
// add focus to the search field
|
||||||
setTimeout(() => searchInputEl.focus(), 1);
|
setTimeout(() => searchInputEl.focus(), 1);
|
||||||
}
|
}
|
||||||
init();
|
init();
|
||||||
|
|
||||||
/**
|
// get query value pair that matches provided id from url. // for buildQueryFromURL()
|
||||||
* filter all components, based on the location's hash and render them into the component box
|
function getQueryValue(id) {
|
||||||
*/
|
let index = id.length + 2; // id + '#' and '/' => '#search/'
|
||||||
function applyFilter() {
|
if (id === 'cat' || id === 'brands') {
|
||||||
const hash = location.hash || '';
|
index = 1;
|
||||||
let components = [];
|
}
|
||||||
|
|
||||||
|
// get value from #hash url
|
||||||
|
let query = decodeURIComponent(location.hash).substring(index).toLowerCase();
|
||||||
|
|
||||||
|
// get value from ?query url if it's present
|
||||||
|
let queryParams = new URLSearchParams(location.search);
|
||||||
|
if (queryParams.has(id)) {
|
||||||
|
query = queryParams.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjust query for wwha # url
|
||||||
|
if (query === 'works-with-home-assistant') {
|
||||||
|
query = 'partners';
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
// build out query object of active filters for applyFilter()
|
||||||
|
function buildQueryFromURL() {
|
||||||
|
let queryFilter = {};
|
||||||
|
let hash = location.hash || '';
|
||||||
|
let queryParams = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
if (hash.startsWith('#featured') || queryParams.get('brands') === 'featured') {
|
||||||
|
// only show those with featured = true
|
||||||
|
let query = getQueryValue('brands');
|
||||||
|
queryFilter.brands = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash.startsWith('#works-with-home-assistant') || queryParams.get('brands') === 'partners') {
|
||||||
|
// only show those with partners = true
|
||||||
|
let query = getQueryValue('brands');
|
||||||
|
queryFilter.brands = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash.startsWith('#search/') || queryParams.has('search')) {
|
||||||
|
let query = getQueryValue('search');
|
||||||
|
queryFilter.search = query;
|
||||||
|
} else {
|
||||||
|
// reset search box when not searching
|
||||||
|
$searchFilter.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash.startsWith('#version/') || queryParams.has('version')) {
|
||||||
|
// filter by a version
|
||||||
|
let query = getQueryValue('version');
|
||||||
|
queryFilter.version = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
// check for old # categories urls
|
||||||
|
(hash.startsWith('#') || queryParams.has("cat"))
|
||||||
|
&& !(hash.startsWith('#all'))
|
||||||
|
&& !(hash.startsWith('#featured'))
|
||||||
|
&& !(hash.startsWith('#works-with-home-assistant'))
|
||||||
|
&& !(hash.startsWith('#search'))
|
||||||
|
&& !(hash.startsWith('#version'))
|
||||||
|
&& !(hash.startsWith('#iot_class'))
|
||||||
|
&& !(hash.startsWith('#quality_scale'))
|
||||||
|
) {
|
||||||
|
// regular filter categories
|
||||||
|
let query = getQueryValue('cat');
|
||||||
|
queryFilter.cat = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash.startsWith('#iot_class/') || queryParams.has('iot_class')) {
|
||||||
|
let query = getQueryValue('iot_class');
|
||||||
|
queryFilter.iot_class = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash.startsWith('#quality_scale/') || queryParams.has('quality_scale')) {
|
||||||
|
let query = getQueryValue('quality_scale');
|
||||||
|
queryFilter.quality_scale = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash.startsWith('#all')) {
|
||||||
|
queryFilter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter all components by comparing against query object
|
||||||
|
return queryFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter all components, based on the url hash/search query string and render them into the component box
|
||||||
|
// if a custom query is provided, this will override all current filters, and update the url / elements accordingly
|
||||||
|
function applyFilter(query = false) {
|
||||||
|
var data = {
|
||||||
|
components: []
|
||||||
|
};
|
||||||
|
|
||||||
// fade-out css effect on the old elements. This is actually not visible on fast browsers
|
// fade-out css effect on the old elements. This is actually not visible on fast browsers
|
||||||
componentContainerEl.classList.add('remove-items');
|
componentContainerEl.classList.add('remove-items');
|
||||||
|
|
||||||
if (hash.indexOf('#search/') === -1) {
|
// make sure no query exists and its not an event object being passed in
|
||||||
// reset search box when not searching
|
if (!query || query.target) {
|
||||||
searchInputEl.value = null;
|
query = buildQueryFromURL();
|
||||||
|
} else {
|
||||||
|
//set url to custom query filter
|
||||||
|
overrideQueryURL(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hash === '#all') {
|
// update dom elements to match new filters
|
||||||
// shortcut: no need to filter
|
updateFilterButtons(query);
|
||||||
components = allComponents;
|
updateFilterElements(query);
|
||||||
} else {
|
|
||||||
let filter, search;
|
|
||||||
if (hash.indexOf(SEARCH_PREFIX) === 0) {
|
|
||||||
// search through title and category
|
|
||||||
search = decodeURIComponent(hash).substring(SEARCH_PREFIX.length).toLowerCase();
|
|
||||||
filter = comp =>
|
|
||||||
comp.search.indexOf(search) !== -1 ||
|
|
||||||
comp.cat.find((c) => c.includes("#")) != undefined;
|
|
||||||
|
|
||||||
} else if (hash === '#featured' || hash === '') {
|
data.components = integrations.filter(integration => {
|
||||||
// only show those with featured = true
|
for (let key in query) {
|
||||||
filter = comp => comp.featured;
|
// compare against search string and categories in integration
|
||||||
|
if (key === 'search') {
|
||||||
|
if (
|
||||||
|
integration[key].indexOf(query[key]) === -1
|
||||||
|
&& integration.cat.find((c) => c.includes(query[key])) == undefined
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else if (hash === '#works-with-home-assistant') {
|
// check for matching categories in integration's category array
|
||||||
// only show those partners of the Works with Home Assistant program
|
if (key === 'cat') {
|
||||||
filter = comp => comp.wwha;
|
if (!integration[key].includes(query[key])) return false;
|
||||||
|
}
|
||||||
|
|
||||||
} else if (hash.indexOf(VERSION_PREFIX) === 0) {
|
// check for matching featured brands in integration
|
||||||
// compare against a version
|
if (key === 'brands' && query[key] === 'featured') {
|
||||||
search = decodeURIComponent(hash).substring(VERSION_PREFIX.length).toLowerCase();
|
if (!(integration['featured'])) return false;
|
||||||
// compare version string against version js
|
}
|
||||||
filter = comp => comp.v === search;
|
|
||||||
|
|
||||||
} else {
|
// check for matching partner brands in integration
|
||||||
// regular filter categories
|
if (key === 'brands' && query[key] === 'partners') {
|
||||||
search = hash.substring(1);
|
if (!integration['wwha']) return false;
|
||||||
filter = comp => comp.cat.includes(search);
|
}
|
||||||
|
|
||||||
|
// check for version / iot / quality matches
|
||||||
|
if (!(key === 'cat' || key === 'search' || key === 'brands')) {
|
||||||
|
if (
|
||||||
|
integration[key] == undefined // match doesn't exist
|
||||||
|
|| integration[key] != query[key] // strings dont match
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter all components using the filter function
|
return true;
|
||||||
components = allComponents.filter(filter);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
|
// let rendered = Mustache.render(template, data);
|
||||||
let rendered;
|
let rendered;
|
||||||
if (components.length > 0) {
|
if (data.components.length > 0) {
|
||||||
// Note: Assumes all data has already been sanitized
|
// Note: Assumes all data has already been sanitized
|
||||||
rendered = components.map(component => `
|
rendered = data.components.map(component => `
|
||||||
<a href="${component.url}" class="option-card">
|
<a href="${component.url}" class="option-card">
|
||||||
<div class="img-container">${buildImageEl(component)}</div>
|
<div class="img-container">${buildImageEl(component)}</div>
|
||||||
<div class='title'>${component.title}</div>
|
<div class='title'>${component.title}</div>
|
||||||
</a>
|
</a>
|
||||||
`).join('\n');
|
`).join('\n');
|
||||||
} else {
|
} else {
|
||||||
rendered = '<div class="alert alert-note"><p class="alert-content">Nothing found!</p></div>';
|
rendered = '<div class="alert alert-note"><p class="alert-content">No results matching this filter</p></div>';
|
||||||
}
|
|
||||||
|
|
||||||
// set active class on active menu item
|
|
||||||
document.querySelector('.filter-button-group a.active')?.classList?.remove?.('active');
|
|
||||||
document.querySelector(`.filter-button-group a[href="${hash}"]`)?.classList?.add?.('active');
|
|
||||||
if (hash === "") {
|
|
||||||
document.querySelector('.filter-button-group a[href*="#featured"]').classList.add('active');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove previous elements and css classes, add the new stuff and then trigger the fade-in css animation
|
// remove previous elements and css classes, add the new stuff and then trigger the fade-in css animation
|
||||||
@ -264,6 +421,296 @@ allComponents.pop(); // remove placeholder element at the end
|
|||||||
componentContainerEl.classList.add('show-items');
|
componentContainerEl.classList.add('show-items');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add / update a filter in the url, upated # urls to query string urls
|
||||||
|
function setQueryURL(id, value) {
|
||||||
|
const hashExists = location.hash !== '';
|
||||||
|
let queryParams = new URLSearchParams(location.search);
|
||||||
|
let query = buildQueryFromURL();
|
||||||
|
|
||||||
|
// append new query string
|
||||||
|
if (hashExists) {
|
||||||
|
location.hash = '';
|
||||||
|
|
||||||
|
if (query !== null) {
|
||||||
|
let currentId = Object.keys(query)[0];
|
||||||
|
let currentVal = Object.values(query)[0];
|
||||||
|
queryParams.set(currentId, currentVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParams.set(id, value);
|
||||||
|
newURL = `?${queryParams.toString()}`;
|
||||||
|
|
||||||
|
if ('pushState' in history) {
|
||||||
|
history.pushState('', '', newURL);
|
||||||
|
} else {
|
||||||
|
location.search = newURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
//make sure search input keeps text after url is adjusted
|
||||||
|
if (queryParams.has('search')) {
|
||||||
|
let searchText = queryParams.get('search');
|
||||||
|
$searchFilter.value = searchText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//remove a filter in the url
|
||||||
|
function removeQueryFromURL(queryId) {
|
||||||
|
const hashExists = location.hash !== '';
|
||||||
|
let newURL;
|
||||||
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
if (hashExists) {
|
||||||
|
location.hash = '';
|
||||||
|
} else {
|
||||||
|
queryParams.delete(queryId);
|
||||||
|
if (queryParams.toString() !== '') {
|
||||||
|
let baseURL = getCleanURL();
|
||||||
|
newURL = `${baseURL}?${queryParams.toString()}`
|
||||||
|
} else {
|
||||||
|
newURL = getCleanURL();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('pushState' in history) {
|
||||||
|
history.pushState('', '', newURL);
|
||||||
|
} else {
|
||||||
|
location.href = newURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//override current url filters with a new custom filter
|
||||||
|
function overrideQueryURL(query) {
|
||||||
|
const hashExists = location.hash !== '';
|
||||||
|
let newURL;
|
||||||
|
const newSearchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
if (hashExists) {
|
||||||
|
location.hash = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(query).forEach(key => {
|
||||||
|
newSearchParams.set(key, query[key])
|
||||||
|
})
|
||||||
|
|
||||||
|
if (newSearchParams.toString() !== '') {
|
||||||
|
let baseURL = getCleanURL();
|
||||||
|
newURL = `${baseURL}?${newSearchParams.toString()}`
|
||||||
|
} else {
|
||||||
|
newURL = getCleanURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('pushState' in history) {
|
||||||
|
history.pushState('', '', newURL);
|
||||||
|
} else {
|
||||||
|
location.href = newURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCleanURL() {
|
||||||
|
const baseUrlRegex = /(^.*)(?=[\?#])/;
|
||||||
|
let url = '';
|
||||||
|
let urlMatch = location.href.match(baseUrlRegex);
|
||||||
|
|
||||||
|
if (urlMatch !== null) {
|
||||||
|
url = urlMatch[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearURL() {
|
||||||
|
history.pushState({}, '', window.location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
//update a specific filter radio / dropdown / search. used in updateFilterElements()
|
||||||
|
function matchFilterElementToQuery(id, query) {
|
||||||
|
//dynamically grab select variables
|
||||||
|
let $filterElement = window[`$${id}Filter`];
|
||||||
|
if (id === 'search') {
|
||||||
|
if ($filterElement.value !== query) {
|
||||||
|
// set value of search input
|
||||||
|
$filterElement.value = query;
|
||||||
|
}
|
||||||
|
} else if (id === 'brands') {
|
||||||
|
$filterElement.forEach(option => {
|
||||||
|
if (option.value.trim() === query.trim() && !option.checked) {
|
||||||
|
option.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let options = [...$filterElement.querySelectorAll('option')];
|
||||||
|
//set the value of select dropdown if it doesn't currently match the query in the url
|
||||||
|
if ($filterElement.value !== query) {
|
||||||
|
|
||||||
|
// only change the select dropdown if it matches an available dropdown value
|
||||||
|
options.forEach(option => {
|
||||||
|
if (option.value === query) {
|
||||||
|
$filterElement.value = query;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update all filter elements to match current filter obj
|
||||||
|
function updateFilterElements(queryFilter) {
|
||||||
|
|
||||||
|
let filterElements = [...document.querySelectorAll('.integration-filter select, input.search, .integration-filter input')];
|
||||||
|
let activeFilterElements = filterElements.filter(el => !(el.value === '#' || el.value === '' || (el.id !== 'search' && el.checked === false)));
|
||||||
|
|
||||||
|
if (queryFilter === null) {
|
||||||
|
// resetFilterElements will match elements to query string filter in url
|
||||||
|
resetFilterElements(activeFilterElements);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear filters if there's more than the current queryFilter
|
||||||
|
if (activeFilterElements.length > Object.keys(queryFilter).length) {
|
||||||
|
// filter out filter elements that don't match query object
|
||||||
|
let filtersToReset = activeFilterElements.filter(el => {
|
||||||
|
for (key in queryFilter) {
|
||||||
|
if (el.getAttribute('data-id') === key) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// set filter elements to default 'All' value
|
||||||
|
resetFilterElements(filtersToReset);
|
||||||
|
} else {
|
||||||
|
// update new filters
|
||||||
|
if (Object.keys(queryFilter).length) {
|
||||||
|
for (key in queryFilter) {
|
||||||
|
matchFilterElementToQuery(key, queryFilter[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilterElements(elementArray) {
|
||||||
|
elementArray.forEach(element => {
|
||||||
|
let queryId = element.getAttribute('data-id');
|
||||||
|
if (queryId === 'search') {
|
||||||
|
matchFilterElementToQuery(queryId, '');
|
||||||
|
} else if (queryId === 'brands') {
|
||||||
|
matchFilterElementToQuery(queryId, 'all');
|
||||||
|
} else {
|
||||||
|
matchFilterElementToQuery(queryId, '#');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAllFilters() {
|
||||||
|
clearURL();
|
||||||
|
applyFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter button name cleanup. used in updateFilterButtons()
|
||||||
|
function getButtonText(id, key) {
|
||||||
|
let category;
|
||||||
|
switch (id) {
|
||||||
|
case 'cat':
|
||||||
|
category = 'Category';
|
||||||
|
break;
|
||||||
|
case 'iot_class':
|
||||||
|
category = 'IoT class';
|
||||||
|
break;
|
||||||
|
case 'quality_scale':
|
||||||
|
category = 'Quality scale';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
category = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id !== 'search' && id !== 'brands') {
|
||||||
|
let $filterElement = window[`$${id}Filter`];
|
||||||
|
let options = [...$filterElement.querySelectorAll('option')]
|
||||||
|
let filteredOption = options.filter(option => option.value === key)[0];
|
||||||
|
|
||||||
|
if (filteredOption) {
|
||||||
|
return `${category}: ${filteredOption.innerText}`;
|
||||||
|
} else {
|
||||||
|
return `${category}: Not Found`;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return `${category}: ${key}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update all filter buttons to match current obj filter
|
||||||
|
function updateFilterButtons(queryFilter) {
|
||||||
|
let filterButtonParent = document.querySelector('.component-search');
|
||||||
|
let currentFilterButtons = [...document.querySelectorAll('.component-search .active-filter')];
|
||||||
|
|
||||||
|
if (queryFilter === null) {
|
||||||
|
currentFilterButtons.forEach(button => button.remove());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update text of buttons if filter value has changed
|
||||||
|
updateFilterButtonsText(currentFilterButtons, queryFilter);
|
||||||
|
|
||||||
|
// create a filter button if missing for current filter
|
||||||
|
createFilterButtons(filterButtonParent, queryFilter)
|
||||||
|
|
||||||
|
// remove buttons that don't match current filter
|
||||||
|
if (currentFilterButtons.length >= Object.keys(queryFilter).length) {
|
||||||
|
// filter out buttons that don't match query obj
|
||||||
|
removeFilterButtons(currentFilterButtons, queryFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update text of buttons if filter value has changed // used in updateFilterButtons
|
||||||
|
function updateFilterButtonsText(buttons, query) {
|
||||||
|
buttons.forEach(button => {
|
||||||
|
for (key in query) {
|
||||||
|
if (button.dataset.id === key) {
|
||||||
|
// check for mismatched filter value
|
||||||
|
if (button.dataset.filter !== query[key]) {
|
||||||
|
button.dataset.filter = query[key];
|
||||||
|
button.innerHTML = `<span>${getButtonText(key, query[key])} <iconify-icon icon="mdi:close-circle"></iconify-icon></span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a filter button if it's missing from current applied filter // used in updateFilterButtons
|
||||||
|
function createFilterButtons(buttonParent, query) {
|
||||||
|
for (key in query) {
|
||||||
|
if (document.querySelector(`.component-search .active-filter[data-id="${key}"]`) === null) {
|
||||||
|
let button = document.createElement('button');
|
||||||
|
button.innerHTML = `<span>${getButtonText(key, query[key])} <iconify-icon icon="mdi:close-circle"></iconify-icon></span>`;
|
||||||
|
button.classList.add('active-filter');
|
||||||
|
button.dataset.id = key;
|
||||||
|
button.dataset.filter = query[key]
|
||||||
|
// add click listener to button to remove filter
|
||||||
|
button.addEventListener('click', function () {
|
||||||
|
removeQueryFromURL(this.dataset.id);
|
||||||
|
applyFilter();
|
||||||
|
// remove event listener after click
|
||||||
|
}, { once: true });
|
||||||
|
|
||||||
|
//add button to the dom
|
||||||
|
buttonParent.appendChild(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove buttons that don't match current filter // used in updateFilterButtons
|
||||||
|
function removeFilterButtons(buttons, query) {
|
||||||
|
let buttonsToRemove = buttons.filter(button => {
|
||||||
|
for (key in query) {
|
||||||
|
if (button.dataset.id === key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
buttonsToRemove.forEach(button => button.remove());
|
||||||
|
}
|
||||||
|
|
||||||
function buildImageEl(component) {
|
function buildImageEl(component) {
|
||||||
const urlBase = [
|
const urlBase = [
|
||||||
'https://brands.home-assistant.io',
|
'https://brands.home-assistant.io',
|
||||||
@ -275,28 +722,39 @@ allComponents.pop(); // remove placeholder element at the end
|
|||||||
return `<img src="${urlBase}.png" srcset="${urlBase}@2x.png 2x" loading="lazy">`;
|
return `<img src="${urlBase}.png" srcset="${urlBase}@2x.png 2x" loading="lazy">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update view by filter selection
|
|
||||||
for (let filterLink of document.querySelectorAll('.filter-button-group a')) {
|
|
||||||
filterLink.addEventListener('click', () => {
|
|
||||||
history.pushState('', '', filterLink.getAttribute('href'));
|
|
||||||
applyFilter();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// update view on select change
|
// update view on select change
|
||||||
const versionsEl = document.getElementById('versions');
|
integrationSelectFilterElements.forEach(select => select.addEventListener("change", () => {
|
||||||
versionsEl.addEventListener('change', () => {
|
let id = select.dataset.id;
|
||||||
history.pushState('', '', versionsEl.value);
|
let value = select.value;
|
||||||
applyFilter();
|
|
||||||
});
|
|
||||||
|
|
||||||
const categoriesEl = document.getElementById('categories');
|
if (value !== '#') {
|
||||||
categoriesEl.addEventListener('change', () => {
|
setQueryURL(id, value);
|
||||||
history.pushState('', '', categoriesEl.value);
|
} else {
|
||||||
applyFilter();
|
// clear current filter
|
||||||
});
|
removeQueryFromURL(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// update view on radio change
|
||||||
|
integrationRadioFilterElements.forEach(radioInput => radioInput.addEventListener("click", () => {
|
||||||
|
let id = radioInput.name;
|
||||||
|
let value = radioInput.value;
|
||||||
|
|
||||||
|
if (radioInput.checked && value !== 'all') {
|
||||||
|
//clear all current filters, append new brand filter selection
|
||||||
|
applyFilter({ 'brands': value });
|
||||||
|
} else if (radioInput.checked && value === 'all') {
|
||||||
|
// clear current filter
|
||||||
|
removeQueryFromURL(id);
|
||||||
|
applyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}))
|
||||||
/**
|
/**
|
||||||
* Simple debounce implementation
|
* Simple debounce implementation
|
||||||
*/
|
*/
|
||||||
@ -310,25 +768,30 @@ allComponents.pop(); // remove placeholder element at the end
|
|||||||
|
|
||||||
// update view by search text
|
// update view by search text
|
||||||
let lastSearchText = '';
|
let lastSearchText = '';
|
||||||
searchInputEl.addEventListener('keyup', debounce(() => {
|
searchInputEl.addEventListener('input', debounce(() => {
|
||||||
const text = searchInputEl.value
|
let value = searchInputEl.value
|
||||||
|
.toLowerCase()
|
||||||
// sanitize input
|
// sanitize input
|
||||||
.replace(/[(\?|\&\{\}\(\))]/gi, '')
|
.replace(/[(\?|\&\{\}\(\))]/gi, '')
|
||||||
.trim();
|
.trim();
|
||||||
|
if (typeof value === "string" && value.length >= 1) {
|
||||||
|
if (!searchInputDirty) {
|
||||||
|
resetAllFilters();
|
||||||
|
searchInputDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Only apply filter if value has changed
|
if (value !== lastSearchText) {
|
||||||
if (lastSearchText !== text) {
|
lastSearchText = value;
|
||||||
lastSearchText = text;
|
|
||||||
const newHash = typeof text === "string" && text.length >= 1
|
setQueryURL('search', value);
|
||||||
? SEARCH_PREFIX + text
|
applyFilter();
|
||||||
: '#all';
|
}
|
||||||
history.pushState('', '', newHash);
|
}
|
||||||
|
else {
|
||||||
|
removeQueryFromURL('search');
|
||||||
applyFilter();
|
applyFilter();
|
||||||
}
|
}
|
||||||
}, 500));
|
}, 500));
|
||||||
|
|
||||||
window.addEventListener('hashchange', applyFilter);
|
|
||||||
applyFilter();
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user