From e8a29e670287df969e439b166047c4daccd92bfb Mon Sep 17 00:00:00 2001 From: lubber-de Date: Mon, 11 Dec 2023 22:33:20 +0100 Subject: [PATCH 1/8] feat(dropdown): highlightMatches option --- src/definitions/modules/dropdown.js | 31 ++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/definitions/modules/dropdown.js b/src/definitions/modules/dropdown.js index 5c0576f7a4..01bc43b8fd 100644 --- a/src/definitions/modules/dropdown.js +++ b/src/definitions/modules/dropdown.js @@ -895,6 +895,7 @@ regExpFlags = (settings.ignoreSearchCase ? 'i' : '') + 'gm', beginsWithRegExp = new RegExp('^' + escapedTerm, regExpFlags) ; + module.remove.filteredItem(); // avoid loop if we're matching nothing if (module.has.query()) { results = []; @@ -938,12 +939,33 @@ ; } module.debug('Showing only matched items', searchTerm); - module.remove.filteredItem(); if (results) { $item .not(results) .addClass(className.filtered) ; + if (settings.highlightMatches && (settings.match === 'both' || settings.match === 'text')) { + var querySplit = query.split(''), + diacriticReg = settings.ignoreDiacritics ? '[\u0300-\u036F]?' : '', + htmlReg = '(?![^<]*>)', + markedRegExp = new RegExp(htmlReg + '(' + querySplit.join(diacriticReg + ')(.*)' + htmlReg +'(') + diacriticReg + ')', regExpFlags), + markedReplacer = function(){ + var args = [].slice.call(arguments,1, querySplit.length * 2).map(function(x, i){ + return i & 1 ? x : '' + x + ''}) + ; + return args.join(''); + } + ; + $.each(results, function(index, result) { + var $result = $(result), + markedHTML = module.get.choiceText($result) + ; + if (settings.ignoreDiacritics) { + markedHTML = markedHTML.normalize('NFD'); + } + $result.html(markedHTML.replace(markedRegExp, markedReplacer)); + }); + } } if (!module.has.query()) { @@ -3080,6 +3102,12 @@ $item.removeClass(className.active); }, filteredItem: function () { + if (settings.highlightMatches) { + $.each($item, function (index, item) { + var $markItem = $(item); + $markItem.html($markItem.html().replace(/<\/?mark>/g, '')); + }); + } if (settings.useLabels && module.has.maxSelections()) { return; } @@ -4005,6 +4033,7 @@ match: 'both', // what to match against with search selection (both, text, or label) fullTextSearch: 'exact', // search anywhere in value (set to 'exact' to require exact matches) + highlightMatches: false, // Whether search result should highlight matching strings ignoreDiacritics: false, // match results also if they contain diacritics of the same base character (for example searching for "a" will also match "á" or "â" or "à", etc...) hideDividers: false, // Whether to hide any divider elements (specified in selector.divider) that are sibling to any items when searched (set to true will hide all dividers, set to 'empty' will hide them when they are not followed by a visible item) From 35fe60407c760d2d7ee52111ba3c9ea8e333558b Mon Sep 17 00:00:00 2001 From: lubber-de Date: Thu, 14 Dec 2023 23:24:54 +0100 Subject: [PATCH 2/8] feat(search): highlightMatches option --- src/definitions/modules/dropdown.js | 15 +++-- src/definitions/modules/dropdown.less | 7 +++ src/definitions/modules/search.js | 60 ++++++++++++++++--- src/definitions/modules/search.less | 11 +++- src/themes/default/globals/site.variables | 3 + .../default/globals/variation.variables | 2 + src/themes/default/modules/dropdown.variables | 3 + src/themes/default/modules/search.variables | 3 + 8 files changed, 89 insertions(+), 15 deletions(-) diff --git a/src/definitions/modules/dropdown.js b/src/definitions/modules/dropdown.js index 01bc43b8fd..5f23d8407b 100644 --- a/src/definitions/modules/dropdown.js +++ b/src/definitions/modules/dropdown.js @@ -890,9 +890,10 @@ ? query : module.get.query() ), - results = null, - escapedTerm = module.escape.string(searchTerm), - regExpFlags = (settings.ignoreSearchCase ? 'i' : '') + 'gm', + results = null, + escapedTerm = module.escape.string(searchTerm), + regExpIgnore = settings.ignoreSearchCase ? 'i' : '', + regExpFlags = regExpIgnore + 'gm', beginsWithRegExp = new RegExp('^' + escapedTerm, regExpFlags) ; module.remove.filteredItem(); @@ -948,7 +949,7 @@ var querySplit = query.split(''), diacriticReg = settings.ignoreDiacritics ? '[\u0300-\u036F]?' : '', htmlReg = '(?![^<]*>)', - markedRegExp = new RegExp(htmlReg + '(' + querySplit.join(diacriticReg + ')(.*)' + htmlReg +'(') + diacriticReg + ')', regExpFlags), + markedRegExp = new RegExp(htmlReg + '(' + querySplit.join(diacriticReg + ')(.*)' + htmlReg +'(') + diacriticReg + ')', regExpIgnore), markedReplacer = function(){ var args = [].slice.call(arguments,1, querySplit.length * 2).map(function(x, i){ return i & 1 ? x : '' + x + ''}) @@ -1001,8 +1002,10 @@ termLength = term.length, queryLength = query.length ; - query = settings.ignoreSearchCase ? query.toLowerCase() : query; - term = settings.ignoreSearchCase ? term.toLowerCase() : term; + if (settings.ignoreSearchCase) { + query = query.toLowerCase(); + term = term.toLowerCase(); + } if (queryLength > termLength) { return false; } diff --git a/src/definitions/modules/dropdown.less b/src/definitions/modules/dropdown.less index 2912de588d..5451a57e39 100755 --- a/src/definitions/modules/dropdown.less +++ b/src/definitions/modules/dropdown.less @@ -1860,6 +1860,13 @@ select.ui.dropdown { }); } +& when (@variationDropdownHighlightMatches) { + .ui.dropdown .menu > .item mark { + background: @highlightMatchesBackground; + color: @highlightMatchesColor; + } +} + & when (@variationDropdownInverted) { /* -------------- Inverted diff --git a/src/definitions/modules/search.js b/src/definitions/modules/search.js index 401f2e1382..becd456606 100644 --- a/src/definitions/modules/search.js +++ b/src/definitions/modules/search.js @@ -135,7 +135,10 @@ // this makes sure $.extend does not add specified search fields to default fields // this is the only setting which should not extend defaults if (parameters && parameters.searchFields !== undefined) { - settings.searchFields = parameters.searchFields; + settings.searchFields = Array.isArray(parameters.searchFields) + ? parameters.searchFields + : [parameters.searchFields] + ; } }, }, @@ -629,7 +632,7 @@ exactResults = [], fuzzyResults = [], searchExp = searchTerm.replace(regExp.escape, '\\$&'), - matchRegExp = new RegExp(regExp.beginsWith + searchExp, 'i'), + matchRegExp= new RegExp(regExp.beginsWith + searchExp, settings.ignoreSearchCase ? 'i' : ''), // avoid duplicates when pushing results addResult = function (array, result) { @@ -665,13 +668,14 @@ var concatenatedContent = []; $.each(searchFields, function (index, field) { var - fieldExists = (typeof content[field] === 'string') || (typeof content[field] === 'number') + fieldExists = typeof content[field] === 'string' || typeof content[field] === 'number' ; if (fieldExists) { var text; text = typeof content[field] === 'string' ? module.remove.diacritics(content[field]) : content[field].toString(); + text = $('
', { html: text }).text().trim(); if (settings.fullTextSearch === 'all') { concatenatedContent.push(text); if (index < lastSearchFieldIndex) { @@ -702,8 +706,10 @@ }, }, exactSearch: function (query, term) { - query = query.toLowerCase(); - term = term.toLowerCase(); + if (settings.ignoreSearchCase) { + query = query.toLowerCase(); + term = term.toLowerCase(); + } return term.indexOf(query) > -1; }, @@ -730,8 +736,10 @@ if (typeof query !== 'string') { return false; } - query = query.toLowerCase(); - term = term.toLowerCase(); + if (settings.ignoreSearchCase) { + query = query.toLowerCase(); + term = term.toLowerCase(); + } if (queryLength > termLength) { return false; } @@ -1086,6 +1094,38 @@ response[fields.results] = response[fields.results].slice(0, settings.maxResults); } } + if (settings.highlightMatches) { + var results = response[fields.results], + regExpIgnore= settings.ignoreSearchCase ? 'i' : '', + querySplit = module.get.value().split(''), + diacriticReg = settings.ignoreDiacritics ? '[\u0300-\u036F]?' : '', + htmlReg = '(?![^<]*>)', + markedRegExp = new RegExp(htmlReg + '(' + querySplit.join(diacriticReg + ')(.*)' + htmlReg +'(') + diacriticReg + ')', regExpIgnore), + markedReplacer = function(){ + var args = [].slice.call(arguments,1, querySplit.length * 2).map(function(x, i){ + return i & 1 ? x : '' + x + ''}) + ; + return args.join(''); + } + ; + $.each(results, function (label, content) { + $.each(settings.searchFields, function (index, field) { + var + fieldExists = typeof content[field] === 'string' || typeof content[field] === 'number' + ; + if (fieldExists) { + var markedHTML = typeof content[field] === 'string' + ? content[field] + : content[field].toString(); + if (settings.ignoreDiacritics) { + markedHTML = markedHTML.normalize('NFD'); + } + markedHTML = markedHTML.replace(/<\/?mark>/g, ''); + response[fields.results][label][field] = markedHTML.replace(markedRegExp, markedReplacer); + } + }); + }); + } if (isFunction(template)) { html = template(response, fields, settings.preserveHTML); } else { @@ -1312,9 +1352,15 @@ // search anywhere in value (set to 'exact' to require exact matches fullTextSearch: 'exact', + // Whether search result should highlight matching strings + highlightMatches: false, + // match results also if they contain diacritics of the same base character (for example searching for "a" will also match "á" or "â" or "à", etc...) ignoreDiacritics: false, + // whether to consider case sensitivity on local searching + ignoreSearchCase: true, + // whether to add events to prompt automatically automatic: true, diff --git a/src/definitions/modules/search.less b/src/definitions/modules/search.less index 54cdf5b35e..9d4f675359 100755 --- a/src/definitions/modules/search.less +++ b/src/definitions/modules/search.less @@ -565,8 +565,8 @@ .ui.search { font-size: @relativeMedium; } -& when not (@variationFeedSizes = false) { - each(@variationFeedSizes, { +& when not (@variationSearchSizes = false) { + each(@variationSearchSizes, { @s: @{value}SearchSize; .ui.@{value}.search { font-size: @@s; @@ -574,6 +574,13 @@ }); } +& when (@variationSearchHighlightMatches) { + .ui.search > .results mark { + background: @highlightMatchesBackground; + color: @highlightMatchesColor; + } +} + /* -------------- Mobile --------------- */ diff --git a/src/themes/default/globals/site.variables b/src/themes/default/globals/site.variables index e017be88fd..c6f4bef107 100755 --- a/src/themes/default/globals/site.variables +++ b/src/themes/default/globals/site.variables @@ -1538,3 +1538,6 @@ @inputWarningPlaceholderColor: if(iscolor(@formWarningColor), lighten(@formWarningColor, 40), @formWarningColor); @inputWarningPlaceholderFocusColor: if(iscolor(@formWarningColor), lighten(@formWarningColor, 30), @formWarningColor); + +@defaultHighlightMatchesBackground: revert; +@defaultHighlightMatchesColor: revert; diff --git a/src/themes/default/globals/variation.variables b/src/themes/default/globals/variation.variables index c270fb4b5c..d5b633143e 100644 --- a/src/themes/default/globals/variation.variables +++ b/src/themes/default/globals/variation.variables @@ -573,6 +573,7 @@ @variationDropdownPointing: true; @variationDropdownColumnar: true; @variationDropdownScrollhint: true; +@variationDropdownHighlightMatches: false; @variationDropdownSizes: @variationAllSizes; /* Embed */ @@ -678,6 +679,7 @@ @variationSearchVeryLong: true; @variationSearchResizable: true; @variationSearchScrolling: true; +@variationSearchHighlightMatches: false; @variationSearchSizes: @variationAllSizes; /* Shape */ diff --git a/src/themes/default/modules/dropdown.variables b/src/themes/default/modules/dropdown.variables index 139d6d645a..c48e12f4df 100755 --- a/src/themes/default/modules/dropdown.variables +++ b/src/themes/default/modules/dropdown.variables @@ -480,3 +480,6 @@ /* Resizable */ @resizableDirection: vertical; + +@highlightMatchesBackground: @defaultHighlightMatchesBackground; +@highlightMatchesColor: @defaultHighlightMatchesColor; diff --git a/src/themes/default/modules/search.variables b/src/themes/default/modules/search.variables index 7e35550134..f471d659c5 100644 --- a/src/themes/default/modules/search.variables +++ b/src/themes/default/modules/search.variables @@ -177,3 +177,6 @@ /* Resizable */ @resizableDirection: vertical; + +@highlightMatchesBackground: @defaultHighlightMatchesBackground; +@highlightMatchesColor: @defaultHighlightMatchesColor; From e601bc8865e31e4716c23298fe61ffa8c228b010 Mon Sep 17 00:00:00 2001 From: lubber-de Date: Thu, 14 Dec 2023 23:36:48 +0100 Subject: [PATCH 3/8] feat(search): non greedy regexp match --- src/definitions/modules/dropdown.js | 2 +- src/definitions/modules/search.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/definitions/modules/dropdown.js b/src/definitions/modules/dropdown.js index 5f23d8407b..3da1bde62c 100644 --- a/src/definitions/modules/dropdown.js +++ b/src/definitions/modules/dropdown.js @@ -949,7 +949,7 @@ var querySplit = query.split(''), diacriticReg = settings.ignoreDiacritics ? '[\u0300-\u036F]?' : '', htmlReg = '(?![^<]*>)', - markedRegExp = new RegExp(htmlReg + '(' + querySplit.join(diacriticReg + ')(.*)' + htmlReg +'(') + diacriticReg + ')', regExpIgnore), + markedRegExp = new RegExp(htmlReg + '(' + querySplit.join(diacriticReg + ')(.*?)' + htmlReg +'(') + diacriticReg + ')', regExpIgnore), markedReplacer = function(){ var args = [].slice.call(arguments,1, querySplit.length * 2).map(function(x, i){ return i & 1 ? x : '' + x + ''}) diff --git a/src/definitions/modules/search.js b/src/definitions/modules/search.js index becd456606..8502c9e47e 100644 --- a/src/definitions/modules/search.js +++ b/src/definitions/modules/search.js @@ -1100,7 +1100,7 @@ querySplit = module.get.value().split(''), diacriticReg = settings.ignoreDiacritics ? '[\u0300-\u036F]?' : '', htmlReg = '(?![^<]*>)', - markedRegExp = new RegExp(htmlReg + '(' + querySplit.join(diacriticReg + ')(.*)' + htmlReg +'(') + diacriticReg + ')', regExpIgnore), + markedRegExp = new RegExp(htmlReg + '(' + querySplit.join(diacriticReg + ')(.*?)' + htmlReg +'(') + diacriticReg + ')', regExpIgnore), markedReplacer = function(){ var args = [].slice.call(arguments,1, querySplit.length * 2).map(function(x, i){ return i & 1 ? x : '' + x + ''}) From 314d63688e672486adaeb63a6bfae577dd249029 Mon Sep 17 00:00:00 2001 From: lubber-de Date: Thu, 14 Dec 2023 23:47:49 +0100 Subject: [PATCH 4/8] feat(search): linting fix --- src/definitions/modules/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/definitions/modules/search.js b/src/definitions/modules/search.js index 8502c9e47e..4529993f6a 100644 --- a/src/definitions/modules/search.js +++ b/src/definitions/modules/search.js @@ -632,7 +632,7 @@ exactResults = [], fuzzyResults = [], searchExp = searchTerm.replace(regExp.escape, '\\$&'), - matchRegExp= new RegExp(regExp.beginsWith + searchExp, settings.ignoreSearchCase ? 'i' : ''), + matchRegExp = new RegExp(regExp.beginsWith + searchExp, settings.ignoreSearchCase ? 'i' : ''), // avoid duplicates when pushing results addResult = function (array, result) { From 13f12ebfc9652be646e6b1356bf6b1a5ff4f864f Mon Sep 17 00:00:00 2001 From: lubber-de Date: Fri, 15 Dec 2023 00:01:28 +0100 Subject: [PATCH 5/8] feat(search): linting fixes --- src/definitions/modules/dropdown.js | 10 +++++----- src/definitions/modules/search.js | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/definitions/modules/dropdown.js b/src/definitions/modules/dropdown.js index 3da1bde62c..7b810e68a7 100644 --- a/src/definitions/modules/dropdown.js +++ b/src/definitions/modules/dropdown.js @@ -949,11 +949,11 @@ var querySplit = query.split(''), diacriticReg = settings.ignoreDiacritics ? '[\u0300-\u036F]?' : '', htmlReg = '(?![^<]*>)', - markedRegExp = new RegExp(htmlReg + '(' + querySplit.join(diacriticReg + ')(.*?)' + htmlReg +'(') + diacriticReg + ')', regExpIgnore), - markedReplacer = function(){ - var args = [].slice.call(arguments,1, querySplit.length * 2).map(function(x, i){ - return i & 1 ? x : '' + x + ''}) - ; + markedRegExp = new RegExp(htmlReg + '(' + querySplit.join(diacriticReg + ')(.*?)' + htmlReg + '(') + diacriticReg + ')', regExpIgnore), + markedReplacer = function () { + var args = [].slice.call(arguments, 1, querySplit.length * 2).map(function (x, i) { + return i & 1 ? x : '' + x + ''; // eslint-disable-line no-bitwise + }); return args.join(''); } ; diff --git a/src/definitions/modules/search.js b/src/definitions/modules/search.js index 4529993f6a..8e00804a2f 100644 --- a/src/definitions/modules/search.js +++ b/src/definitions/modules/search.js @@ -1100,11 +1100,11 @@ querySplit = module.get.value().split(''), diacriticReg = settings.ignoreDiacritics ? '[\u0300-\u036F]?' : '', htmlReg = '(?![^<]*>)', - markedRegExp = new RegExp(htmlReg + '(' + querySplit.join(diacriticReg + ')(.*?)' + htmlReg +'(') + diacriticReg + ')', regExpIgnore), - markedReplacer = function(){ - var args = [].slice.call(arguments,1, querySplit.length * 2).map(function(x, i){ - return i & 1 ? x : '' + x + ''}) - ; + markedRegExp = new RegExp(htmlReg + '(' + querySplit.join(diacriticReg + ')(.*?)' + htmlReg + '(') + diacriticReg + ')', regExpIgnore), + markedReplacer = function () { + var args = [].slice.call(arguments, 1, querySplit.length * 2).map(function (x, i) { + return i & 1 ? x : '' + x + ''; // eslint-disable-line no-bitwise + }); return args.join(''); } ; From 65c2d5dba5ec71c8919e9ab7ff7a6ed097c0c413 Mon Sep 17 00:00:00 2001 From: lubber-de Date: Fri, 15 Dec 2023 00:05:37 +0100 Subject: [PATCH 6/8] feat(search): linting fixes --- src/definitions/modules/dropdown.js | 3 ++- src/definitions/modules/search.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/definitions/modules/dropdown.js b/src/definitions/modules/dropdown.js index 7b810e68a7..57561b0497 100644 --- a/src/definitions/modules/dropdown.js +++ b/src/definitions/modules/dropdown.js @@ -954,10 +954,11 @@ var args = [].slice.call(arguments, 1, querySplit.length * 2).map(function (x, i) { return i & 1 ? x : '' + x + ''; // eslint-disable-line no-bitwise }); + return args.join(''); } ; - $.each(results, function(index, result) { + $.each(results, function (index, result) { var $result = $(result), markedHTML = module.get.choiceText($result) ; diff --git a/src/definitions/modules/search.js b/src/definitions/modules/search.js index 8e00804a2f..6377ea260e 100644 --- a/src/definitions/modules/search.js +++ b/src/definitions/modules/search.js @@ -1096,7 +1096,7 @@ } if (settings.highlightMatches) { var results = response[fields.results], - regExpIgnore= settings.ignoreSearchCase ? 'i' : '', + regExpIgnore = settings.ignoreSearchCase ? 'i' : '', querySplit = module.get.value().split(''), diacriticReg = settings.ignoreDiacritics ? '[\u0300-\u036F]?' : '', htmlReg = '(?![^<]*>)', @@ -1105,6 +1105,7 @@ var args = [].slice.call(arguments, 1, querySplit.length * 2).map(function (x, i) { return i & 1 ? x : '' + x + ''; // eslint-disable-line no-bitwise }); + return args.join(''); } ; From 3583eb09b5a2d711dd447b6144acf3d5e540c144 Mon Sep 17 00:00:00 2001 From: lubber-de Date: Fri, 15 Dec 2023 10:25:59 +0100 Subject: [PATCH 7/8] feat(dropdown,search): respect preserveHTML --- src/definitions/modules/dropdown.js | 9 +++++---- src/definitions/modules/search.js | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/definitions/modules/dropdown.js b/src/definitions/modules/dropdown.js index 57561b0497..bb6ed439c9 100644 --- a/src/definitions/modules/dropdown.js +++ b/src/definitions/modules/dropdown.js @@ -962,6 +962,9 @@ var $result = $(result), markedHTML = module.get.choiceText($result) ; + if (!settings.preserveHTML) { + markedHTML = module.escape.htmlEntities(markedHTML); + } if (settings.ignoreDiacritics) { markedHTML = markedHTML.normalize('NFD'); } @@ -3833,8 +3836,7 @@ ; if (shouldEscape.test(string)) { string = string.replace(forceAmpersand ? /&/g : /&(?![\d#a-z]{1,12};)/gi, '&'); - - return string.replace(badChars, escapedChar); + string = string.replace(badChars, escapedChar); } return string; @@ -4262,8 +4264,7 @@ ; if (shouldEscape.test(string)) { string = string.replace(/&(?![\d#a-z]{1,12};)/gi, '&'); - - return string.replace(badChars, escapedChar); + string = string.replace(badChars, escapedChar); } return string; diff --git a/src/definitions/modules/search.js b/src/definitions/modules/search.js index 6377ea260e..93058f16b3 100644 --- a/src/definitions/modules/search.js +++ b/src/definitions/modules/search.js @@ -1479,8 +1479,9 @@ }; if (shouldEscape.test(string)) { string = string.replace(/&(?![\d#a-z]{1,12};)/gi, '&'); - - return string.replace(badChars, escapedChar); + string = string.replace(badChars, escapedChar); + // FUI controlled HTML is still allowed + string = string.replace(/<(\/)*mark>/g, '<$1mark>'); } return string; From 3ac924d71cce4d141820e1e8e027c581577a49a1 Mon Sep 17 00:00:00 2001 From: lubber-de Date: Fri, 15 Dec 2023 11:26:07 +0100 Subject: [PATCH 8/8] feat(dropdown): always get original html for marking --- src/definitions/modules/dropdown.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/definitions/modules/dropdown.js b/src/definitions/modules/dropdown.js index bb6ed439c9..ae208a29b0 100644 --- a/src/definitions/modules/dropdown.js +++ b/src/definitions/modules/dropdown.js @@ -960,11 +960,8 @@ ; $.each(results, function (index, result) { var $result = $(result), - markedHTML = module.get.choiceText($result) + markedHTML = module.get.choiceText($result, true) ; - if (!settings.preserveHTML) { - markedHTML = module.escape.htmlEntities(markedHTML); - } if (settings.ignoreDiacritics) { markedHTML = markedHTML.normalize('NFD'); }