summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/filtered_search/dropdown_utils.js
blob: 432b0c0dfd2e3242d0888ad4789349bce4f2f960 (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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import FilteredSearchContainer from './container';

(() => {
  class DropdownUtils {
    static getEscapedText(text) {
      let escapedText = text;
      const hasSpace = text.indexOf(' ') !== -1;
      const hasDoubleQuote = text.indexOf('"') !== -1;

      // Encapsulate value with quotes if it has spaces
      // Known side effect: values's with both single and double quotes
      // won't escape properly
      if (hasSpace) {
        if (hasDoubleQuote) {
          escapedText = `'${text}'`;
        } else {
          // Encapsulate singleQuotes or if it hasSpace
          escapedText = `"${text}"`;
        }
      }

      return escapedText;
    }

    static filterWithSymbol(filterSymbol, input, item) {
      const updatedItem = item;
      const searchInput = gl.DropdownUtils.getSearchInput(input);

      const title = updatedItem.title.toLowerCase();
      let value = searchInput.toLowerCase();
      let symbol = '';

      // Remove the symbol for filter
      if (value[0] === filterSymbol) {
        symbol = value[0];
        value = value.slice(1);
      }

      // Removes the first character if it is a quotation so that we can search
      // with multiple words
      if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
        value = value.slice(1);
      }

      // Eg. filterSymbol = ~ for labels
      const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
      const match = title.indexOf(`${symbol}${value}`) !== -1;

      updatedItem.droplab_hidden = !match && !matchWithoutSymbol;

      return updatedItem;
    }

    static filterHint(input, item) {
      const updatedItem = item;
      const searchInput = gl.DropdownUtils.getSearchQuery(input);
      const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput);
      const lastKey = lastToken.key || lastToken || '';
      const allowMultiple = item.type === 'array';
      const itemInExistingTokens = tokens.some(t => t.key === item.hint);

      if (!allowMultiple && itemInExistingTokens) {
        updatedItem.droplab_hidden = true;
      } else if (!lastKey || searchInput.split('').last() === ' ') {
        updatedItem.droplab_hidden = false;
      } else if (lastKey) {
        const split = lastKey.split(':');
        const tokenName = split[0].split(' ').last();

        const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
        updatedItem.droplab_hidden = tokenName ? match : false;
      }

      return updatedItem;
    }

    static setDataValueIfSelected(filter, selected) {
      const dataValue = selected.getAttribute('data-value');

      if (dataValue) {
        gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
      }

      // Return boolean based on whether it was set
      return dataValue !== null;
    }

    // Determines the full search query (visual tokens + input)
    static getSearchQuery(untilInput = false) {
      const container = FilteredSearchContainer.container;
      const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
      const values = [];

      if (untilInput) {
        const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
        // Add one to include input-token to the tokens array
        tokens.splice(inputIndex + 1);
      }

      tokens.forEach((token) => {
        if (token.classList.contains('js-visual-token')) {
          const name = token.querySelector('.name');
          const value = token.querySelector('.value');
          const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
          let valueText = '';

          if (value && value.innerText) {
            valueText = value.innerText;
          }

          if (token.className.indexOf('filtered-search-token') !== -1) {
            values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
          } else {
            values.push(name.innerText);
          }
        } else if (token.classList.contains('input-token')) {
          const { isLastVisualTokenValid } =
            gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();

          const input = FilteredSearchContainer.container.querySelector('.filtered-search');
          const inputValue = input && input.value;

          if (isLastVisualTokenValid) {
            values.push(inputValue);
          } else {
            const previous = values.pop();
            values.push(`${previous}${inputValue}`);
          }
        }
      });

      return values.join(' ');
    }

    static getSearchInput(filteredSearchInput) {
      const inputValue = filteredSearchInput.value;
      const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);

      return inputValue.slice(0, right);
    }

    static getInputSelectionPosition(input) {
      const selectionStart = input.selectionStart;
      let inputValue = input.value;
      // Replace all spaces inside quote marks with underscores
      // (will continue to match entire string until an end quote is found if any)
      // This helps with matching the beginning & end of a token:key
      inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));

      // Get the right position for the word selected
      // Regex matches first space
      let right = inputValue.slice(selectionStart).search(/\s/);

      if (right >= 0) {
        right += selectionStart;
      } else if (right < 0) {
        right = inputValue.length;
      }

      // Get the left position for the word selected
      // Regex matches last non-whitespace character
      let left = inputValue.slice(0, right).search(/\S+$/);

      if (selectionStart === 0) {
        left = 0;
      } else if (selectionStart === inputValue.length && left < 0) {
        left = inputValue.length;
      } else if (left < 0) {
        left = selectionStart;
      }

      return {
        left,
        right,
      };
    }
  }

  window.gl = window.gl || {};
  gl.DropdownUtils = DropdownUtils;
})();