/* eslint-disable max-classes-per-file, one-var, consistent-return */ 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'; import renderItem from './gl_dropdown/render'; const BLUR_KEYCODES = [27, 40]; const HAS_VALUE_CLASS = 'has-value'; const LOADING_CLASS = 'is-loading'; const PAGE_TWO_CLASS = 'is-page-two'; const ACTIVE_CLASS = 'is-active'; const INDETERMINATE_CLASS = 'is-indeterminate'; let currentIndex = -1; const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item'; const SELECTABLE_CLASSES = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES}, .option-hidden)`; const CURSOR_SELECT_SCROLL_PADDING = 5; const FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-filter)'; const NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter'; class GitLabDropdownInput { constructor(input, options) { this.input = input; this.options = options; this.fieldName = this.options.fieldName || 'field-name'; const $inputContainer = this.input.parent(); const $clearButton = $inputContainer.find('.js-dropdown-input-clear'); $clearButton.on('click', e => { // Clear click e.preventDefault(); e.stopPropagation(); return this.input .val('') .trigger('input') .focus(); }); this.input .on('keydown', e => { const keyCode = e.which; if (keyCode === 13 && !options.elIsInput) { e.preventDefault(); } }) .on('input', e => { let 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); }); } onInput(cb) { this.cb = cb; } } class GitLabDropdownFilter { constructor(input, options) { let ref, timeout; this.input = input; this.options = options; // eslint-disable-next-line no-cond-assign this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; const $inputContainer = this.input.parent(); const $clearButton = $inputContainer.find('.js-dropdown-input-clear'); $clearButton.on('click', e => { // Clear click e.preventDefault(); e.stopPropagation(); return this.input .val('') .trigger('input') .focus(); }); // Key events timeout = ''; this.input .on('keydown', e => { const keyCode = e.which; if (keyCode === 13 && !options.elIsInput) { e.preventDefault(); } }) .on('input', () => { 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); // eslint-disable-next-line no-return-assign return (timeout = setTimeout(() => { $inputContainer.parent().addClass('is-loading'); return this.options.query(this.input.val(), data => { $inputContainer.parent().removeClass('is-loading'); return this.options.callback(data); }); }, 250)); } return this.filter(this.input.val()); }); } static shouldBlur(keyCode) { return BLUR_KEYCODES.indexOf(keyCode) !== -1; } filter(searchText) { let group, results, tmp; if (this.options.onFilter) { this.options.onFilter(searchText); } const data = this.options.data(); if (data != null && !this.options.filterByText) { results = data; if (searchText !== '') { // When data is an array of objects therefore [object Array] e.g. // [ // { prop: 'foo' }, // { prop: 'baz' } // ] if (_.isArray(data)) { results = fuzzaldrinPlus.filter(data, searchText, { key: this.options.keys, }); } // If data is grouped therefore an [object Object]. e.g. // { // groupName1: [ // { prop: 'foo' }, // { prop: 'baz' } // ], // groupName2: [ // { prop: 'abc' }, // { prop: 'def' } // ] // } else if (isObject(data)) { results = {}; Object.keys(data).forEach(key => { group = data[key]; tmp = fuzzaldrinPlus.filter(group, searchText, { key: this.options.keys, }); if (tmp.length) { results[key] = tmp.map(item => item); } }); } } return this.options.callback(results); } const elements = this.options.elements(); if (searchText) { // eslint-disable-next-line func-names elements.each(function() { const $el = $(this); const matches = fuzzaldrinPlus.match($el.text().trim(), searchText); if (!$el.is('.dropdown-header')) { if (matches.length) { return $el.show().removeClass('option-hidden'); } return $el.hide().addClass('option-hidden'); } }); } else { elements.show().removeClass('option-hidden'); } elements .parent() .find('.dropdown-menu-empty-item') .toggleClass('hidden', elements.is(':visible')); } } class GitLabDropdownRemote { constructor(dataEndpoint, options) { this.dataEndpoint = dataEndpoint; this.options = options; } execute() { if (typeof this.dataEndpoint === 'string') { return this.fetchData(); } else if (typeof this.dataEndpoint === 'function') { if (this.options.beforeSend) { this.options.beforeSend(); } return this.dataEndpoint('', data => { // Fetch the data by calling the data function if (this.options.success) { this.options.success(data); } if (this.options.beforeSend) { return this.options.beforeSend(); } }); } } fetchData() { 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); } }); } } class GitLabDropdown { constructor(el1, options) { let 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 = Boolean(this.options.highlight); this.icon = Boolean(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); } const 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: data => { this.fullData = data; this.parseData(this.fullData); this.focusTextInput(); // Update dropdown position since remote data may have changed dropdown size this.dropdown.find('.dropdown-menu-toggle').dropdown('update'); if ( this.options.filterable && this.filter && this.filter.input && this.filter.input.val() && this.filter.input.val().trim() !== '' ) { return this.filter.input.trigger('input'); } }, 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: () => { selector = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`; if (this.dropdown.find('.dropdown-toggle-page').length) { selector = `.dropdown-page-one ${selector}`; } return $(selector, this.dropdown); }, data: () => this.fullData, callback: 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; } } }, }); } // 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', e => { // Escape key if (e.which === 27) { return $('.dropdown-menu-close', this.dropdown).trigger('click'); } }); this.dropdown.on('blur', 'a', e => { let $dropdownMenu, $relatedTarget; if (e.relatedTarget != null) { $relatedTarget = $(e.relatedTarget); $dropdownMenu = $relatedTarget.closest('.dropdown-menu'); if ($dropdownMenu.length === 0) { return this.dropdown.removeClass('show'); } } }); if (this.dropdown.find('.dropdown-toggle-page').length) { this.dropdown.find('.dropdown-toggle-page, .dropdown-menu-back').on('click', e => { e.preventDefault(); e.stopPropagation(); return this.togglePage(); }); } 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, e => { const $el = $(e.currentTarget); const selected = self.rowClicked($el); const selectedObj = selected ? selected[0] : null; const 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'); }); } } // Finds an element inside wrapper element getElement(selector) { return this.dropdown.find(selector); } toggleLoading() { return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS); } togglePage() { const 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(); } parseData(data) { let groupData, html; this.renderedData = data; if (this.options.filterable && data.length === 0) { // render no matching results html = [this.noResults()]; } // Handle array groups else if (isObject(data)) { html = []; Object.keys(data).forEach(name => { groupData = data[name]; html.push( this.renderItem( { content: name, type: 'header', }, name, ), ); this.renderData(groupData, name).map(item => html.push(item)); }); } else { // Render each row html = this.renderData(data); } // Render the full menu const fullHtml = this.renderMenu(html); return this.appendMenu(fullHtml); } renderData(data, group) { return data.map((obj, index) => this.renderItem(obj, group || false, index)); } shouldPropagate(e) { let $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(); // This prevents automatic scrolling to the top if ($target.closest('a').length) { return false; } } return true; } } filteredFullData() { return this.fullData.filter( r => typeof r === 'object' && !Object.prototype.hasOwnProperty.call(r, 'beforeDivider') && !Object.prototype.hasOwnProperty.call(r, 'header'), ); } opened(e) { 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), ); } const 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'); } positionMenuAbove() { const $menu = this.dropdown.find('.dropdown-menu'); $menu.addClass('dropdown-open-top'); $menu.css('top', 'initial'); $menu.css('bottom', '100%'); } hidden(e) { this.resetRows(); this.removeArrowKeyEvent(); const $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 renderMenu(html) { if (this.options.renderMenu) { return this.options.renderMenu(html); } return $('