diff options
Diffstat (limited to 'app/assets/javascripts/filtered_search/droplab')
12 files changed, 1021 insertions, 0 deletions
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; |