From 24b01b99e2a26cacceeae0eef336a293ff638dd6 Mon Sep 17 00:00:00 2001 From: Norbert Micheel Date: Sun, 28 Jul 2024 02:08:25 +0200 Subject: [PATCH 1/2] made autocomplete part as "suggest" --- .../jquery.suggest.css => assets/suggest.css | 0 assets/suggest.js | 211 ++++++++++++++++++ boot.php | 27 +++ docs/30_autocomplete.md | 41 ++++ lang/de_de.lang | 23 ++ lang/en_gb.lang | 22 ++ lang/es_es.lang | 23 ++ lang/pt_br.lang | 23 ++ lang/sv_se.lang | 23 ++ lib/search_it_autocomplete.php | 173 ++++++++++++++ package.yml | 1 + pages/index.php | 2 - pages/suggest.php | 151 +++++++++++++ plugins/autocomplete/assets/suggest.css | 46 ++++ plugins/autocomplete/assets/suggest.js | 211 ++++++++++++++++++ plugins/autocomplete/boot.php | 3 - plugins/autocomplete/lang/de_de.lang | 6 +- 17 files changed, 978 insertions(+), 8 deletions(-) rename plugins/autocomplete/assets/jquery.suggest.css => assets/suggest.css (100%) create mode 100644 assets/suggest.js create mode 100644 docs/30_autocomplete.md create mode 100644 lib/search_it_autocomplete.php create mode 100644 pages/suggest.php create mode 100644 plugins/autocomplete/assets/suggest.css create mode 100644 plugins/autocomplete/assets/suggest.js diff --git a/plugins/autocomplete/assets/jquery.suggest.css b/assets/suggest.css similarity index 100% rename from plugins/autocomplete/assets/jquery.suggest.css rename to assets/suggest.css diff --git a/assets/suggest.js b/assets/suggest.js new file mode 100644 index 0000000..6350086 --- /dev/null +++ b/assets/suggest.js @@ -0,0 +1,211 @@ +document.addEventListener('DOMContentLoaded', function () { + function suggest(input, options) { + var cache = [], + cacheSize = 0, + results = document.createElement('ul'), + timeout = null, + prevLength = 0; + + results.className = options.resultsClass; + document.body.appendChild(results); + + function getPosition(el) { + var rect = el.getBoundingClientRect(); + return { + top: rect.top + window.scrollY, + left: rect.left + window.scrollX + }; + } + + function updatePosition() { + var pos = getPosition(input); + results.style.top = pos.top + input.offsetHeight + 'px'; + results.style.left = pos.left + 'px'; + } + + function handleKeyup(e) { + if (/27$|38$|40$/.test(e.keyCode) && results.style.display !== 'none' || /^13$|^9$/.test(e.keyCode) && getCurrentResult()) { + e.preventDefault(); + e.stopPropagation(); + switch (e.keyCode) { + case 38: + selectPrevious(); + break; + case 40: + selectNext(); + break; + case 9: + case 13: + chooseResult(); + break; + case 27: + results.style.display = 'none'; + break; + } + } else { + if (input.value.length !== prevLength) { + clearTimeout(timeout); + timeout = setTimeout(fetchSuggestions, options.delay); + prevLength = input.value.length; + } + } + } + + function fetchSuggestions() { + var query = input.value.trim(); + if (query.length >= options.minchars) { + var cached = getCached(query); + if (cached) { + displayResults(cached.items); + } else { + fetch(options.source + '&q=' + encodeURIComponent(query)) + .then(response => response.text()) + .then(data => { + var items = parseResults(data, query); + displayResults(items); + addToCache(query, items, data.length); + }); + } + } else { + results.style.display = 'none'; + } + } + + function getCached(query) { + for (var i = 0; i < cache.length; i++) { + if (cache[i].q === query) { + cache.unshift(cache.splice(i, 1)[0]); + return cache[0]; + } + } + return false; + } + + function addToCache(query, items, size) { + while (cache.length && cacheSize + size > options.maxCacheSize) { + var cached = cache.pop(); + cacheSize -= cached.size; + } + cache.push({q: query, size: size, items: items}); + cacheSize += size; + } + + function displayResults(items) { + if (items.length) { + results.innerHTML = ''; + for (var i = 0; i < items.length; i++) { + var li = document.createElement('li'); + li.innerHTML = items[i]; + li.addEventListener('mouseover', function () { + var children = results.children; + for (var j = 0; j < children.length; j++) { + children[j].classList.remove(options.selectClass); + } + this.classList.add(options.selectClass); + }); + li.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + chooseResult(); + }); + results.appendChild(li); + } + results.style.display = 'block'; + } else { + results.style.display = 'none'; + } + } + + function parseResults(data, query) { + var items = data.split(options.delimiter).map(item => item.trim()).filter(item => item); + var regex = new RegExp(query, 'ig'); + return items.map(item => item.replace(regex, function (match) { + return '' + match + ''; + })); + } + + function getCurrentResult() { + var selected = results.querySelector('.' + options.selectClass); + return selected || false; + } + + function chooseResult() { + var currentResult = getCurrentResult(); + if (currentResult) { + input.value = currentResult.textContent; + results.style.display = 'none'; + if (options.onSelect) { + options.onSelect.apply(input, [currentResult.textContent]); + } + } + } + + function selectNext() { + var currentResult = getCurrentResult(); + if (currentResult) { + var next = currentResult.nextElementSibling; + currentResult.classList.remove(options.selectClass); + if (next) { + next.classList.add(options.selectClass); + } else { + results.firstElementChild.classList.add(options.selectClass); + } + } else { + results.firstElementChild.classList.add(options.selectClass); + } + } + + function selectPrevious() { + var currentResult = getCurrentResult(); + if (currentResult) { + var prev = currentResult.previousElementSibling; + currentResult.classList.remove(options.selectClass); + if (prev) { + prev.classList.add(options.selectClass); + } else { + results.lastElementChild.classList.add(options.selectClass); + } + } else { + results.lastElementChild.classList.add(options.selectClass); + } + } + + input.addEventListener('keyup', handleKeyup); + input.addEventListener('blur', function () { + setTimeout(function () { + results.style.display = 'none'; + }, 200); + }); + window.addEventListener('resize', updatePosition); + window.addEventListener('load', updatePosition); + + updatePosition(); + } + + function initSuggest() { + var searchInput = document.querySelector(".search_it-form input[name=search]"); + + if (searchInput) { + suggest(searchInput, { + source: 'index.php?rex-api-call=search_it_autocomplete&rnd=' + Math.random(), + delay: 100, + resultsClass: 'ac_results', + selectClass: 'ac_over', + matchClass: 'ac_match', + minchars: 2, + delimiter: '\n', + onSelect: function (value) { + var searchForm = searchInput.closest('.search_it-form'); + if (searchForm.classList.contains('search_it-form-autocomplete')) { + searchForm.submit(); + return false; + } + + }, + maxCacheSize: 65536 + }); + } + } + + initSuggest(); +}); diff --git a/boot.php b/boot.php index 1daecd3..78d9634 100644 --- a/boot.php +++ b/boot.php @@ -83,6 +83,33 @@ } } +// autocomplete +if ($this->getConfig('autoComplete') == 1) { + if (rex::isBackend()) { + + rex_view::addCssFile($this->getAssetsUrl('suggest.css')); + rex_view::addJsFile($this->getAssetsUrl('suggest.js')); + + if (!$this->hasConfig()) { + $this->setConfig(array( + 'modus' => 'keywords', + 'maxSuggestion' => 10, + 'similarwordsmode' => '0', + 'autoSubmitForm' => 1 + )); + } + } else { + if ($this->getConfig('autoSubmitForm') == 1) { + rex_extension::register('OUTPUT_FILTER', function (rex_extension_point $ep) { + $subject = $ep->getSubject(); + return str_replace(['search_it-form', '###AUTOSUBMIT###'], + ['search_it-form search_it-form-autocomplete', ''], + $subject); + }); + } + } +} + if (rex::isBackend() && rex::getUser()) { // automatic indexing if (rex_addon::get('search_it')->getConfig('automaticindex') == true) { diff --git a/docs/30_autocomplete.md b/docs/30_autocomplete.md new file mode 100644 index 0000000..043a5ce --- /dev/null +++ b/docs/30_autocomplete.md @@ -0,0 +1,41 @@ +# Installation Autocomplete + +Es stellt ein "Suggest"-PlugIn für die Autovervollständigung bei +der Suche im Frontend zur Verfügung und generiert einen Code welcher im Template +eingebunden werden muss. + +## Requirements + +* Funktionierendes Suchformular, das die HTML-Klasse "search_it-form", + sowie ein HTML-Eingabefeld für die Suche mit dem Namen "search" beinhaltet. + +## Installation + +1. Autocomplete und aktivieren +2. Konfiguration im Plugin vornehmen und speichern +3. Den generierten Code für das Template herauskopieren und in das Template, + welches für das Suchfeld verwendet wird, vor dem schließenden `` Tag + hinzufügen +4. Sollte das Suchfeld überall verwendet werden, beispielsweise im Kopf der + Seite, muss der generierte Code in das entsprechende Template hinzugefügt + werden +5. Optional: CSS und JS Datei in den eigenen Frontend_prozess einbauen ( z.B. + per Minify oder im Bimmelbam ) + +## Lizenz + +"Autocomplete" von Manétage steht unter MIT Lizenz. + +## Rechtliches + +Verwendung auf eigene Gefahr. + +## Autor + +**Manetage** - Ronny Kemmereit / Pascal Schuchmann +Norbert Micheel + +**Friends Of REDAXO** + +* http://www.redaxo.org +* https://github.com/FriendsOfREDAXO diff --git a/lang/de_de.lang b/lang/de_de.lang index 37239f5..b12df28 100644 --- a/lang/de_de.lang +++ b/lang/de_de.lang @@ -173,3 +173,26 @@ search_it_docs_module-pagination = Paginierung search_it_docs_module-simsearch = Ähnlichkeitssuche search_it_docs_module-url = URL-Addon search_it_docs_faq = FAQ + + +search_it_autocomplete_title = Autocomplete +search_it_autocomplete_info = Autocomplete für Search it + +search_it_autocomplete_config = Konfiguration +search_it_autocomplete_config_saved = Einstellungen gespeichert. +search_it_autocomplete_config_save = Einstellungen speichern + +search_it_autocomplete_config_autoComplete = Autovervollständigung aktivieren +search_it_autocomplete_config_modus = Modus der Ausgabe +search_it_autocomplete_config_maxSuggestion = Maximale Trefferanzahl +search_it_autocomplete_config_similarwords_label = Ähnlichkeitssuche +search_it_autocomplete_config_similarwords_none = Deaktivieren +search_it_autocomplete_config_similarwords_soundex = Soundex (Allgemein) +search_it_autocomplete_config_similarwords_metaphone = Metaphone (EN) +search_it_autocomplete_config_similarwords_cologne = Kölner Phonetik (DE) +search_it_autocomplete_config_similarwords_all = Alle + +search_it_autocomplete_config_autoSubmitForm = Auto Submit Form + +search_it_autocomplete_config_install = Installation +search_it_autocomplete_config_codesnippet = Generierter Code zum Einbinden in Template diff --git a/lang/en_gb.lang b/lang/en_gb.lang index 3220f3d..793235d 100644 --- a/lang/en_gb.lang +++ b/lang/en_gb.lang @@ -173,3 +173,25 @@ search_it_docs_module-pagination = Paging search_it_docs_module-simsearch = similarity search search_it_docs_module-url = URL-AddOn search_it_docs_faq = FAQ + +search_it_autocomplete_title = Autocomplete +search_it_autocomplete_info = Autocomplete for Search it + +search_it_autocomplete_config = Configuration +search_it_autocomplete_config_saved = Settings saved. +search_it_autocomplete_config_save = Save settings + +search_it_autocomplete_config_autoComplete = Activate autocomplete +search_it_autocomplete_config_modus = Output mode +search_it_autocomplete_config_maxSuggestion = Maximum number of hits +search_it_autocomplete_config_similarwords_label = Similarity search +search_it_autocomplete_config_similarwords_none = Deactivate +search_it_autocomplete_config_similarwords_soundex = Soundex (General) +search_it_autocomplete_config_similarwords_metaphone = Metaphone (EN) +search_it_autocomplete_config_similarwords_cologne = Cologne Phonetics (DE) +search_it_autocomplete_config_similarwords_all = All + +search_it_autocomplete_config_autoSubmitForm = Auto Submit Form + +search_it_autocomplete_config_install = Installation +search_it_autocomplete_config_codesnippet = Generated code for insert in template diff --git a/lang/es_es.lang b/lang/es_es.lang index 90632f8..7985120 100644 --- a/lang/es_es.lang +++ b/lang/es_es.lang @@ -171,3 +171,26 @@ search_it_docs_module-pagination = Paging search_it_docs_module-simsearch = similarity search search_it_docs_module-url = URL-AddOn search_it_docs_faq = FAQ + + +search_it_autocomplete_title = Autocomplete +search_it_autocomplete_info = Autocomplete für Search it + +search_it_autocomplete_config = Konfiguration +search_it_autocomplete_config_saved = Einstellungen gespeichert. +search_it_autocomplete_config_save = Einstellungen speichern + +search_it_autocomplete_config_autoComplete = Autovervollständigung aktivieren +search_it_autocomplete_config_modus = Modus der Ausgabe +search_it_autocomplete_config_maxSuggestion = Maximale Trefferanzahl +search_it_autocomplete_config_similarwords_label = Ähnlichkeitssuche +search_it_autocomplete_config_similarwords_none = Deaktivieren +search_it_autocomplete_config_similarwords_soundex = Soundex (Allgemein) +search_it_autocomplete_config_similarwords_metaphone = Metaphone (EN) +search_it_autocomplete_config_similarwords_cologne = Kölner Phonetik (DE) +search_it_autocomplete_config_similarwords_all = Alle + +search_it_autocomplete_config_autoSubmitForm = Auto Submit Form + +search_it_autocomplete_config_install = Installation +search_it_autocomplete_config_codesnippet = Código generado para su inclusión en la plantilla diff --git a/lang/pt_br.lang b/lang/pt_br.lang index 07d3884..3e12c38 100644 --- a/lang/pt_br.lang +++ b/lang/pt_br.lang @@ -171,3 +171,26 @@ search_it_docs_module-pagination = Paging search_it_docs_module-simsearch = similarity search search_it_docs_module-url = URL-AddOn search_it_docs_faq = FAQ + + +search_it_autocomplete_title = Autocomplete +search_it_autocomplete_info = Autocomplete para Search it + +search_it_autocomplete_config = Configurações +search_it_autocomplete_config_saved = Configurações salvas. +search_it_autocomplete_config_save = Salvar configurações + +search_it_autocomplete_config_autoComplete = Autovervollständigung aktivieren +search_it_autocomplete_config_modus = Modo de emissão +search_it_autocomplete_config_maxSuggestion = Número máximo de visitas +search_it_autocomplete_config_similarwords_label = Número máximo de visitas +search_it_autocomplete_config_similarwords_none = Desativar +search_it_autocomplete_config_similarwords_soundex = Soundex (Geral) +search_it_autocomplete_config_similarwords_metaphone = Metaphone (EN) +search_it_autocomplete_config_similarwords_cologne = Fonética de Colônia (DE) +search_it_autocomplete_config_similarwords_all = Tudo + +search_it_autocomplete_config_autoSubmitForm = Auto Submit Form + +search_it_autocomplete_config_install = Instalação +search_it_autocomplete_config_codesnippet = Código gerado para Template diff --git a/lang/sv_se.lang b/lang/sv_se.lang index b4bc93f..309eeb4 100644 --- a/lang/sv_se.lang +++ b/lang/sv_se.lang @@ -170,3 +170,26 @@ search_it_docs_module-pagination = Paging search_it_docs_module-simsearch = similarity search search_it_docs_module-url = URL-AddOn search_it_docs_faq = FAQ + + +search_it_autocomplete_title = Autocomplete +search_it_autocomplete_info = Autocomplete für Search it + +search_it_autocomplete_config = Konfiguration +search_it_autocomplete_config_saved = Einstellungen gespeichert. +search_it_autocomplete_config_save = Einstellungen speichern + +search_it_autocomplete_config_autoComplete = Autovervollständigung aktivieren +search_it_autocomplete_config_modus = Modus der Ausgabe +search_it_autocomplete_config_maxSuggestion = Maximale Trefferanzahl +search_it_autocomplete_config_similarwords_label = Ähnlichkeitssuche +search_it_autocomplete_config_similarwords_none = Deaktivieren +search_it_autocomplete_config_similarwords_soundex = Soundex (Allgemein) +search_it_autocomplete_config_similarwords_metaphone = Metaphone (EN) +search_it_autocomplete_config_similarwords_cologne = Kölner Phonetik (DE) +search_it_autocomplete_config_similarwords_all = Alle + +search_it_autocomplete_config_autoSubmitForm = Auto Submit Form + +search_it_autocomplete_config_install = Installation +search_it_autocomplete_config_codesnippet = Genererad kod för mallar diff --git a/lib/search_it_autocomplete.php b/lib/search_it_autocomplete.php new file mode 100644 index 0000000..fb0f94d --- /dev/null +++ b/lib/search_it_autocomplete.php @@ -0,0 +1,173 @@ +getConfig('modus'); + $maxSuggestion = (int) $addOn->getConfig('maxSuggestion'); + $similarWordsMode = $addOn->getConfig('similarwordsmode'); + + if ($q != '') { + + if ($modus == "highlightedtext" || $modus == "articlename") { + + $search_it = new search_it(); + //$search_it->similarwordsMode = false; //keine speichern der keywords pro eingabe + $search_it->setMaxHighlightedTextChars(20); + //$search_it->ellipsis = ''; + //$search_it->cache = false; + //$search_it->setHighlightType("surroundtext"); + $result = $search_it->search($q); + //dd($q, $result); + if ($result['count'] > 0) { + $ids = []; + + foreach ($result['hits'] as $hit) { + + if (!in_array($hit['fid'], $ids)) { + + $article = rex_article::get($hit['fid']); + + if ($hit['type'] == 'db_column' and $hit['table'] == rex::getTable('article')) { + + $text = $hit['article_teaser']; + echo $text . "\n"; + + } else { + + $text = $hit['highlightedtext']; + + $ids[] = $hit['fid']; + + if ($modus == "highlightedtext") { + echo $text; + } elseif ($modus == "articlename") { + echo $article->getName(); + } + echo "\n"; + } + } + + if (count($ids) >= $maxSuggestion) { + break; + } + }//enforeach + }//ifresults + + } //modus + elseif ($modus == "keywords") { + + // 0 = Deaktiviert , 1 = soundex, 2 = metaphone, 7 = ALL + $db = rex_sql::factory(); + $db->setDebug(false); + + if ($similarWordsMode == '0') { + + $sql = sprintf(" + SELECT keyword FROM `%s` WHERE ( keyword LIKE :keyword ) AND (clang = -1 OR clang = :clang) GROUP BY keyword ORDER BY count ", + rex::getTablePrefix() . rex::getTempPrefix() . 'search_it_keywords', + ); + + $params = [ + 'keyword' => str_replace(['_', '%'], ['\_', '\%'], $q) . '%', + 'clang' => rex_clang::getCurrentId(), + ]; + } + + if ($similarWordsMode == '1') { + + $sql = sprintf(" + SELECT keyword FROM `%s` WHERE ( keyword LIKE :keyword OR soundex = :soundex ) AND (clang = -1 OR clang = :clang) GROUP BY keyword ORDER BY count ", + rex::getTablePrefix() . rex::getTempPrefix() . 'search_it_keywords', + ); + + $params = [ + 'keyword' => str_replace(['_', '%'], ['\_', '\%'], $q) . '%', + 'soundex' => soundex($q), + 'clang' => rex_clang::getCurrentId(), + ]; + } + + if ($similarWordsMode == '2') { + + $sql = sprintf(" + SELECT keyword FROM `%s` WHERE ( keyword LIKE :keyword OR metaphone = :metaphone ) AND (clang = -1 OR clang = :clang) GROUP BY keyword ORDER BY count ", + rex::getTablePrefix() . rex::getTempPrefix() . 'search_it_keywords', + ); + + $params = [ + 'keyword' => str_replace(['_', '%'], ['\_', '\%'], $q) . '%', + 'metaphone' => metaphone($q), + 'clang' => rex_clang::getCurrentId(), + ]; + } + + if ($similarWordsMode == '3') { + + $sql = sprintf(" + SELECT keyword FROM `%s` WHERE ( keyword LIKE :keyword OR colognephone = :soundex_ger ) AND (clang = -1 OR clang = :clang) GROUP BY keyword ORDER BY count ", + rex::getTablePrefix() . rex::getTempPrefix() . 'search_it_keywords', + ); + + $params = [ + 'keyword' => str_replace(['_', '%'], ['\_', '\%'], $q) . '%', + 'soundex_ger' => soundex_ger($q), + 'clang' => rex_clang::getCurrentId(), + ]; + } + + if ($similarWordsMode == '7') { + + $sql = sprintf(" + SELECT keyword FROM `%s` WHERE ( keyword LIKE :keyword OR soundex = :soundex OR metaphone = :metaphone OR colognephone = :soundex_ger) AND (clang = -1 OR clang = :clang) GROUP BY keyword ORDER BY count ", + rex::getTablePrefix() . rex::getTempPrefix() . 'search_it_keywords', + ); + + $params = [ + 'keyword' => str_replace(['_', '%'], ['\_', '\%'], $q) . '%', + 'soundex' => soundex($q), + 'metaphone' => metaphone($q), + 'soundex_ger' => soundex_ger($q), + 'clang' => rex_clang::getCurrentId(), + ]; + } + + //dd($sql, $params); + $db->setQuery($sql, $params); + + if ($db->getRows() > 0) { + for ($i = 0; $i < $db->getRows(); $i++) { + + echo $db->getValue("keyword") . "\n"; + + if ($i >= $maxSuggestion - 1) { + break; + } + + $db->next(); + } + } + }//endifmodus + + exit; + + } else { + echo 'empty q'; + + exit; + + }//ifempty + + } +} diff --git a/package.yml b/package.yml index 72a6b48..28f4c14 100644 --- a/package.yml +++ b/package.yml @@ -20,6 +20,7 @@ page: result: { title: 'translate:settings_result' } addsources: { title: 'translate:settings_addsources' } blacklist: { title: 'translate:settings_blacklist' } + suggest: { title: 'translate:search_it_autocomplete_title', icon: rex-icon fa-commenting } test: { title: 'translate:test', icon: rex-icon fa-flask } docs: title: 'translate:search_it_docs' diff --git a/pages/index.php b/pages/index.php index 43001d4..22802d8 100644 --- a/pages/index.php +++ b/pages/index.php @@ -8,6 +8,4 @@ exit; } -//echo rex_view::title($this->i18n('title') . ' (' . $this->getProperty('version') . ')'); - rex_be_controller::includeCurrentPageSubPath(); diff --git a/pages/suggest.php b/pages/suggest.php new file mode 100644 index 0000000..af22913 --- /dev/null +++ b/pages/suggest.php @@ -0,0 +1,151 @@ +i18n('title') . ' (' . $this->getProperty('version') . ')'); + +$content = ''; +$buttons = ''; + +// Einstellungen speichern +if (rex_post('formsubmit', 'string') == '1') { + $this->setConfig(rex_post('config', [ + ['autoComplete', 'int'], + ['modus', 'string'], + ['similarwordsmode', 'string'], + ['maxSuggestion', 'string'], + ['autoSubmitForm', 'int'] + ])); + + echo rex_view::success($this->i18n('search_it_autocomplete_config_saved')); + +} + +// autoComplete +$formElements = []; +$n = []; +$n['label'] = ''; +$n['field'] = 'getConfig('autoComplete')) && $this->getConfig('autoComplete') == '1' ? ' checked="checked"' : '') . ' value="1" />'; +$formElements[] = $n; + +$fragment = new rex_fragment(); +$fragment->setVar('elements', $formElements, false); +$content .= $fragment->parse('core/form/checkbox.php'); + +// modus +$formElements = []; +$n = []; +$n['label'] = ''; +$select = new rex_select(); +$select->setId('modus'); +$select->setAttribute('class', 'form-control'); +$select->setName('config[modus]'); +$select->addOption('Keywords', 'keywords'); +$select->addOption('Highlightedtext', 'highlightedtext'); +$select->addOption('Artikelname', 'articlename'); +$select->setSelected($this->getConfig('modus')); +$n['field'] = $select->get(); +$formElements[] = $n; + +$fragment = new rex_fragment(); +$fragment->setVar('elements', $formElements, false); +$content .= $fragment->parse('core/form/container.php'); + + +// similarwordsmode +$formElements = []; +$n = []; +$n['label'] = ''; +$select = new rex_select(); +$select->setId('modus'); +$select->setAttribute('class', 'form-control'); +$select->setName('config[similarwordsmode]'); +$select->addOption($this->i18n('search_it_autocomplete_config_similarwords_none'), '0'); +$select->addOption($this->i18n('search_it_autocomplete_config_similarwords_soundex'), '1'); +$select->addOption($this->i18n('search_it_autocomplete_config_similarwords_metaphone'), '2'); +$select->addOption($this->i18n('search_it_autocomplete_config_similarwords_cologne'), '3'); +$select->addOption($this->i18n('search_it_autocomplete_config_similarwords_all'), '7'); +$select->setSelected($this->getConfig('similarwordsmode')); +$n['field'] = $select->get(); +$formElements[] = $n; + +$fragment = new rex_fragment(); +$fragment->setVar('elements', $formElements, false); +$content .= $fragment->parse('core/form/container.php'); + + +// maxSuggestion +$formElements = []; +$n = []; +$n['label'] = ''; +$n['field'] = ''; +$formElements[] = $n; + +$fragment = new rex_fragment(); +$fragment->setVar('elements', $formElements, false); +$content .= $fragment->parse('core/form/container.php'); + +// autoSubmitForm +$formElements = []; +$n = []; +$n['label'] = ''; +$n['field'] = 'getConfig('autoSubmitForm')) && $this->getConfig('autoSubmitForm') == '1' ? ' checked="checked"' : '') . ' value="1" />'; +$formElements[] = $n; + +$fragment = new rex_fragment(); +$fragment->setVar('elements', $formElements, false); +$content .= $fragment->parse('core/form/checkbox.php'); + +// Save-Button +$formElements = []; +$n = []; +$n['field'] = ''; +$formElements[] = $n; + +$fragment = new rex_fragment(); +$fragment->setVar('elements', $formElements, false); +$buttons = $fragment->parse('core/form/submit.php'); +$buttons = ' +
+ ' . $buttons . ' +
+'; + + +// Ausgabe Formular +$fragment = new rex_fragment(); +$fragment->setVar('class', 'edit'); +$fragment->setVar('title', $this->i18n('search_it_autocomplete_config')); +$fragment->setVar('body', $content, false); +$fragment->setVar('buttons', $buttons, false); +$output = $fragment->parse('core/page/section.php'); + +$output = ' +
+ + ' . $output . ' +
+'; + +echo $output; + + +$file = rex_file::get(rex_path::addOn('search_it', 'docs/30_autocomplete.md')); +$body = rex_markdown::factory()->parse($file); +$fragment = new rex_fragment(); +$fragment->setVar('title', $this->i18n('search_it_autocomplete_config_install')); +$fragment->setVar('body', $body, false); +$content = $fragment->parse('core/page/section.php'); +echo $content; + + +$code = ' +'; +$code = preg_replace("#[\n]#", '', $code); + +$content = '
' . highlight_string($code, true) . '
'; + +$fragment = new rex_fragment(); +$fragment->setVar('title', $this->i18n('search_it_autocomplete_config_codesnippet')); +$fragment->setVar('body', $content, false); + +echo $fragment->parse('core/page/section.php'); + diff --git a/plugins/autocomplete/assets/suggest.css b/plugins/autocomplete/assets/suggest.css new file mode 100644 index 0000000..5435528 --- /dev/null +++ b/plugins/autocomplete/assets/suggest.css @@ -0,0 +1,46 @@ +.ac_results { + background-color: white; + border: 1px solid #008979; + display: none; + list-style: outside none none; + margin: -1px 0 0; + overflow: hidden; + padding: 0; + position: absolute; + z-index: 200; + width: 100%; + max-width: 1150px; +} + +.ac_results li { + color: #333; + line-height: 30px; + padding: 2px 5px 2px 12px; + text-align: left; + white-space: nowrap; +} + +body > .ac_results:first-child { + background: red; +} + +body > .ac_results:last-child { + max-width: 210px !important; + z-index: 5000; + left: calc(100% - 227px) !important; +} + +.ac_over { + background-color: #008979; + color: #fff !important; + cursor: pointer; +} + +.ac_over .ac_match { + color: #fff !important; +} + +.ac_match { + color: #333; + text-decoration: underline; +} diff --git a/plugins/autocomplete/assets/suggest.js b/plugins/autocomplete/assets/suggest.js new file mode 100644 index 0000000..7703697 --- /dev/null +++ b/plugins/autocomplete/assets/suggest.js @@ -0,0 +1,211 @@ +document.addEventListener('DOMContentLoaded', function () { + function suggest(input, options) { + var cache = [], + cacheSize = 0, + results = document.createElement('ul'), + timeout = null, + prevLength = 0; + + results.className = options.resultsClass; + document.body.appendChild(results); + + function getPosition(el) { + var rect = el.getBoundingClientRect(); + return { + top: rect.top + window.scrollY, + left: rect.left + window.scrollX + }; + } + + function updatePosition() { + var pos = getPosition(input); + results.style.top = pos.top + input.offsetHeight + 'px'; + results.style.left = pos.left + 'px'; + } + + function handleKeyup(e) { + if (/27$|38$|40$/.test(e.keyCode) && results.style.display !== 'none' || /^13$|^9$/.test(e.keyCode) && getCurrentResult()) { + e.preventDefault(); + e.stopPropagation(); + switch (e.keyCode) { + case 38: + selectPrevious(); + break; + case 40: + selectNext(); + break; + case 9: + case 13: + chooseResult(); + break; + case 27: + results.style.display = 'none'; + break; + } + } else { + if (input.value.length !== prevLength) { + clearTimeout(timeout); + timeout = setTimeout(fetchSuggestions, options.delay); + prevLength = input.value.length; + } + } + } + + function fetchSuggestions() { + var query = input.value.trim(); + if (query.length >= options.minchars) { + var cached = getCached(query); + if (cached) { + displayResults(cached.items); + } else { + fetch(options.source + '&q=' + encodeURIComponent(query)) + .then(response => response.text()) + .then(data => { + var items = parseResults(data, query); + displayResults(items); + addToCache(query, items, data.length); + }); + } + } else { + results.style.display = 'none'; + } + } + + function getCached(query) { + for (var i = 0; i < cache.length; i++) { + if (cache[i].q === query) { + cache.unshift(cache.splice(i, 1)[0]); + return cache[0]; + } + } + return false; + } + + function addToCache(query, items, size) { + while (cache.length && cacheSize + size > options.maxCacheSize) { + var cached = cache.pop(); + cacheSize -= cached.size; + } + cache.push({q: query, size: size, items: items}); + cacheSize += size; + } + + function displayResults(items) { + if (items.length) { + results.innerHTML = ''; + for (var i = 0; i < items.length; i++) { + var li = document.createElement('li'); + li.innerHTML = items[i]; + li.addEventListener('mouseover', function () { + var children = results.children; + for (var j = 0; j < children.length; j++) { + children[j].classList.remove(options.selectClass); + } + this.classList.add(options.selectClass); + }); + li.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + chooseResult(); + }); + results.appendChild(li); + } + results.style.display = 'block'; + } else { + results.style.display = 'none'; + } + } + + function parseResults(data, query) { + var items = data.split(options.delimiter).map(item => item.trim()).filter(item => item); + var regex = new RegExp(query, 'ig'); + return items.map(item => item.replace(regex, function (match) { + return '' + match + ''; + })); + } + + function getCurrentResult() { + var selected = results.querySelector('.' + options.selectClass); + return selected || false; + } + + function chooseResult() { + var currentResult = getCurrentResult(); + if (currentResult) { + input.value = currentResult.textContent; + results.style.display = 'none'; + if (options.onSelect) { + options.onSelect.apply(input, [currentResult.textContent]); + } + } + } + + function selectNext() { + var currentResult = getCurrentResult(); + if (currentResult) { + var next = currentResult.nextElementSibling; + currentResult.classList.remove(options.selectClass); + if (next) { + next.classList.add(options.selectClass); + } else { + results.firstElementChild.classList.add(options.selectClass); + } + } else { + results.firstElementChild.classList.add(options.selectClass); + } + } + + function selectPrevious() { + var currentResult = getCurrentResult(); + if (currentResult) { + var prev = currentResult.previousElementSibling; + currentResult.classList.remove(options.selectClass); + if (prev) { + prev.classList.add(options.selectClass); + } else { + results.lastElementChild.classList.add(options.selectClass); + } + } else { + results.lastElementChild.classList.add(options.selectClass); + } + } + + input.addEventListener('keyup', handleKeyup); + input.addEventListener('blur', function () { + setTimeout(function () { + results.style.display = 'none'; + }, 200); + }); + window.addEventListener('resize', updatePosition); + window.addEventListener('load', updatePosition); + + updatePosition(); + } + + function initSuggest() { + var searchInput = document.querySelector(".search_it-form input[name=search]"); + + if (searchInput) { + suggest(searchInput, { + source: 'index.php?rex-api-call=search_it_autocomplete_getSimilarWords&rnd=' + Math.random(), + delay: 100, + resultsClass: 'ac_results', + selectClass: 'ac_over', + matchClass: 'ac_match', + minchars: 2, + delimiter: '\n', + onSelect: function (value) { + var searchForm = searchInput.closest('.search_it-form'); + if (searchForm.classList.contains('search_it-form-autocomplete')) { + searchForm.submit(); + return false; + } + + }, + maxCacheSize: 65536 + }); + } + } + + initSuggest(); +}); diff --git a/plugins/autocomplete/boot.php b/plugins/autocomplete/boot.php index 76409a6..44c71df 100644 --- a/plugins/autocomplete/boot.php +++ b/plugins/autocomplete/boot.php @@ -1,8 +1,5 @@ getAssetsUrl('jquery.suggest.css')); diff --git a/plugins/autocomplete/lang/de_de.lang b/plugins/autocomplete/lang/de_de.lang index 7a47b49..5987e4c 100644 --- a/plugins/autocomplete/lang/de_de.lang +++ b/plugins/autocomplete/lang/de_de.lang @@ -1,5 +1,5 @@ -search_it_autocomplete_title = Autocomplete -search_it_autocomplete_plugin_title = Autocomplete +search_it_autocomplete_title = Autocomplete jQuery +search_it_autocomplete_plugin_title = Autocomplete jQuery search_it_autocomplete_plugin_info = Autocomplete für Search it search_it_autocomplete_config = Konfiguration @@ -18,4 +18,4 @@ search_it_autocomplete_config_similarwords_all = Alle search_it_autocomplete_config_autoSubmitForm = Auto Submit Form search_it_autocomplete_config_install = Installation -search_it_autocomplete_config_codesnippet = Generierter Code zum Einbinden in Template \ No newline at end of file +search_it_autocomplete_config_codesnippet = Generierter Code zum Einbinden in Template From 4801dea597fdfe09a326028ac7b5ac34ccca1d4e Mon Sep 17 00:00:00 2001 From: Norbert Micheel Date: Sun, 28 Jul 2024 02:43:52 +0200 Subject: [PATCH 2/2] readme und changelog --- CHANGELOG.md | 2 + plugins/autocomplete/README.md | 3 + plugins/autocomplete/assets/suggest.js | 211 ------------------------- 3 files changed, 5 insertions(+), 211 deletions(-) delete mode 100644 plugins/autocomplete/assets/suggest.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2946e55..377b624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +- autocomplete Plugin is deprecated, use the new subpage instead +- added autocomplete as function without jquery on subpage - documentation plugin removed, turned into subpages thx @alexplusde - Fix Link to SysLog in Backend thx @godsdog diff --git a/plugins/autocomplete/README.md b/plugins/autocomplete/README.md index 45c348a..c2913d5 100644 --- a/plugins/autocomplete/README.md +++ b/plugins/autocomplete/README.md @@ -1,3 +1,6 @@ +** das PlugIn ist deprecated. Es wird in Version 7 entfernt. +Bitte in Zukunft die eingebaute autocomplete-Funktion nutzen ** + # Installation Autocomplete Das Plugin stellt das "Suggest"-jQuery-PlugIn für die Autovervollständigung bei diff --git a/plugins/autocomplete/assets/suggest.js b/plugins/autocomplete/assets/suggest.js deleted file mode 100644 index 7703697..0000000 --- a/plugins/autocomplete/assets/suggest.js +++ /dev/null @@ -1,211 +0,0 @@ -document.addEventListener('DOMContentLoaded', function () { - function suggest(input, options) { - var cache = [], - cacheSize = 0, - results = document.createElement('ul'), - timeout = null, - prevLength = 0; - - results.className = options.resultsClass; - document.body.appendChild(results); - - function getPosition(el) { - var rect = el.getBoundingClientRect(); - return { - top: rect.top + window.scrollY, - left: rect.left + window.scrollX - }; - } - - function updatePosition() { - var pos = getPosition(input); - results.style.top = pos.top + input.offsetHeight + 'px'; - results.style.left = pos.left + 'px'; - } - - function handleKeyup(e) { - if (/27$|38$|40$/.test(e.keyCode) && results.style.display !== 'none' || /^13$|^9$/.test(e.keyCode) && getCurrentResult()) { - e.preventDefault(); - e.stopPropagation(); - switch (e.keyCode) { - case 38: - selectPrevious(); - break; - case 40: - selectNext(); - break; - case 9: - case 13: - chooseResult(); - break; - case 27: - results.style.display = 'none'; - break; - } - } else { - if (input.value.length !== prevLength) { - clearTimeout(timeout); - timeout = setTimeout(fetchSuggestions, options.delay); - prevLength = input.value.length; - } - } - } - - function fetchSuggestions() { - var query = input.value.trim(); - if (query.length >= options.minchars) { - var cached = getCached(query); - if (cached) { - displayResults(cached.items); - } else { - fetch(options.source + '&q=' + encodeURIComponent(query)) - .then(response => response.text()) - .then(data => { - var items = parseResults(data, query); - displayResults(items); - addToCache(query, items, data.length); - }); - } - } else { - results.style.display = 'none'; - } - } - - function getCached(query) { - for (var i = 0; i < cache.length; i++) { - if (cache[i].q === query) { - cache.unshift(cache.splice(i, 1)[0]); - return cache[0]; - } - } - return false; - } - - function addToCache(query, items, size) { - while (cache.length && cacheSize + size > options.maxCacheSize) { - var cached = cache.pop(); - cacheSize -= cached.size; - } - cache.push({q: query, size: size, items: items}); - cacheSize += size; - } - - function displayResults(items) { - if (items.length) { - results.innerHTML = ''; - for (var i = 0; i < items.length; i++) { - var li = document.createElement('li'); - li.innerHTML = items[i]; - li.addEventListener('mouseover', function () { - var children = results.children; - for (var j = 0; j < children.length; j++) { - children[j].classList.remove(options.selectClass); - } - this.classList.add(options.selectClass); - }); - li.addEventListener('click', function (e) { - e.preventDefault(); - e.stopPropagation(); - chooseResult(); - }); - results.appendChild(li); - } - results.style.display = 'block'; - } else { - results.style.display = 'none'; - } - } - - function parseResults(data, query) { - var items = data.split(options.delimiter).map(item => item.trim()).filter(item => item); - var regex = new RegExp(query, 'ig'); - return items.map(item => item.replace(regex, function (match) { - return '' + match + ''; - })); - } - - function getCurrentResult() { - var selected = results.querySelector('.' + options.selectClass); - return selected || false; - } - - function chooseResult() { - var currentResult = getCurrentResult(); - if (currentResult) { - input.value = currentResult.textContent; - results.style.display = 'none'; - if (options.onSelect) { - options.onSelect.apply(input, [currentResult.textContent]); - } - } - } - - function selectNext() { - var currentResult = getCurrentResult(); - if (currentResult) { - var next = currentResult.nextElementSibling; - currentResult.classList.remove(options.selectClass); - if (next) { - next.classList.add(options.selectClass); - } else { - results.firstElementChild.classList.add(options.selectClass); - } - } else { - results.firstElementChild.classList.add(options.selectClass); - } - } - - function selectPrevious() { - var currentResult = getCurrentResult(); - if (currentResult) { - var prev = currentResult.previousElementSibling; - currentResult.classList.remove(options.selectClass); - if (prev) { - prev.classList.add(options.selectClass); - } else { - results.lastElementChild.classList.add(options.selectClass); - } - } else { - results.lastElementChild.classList.add(options.selectClass); - } - } - - input.addEventListener('keyup', handleKeyup); - input.addEventListener('blur', function () { - setTimeout(function () { - results.style.display = 'none'; - }, 200); - }); - window.addEventListener('resize', updatePosition); - window.addEventListener('load', updatePosition); - - updatePosition(); - } - - function initSuggest() { - var searchInput = document.querySelector(".search_it-form input[name=search]"); - - if (searchInput) { - suggest(searchInput, { - source: 'index.php?rex-api-call=search_it_autocomplete_getSimilarWords&rnd=' + Math.random(), - delay: 100, - resultsClass: 'ac_results', - selectClass: 'ac_over', - matchClass: 'ac_match', - minchars: 2, - delimiter: '\n', - onSelect: function (value) { - var searchForm = searchInput.closest('.search_it-form'); - if (searchForm.classList.contains('search_it-form-autocomplete')) { - searchForm.submit(); - return false; - } - - }, - maxCacheSize: 65536 - }); - } - } - - initSuggest(); -});