/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, vars-on-top, no-unused-vars, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */ /* global fuzzaldrinPlus */ import $ from 'jquery'; import _ from 'underscore'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; import axios from './lib/utils/axios_utils'; import { visitUrl } from './lib/utils/url_utility'; import { isObject } from './lib/utils/type_utility'; var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput; GitLabDropdownInput = (function() { function GitLabDropdownInput(input, options) { var $inputContainer, $clearButton; var _this = this; this.input = input; this.options = options; this.fieldName = this.options.fieldName || 'field-name'; $inputContainer = this.input.parent(); $clearButton = $inputContainer.find('.js-dropdown-input-clear'); $clearButton.on( 'click', (function(_this) { // Clear click return function(e) { e.preventDefault(); e.stopPropagation(); return _this.input .val('') .trigger('input') .focus(); }; })(this), ); this.input .on('keydown', function(e) { var keyCode = e.which; if (keyCode === 13 && !options.elIsInput) { e.preventDefault(); } }) .on('input', function(e) { var val = e.currentTarget.value || _this.options.inputFieldName; val = val .split(' ') .join('-') // replaces space with dash .replace(/[^a-zA-Z0-9 -]/g, '') .toLowerCase() // replace non alphanumeric .replace(/(-)\1+/g, '-'); // replace repeated dashes _this.cb(_this.options.fieldName, val, {}, true); _this.input .closest('.dropdown') .find('.dropdown-toggle-text') .text(val); }); } GitLabDropdownInput.prototype.onInput = function(cb) { this.cb = cb; }; return GitLabDropdownInput; })(); GitLabDropdownFilter = (function() { var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS; BLUR_KEYCODES = [27, 40]; ARROW_KEY_CODES = [38, 40]; HAS_VALUE_CLASS = 'has-value'; function GitLabDropdownFilter(input, options) { var $clearButton, $inputContainer, ref, timeout; this.input = input; this.options = options; this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; $inputContainer = this.input.parent(); $clearButton = $inputContainer.find('.js-dropdown-input-clear'); $clearButton.on( 'click', (function(_this) { // Clear click return function(e) { e.preventDefault(); e.stopPropagation(); return _this.input .val('') .trigger('input') .focus(); }; })(this), ); // Key events timeout = ''; this.input .on('keydown', function(e) { var keyCode = e.which; if (keyCode === 13 && !options.elIsInput) { e.preventDefault(); } }) .on( 'input', function() { if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { $inputContainer.addClass(HAS_VALUE_CLASS); } else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) { $inputContainer.removeClass(HAS_VALUE_CLASS); } // Only filter asynchronously only if option remote is set if (this.options.remote) { clearTimeout(timeout); return (timeout = setTimeout( function() { $inputContainer.parent().addClass('is-loading'); return this.options.query( this.input.val(), function(data) { $inputContainer.parent().removeClass('is-loading'); return this.options.callback(data); }.bind(this), ); }.bind(this), 250, )); } else { return this.filter(this.input.val()); } }.bind(this), ); } GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { return BLUR_KEYCODES.indexOf(keyCode) !== -1; }; GitLabDropdownFilter.prototype.filter = function(search_text) { var data, elements, group, key, results, tmp; if (this.options.onFilter) { this.options.onFilter(search_text); } data = this.options.data(); if (data != null && !this.options.filterByText) { results = data; if (search_text !== '') { // When data is an array of objects therefore [object Array] e.g. // [ // { prop: 'foo' }, // { prop: 'baz' } // ] if (_.isArray(data)) { results = fuzzaldrinPlus.filter(data, search_text, { key: this.options.keys, }); } else { // If data is grouped therefore an [object Object]. e.g. // { // groupName1: [ // { prop: 'foo' }, // { prop: 'baz' } // ], // groupName2: [ // { prop: 'abc' }, // { prop: 'def' } // ] // } if (isObject(data)) { results = {}; for (key in data) { group = data[key]; tmp = fuzzaldrinPlus.filter(group, search_text, { key: this.options.keys, }); if (tmp.length) { results[key] = tmp.map(function(item) { return item; }); } } } } } return this.options.callback(results); } else { elements = this.options.elements(); if (search_text) { elements.each(function() { var $el, matches; $el = $(this); matches = fuzzaldrinPlus.match($el.text().trim(), search_text); if (!$el.is('.dropdown-header')) { if (matches.length) { return $el.show().removeClass('option-hidden'); } else { return $el.hide().addClass('option-hidden'); } } }); } else { elements.show().removeClass('option-hidden'); } elements .parent() .find('.dropdown-menu-empty-item') .toggleClass('hidden', elements.is(':visible')); } }; return GitLabDropdownFilter; })(); GitLabDropdownRemote = (function() { function GitLabDropdownRemote(dataEndpoint, options) { this.dataEndpoint = dataEndpoint; this.options = options; } GitLabDropdownRemote.prototype.execute = function() { if (typeof this.dataEndpoint === 'string') { return this.fetchData(); } else if (typeof this.dataEndpoint === 'function') { if (this.options.beforeSend) { this.options.beforeSend(); } return this.dataEndpoint( '', (function(_this) { // Fetch the data by calling the data funcfion return function(data) { if (_this.options.success) { _this.options.success(data); } if (_this.options.beforeSend) { return _this.options.beforeSend(); } }; })(this), ); } }; GitLabDropdownRemote.prototype.fetchData = function() { if (this.options.beforeSend) { this.options.beforeSend(); } // Fetch the data through ajax if the data is a string return axios.get(this.dataEndpoint).then(({ data }) => { if (this.options.success) { return this.options.success(data); } }); }; return GitLabDropdownRemote; })(); GitLabDropdown = (function() { var ACTIVE_CLASS, FILTER_INPUT, NO_FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex; LOADING_CLASS = 'is-loading'; PAGE_TWO_CLASS = 'is-page-two'; ACTIVE_CLASS = 'is-active'; INDETERMINATE_CLASS = 'is-indeterminate'; currentIndex = -1; NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item'; SELECTABLE_CLASSES = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ', .option-hidden)'; CURSOR_SELECT_SCROLL_PADDING = 5; FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-filter)'; NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter'; function GitLabDropdown(el1, options) { var searchFields, selector, self; this.el = el1; this.options = options; this.updateLabel = this.updateLabel.bind(this); this.hidden = this.hidden.bind(this); this.opened = this.opened.bind(this); this.shouldPropagate = this.shouldPropagate.bind(this); self = this; selector = $(this.el).data('target'); this.dropdown = selector != null ? $(selector) : $(this.el).parent(); // Set Defaults this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT); this.highlight = !!this.options.highlight; this.icon = !!this.options.icon; this.filterInputBlur = this.options.filterInputBlur != null ? this.options.filterInputBlur : true; // If no input is passed create a default one self = this; // If selector was passed if (_.isString(this.filterInput)) { this.filterInput = this.getElement(this.filterInput); } searchFields = this.options.search ? this.options.search.fields : []; if (this.options.data) { // If we provided data // data could be an array of objects or a group of arrays if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { this.fullData = this.options.data; currentIndex = -1; this.parseData(this.options.data); this.focusTextInput(); } else { this.remote = new GitLabDropdownRemote(this.options.data, { dataType: this.options.dataType, beforeSend: this.toggleLoading.bind(this), success: (function(_this) { return function(data) { _this.fullData = data; _this.parseData(_this.fullData); _this.focusTextInput(); if ( _this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '' ) { return _this.filter.input.trigger('input'); } }; // Remote data })(this), instance: this, }); } } if (this.noFilterInput.length) { this.plainInput = new GitLabDropdownInput(this.noFilterInput, this.options); this.plainInput.onInput(this.addInput.bind(this)); } // Init filterable if (this.options.filterable) { this.filter = new GitLabDropdownFilter(this.filterInput, { elIsInput: $(this.el).is('input'), filterInputBlur: this.filterInputBlur, filterByText: this.options.filterByText, onFilter: this.options.onFilter, remote: this.options.filterRemote, query: this.options.data, keys: searchFields, instance: this, elements: (function(_this) { return function() { selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')'; if (_this.dropdown.find('.dropdown-toggle-page').length) { selector = '.dropdown-page-one ' + selector; } return $(selector, this.instance.dropdown); }; })(this), data: (function(_this) { return function() { return _this.fullData; }; })(this), callback: (function(_this) { return function(data) { _this.parseData(data); if (_this.filterInput.val() !== '') { selector = SELECTABLE_CLASSES; if (_this.dropdown.find('.dropdown-toggle-page').length) { selector = '.dropdown-page-one ' + selector; } if ($(_this.el).is('input')) { currentIndex = -1; } else { $(selector, _this.dropdown) .first() .find('a') .addClass('is-focused'); currentIndex = 0; } } }; })(this), }); } // Event listeners this.dropdown.on('shown.bs.dropdown', this.opened); this.dropdown.on('hidden.bs.dropdown', this.hidden); $(this.el).on('update.label', this.updateLabel); this.dropdown.on('click', '.dropdown-menu, .dropdown-menu-close', this.shouldPropagate); this.dropdown.on( 'keyup', (function(_this) { return function(e) { // Escape key if (e.which === 27) { return $('.dropdown-menu-close', _this.dropdown).trigger('click'); } }; })(this), ); this.dropdown.on( 'blur', 'a', (function(_this) { return function(e) { var $dropdownMenu, $relatedTarget; if (e.relatedTarget != null) { $relatedTarget = $(e.relatedTarget); $dropdownMenu = $relatedTarget.closest('.dropdown-menu'); if ($dropdownMenu.length === 0) { return _this.dropdown.removeClass('show'); } } }; })(this), ); if (this.dropdown.find('.dropdown-toggle-page').length) { this.dropdown.find('.dropdown-toggle-page, .dropdown-menu-back').on( 'click', (function(_this) { return function(e) { e.preventDefault(); e.stopPropagation(); return _this.togglePage(); }; })(this), ); } if (this.options.selectable) { selector = '.dropdown-content a'; if (this.dropdown.find('.dropdown-toggle-page').length) { selector = '.dropdown-page-one .dropdown-content a'; } this.dropdown.on( 'click', selector, function(e) { var $el, selected, selectedObj, isMarking; $el = $(e.currentTarget); selected = self.rowClicked($el); selectedObj = selected ? selected[0] : null; isMarking = selected ? selected[1] : null; if (this.options.clicked) { this.options.clicked.call(this, { selectedObj, $el, e, isMarking, }); } // Update label right after all modifications in dropdown has been done if (this.options.toggleLabel) { this.updateLabel(selectedObj, $el, this); } $el.trigger('blur'); }.bind(this), ); } } // Finds an element inside wrapper element GitLabDropdown.prototype.getElement = function(selector) { return this.dropdown.find(selector); }; GitLabDropdown.prototype.toggleLoading = function() { return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS); }; GitLabDropdown.prototype.togglePage = function() { var menu; menu = $('.dropdown-menu', this.dropdown); if (menu.hasClass(PAGE_TWO_CLASS)) { if (this.remote) { this.remote.execute(); } } menu.toggleClass(PAGE_TWO_CLASS); // Focus first visible input on active page return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus(); }; GitLabDropdown.prototype.parseData = function(data) { var full_html, groupData, html, name; this.renderedData = data; if (this.options.filterable && data.length === 0) { // render no matching results html = [this.noResults()]; } else { // Handle array groups if (isObject(data)) { html = []; for (name in data) { groupData = data[name]; html.push( this.renderItem( { header: name, // Add header for each group }, name, ), ); this.renderData(groupData, name).map(function(item) { return html.push(item); }); } } else { // Render each row html = this.renderData(data); } } // Render the full menu full_html = this.renderMenu(html); return this.appendMenu(full_html); }; GitLabDropdown.prototype.renderData = function(data, group) { if (group == null) { group = false; } return data.map( (function(_this) { return function(obj, index) { return _this.renderItem(obj, group, index); }; })(this), ); }; GitLabDropdown.prototype.shouldPropagate = function(e) { var $target; if (this.options.multiSelect || this.options.shouldPropagate === false) { $target = $(e.target); if ( $target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('isLink') ) { e.stopPropagation(); return false; } else { return true; } } }; GitLabDropdown.prototype.filteredFullData = function() { return this.fullData.filter( r => typeof r === 'object' && !Object.prototype.hasOwnProperty.call(r, 'beforeDivider') && !Object.prototype.hasOwnProperty.call(r, 'header'), ); }; GitLabDropdown.prototype.opened = function(e) { var contentHtml; this.resetRows(); this.addArrowKeyEvent(); const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle'); const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update'); const shouldRefreshOnOpen = dropdownToggle.hasClass('js-gl-dropdown-refresh-on-open'); const hasMultiSelect = dropdownToggle.hasClass('js-multiselect'); // Makes indeterminate items effective if (this.fullData && (shouldRefreshOnOpen || hasFilterBulkUpdate)) { this.parseData(this.fullData); } // Process the data to make sure rendered data // matches the correct layout const inputValue = this.filterInput.val(); if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) { this.options.processData.call( this.options, inputValue, this.filteredFullData(), this.parseData.bind(this), ); } contentHtml = $('.dropdown-content', this.dropdown).html(); if (this.remote && contentHtml === '') { this.remote.execute(); } else { this.focusTextInput(); } if (this.options.showMenuAbove) { this.positionMenuAbove(); } if (this.options.opened) { if (this.options.preserveContext) { this.options.opened(e); } else { this.options.opened.call(this, e); } } return this.dropdown.trigger('shown.gl.dropdown'); }; GitLabDropdown.prototype.positionMenuAbove = function() { var $menu = this.dropdown.find('.dropdown-menu'); $menu.addClass('dropdown-open-top'); $menu.css('top', 'initial'); $menu.css('bottom', '100%'); }; GitLabDropdown.prototype.hidden = function(e) { var $input; this.resetRows(); this.removeArrayKeyEvent(); $input = this.dropdown.find('.dropdown-input-field'); if (this.options.filterable) { $input.blur(); } if (this.dropdown.find('.dropdown-toggle-page').length) { $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); } if (this.options.hidden) { this.options.hidden.call(this, e); } return this.dropdown.trigger('hidden.gl.dropdown'); }; // Render the full menu GitLabDropdown.prototype.renderMenu = function(html) { if (this.options.renderMenu) { return this.options.renderMenu(html); } else { var ul = document.createElement('ul'); for (var i = 0; i < html.length; i += 1) { var el = html[i]; if (el instanceof $) { el = el.get(0); } if (typeof el === 'string') { ul.innerHTML += el; } else { ul.appendChild(el); } } return ul; } }; // Append the menu into the dropdown GitLabDropdown.prototype.appendMenu = function(html) { return this.clearMenu().append(html); }; GitLabDropdown.prototype.clearMenu = function() { var selector; selector = '.dropdown-content'; if (this.dropdown.find('.dropdown-toggle-page').length) { if (this.options.containerSelector) { selector = this.options.containerSelector; } else { selector = '.dropdown-page-one .dropdown-content'; } } return $(selector, this.dropdown).empty(); }; GitLabDropdown.prototype.renderItem = function(data, group, index) { var field, html, selected, text, url, value, rowHidden; if (!this.options.renderRow) { value = this.options.id ? this.options.id(data) : data.id; if (value) { value = value.toString().replace(/'/g, "\\'"); } } // Hide element if (this.options.hideRow && this.options.hideRow(value)) { rowHidden = true; } if (group == null) { group = false; } if (index == null) { // Render the row index = false; } html = document.createElement('li'); if (data === 'divider' || data === 'separator') { html.className = data; return html; } // Header if (data.header != null) { html.className = 'dropdown-header'; html.innerHTML = data.header; return html; } if (this.options.renderRow) { // Call the render function html = this.options.renderRow.call(this.options, data, this); } else { if (!selected) { const { fieldName } = this.options; if (value) { field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`); if (field.length) { selected = true; } } else { field = this.dropdown.parent().find(`input[name='${fieldName}']`); selected = !field.length; } } // Set URL if (this.options.url != null) { url = this.options.url(data); } else { url = data.url != null ? data.url : '#'; } // Set Text if (this.options.text != null) { text = this.options.text(data); } else { text = data.text != null ? data.text : ''; } if (this.highlight) { text = data.template ? this.highlightTemplate(text, data.template) : this.highlightTextMatches(text, this.filterInput.val()); } // Create the list item & the link var link = document.createElement('a'); link.href = url; if (this.icon) { text = `${text}`; link.classList.add('d-flex', 'align-items-center'); link.innerHTML = data.icon ? data.icon + text : text; } else if (this.highlight) { link.innerHTML = text; } else { link.textContent = text; } if (selected) { link.classList.add('is-active'); } if (group) { link.dataset.group = group; link.dataset.index = index; } html.appendChild(link); } return html; }; GitLabDropdown.prototype.highlightTemplate = function(text, template) { return `"${_.escape(text)}" ${template}`; }; GitLabDropdown.prototype.highlightTextMatches = function(text, term) { const occurrences = fuzzaldrinPlus.match(text, term); const { indexOf } = []; return text .split('') .map(function(character, i) { if (indexOf.call(occurrences, i) !== -1) { return '' + character + ''; } else { return character; } }) .join(''); }; GitLabDropdown.prototype.noResults = function() { var html; return '