summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/filtered_search
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-10-20 08:43:02 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-10-20 08:43:02 +0000
commitd9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch)
tree2341ef426af70ad1e289c38036737e04b0aa5007 /app/assets/javascripts/filtered_search
parentd6e514dd13db8947884cd58fe2a9c2a063400a9b (diff)
downloadgitlab-ce-d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb.tar.gz
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'app/assets/javascripts/filtered_search')
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_ajax_filter.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_operator.js2
-rw-r--r--app/assets/javascripts/filtered_search/droplab/constants.js9
-rw-r--r--app/assets/javascripts/filtered_search/droplab/drop_down.js174
-rw-r--r--app/assets/javascripts/filtered_search/droplab/drop_lab_deprecated.js170
-rw-r--r--app/assets/javascripts/filtered_search/droplab/hook.js15
-rw-r--r--app/assets/javascripts/filtered_search/droplab/hook_button.js60
-rw-r--r--app/assets/javascripts/filtered_search/droplab/hook_input.js117
-rw-r--r--app/assets/javascripts/filtered_search/droplab/keyboard.js122
-rw-r--r--app/assets/javascripts/filtered_search/droplab/plugins/ajax.js54
-rw-r--r--app/assets/javascripts/filtered_search/droplab/plugins/ajax_filter.js114
-rw-r--r--app/assets/javascripts/filtered_search/droplab/plugins/filter.js96
-rw-r--r--app/assets/javascripts/filtered_search/droplab/plugins/input_setter.js50
-rw-r--r--app/assets/javascripts/filtered_search/droplab/utils.js40
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js2
18 files changed, 1029 insertions, 8 deletions
diff --git a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
index 545719ee681..9726b2164b7 100644
--- a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
+++ b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
@@ -1,6 +1,6 @@
import createFlash from '~/flash';
import { __ } from '~/locale';
-import AjaxFilter from '../droplab/plugins/ajax_filter';
+import AjaxFilter from './droplab/plugins/ajax_filter';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index a7648a3c463..5adc074b3ce 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -1,7 +1,7 @@
import createFlash from '~/flash';
import { __ } from '~/locale';
-import Ajax from '../droplab/plugins/ajax';
-import Filter from '../droplab/plugins/filter';
+import Ajax from './droplab/plugins/ajax';
+import Filter from './droplab/plugins/filter';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 47f350dc6a2..9d29782c9a7 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -1,5 +1,5 @@
-import Filter from '~/droplab/plugins/filter';
import { __ } from '~/locale';
+import Filter from './droplab/plugins/filter';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index f78644a3893..ddc3c06a9d1 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -1,7 +1,7 @@
import createFlash from '~/flash';
import { __ } from '~/locale';
-import Ajax from '../droplab/plugins/ajax';
-import Filter from '../droplab/plugins/filter';
+import Ajax from './droplab/plugins/ajax';
+import Filter from './droplab/plugins/filter';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
diff --git a/app/assets/javascripts/filtered_search/dropdown_operator.js b/app/assets/javascripts/filtered_search/dropdown_operator.js
index f933338514a..fb9f25a8c45 100644
--- a/app/assets/javascripts/filtered_search/dropdown_operator.js
+++ b/app/assets/javascripts/filtered_search/dropdown_operator.js
@@ -1,5 +1,5 @@
-import Filter from '~/droplab/plugins/filter';
import { __ } from '~/locale';
+import Filter from './droplab/plugins/filter';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
diff --git a/app/assets/javascripts/filtered_search/droplab/constants.js b/app/assets/javascripts/filtered_search/droplab/constants.js
new file mode 100644
index 00000000000..6451af49d36
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/droplab/constants.js
@@ -0,0 +1,9 @@
+const DATA_TRIGGER = 'data-dropdown-trigger';
+const DATA_DROPDOWN = 'data-dropdown';
+const SELECTED_CLASS = 'droplab-item-selected';
+const ACTIVE_CLASS = 'droplab-item-active';
+const IGNORE_CLASS = 'droplab-item-ignore';
+// Matches `{{anything}}` and `{{ everything }}`.
+const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
+
+export { DATA_TRIGGER, DATA_DROPDOWN, SELECTED_CLASS, ACTIVE_CLASS, TEMPLATE_REGEX, IGNORE_CLASS };
diff --git a/app/assets/javascripts/filtered_search/droplab/drop_down.js b/app/assets/javascripts/filtered_search/droplab/drop_down.js
new file mode 100644
index 00000000000..05b741af191
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/droplab/drop_down.js
@@ -0,0 +1,174 @@
+import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
+import utils from './utils';
+
+class DropDown {
+ constructor(list, config = {}) {
+ this.currentIndex = 0;
+ this.hidden = true;
+ this.list = typeof list === 'string' ? document.querySelector(list) : list;
+ this.items = [];
+ this.eventWrapper = {};
+ this.hideOnClick = config.hideOnClick !== false;
+
+ if (config.addActiveClassToDropdownButton) {
+ this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle');
+ }
+
+ this.getItems();
+ this.initTemplateString();
+ this.addEvents();
+
+ this.initialState = list.innerHTML;
+ }
+
+ getItems() {
+ this.items = [].slice.call(this.list.querySelectorAll('li'));
+ return this.items;
+ }
+
+ initTemplateString() {
+ const items = this.items || this.getItems();
+
+ let templateString = '';
+ if (items.length > 0) templateString = items[items.length - 1].outerHTML;
+ this.templateString = templateString;
+
+ return this.templateString;
+ }
+
+ clickEvent(e) {
+ if (e.target.tagName === 'UL') return;
+ if (e.target.closest(`.${IGNORE_CLASS}`)) return;
+
+ const selected = e.target.closest('li');
+ if (!selected) return;
+
+ this.addSelectedClass(selected);
+
+ e.preventDefault();
+ if (this.hideOnClick) {
+ this.hide();
+ }
+
+ const listEvent = new CustomEvent('click.dl', {
+ detail: {
+ list: this,
+ selected,
+ data: e.target.dataset,
+ },
+ });
+ this.list.dispatchEvent(listEvent);
+ }
+
+ addSelectedClass(selected) {
+ this.removeSelectedClasses();
+ selected.classList.add(SELECTED_CLASS);
+ }
+
+ removeSelectedClasses() {
+ const items = this.items || this.getItems();
+
+ items.forEach((item) => item.classList.remove(SELECTED_CLASS));
+ }
+
+ addEvents() {
+ this.eventWrapper.clickEvent = this.clickEvent.bind(this);
+ this.eventWrapper.closeDropdown = this.closeDropdown.bind(this);
+
+ this.list.addEventListener('click', this.eventWrapper.clickEvent);
+ this.list.addEventListener('keyup', this.eventWrapper.closeDropdown);
+ }
+
+ closeDropdown(event) {
+ // `ESC` key closes the dropdown.
+ if (event.keyCode === 27) {
+ event.preventDefault();
+ return this.toggle();
+ }
+
+ return true;
+ }
+
+ setData(data) {
+ this.data = data;
+ this.render(data);
+ }
+
+ addData(data) {
+ this.data = (this.data || []).concat(data);
+ this.render(this.data);
+ }
+
+ render(data) {
+ const children = data ? data.map(this.renderChildren.bind(this)) : [];
+
+ if (this.list.querySelector('.filter-dropdown-loading')) {
+ return;
+ }
+
+ const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list;
+
+ renderableList.innerHTML = children.join('');
+
+ const listEvent = new CustomEvent('render.dl', {
+ detail: {
+ list: this,
+ },
+ });
+ this.list.dispatchEvent(listEvent);
+ }
+
+ renderChildren(data) {
+ const html = utils.template(this.templateString, data);
+ const template = document.createElement('div');
+
+ template.innerHTML = html;
+ DropDown.setImagesSrc(template);
+ template.firstChild.style.display = data.droplab_hidden ? 'none' : 'block';
+
+ return template.firstChild.outerHTML;
+ }
+
+ show() {
+ if (!this.hidden) return;
+ this.list.style.display = 'block';
+ this.currentIndex = 0;
+ this.hidden = false;
+
+ if (this.dropdownToggle) this.dropdownToggle.classList.add('active');
+ }
+
+ hide() {
+ if (this.hidden) return;
+ this.list.style.display = 'none';
+ this.currentIndex = 0;
+ this.hidden = true;
+
+ if (this.dropdownToggle) this.dropdownToggle.classList.remove('active');
+ }
+
+ toggle() {
+ if (this.hidden) return this.show();
+
+ return this.hide();
+ }
+
+ destroy() {
+ this.hide();
+ this.list.removeEventListener('click', this.eventWrapper.clickEvent);
+ this.list.removeEventListener('keyup', this.eventWrapper.closeDropdown);
+ }
+
+ static setImagesSrc(template) {
+ const images = [...template.querySelectorAll('img[data-src]')];
+
+ images.forEach((image) => {
+ const img = image;
+
+ img.src = img.getAttribute('data-src');
+ img.removeAttribute('data-src');
+ });
+ }
+}
+
+export default DropDown;
diff --git a/app/assets/javascripts/filtered_search/droplab/drop_lab_deprecated.js b/app/assets/javascripts/filtered_search/droplab/drop_lab_deprecated.js
new file mode 100644
index 00000000000..15c4a4b7c6b
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/droplab/drop_lab_deprecated.js
@@ -0,0 +1,170 @@
+/**
+ * This library is deprecated and scheduled to be removed once the
+ * filtered_search component is replaced with GitLab's new Pajamas
+ * filter vue component.
+ *
+ * The documentation has been removed from the gitlab codebase but
+ * can still be found in the commit history here:
+ * https://gitlab.com/gitlab-org/gitlab/-/blob/28f20e28/doc/development/fe_guide/droplab/droplab.md
+ */
+
+import { DATA_TRIGGER } from './constants';
+import HookButton from './hook_button';
+import HookInput from './hook_input';
+import Keyboard from './keyboard';
+import utils from './utils';
+
+class DropLab {
+ constructor() {
+ this.ready = false;
+ this.hooks = [];
+ this.queuedData = [];
+ this.config = {};
+
+ this.eventWrapper = {};
+ }
+
+ loadStatic() {
+ const dropdownTriggers = [].slice.apply(document.querySelectorAll(`[${DATA_TRIGGER}]`));
+ this.addHooks(dropdownTriggers);
+ }
+
+ addData(...args) {
+ this.applyArgs(args, 'processAddData');
+ }
+
+ setData(...args) {
+ this.applyArgs(args, 'processSetData');
+ }
+
+ destroy() {
+ this.hooks.forEach((hook) => hook.destroy());
+ this.hooks = [];
+ this.removeEvents();
+ }
+
+ applyArgs(args, methodName) {
+ if (this.ready) return this[methodName](...args);
+
+ this.queuedData = this.queuedData || [];
+ this.queuedData.push(args);
+
+ return this.ready;
+ }
+
+ processAddData(trigger, data) {
+ this.processData(trigger, data, 'addData');
+ }
+
+ processSetData(trigger, data) {
+ this.processData(trigger, data, 'setData');
+ }
+
+ processData(trigger, data, methodName) {
+ this.hooks.forEach((hook) => {
+ if (Array.isArray(trigger)) hook.list[methodName](trigger);
+
+ if (hook.trigger.id === trigger) hook.list[methodName](data);
+ });
+ }
+
+ addEvents() {
+ this.eventWrapper.documentClicked = this.documentClicked.bind(this);
+ document.addEventListener('click', this.eventWrapper.documentClicked);
+ }
+
+ documentClicked(e) {
+ if (e.defaultPrevented) return;
+
+ if (utils.isDropDownParts(e.target)) return;
+
+ if (e.target.tagName !== 'UL') {
+ const closestUl = utils.closest(e.target, 'UL');
+ if (utils.isDropDownParts(closestUl)) return;
+ }
+
+ this.hooks.forEach((hook) => hook.list.hide());
+ }
+
+ removeEvents() {
+ document.removeEventListener('click', this.eventWrapper.documentClicked);
+ }
+
+ changeHookList(trigger, list, plugins, config) {
+ const availableTrigger =
+ typeof trigger === 'string' ? document.getElementById(trigger) : trigger;
+
+ this.hooks.forEach((hook, i) => {
+ const aHook = hook;
+
+ aHook.list.list.dataset.dropdownActive = false;
+
+ if (aHook.trigger !== availableTrigger) return;
+
+ aHook.destroy();
+ this.hooks.splice(i, 1);
+ this.addHook(availableTrigger, list, plugins, config);
+ });
+ }
+
+ addHook(hook, list, plugins, config) {
+ const availableHook = typeof hook === 'string' ? document.querySelector(hook) : hook;
+ let availableList;
+
+ if (typeof list === 'string') {
+ availableList = document.querySelector(list);
+ } else if (list instanceof Element) {
+ availableList = list;
+ } else {
+ availableList = document.querySelector(hook.dataset[utils.toCamelCase(DATA_TRIGGER)]);
+ }
+
+ availableList.dataset.dropdownActive = true;
+
+ const HookObject = availableHook.tagName === 'INPUT' ? HookInput : HookButton;
+ this.hooks.push(new HookObject(availableHook, availableList, plugins, config));
+
+ return this;
+ }
+
+ addHooks(hooks, plugins, config) {
+ hooks.forEach((hook) => this.addHook(hook, null, plugins, config));
+ return this;
+ }
+
+ setConfig(obj) {
+ this.config = obj;
+ }
+
+ fireReady() {
+ const readyEvent = new CustomEvent('ready.dl', {
+ detail: {
+ dropdown: this,
+ },
+ });
+ document.dispatchEvent(readyEvent);
+
+ this.ready = true;
+ }
+
+ init(hook, list, plugins, config) {
+ if (hook) {
+ this.addHook(hook, list, plugins, config);
+ } else {
+ this.loadStatic();
+ }
+
+ this.addEvents();
+
+ Keyboard();
+
+ this.fireReady();
+
+ this.queuedData.forEach((data) => this.addData(data));
+ this.queuedData = [];
+
+ return this;
+ }
+}
+
+export default DropLab;
diff --git a/app/assets/javascripts/filtered_search/droplab/hook.js b/app/assets/javascripts/filtered_search/droplab/hook.js
new file mode 100644
index 00000000000..8a8dcde9f88
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/droplab/hook.js
@@ -0,0 +1,15 @@
+import DropDown from './drop_down';
+
+class Hook {
+ constructor(trigger, list, plugins, config) {
+ this.trigger = trigger;
+ this.list = new DropDown(list, config);
+ this.type = 'Hook';
+ this.event = 'click';
+ this.plugins = plugins || [];
+ this.config = config || {};
+ this.id = trigger.id;
+ }
+}
+
+export default Hook;
diff --git a/app/assets/javascripts/filtered_search/droplab/hook_button.js b/app/assets/javascripts/filtered_search/droplab/hook_button.js
new file mode 100644
index 00000000000..c51d6167fa3
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/droplab/hook_button.js
@@ -0,0 +1,60 @@
+import Hook from './hook';
+
+class HookButton extends Hook {
+ constructor(trigger, list, plugins, config) {
+ super(trigger, list, plugins, config);
+
+ this.type = 'button';
+ this.event = 'click';
+
+ this.eventWrapper = {};
+
+ this.addEvents();
+ this.addPlugins();
+ }
+
+ addPlugins() {
+ this.plugins.forEach((plugin) => plugin.init(this));
+ }
+
+ clicked(e) {
+ e.preventDefault();
+
+ const buttonEvent = new CustomEvent('click.dl', {
+ detail: {
+ hook: this,
+ },
+ bubbles: true,
+ cancelable: true,
+ });
+ e.target.dispatchEvent(buttonEvent);
+
+ this.list.toggle();
+ }
+
+ addEvents() {
+ this.eventWrapper.clicked = this.clicked.bind(this);
+ this.trigger.addEventListener('click', this.eventWrapper.clicked);
+ }
+
+ removeEvents() {
+ this.trigger.removeEventListener('click', this.eventWrapper.clicked);
+ }
+
+ restoreInitialState() {
+ this.list.list.innerHTML = this.list.initialState;
+ }
+
+ removePlugins() {
+ this.plugins.forEach((plugin) => plugin.destroy());
+ }
+
+ destroy() {
+ this.restoreInitialState();
+
+ this.removeEvents();
+ this.removePlugins();
+ }
+}
+
+export default HookButton;
diff --git a/app/assets/javascripts/filtered_search/droplab/hook_input.js b/app/assets/javascripts/filtered_search/droplab/hook_input.js
new file mode 100644
index 00000000000..c523dae347f
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/droplab/hook_input.js
@@ -0,0 +1,117 @@
+import Hook from './hook';
+
+class HookInput extends Hook {
+ constructor(trigger, list, plugins, config) {
+ super(trigger, list, plugins, config);
+
+ this.type = 'input';
+ this.event = 'input';
+
+ this.eventWrapper = {};
+
+ this.addEvents();
+ this.addPlugins();
+ }
+
+ addPlugins() {
+ this.plugins.forEach((plugin) => plugin.init(this));
+ }
+
+ addEvents() {
+ this.eventWrapper.mousedown = this.mousedown.bind(this);
+ this.eventWrapper.input = this.input.bind(this);
+ this.eventWrapper.keyup = this.keyup.bind(this);
+ this.eventWrapper.keydown = this.keydown.bind(this);
+
+ this.trigger.addEventListener('mousedown', this.eventWrapper.mousedown);
+ this.trigger.addEventListener('input', this.eventWrapper.input);
+ this.trigger.addEventListener('keyup', this.eventWrapper.keyup);
+ this.trigger.addEventListener('keydown', this.eventWrapper.keydown);
+ }
+
+ removeEvents() {
+ this.hasRemovedEvents = true;
+
+ this.trigger.removeEventListener('mousedown', this.eventWrapper.mousedown);
+ this.trigger.removeEventListener('input', this.eventWrapper.input);
+ this.trigger.removeEventListener('keyup', this.eventWrapper.keyup);
+ this.trigger.removeEventListener('keydown', this.eventWrapper.keydown);
+ }
+
+ input(e) {
+ if (this.hasRemovedEvents) return;
+
+ this.list.show();
+
+ const inputEvent = new CustomEvent('input.dl', {
+ detail: {
+ hook: this,
+ text: e.target.value,
+ },
+ bubbles: true,
+ cancelable: true,
+ });
+ e.target.dispatchEvent(inputEvent);
+ }
+
+ mousedown(e) {
+ if (this.hasRemovedEvents) return;
+
+ const mouseEvent = new CustomEvent('mousedown.dl', {
+ detail: {
+ hook: this,
+ text: e.target.value,
+ },
+ bubbles: true,
+ cancelable: true,
+ });
+ e.target.dispatchEvent(mouseEvent);
+ }
+
+ keyup(e) {
+ if (this.hasRemovedEvents) return;
+
+ this.keyEvent(e, 'keyup.dl');
+ }
+
+ keydown(e) {
+ if (this.hasRemovedEvents) return;
+
+ this.keyEvent(e, 'keydown.dl');
+ }
+
+ keyEvent(e, eventName) {
+ this.list.show();
+
+ const keyEvent = new CustomEvent(eventName, {
+ detail: {
+ hook: this,
+ text: e.target.value,
+ which: e.which,
+ key: e.key,
+ },
+ bubbles: true,
+ cancelable: true,
+ });
+ e.target.dispatchEvent(keyEvent);
+ }
+
+ restoreInitialState() {
+ this.list.list.innerHTML = this.list.initialState;
+ }
+
+ removePlugins() {
+ this.plugins.forEach((plugin) => plugin.destroy());
+ }
+
+ destroy() {
+ this.restoreInitialState();
+
+ this.removeEvents();
+ this.removePlugins();
+
+ this.list.destroy();
+ }
+}
+
+export default HookInput;
diff --git a/app/assets/javascripts/filtered_search/droplab/keyboard.js b/app/assets/javascripts/filtered_search/droplab/keyboard.js
new file mode 100644
index 00000000000..fe1ea2fa6b0
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/droplab/keyboard.js
@@ -0,0 +1,122 @@
+/* eslint-disable */
+
+import { ACTIVE_CLASS } from './constants';
+
+const Keyboard = function () {
+ var currentKey;
+ var currentFocus;
+ var isUpArrow = false;
+ var isDownArrow = false;
+ var removeHighlight = function removeHighlight(list) {
+ var itemElements = Array.prototype.slice.call(
+ list.list.querySelectorAll('li:not(.divider):not(.hidden)'),
+ 0,
+ );
+ var listItems = [];
+ for (var i = 0; i < itemElements.length; i++) {
+ var listItem = itemElements[i];
+ listItem.classList.remove(ACTIVE_CLASS);
+
+ if (listItem.style.display !== 'none') {
+ listItems.push(listItem);
+ }
+ }
+ return listItems;
+ };
+
+ var setMenuForArrows = function setMenuForArrows(list) {
+ var listItems = removeHighlight(list);
+ if (list.currentIndex > 0) {
+ if (!listItems[list.currentIndex - 1]) {
+ list.currentIndex = list.currentIndex - 1;
+ }
+
+ if (listItems[list.currentIndex - 1]) {
+ var el = listItems[list.currentIndex - 1];
+ var filterDropdownEl = el.closest('.filter-dropdown');
+ el.classList.add(ACTIVE_CLASS);
+
+ if (filterDropdownEl) {
+ var filterDropdownBottom = filterDropdownEl.offsetHeight;
+ var elOffsetTop = el.offsetTop - 30;
+
+ if (elOffsetTop > filterDropdownBottom) {
+ filterDropdownEl.scrollTop = elOffsetTop - filterDropdownBottom;
+ }
+ }
+ }
+ }
+ };
+
+ var mousedown = function mousedown(e) {
+ var list = e.detail.hook.list;
+ removeHighlight(list);
+ list.show();
+ list.currentIndex = 0;
+ isUpArrow = false;
+ isDownArrow = false;
+ };
+ var selectItem = function selectItem(list) {
+ var listItems = removeHighlight(list);
+ var currentItem = listItems[list.currentIndex - 1];
+ var listEvent = new CustomEvent('click.dl', {
+ detail: {
+ list: list,
+ selected: currentItem,
+ data: currentItem.dataset,
+ },
+ });
+ list.list.dispatchEvent(listEvent);
+ list.hide();
+ };
+
+ var keydown = function keydown(e) {
+ var typedOn = e.target;
+ var list = e.detail.hook.list;
+ var currentIndex = list.currentIndex;
+ isUpArrow = false;
+ isDownArrow = false;
+
+ if (e.detail.which) {
+ currentKey = e.detail.which;
+ if (currentKey === 13) {
+ selectItem(e.detail.hook.list);
+ return;
+ }
+ if (currentKey === 38) {
+ isUpArrow = true;
+ }
+ if (currentKey === 40) {
+ isDownArrow = true;
+ }
+ } else if (e.detail.key) {
+ currentKey = e.detail.key;
+ if (currentKey === 'Enter') {
+ selectItem(e.detail.hook.list);
+ return;
+ }
+ if (currentKey === 'ArrowUp') {
+ isUpArrow = true;
+ }
+ if (currentKey === 'ArrowDown') {
+ isDownArrow = true;
+ }
+ }
+ if (isUpArrow) {
+ currentIndex--;
+ }
+ if (isDownArrow) {
+ currentIndex++;
+ }
+ if (currentIndex < 0) {
+ currentIndex = 0;
+ }
+ list.currentIndex = currentIndex;
+ setMenuForArrows(e.detail.hook.list);
+ };
+
+ document.addEventListener('mousedown.dl', mousedown);
+ document.addEventListener('keydown.dl', keydown);
+};
+
+export default Keyboard;
diff --git a/app/assets/javascripts/filtered_search/droplab/plugins/ajax.js b/app/assets/javascripts/filtered_search/droplab/plugins/ajax.js
new file mode 100644
index 00000000000..77d60454d1a
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/droplab/plugins/ajax.js
@@ -0,0 +1,54 @@
+/* eslint-disable */
+
+import AjaxCache from '~/lib/utils/ajax_cache';
+
+const Ajax = {
+ _loadData: function _loadData(data, config, self) {
+ if (config.loadingTemplate) {
+ var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
+ if (dataLoadingTemplate) dataLoadingTemplate.outerHTML = self.listTemplate;
+ }
+
+ if (!self.destroyed) self.hook.list[config.method].call(self.hook.list, data);
+ },
+ preprocessing: function preprocessing(config, data) {
+ let results = data;
+
+ if (config.preprocessing && !data.preprocessed) {
+ results = config.preprocessing(data);
+ AjaxCache.override(config.endpoint, results);
+ }
+
+ return results;
+ },
+ init: function init(hook) {
+ var self = this;
+ self.destroyed = false;
+ var config = hook.config.Ajax;
+ this.hook = hook;
+ if (!config || !config.endpoint || !config.method) {
+ return;
+ }
+ if (config.method !== 'setData' && config.method !== 'addData') {
+ return;
+ }
+ if (config.loadingTemplate) {
+ var dynamicList = hook.list.list.querySelector('[data-dynamic]');
+ var loadingTemplate = document.createElement('div');
+ loadingTemplate.innerHTML = config.loadingTemplate;
+ loadingTemplate.setAttribute('data-loading-template', '');
+ this.listTemplate = dynamicList.outerHTML;
+ dynamicList.outerHTML = loadingTemplate.outerHTML;
+ }
+
+ return AjaxCache.retrieve(config.endpoint)
+ .then(self.preprocessing.bind(null, config))
+ .then((data) => self._loadData(data, config, self))
+ .catch(config.onError);
+ },
+ destroy: function () {
+ this.destroyed = true;
+ },
+};
+
+export default Ajax;
diff --git a/app/assets/javascripts/filtered_search/droplab/plugins/ajax_filter.js b/app/assets/javascripts/filtered_search/droplab/plugins/ajax_filter.js
new file mode 100644
index 00000000000..d0f2d205bb6
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/droplab/plugins/ajax_filter.js
@@ -0,0 +1,114 @@
+/* eslint-disable */
+
+import AjaxCache from '~/lib/utils/ajax_cache';
+
+const AjaxFilter = {
+ init: function (hook) {
+ this.destroyed = false;
+ this.hook = hook;
+ this.notLoading();
+
+ this.eventWrapper = {};
+ this.eventWrapper.debounceTrigger = this.debounceTrigger.bind(this);
+ this.hook.trigger.addEventListener('keydown.dl', this.eventWrapper.debounceTrigger);
+ this.hook.trigger.addEventListener('focus', this.eventWrapper.debounceTrigger);
+
+ this.trigger(true);
+ },
+
+ notLoading: function notLoading() {
+ this.loading = false;
+ },
+
+ debounceTrigger: function debounceTrigger(e) {
+ var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93];
+ var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1;
+ var focusEvent = e.type === 'focus';
+ if (invalidKeyPressed || this.loading) {
+ return;
+ }
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+ this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200);
+ },
+
+ trigger: function trigger(getEntireList) {
+ var config = this.hook.config.AjaxFilter;
+ var searchValue = this.trigger.value;
+ if (!config || !config.endpoint || !config.searchKey) {
+ return;
+ }
+ if (config.searchValueFunction) {
+ searchValue = config.searchValueFunction();
+ }
+ if (
+ (config.loadingTemplate && this.hook.list.data === undefined) ||
+ this.hook.list.data.length === 0
+ ) {
+ var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ var loadingTemplate = document.createElement('div');
+ loadingTemplate.innerHTML = config.loadingTemplate;
+ loadingTemplate.setAttribute('data-loading-template', true);
+ this.listTemplate = dynamicList.outerHTML;
+ dynamicList.outerHTML = loadingTemplate.outerHTML;
+ }
+ if (getEntireList) {
+ searchValue = '';
+ }
+ if (config.searchKey === searchValue) {
+ return this.list.show();
+ }
+ this.loading = true;
+ var params = config.params || {};
+ params[config.searchKey] = searchValue;
+ var url = config.endpoint + this.buildParams(params);
+ return AjaxCache.retrieve(url)
+ .then((data) => {
+ this._loadData(data, config);
+ if (config.onLoadingFinished) {
+ config.onLoadingFinished(data);
+ }
+ })
+ .catch(config.onError);
+ },
+
+ _loadData(data, config) {
+ const list = this.hook.list;
+ if ((config.loadingTemplate && list.data === undefined) || list.data.length === 0) {
+ const dataLoadingTemplate = list.list.querySelector('[data-loading-template]');
+ if (dataLoadingTemplate) {
+ dataLoadingTemplate.outerHTML = this.listTemplate;
+ }
+ }
+ if (!this.destroyed) {
+ var hookListChildren = list.list.children;
+ var onlyDynamicList =
+ hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic');
+ if (onlyDynamicList && data.length === 0) {
+ list.hide();
+ }
+ list.setData.call(list, data);
+ }
+ this.notLoading();
+ list.currentIndex = 0;
+ },
+
+ buildParams: function (params) {
+ if (!params) return '';
+ var paramsArray = Object.keys(params).map(function (param) {
+ return param + '=' + (params[param] || '');
+ });
+ return '?' + paramsArray.join('&');
+ },
+
+ destroy: function destroy() {
+ if (this.timeout) clearTimeout(this.timeout);
+ this.destroyed = true;
+
+ this.hook.trigger.removeEventListener('keydown.dl', this.eventWrapper.debounceTrigger);
+ this.hook.trigger.removeEventListener('focus', this.eventWrapper.debounceTrigger);
+ },
+};
+
+export default AjaxFilter;
diff --git a/app/assets/javascripts/filtered_search/droplab/plugins/filter.js b/app/assets/javascripts/filtered_search/droplab/plugins/filter.js
new file mode 100644
index 00000000000..06391668928
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/droplab/plugins/filter.js
@@ -0,0 +1,96 @@
+/* eslint-disable */
+
+const Filter = {
+ keydown: function (e) {
+ if (this.destroyed) return;
+
+ var hiddenCount = 0;
+ var dataHiddenCount = 0;
+
+ var list = e.detail.hook.list;
+ var data = list.data;
+ var value = e.detail.hook.trigger.value.toLowerCase();
+ var config = e.detail.hook.config.Filter;
+ var matches = [];
+ var filterFunction;
+ // will only work on dynamically set data
+ if (!data) {
+ return;
+ }
+
+ if (config && config.filterFunction && typeof config.filterFunction === 'function') {
+ filterFunction = config.filterFunction;
+ } else {
+ filterFunction = function (o) {
+ // cheap string search
+ o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1;
+ return o;
+ };
+ }
+
+ dataHiddenCount = data.filter(function (o) {
+ return !o.droplab_hidden;
+ }).length;
+
+ matches = data.map(function (o) {
+ return filterFunction(o, value);
+ });
+
+ hiddenCount = matches.filter(function (o) {
+ return !o.droplab_hidden;
+ }).length;
+
+ if (dataHiddenCount !== hiddenCount) {
+ list.setData(matches);
+ list.currentIndex = 0;
+ }
+ },
+
+ debounceKeydown: function debounceKeydown(e) {
+ if (
+ [
+ 13, // enter
+ 16, // shift
+ 17, // ctrl
+ 18, // alt
+ 20, // caps lock
+ 37, // left arrow
+ 38, // up arrow
+ 39, // right arrow
+ 40, // down arrow
+ 91, // left window
+ 92, // right window
+ 93, // select
+ ].indexOf(e.detail.which || e.detail.keyCode) > -1
+ )
+ return;
+
+ if (this.timeout) clearTimeout(this.timeout);
+ this.timeout = setTimeout(this.keydown.bind(this, e), 200);
+ },
+
+ init: function init(hook) {
+ var config = hook.config.Filter;
+
+ if (!config || !config.template) return;
+
+ this.hook = hook;
+ this.destroyed = false;
+
+ this.eventWrapper = {};
+ this.eventWrapper.debounceKeydown = this.debounceKeydown.bind(this);
+
+ this.hook.trigger.addEventListener('keydown.dl', this.eventWrapper.debounceKeydown);
+ this.hook.trigger.addEventListener('mousedown.dl', this.eventWrapper.debounceKeydown);
+ },
+
+ destroy: function destroy() {
+ if (this.timeout) clearTimeout(this.timeout);
+ this.destroyed = true;
+
+ this.hook.trigger.removeEventListener('keydown.dl', this.eventWrapper.debounceKeydown);
+ this.hook.trigger.removeEventListener('mousedown.dl', this.eventWrapper.debounceKeydown);
+ },
+};
+
+export default Filter;
diff --git a/app/assets/javascripts/filtered_search/droplab/plugins/input_setter.js b/app/assets/javascripts/filtered_search/droplab/plugins/input_setter.js
new file mode 100644
index 00000000000..148d9a35b81
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/droplab/plugins/input_setter.js
@@ -0,0 +1,50 @@
+/* eslint-disable */
+
+const InputSetter = {
+ init(hook) {
+ this.hook = hook;
+ this.destroyed = false;
+ this.config = hook.config.InputSetter || (this.hook.config.InputSetter = {});
+
+ this.eventWrapper = {};
+
+ this.addEvents();
+ },
+
+ addEvents() {
+ this.eventWrapper.setInputs = this.setInputs.bind(this);
+ this.hook.list.list.addEventListener('click.dl', this.eventWrapper.setInputs);
+ },
+
+ removeEvents() {
+ this.hook.list.list.removeEventListener('click.dl', this.eventWrapper.setInputs);
+ },
+
+ setInputs(e) {
+ if (this.destroyed) return;
+
+ const selectedItem = e.detail.selected;
+
+ if (!Array.isArray(this.config)) this.config = [this.config];
+
+ this.config.forEach((config) => this.setInput(config, selectedItem));
+ },
+
+ setInput(config, selectedItem) {
+ const input = config.input || this.hook.trigger;
+ const newValue = selectedItem.getAttribute(config.valueAttribute);
+ const inputAttribute = config.inputAttribute;
+
+ if (input.hasAttribute(inputAttribute)) return input.setAttribute(inputAttribute, newValue);
+ if (input.tagName === 'INPUT') return (input.value = newValue);
+ return (input.textContent = newValue);
+ },
+
+ destroy() {
+ this.destroyed = true;
+
+ this.removeEvents();
+ },
+};
+
+export default InputSetter;
diff --git a/app/assets/javascripts/filtered_search/droplab/utils.js b/app/assets/javascripts/filtered_search/droplab/utils.js
new file mode 100644
index 00000000000..d7f49bf19d8
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/droplab/utils.js
@@ -0,0 +1,40 @@
+/* eslint-disable */
+
+import { template as _template } from 'lodash';
+import { DATA_TRIGGER, DATA_DROPDOWN, TEMPLATE_REGEX } from './constants';
+
+const utils = {
+ toCamelCase(attr) {
+ return this.camelize(attr.split('-').slice(1).join(' '));
+ },
+
+ template(templateString, data) {
+ const template = _template(templateString, {
+ escape: TEMPLATE_REGEX,
+ });
+
+ return template(data);
+ },
+
+ camelize(str) {
+ return str
+ .replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => {
+ return index === 0 ? letter.toLowerCase() : letter.toUpperCase();
+ })
+ .replace(/\s+/g, '');
+ },
+
+ closest(thisTag, stopTag) {
+ while (thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML') {
+ thisTag = thisTag.parentNode;
+ }
+ return thisTag;
+ },
+
+ isDropDownParts(target) {
+ if (!target || !target.hasAttribute || target.tagName === 'HTML') return false;
+ return target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN);
+ },
+};
+
+export default utils;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index ebaa3ef98b1..e467e97dda9 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -1,6 +1,6 @@
import { last } from 'lodash';
import AvailableDropdownMappings from 'ee_else_ce/filtered_search/available_dropdown_mappings';
-import DropLab from '~/droplab/drop_lab';
+import DropLab from './droplab/drop_lab_deprecated';
import { DROPDOWN_TYPE } from './constants';
import FilteredSearchContainer from './container';
import DropdownUtils from './dropdown_utils';