Commit 63ed3a22 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '325416-improve-search-setting-highlight' into 'master'

Fix: search setting only highlight matched text

See merge request gitlab-org/gitlab!71602
parents 14435c99 4cdf9c9d
<script>
import { GlSearchBoxByType } from '@gitlab/ui';
import { uniq } from 'lodash';
import { EXCLUDED_NODES, HIDE_CLASS, HIGHLIGHT_CLASS, TYPING_DELAY } from '../constants';
import { uniq, escapeRegExp } from 'lodash';
import {
EXCLUDED_NODES,
HIDE_CLASS,
HIGHLIGHT_CLASS,
NONE_PADDING_CLASS,
TYPING_DELAY,
} from '../constants';
const origExpansions = new Map();
......@@ -37,9 +43,13 @@ const resetSections = ({ sectionSelector }) => {
};
const clearHighlights = () => {
document
.querySelectorAll(`.${HIGHLIGHT_CLASS}`)
.forEach((element) => element.classList.remove(HIGHLIGHT_CLASS));
document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach((element) => {
const { parentNode } = element;
const textNode = document.createTextNode(element.textContent);
parentNode.replaceChild(textNode, element);
parentNode.normalize();
});
};
const hideSectionsExcept = (sectionSelector, visibleSections) => {
......@@ -50,17 +60,41 @@ const hideSectionsExcept = (sectionSelector, visibleSections) => {
});
};
const highlightElements = (elements = []) => {
elements.forEach((element) => element.classList.add(HIGHLIGHT_CLASS));
const transformMatchElement = (element, searchTerm) => {
const textStr = element.textContent;
const escapedSearchTerm = new RegExp(`(${escapeRegExp(searchTerm)})`, 'gi');
const textList = textStr.split(escapedSearchTerm);
const replaceFragment = document.createDocumentFragment();
textList.forEach((text) => {
let addElement = document.createTextNode(text);
if (escapedSearchTerm.test(text)) {
addElement = document.createElement('mark');
addElement.className = `${HIGHLIGHT_CLASS} ${NONE_PADDING_CLASS}`;
addElement.textContent = text;
escapedSearchTerm.lastIndex = 0;
}
replaceFragment.appendChild(addElement);
});
return replaceFragment;
};
const highlightElements = (elements = [], searchTerm) => {
elements.forEach((element) => {
const replaceFragment = transformMatchElement(element, searchTerm);
element.innerHTML = '';
element.appendChild(replaceFragment);
});
};
const displayResults = ({ sectionSelector, expandSection }, matches) => {
const displayResults = ({ sectionSelector, expandSection, searchTerm }, matches) => {
const elements = matches.map((match) => match.parentElement);
const sections = uniq(elements.map((element) => findSettingsSection(sectionSelector, element)));
hideSectionsExcept(sectionSelector, sections);
sections.forEach(expandSection);
highlightElements(elements);
highlightElements(elements, searchTerm);
};
const clearResults = (params) => {
......@@ -116,21 +150,21 @@ export default {
},
methods: {
search(value) {
this.searchTerm = value;
const displayOptions = {
sectionSelector: this.sectionSelector,
expandSection: this.expandSection,
collapseSection: this.collapseSection,
isExpanded: this.isExpandedFn,
searchTerm: this.searchTerm,
};
this.searchTerm = value;
clearResults(displayOptions);
if (value.length) {
saveExpansionState(document.querySelectorAll(this.sectionSelector), displayOptions);
displayResults(displayOptions, search(this.searchRoot, value));
displayResults(displayOptions, search(this.searchRoot, this.searchTerm));
} else {
restoreExpansionState(displayOptions);
}
......
......@@ -7,5 +7,8 @@ export const HIDE_CLASS = 'gl-display-none';
// used to highlight the text that matches the * search term
export const HIGHLIGHT_CLASS = 'gl-bg-orange-100';
// used to remove padding for text that matches the * search term
export const NONE_PADDING_CLASS = 'gl-p-0';
// How many seconds to wait until the user * stops typing
export const TYPING_DELAY = 400;
......@@ -11,6 +11,7 @@ describe('search_settings/components/search_settings.vue', () => {
const GENERAL_SETTINGS_ID = 'js-general-settings';
const ADVANCED_SETTINGS_ID = 'js-advanced-settings';
const EXTRA_SETTINGS_ID = 'js-extra-settings';
const TEXT_CONTAIN_SEARCH_TERM = `This text contain ${SEARCH_TERM} and <script>alert("111")</script> others.`;
let wrapper;
......@@ -33,6 +34,21 @@ describe('search_settings/components/search_settings.vue', () => {
const visibleSectionsCount = () =>
document.querySelectorAll(`${SECTION_SELECTOR}:not(.${HIDE_CLASS})`).length;
const highlightedElementsCount = () => document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).length;
const highlightedTextNodes = () => {
const highlightedList = Array.from(document.querySelectorAll(`.${HIGHLIGHT_CLASS}`));
return highlightedList.every((element) => {
return element.textContent.toLowerCase() === SEARCH_TERM.toLowerCase();
});
};
const matchParentElement = () => {
const highlightedList = Array.from(document.querySelectorAll(`.${HIGHLIGHT_CLASS}`));
return highlightedList.map((element) => {
return element.parentNode;
});
};
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
const search = (term) => {
findSearchBox().vm.$emit('input', term);
......@@ -52,6 +68,7 @@ describe('search_settings/components/search_settings.vue', () => {
</section>
<section id="${EXTRA_SETTINGS_ID}" class="settings">
<span>${SEARCH_TERM}</span>
<span>${TEXT_CONTAIN_SEARCH_TERM}</span>
</section>
</div>
</div>
......@@ -82,7 +99,23 @@ describe('search_settings/components/search_settings.vue', () => {
it('highlight elements that match the search term', () => {
search(SEARCH_TERM);
expect(highlightedElementsCount()).toBe(1);
expect(highlightedElementsCount()).toBe(2);
});
it('highlight only search term and not the whole line', () => {
search(SEARCH_TERM);
expect(highlightedTextNodes()).toBe(true);
});
it('prevents search xss', () => {
search(SEARCH_TERM);
const parentNodeList = matchParentElement();
parentNodeList.forEach((element) => {
const scriptElement = element.getElementsByTagName('script');
expect(scriptElement.length).toBe(0);
});
});
describe('default', () => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment