debian-mirror-gitlab/app/assets/javascripts/search_settings/components/search_settings.vue

190 lines
5 KiB
Vue
Raw Normal View History

2021-03-08 18:12:59 +05:30
<script>
import { GlSearchBoxByType } from '@gitlab/ui';
2021-11-18 22:05:49 +05:30
import { uniq, escapeRegExp } from 'lodash';
import {
EXCLUDED_NODES,
HIDE_CLASS,
HIGHLIGHT_CLASS,
NONE_PADDING_CLASS,
TYPING_DELAY,
} from '../constants';
2021-03-08 18:12:59 +05:30
2021-03-11 19:13:27 +05:30
const origExpansions = new Map();
2021-03-08 18:12:59 +05:30
const findSettingsSection = (sectionSelector, node) => {
return node.parentElement.closest(sectionSelector);
};
2021-03-11 19:13:27 +05:30
const restoreExpansionState = ({ expandSection, collapseSection }) => {
origExpansions.forEach((isExpanded, section) => {
if (isExpanded) {
2021-03-08 18:12:59 +05:30
expandSection(section);
} else {
collapseSection(section);
}
});
2021-03-11 19:13:27 +05:30
origExpansions.clear();
};
const saveExpansionState = (sections, { isExpanded }) => {
// If we've saved expansions before, don't override it.
if (origExpansions.size > 0) {
return;
}
sections.forEach((section) => origExpansions.set(section, isExpanded(section)));
};
const resetSections = ({ sectionSelector }) => {
document.querySelectorAll(sectionSelector).forEach((section) => {
section.classList.remove(HIDE_CLASS);
});
2021-03-08 18:12:59 +05:30
};
const clearHighlights = () => {
2021-11-18 22:05:49 +05:30
document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach((element) => {
const { parentNode } = element;
const textNode = document.createTextNode(element.textContent);
parentNode.replaceChild(textNode, element);
parentNode.normalize();
});
2021-03-08 18:12:59 +05:30
};
const hideSectionsExcept = (sectionSelector, visibleSections) => {
Array.from(document.querySelectorAll(sectionSelector))
.filter((section) => !visibleSections.includes(section))
.forEach((section) => {
section.classList.add(HIDE_CLASS);
});
};
2021-11-18 22:05:49 +05:30
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);
});
2021-03-08 18:12:59 +05:30
};
2021-11-18 22:05:49 +05:30
const displayResults = ({ sectionSelector, expandSection, searchTerm }, matches) => {
2021-03-08 18:12:59 +05:30
const elements = matches.map((match) => match.parentElement);
const sections = uniq(elements.map((element) => findSettingsSection(sectionSelector, element)));
hideSectionsExcept(sectionSelector, sections);
sections.forEach(expandSection);
2021-11-18 22:05:49 +05:30
highlightElements(elements, searchTerm);
2021-03-08 18:12:59 +05:30
};
const clearResults = (params) => {
resetSections(params);
clearHighlights();
};
const includeNode = (node, lowerSearchTerm) =>
node.textContent.toLowerCase().includes(lowerSearchTerm) &&
EXCLUDED_NODES.every((excluded) => !node.parentElement.closest(excluded));
const search = (root, searchTerm) => {
const iterator = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
return includeNode(node, searchTerm.toLowerCase())
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
},
});
const results = [];
for (let currentNode = iterator.nextNode(); currentNode; currentNode = iterator.nextNode()) {
results.push(currentNode);
}
return results;
};
export default {
components: {
GlSearchBoxByType,
},
props: {
searchRoot: {
type: Element,
required: true,
},
sectionSelector: {
type: String,
required: true,
},
2021-03-11 19:13:27 +05:30
isExpandedFn: {
type: Function,
required: false,
// default to a function that returns false
default: () => () => false,
},
2021-03-08 18:12:59 +05:30
},
data() {
return {
searchTerm: '',
};
},
methods: {
search(value) {
2021-11-18 22:05:49 +05:30
this.searchTerm = value;
2021-03-08 18:12:59 +05:30
const displayOptions = {
sectionSelector: this.sectionSelector,
expandSection: this.expandSection,
collapseSection: this.collapseSection,
2021-03-11 19:13:27 +05:30
isExpanded: this.isExpandedFn,
2021-11-18 22:05:49 +05:30
searchTerm: this.searchTerm,
2021-03-08 18:12:59 +05:30
};
clearResults(displayOptions);
if (value.length) {
2021-03-11 19:13:27 +05:30
saveExpansionState(document.querySelectorAll(this.sectionSelector), displayOptions);
2021-11-18 22:05:49 +05:30
displayResults(displayOptions, search(this.searchRoot, this.searchTerm));
2021-03-11 19:13:27 +05:30
} else {
restoreExpansionState(displayOptions);
2021-03-08 18:12:59 +05:30
}
},
expandSection(section) {
this.$emit('expand', section);
},
collapseSection(section) {
this.$emit('collapse', section);
},
},
TYPING_DELAY,
};
</script>
<template>
2021-03-11 19:13:27 +05:30
<gl-search-box-by-type
:value="searchTerm"
:debounce="$options.TYPING_DELAY"
:placeholder="__('Search settings')"
@input="search"
/>
2021-03-08 18:12:59 +05:30
</template>