UNPKG

node-red-contrib-knx-ultimate

Version:

Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control and ETS group address importer. Easy to use and highly configurable.

216 lines (203 loc) 9.31 kB
// 31/03/2020 Search Helper function htmlUtilsfullCSVSearch(sourceText, searchString) { let aSearchWords = []; if (searchString.toLowerCase().includes('exactmatch')) { // Find only the strict exact match of group address. For example, if the string is 0/0/2exactmatch, i return only the item in the csv having // group address 0/0/2 (and not also, for example 0/0/20) // I can have also an exact string like '0/0/1exactmatch 1.000', (GA or any text, plus the datapoint) and i must take it into consideration let aSearchStrings = searchString.split(' '); for (let index = 0; index < aSearchStrings.length; index++) { const element = aSearchStrings[index]; if (element.includes('exactmatch')) { aSearchWords.push(element.replace('exactmatch', ' ')); // The last ' ' allow to return the exact match } else { aSearchWords.push(element); // The last ' ' allow to return the exact match } } } else { aSearchWords = searchString.toLowerCase().split(" "); } // This searches for all words in a string let i = 0; for (let index = 0; index < aSearchWords.length; index++) { if (sourceText.toLowerCase().indexOf(aSearchWords[index]) > -1) i += 1; } return i == aSearchWords.length; } // 2025-09 Secure KNX helpers for GA autocompletes // Cache for secure GA lists per serverId window.__knxSecureGAsCache = window.__knxSecureGAsCache || {}; function KNX_fetchSecureGAs(serverId) { return new Promise((resolve) => { try { if (window.__knxSecureGAsCache[serverId] instanceof Set) { resolve(window.__knxSecureGAsCache[serverId]); return; } $.getJSON("knxUltimateKeyringDataSecureGAs?serverId=" + serverId + "&_=" + new Date().getTime(), (data) => { try { const set = new Set(); if (Array.isArray(data)) data.forEach(ga => { if (typeof ga === 'string') set.add(ga); }); window.__knxSecureGAsCache[serverId] = set; resolve(set); } catch (e) { resolve(new Set()); } }).fail(function(){ resolve(new Set()); }); } catch (e) { resolve(new Set()); } }); } function KNX_enableSecureFormatting($input, serverId) { try { KNX_fetchSecureGAs(serverId).then((secureSet) => { try { const inst = $input.autocomplete("instance"); if (!inst) return; inst._renderItem = function (ul, item) { // Try to detect GA from item.ga or from item.value string let ga = item.ga; if (!ga && typeof item.value === 'string') { const m = item.value.match(/\b\d{1,2}\/\d{1,3}\/\d{1,3}\b/); if (m) ga = m[0]; } const isSecure = ga ? secureSet.has(ga) : false; const colorStyle = isSecure ? 'color: green;' : ''; const shield = isSecure ? '<i class="fa fa-shield"></i> ' : ''; const label = (typeof item.label === 'string') ? item.label : (item.value || ''); return $("<li>").append(`<div style="${colorStyle}">${shield}${label}</div>`).appendTo(ul); }; } catch (e) { } }); } catch (e) { } } // Expose helpers window.htmlUtilsfullCSVSearch = htmlUtilsfullCSVSearch; window.KNX_fetchSecureGAs = KNX_fetchSecureGAs; window.KNX_enableSecureFormatting = KNX_enableSecureFormatting; // 2025-09: Make DPT selects searchable via jQuery UI Autocomplete function KNX_makeSelectSearchable($select) { try { if (!($select && $select.length)) return; const id = $select.attr('id') || ('knx-dpt-' + Math.random().toString(36).slice(2)); $select.attr('id', id); const prevCount = $select.data('knx-options-count'); const curCount = $select.find('option').length; const already = $select.data('knx-searchable') === true; const needsSync = already && prevCount !== curCount; function buildSource() { const items = []; $select.find('option').each(function(){ const v = $(this).attr('value'); const t = $(this).text(); items.push({ label: t, value: v }); }); return items; } function syncFromSelect($input) { try { const val = $select.val(); const txt = ($select.find('option:selected').text()) || ''; if ($input) { $input.val(txt); } // refresh source if ($input && $input.data('ui-autocomplete')) { $input.autocomplete('option', 'source', buildSource()); } $select.data('knx-options-count', curCount); } catch (e) { } } if (!already) { // Create input next to select const width = $select.outerWidth() || 200; const $input = $('<input type="text" class="knx-dpt-combobox" autocomplete="off" />') .attr('id', id + '-search') .css('width', width + 'px'); $select.after($input); // Hide original select but keep in DOM for value binding $select.hide(); // Init autocomplete $input.autocomplete({ minLength: 0, source: buildSource(), select: function(event, ui){ try { event.preventDefault(); } catch(e){} $input.val(ui.item.label); $select.val(ui.item.value).trigger('change'); }, focus: function(event, ui){ try { event.preventDefault(); } catch(e){} $input.val(ui.item.label); } }).on('focus click', function(){ // Always show full list on click/focus try { $(this).autocomplete('search', ''); } catch(e){} }); // Initial sync syncFromSelect($input); // When select value changes programmatically, keep input in sync $select.on('change', function(){ syncFromSelect($input); }); $select.data('knx-searchable', true); } else if (needsSync) { // Only refresh source and text const $input = $('#' + id + '-search'); syncFromSelect($input); } } catch (e) { } } // Auto-enhance any select whose id contains 'dpt' (function(){ if (window.__knx_dptObserverSetup) return; window.__knx_dptObserverSetup = true; function enhance(root){ try { $(root).find('select[id*="dpt"]').each(function(){ KNX_makeSelectSearchable($(this)); }); } catch(e){} } try { const observer = new MutationObserver(function(mutations){ mutations.forEach(function(m){ if (!m.addedNodes) return; m.addedNodes.forEach(function(n){ if (n.nodeType!==1) return; // If a select is added if (n.matches && n.matches('select[id*="dpt"]')) { enhance(n); return; } // If options are added to an existing select if (n.matches && n.matches('option')) { const sel = n.closest && n.closest('select'); if (sel && sel.id && sel.id.indexOf('dpt')>-1) { KNX_makeSelectSearchable($(sel)); } return; } enhance(n); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); // Initial pass if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function(){ enhance(document.body); }); } else { enhance(document.body); } } catch(e){} })(); window.KNX_makeSelectSearchable = KNX_makeSelectSearchable; // Ensure autocomplete lists open immediately on focus/click (function($){ if (!$.ui || !$.ui.autocomplete) return; const _create = $.ui.autocomplete.prototype._create; $.ui.autocomplete.prototype._create = function(){ _create.call(this); const self = this; const showAll = (val) => { if (self.options.disabled) return; const term = (typeof val === 'string') ? val : self.element.val() || ''; const minLen = self.options.minLength || 0; const query = term.length >= minLen ? term : ''; try { self.search(query); } catch (error) { } }; this.element.on('focus.knxAutocomplete click.knxAutocomplete', function(){ clearTimeout(self._knxAutoTimer); self._knxAutoTimer = setTimeout(function(){ showAll(); }, 0); }); this.element.on('mousedown.knxAutocomplete', function(){ clearTimeout(self._knxAutoTimer); self._knxAutoTimer = setTimeout(function(){ showAll(''); }, 0); }); }; })(jQuery);