From 94e4e879664d32d912b9f20b4c9b829978827bce Mon Sep 17 00:00:00 2001 From: Marco 'Lubber' Wienkoop Date: Fri, 9 Feb 2024 19:12:11 +0100 Subject: [PATCH] feat(dropdown,search): highlightMatches option This PR adds a new option highlightMatches to dropdown and search module Customizing the mark (background)-color is prepared in the LESS files for a custom theme, but disabled by default. (as the default would do nothing anyway) I decided to leave this to each browsers defaults as its visuals are most familiar to usual browser search and the mark HTML tag uses the exact same styling. Also fixed some typo inside search.less Also made ignoreSearchCase available for search as we have the same for dropdown already --- src/definitions/modules/dropdown.js | 51 +++++++++++--- src/definitions/modules/dropdown.less | 7 ++ src/definitions/modules/search.js | 66 ++++++++++++++++--- 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, 125 insertions(+), 21 deletions(-) diff --git a/src/definitions/modules/dropdown.js b/src/definitions/modules/dropdown.js index fb1ae04d25..2770b33bbb 100644 --- a/src/definitions/modules/dropdown.js +++ b/src/definitions/modules/dropdown.js @@ -890,11 +890,13 @@ ? 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(); // avoid loop if we're matching nothing if (module.has.query()) { results = []; @@ -938,12 +940,34 @@ ; } 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 + ')', 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(''); + } + ; + $.each(results, function (index, result) { + var $result = $(result), + markedHTML = module.get.choiceText($result, true) + ; + if (settings.ignoreDiacritics) { + markedHTML = markedHTML.normalize('NFD'); + } + $result.html(markedHTML.replace(markedRegExp, markedReplacer)); + }); + } } if (!module.has.query()) { @@ -979,8 +1003,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; } @@ -3084,6 +3110,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; } @@ -3809,8 +3841,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; @@ -4015,6 +4046,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) @@ -4239,8 +4271,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/dropdown.less b/src/definitions/modules/dropdown.less index 393a40dee1..2bc72789c3 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 7b45ed2760..8b68ea0754 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] + ; } }, }, @@ -631,7 +634,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) { @@ -667,13 +670,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) { @@ -704,8 +708,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; }, @@ -732,8 +738,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; } @@ -1088,6 +1096,39 @@ 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 + ''; // eslint-disable-line no-bitwise + }); + + 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 { @@ -1316,9 +1357,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, @@ -1436,8 +1483,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; 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;