summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/search_settings/components/search_settings.vue
blob: 820055dc6565eeafdbd2fadbe7446697c83aa906 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<script>
import { GlSearchBoxByType } from '@gitlab/ui';
import { uniq } from 'lodash';
import { EXCLUDED_NODES, HIDE_CLASS, HIGHLIGHT_CLASS, TYPING_DELAY } from '../constants';

const findSettingsSection = (sectionSelector, node) => {
  return node.parentElement.closest(sectionSelector);
};

const resetSections = ({ sectionSelector, expandSection, collapseSection }) => {
  document.querySelectorAll(sectionSelector).forEach((section, index) => {
    section.classList.remove(HIDE_CLASS);

    if (index === 0) {
      expandSection(section);
    } else {
      collapseSection(section);
    }
  });
};

const clearHighlights = () => {
  document
    .querySelectorAll(`.${HIGHLIGHT_CLASS}`)
    .forEach((element) => element.classList.remove(HIGHLIGHT_CLASS));
};

const hideSectionsExcept = (sectionSelector, visibleSections) => {
  Array.from(document.querySelectorAll(sectionSelector))
    .filter((section) => !visibleSections.includes(section))
    .forEach((section) => {
      section.classList.add(HIDE_CLASS);
    });
};

const highlightElements = (elements = []) => {
  elements.forEach((element) => element.classList.add(HIGHLIGHT_CLASS));
};

const displayResults = ({ sectionSelector, expandSection }, 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);
};

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,
    },
  },
  data() {
    return {
      searchTerm: '',
    };
  },
  methods: {
    search(value) {
      const displayOptions = {
        sectionSelector: this.sectionSelector,
        expandSection: this.expandSection,
        collapseSection: this.collapseSection,
      };

      this.searchTerm = value;

      clearResults(displayOptions);

      if (value.length) {
        displayResults(displayOptions, search(this.searchRoot, value));
      }
    },
    expandSection(section) {
      this.$emit('expand', section);
    },
    collapseSection(section) {
      this.$emit('collapse', section);
    },
  },
  TYPING_DELAY,
};
</script>
<template>
  <div class="gl-mt-5">
    <gl-search-box-by-type
      :value="searchTerm"
      :debounce="$options.TYPING_DELAY"
      :placeholder="__('Search settings')"
      @input="search"
    />
  </div>
</template>