mirror of
https://github.com/home-assistant/home-assistant.io.git
synced 2025-07-16 13:56:53 +00:00
Modernize JavaScript on integrations page (#30964)
Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
parent
1c4c5574ae
commit
db0843f4ca
@ -33,7 +33,7 @@ regenerate: false
|
|||||||
<a href='#featured' class="btn">Featured</a>
|
<a href='#featured' class="btn">Featured</a>
|
||||||
<a href='#works-with-home-assistant' class="btn">Partner brands</a>
|
<a href='#works-with-home-assistant' class="btn">Partner brands</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="version_select">Added in: <select name="versions">
|
<div class="version_select">Added in: <select id="versions" name="versions">
|
||||||
<option value="#"></option>
|
<option value="#"></option>
|
||||||
{%- for group in components_by_version -%}
|
{%- for group in components_by_version -%}
|
||||||
<optgroup label="{{ group.label }} ({{group.new_components_count}})">
|
<optgroup label="{{ group.label }} ({{group.new_components_count}})">
|
||||||
@ -82,35 +82,9 @@ regenerate: false
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script
|
|
||||||
src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
|
|
||||||
integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs="
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
<script
|
|
||||||
src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"
|
|
||||||
integrity="sha384-wlIoxluAn4R0ncWYWAibi4AATy1rxh4LzxfPhzhRfBwpYzbAQT7FDApW3TTf4KC+"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
<script
|
|
||||||
src="https://cdnjs.cloudflare.com/ajax/libs/vanilla-lazyload/10.17.0/lazyload.min.js"
|
|
||||||
integrity="sha384-vJtpZDYI5wEvw5lJzoCEeYTiRzgoR1NmzkWtzy04p4AaQjHXAzxNqSEVlIsutpxa"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
{% raw %}
|
|
||||||
<script id="component-template" type="text/x-custom-template">
|
|
||||||
{{#components}}
|
|
||||||
<a href="{{url}}" class="option-card">
|
|
||||||
<div class="img-container">{{{image}}}</div>
|
|
||||||
<div class='title'>{{title}}</div>
|
|
||||||
</a>
|
|
||||||
{{/components}}
|
|
||||||
{{^components}}
|
|
||||||
<p class='note'>Nothing found!</p>
|
|
||||||
{{/components}}
|
|
||||||
</script>
|
|
||||||
{% endraw %}
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
// This object contains all components we have
|
// This object contains all components we have
|
||||||
var allComponents = [
|
const allComponents = [
|
||||||
{%- 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: '.' -%}
|
||||||
@ -131,38 +105,38 @@ allComponents.pop(); // remove placeholder element at the end
|
|||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
(function () {
|
(function () {
|
||||||
var template = $('#component-template').html();
|
const SEARCH_PREFIX = '#search/';
|
||||||
Mustache.parse(template); // make future calls to render faster
|
const VERSION_PREFIX = '#version/';
|
||||||
|
|
||||||
|
const searchInputEl = document.querySelector('.component-search input');
|
||||||
|
const componentContainerEl = document.querySelector('#componentContainer');
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
// do the lowerCase transformation once
|
// do the lowerCase transformation once
|
||||||
for (i = 0; i < allComponents.length; i++) {
|
for (let component of allComponents) {
|
||||||
title = allComponents[i].title.toLowerCase();
|
const title = component.title.toLowerCase();
|
||||||
domain = allComponents[i].domain;
|
const domain = component.domain;
|
||||||
title_normalized = title
|
const titleNormalized = title
|
||||||
.normalize("NFD")
|
.normalize("NFD")
|
||||||
.replace(/[\u0300-\u036f]/g, "");
|
.replace(/[\u0300-\u036f]/g, "");
|
||||||
title_dedashed = title.replace(/[-_]/g, " ");
|
const titleDedashed = title.replace(/[-_]/g, " ");
|
||||||
title_normalized_dedashed = title_normalized.replace(/[-_]/g, " ");
|
const titleNormalizedDedashed = titleNormalized.replace(/[-_]/g, " ");
|
||||||
|
|
||||||
allComponents[i].titleLC = title;
|
component.titleLC = title;
|
||||||
allComponents[i].search = `${title} ${title_normalized} ${title_dedashed} ${title_normalized_dedashed} ${domain}`;
|
component.search = `${title} ${titleNormalized} ${titleDedashed} ${titleNormalizedDedashed} ${domain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort the components alphabetically
|
// sort the components alphabetically
|
||||||
allComponents.sort(function (a, b) {
|
allComponents.sort((a, b) => a.titleLC.localeCompare(b.titleLC));
|
||||||
return a.titleLC.localeCompare(b.titleLC);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (location.hash !== '' && location.hash.indexOf('#search/') === 0) {
|
if (location.hash !== '' && location.hash.indexOf(SEARCH_PREFIX) === 0) {
|
||||||
// set default value in search from URL
|
// set default value in search from URL
|
||||||
jQuery('.component-search input').val(decodeURIComponent(location.hash).substring(8));
|
const search = decodeURIComponent(location.hash).substring(SEARCH_PREFIX.length);
|
||||||
|
searchInputEl.value = search;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add focus to the search field - even on IE
|
// add focus to the search field
|
||||||
setTimeout(function () {
|
setTimeout(() => searchInputEl.focus(), 1);
|
||||||
jQuery('.component-search input').focus();
|
|
||||||
}, 1);
|
|
||||||
}
|
}
|
||||||
init();
|
init();
|
||||||
|
|
||||||
@ -170,152 +144,134 @@ allComponents.pop(); // remove placeholder element at the end
|
|||||||
* filter all components, based on the location's hash and render them into the component box
|
* filter all components, based on the location's hash and render them into the component box
|
||||||
*/
|
*/
|
||||||
function applyFilter() {
|
function applyFilter() {
|
||||||
var logoLazyLoad = new LazyLoad({
|
const hash = location.hash || '';
|
||||||
elements_selector: ".option-card img"
|
let components = [];
|
||||||
});
|
|
||||||
var rendered, i, filter, search;
|
|
||||||
var hash = location.hash || '';
|
|
||||||
var data = {
|
|
||||||
components: [],
|
|
||||||
image: function () {
|
|
||||||
return `<img src="https://brands.home-assistant.io/${this.ha_brand ? "brands": "_"}/${this.domain}/logo.png" srcset="https://brands.home-assistant.io/${this.ha_brand ? "brands ": "_"}/${this.domain}/logo@2x.png 2x" loading="lazy">`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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
|
||||||
$('#componentContainer').addClass('remove-items');
|
componentContainerEl.classList.add('remove-items');
|
||||||
|
|
||||||
if (hash.indexOf('#search/') === -1) {
|
if (hash.indexOf('#search/') === -1) {
|
||||||
// reset search box when not searching
|
// reset search box when not searching
|
||||||
jQuery('.component-search input').val(null);
|
searchInputEl.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hash === '#all') {
|
if (hash === '#all') {
|
||||||
// shortcut: no need to filter
|
// shortcut: no need to filter
|
||||||
data.components = allComponents;
|
components = allComponents;
|
||||||
} else {
|
} else {
|
||||||
if (hash.indexOf('#search/') === 0) {
|
let filter, search;
|
||||||
|
if (hash.indexOf(SEARCH_PREFIX) === 0) {
|
||||||
// search through title and category
|
// search through title and category
|
||||||
search = decodeURIComponent(hash).substring(8).toLowerCase();
|
search = decodeURIComponent(hash).substring(SEARCH_PREFIX.length).toLowerCase();
|
||||||
filter = function (comp) {
|
filter = comp =>
|
||||||
return (
|
comp.search.indexOf(search) !== -1 ||
|
||||||
comp.search.indexOf(search) !== -1 ||
|
comp.cat.find((c) => c.includes("#")) != undefined;
|
||||||
comp.cat.find((c) => c.includes("#")) != undefined
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
} else if (hash === '#featured' || hash === '') {
|
} else if (hash === '#featured' || hash === '') {
|
||||||
// only show those with featured = true
|
// only show those with featured = true
|
||||||
filter = function (comp) {
|
filter = comp => comp.featured;
|
||||||
return comp.featured;
|
|
||||||
};
|
|
||||||
|
|
||||||
} else if (hash === '#works-with-home-assistant') {
|
} else if (hash === '#works-with-home-assistant') {
|
||||||
// only show those partners of the Works with Home Assistant program
|
// only show those partners of the Works with Home Assistant program
|
||||||
filter = function (comp) {
|
filter = comp => comp.wwha;
|
||||||
return comp.wwha;
|
|
||||||
};
|
|
||||||
|
|
||||||
} else if (hash.indexOf('#version/') === 0) {
|
} else if (hash.indexOf(VERSION_PREFIX) === 0) {
|
||||||
// compare against a version
|
// compare against a version
|
||||||
search = decodeURIComponent(hash).substring(9).toLowerCase();
|
search = decodeURIComponent(hash).substring(VERSION_PREFIX.length).toLowerCase();
|
||||||
filter = function (comp) {
|
// compare version string against version js
|
||||||
// compare version string against version js
|
filter = comp => comp.v === search;
|
||||||
return comp.v === search;
|
|
||||||
};
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// regular filter categories
|
// regular filter categories
|
||||||
search = hash.substring(1);
|
search = hash.substring(1);
|
||||||
filter = function (comp) {
|
filter = comp => comp.cat.includes(search);
|
||||||
return comp.cat.includes(search);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter all components using the filter function
|
// filter all components using the filter function
|
||||||
for (i = 0; i < (allComponents.length); i++) {
|
components = allComponents.filter(filter);
|
||||||
if (filter(allComponents[i])) {
|
|
||||||
data.components.push(allComponents[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rendered = Mustache.render(template, data);
|
let rendered;
|
||||||
|
if (components.length > 0) {
|
||||||
|
// Note: Assumes all data has already been sanitized
|
||||||
|
rendered = components.map(component => `
|
||||||
|
<a href="${component.url}" class="option-card">
|
||||||
|
<div class="img-container">${buildImageEl(component)}</div>
|
||||||
|
<div class='title'>${component.title}</div>
|
||||||
|
</a>
|
||||||
|
`).join('\n');
|
||||||
|
} else {
|
||||||
|
rendered = '<p class="note">Nothing found!</p>';
|
||||||
|
}
|
||||||
|
|
||||||
// set active class on active menu item
|
// set active class on active menu item
|
||||||
jQuery('.filter-button-group a.active').removeClass('active');
|
document.querySelector('.filter-button-group a.active')?.classList?.remove?.('active');
|
||||||
jQuery(`.filter-button-group a[href="${hash}"]`).addClass('active');
|
document.querySelector(`.filter-button-group a[href="${hash}"]`)?.classList?.add?.('active');
|
||||||
if (hash === "") {
|
if (hash === "") {
|
||||||
jQuery('.filter-button-group a[href*="#featured"]').addClass('active');
|
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
|
||||||
$('#componentContainer').html('').removeClass('show-items remove-items').html(rendered).addClass('show-items');
|
componentContainerEl.innerHTML = '';
|
||||||
logoLazyLoad.update();
|
componentContainerEl.classList.remove('show-items');
|
||||||
|
componentContainerEl.classList.remove('remove-items');
|
||||||
|
componentContainerEl.innerHTML = rendered;
|
||||||
|
componentContainerEl.classList.add('show-items');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function buildImageEl(component) {
|
||||||
* update the browser location hash. This enables users to use the browser-history
|
const urlBase = [
|
||||||
*/
|
'https://brands.home-assistant.io',
|
||||||
function updateHash(newHash) {
|
component.ha_brand ? 'brands' : '_',
|
||||||
if ('pushState' in history) {
|
component.domain,
|
||||||
history.pushState('', '', newHash);
|
'logo'
|
||||||
} else {
|
].join('/');
|
||||||
location.hash = newHash;
|
|
||||||
}
|
return `<img src="${urlBase}.png" srcset="${urlBase}@2x.png 2x" loading="lazy">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update view by filter selection
|
// update view by filter selection
|
||||||
jQuery('.filter-button-group a').click(function () {
|
for (let filterLink of document.querySelectorAll('.filter-button-group a')) {
|
||||||
updateHash(this.getAttribute('href'));
|
filterLink.addEventListener('click', () => {
|
||||||
applyFilter();
|
history.pushState('', '', filterLink.getAttribute('href'));
|
||||||
|
applyFilter();
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// update view on select change
|
// update view on select change
|
||||||
jQuery('select').change(function () {
|
const versionsEl = document.getElementById('versions');
|
||||||
updateHash(this.value);
|
versionsEl.addEventListener('change', () => {
|
||||||
|
history.pushState('', '', versionsEl.value);
|
||||||
applyFilter();
|
applyFilter();
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple debounce implementation, based on http://davidwalsh.name/javascript-debounce-function
|
* Simple debounce implementation
|
||||||
*/
|
*/
|
||||||
function debounce(func, wait, immediate) {
|
function debounce(func, wait) {
|
||||||
var timeout;
|
let timeout;
|
||||||
return function () {
|
return () => {
|
||||||
var context = this,
|
|
||||||
args = arguments;
|
|
||||||
var later = function () {
|
|
||||||
timeout = null;
|
|
||||||
if (!immediate) {
|
|
||||||
func.apply(context, args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var callNow = immediate && !timeout;
|
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(later, wait);
|
timeout = setTimeout(func, wait);
|
||||||
if (callNow) {
|
|
||||||
func.apply(context, args);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// update view by search text
|
// update view by search text
|
||||||
$('.component-search input').keyup(debounce(function () {
|
searchInputEl.addEventListener('keyup', debounce(() => {
|
||||||
var text = $(this).val();
|
const text = searchInputEl.value
|
||||||
// sanitize input
|
// sanitize input
|
||||||
text = text.replace(/[(\?|\&\{\}\(\))]/gi, '').trim();
|
.replace(/[(\?|\&\{\}\(\))]/gi, '')
|
||||||
if (typeof text === "string" && text.length >= 1) {
|
.trim();
|
||||||
updateHash('#search/' + text);
|
|
||||||
|
let newHash = typeof text === "string" && text.length >= 1
|
||||||
|
? SEARCH_PREFIX + text
|
||||||
|
: '#all';
|
||||||
|
// Only apply filter if hash has changed
|
||||||
|
if (newHash !== window.location.hash) {
|
||||||
|
history.pushState('', '', newHash);
|
||||||
|
applyFilter();
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
updateHash('#all');
|
|
||||||
}
|
|
||||||
applyFilter();
|
|
||||||
}, 500));
|
}, 500));
|
||||||
|
|
||||||
window.addEventListener('hashchange', applyFilter);
|
window.addEventListener('hashchange', applyFilter);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user