diff options
author | Luke Bennett <lukeeeebennettplus@gmail.com> | 2016-10-04 23:52:10 +0100 |
---|---|---|
committer | Luke Bennett <lukeeeebennettplus@gmail.com> | 2016-10-04 23:52:10 +0100 |
commit | 6f92526ad5f34bd77cc80c1009c8fb43ed4f770d (patch) | |
tree | 601dd409ff2293b8a3b20cdf79021d49d18d2252 | |
parent | ed268bd1e6d8a96146edfbc5137a8ae1ed27ada1 (diff) | |
download | gitlab-ce-21747-rethinking-filters-search.tar.gz |
Added scroll into view from item selection21747-rethinking-filters-search
-rw-r--r-- | app/assets/javascripts/dropdown/gl_dropdown.js.es6 | 59 | ||||
-rw-r--r-- | app/assets/javascripts/issuable_filter/issuable_filter.js.es6 | 45 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/dropdowns.scss | 34 | ||||
-rw-r--r-- | app/views/shared/issuable/_filter.html.haml | 2 | ||||
-rw-r--r-- | app/views/shared/issuable/_issuable_filter_input.html.haml (renamed from app/views/shared/issuable/_issuable_filter.html.haml) | 5 | ||||
-rw-r--r-- | app/views/shared/vue_templates/_gl_dropdown.html.haml | 22 |
6 files changed, 112 insertions, 55 deletions
diff --git a/app/assets/javascripts/dropdown/gl_dropdown.js.es6 b/app/assets/javascripts/dropdown/gl_dropdown.js.es6 index d501577bfa8..b6d86689e04 100644 --- a/app/assets/javascripts/dropdown/gl_dropdown.js.es6 +++ b/app/assets/javascripts/dropdown/gl_dropdown.js.es6 @@ -49,6 +49,7 @@ $($('[slot="header-item"]', this.$el).get().reverse()) .each(this.createItemFromSlot); this.setGlobalIndexes(); + this.$scrollableContainer = $('.scrollable-container', this.$el); if (!this.deferRequest) this.requestData(); }, methods: { @@ -188,36 +189,74 @@ if (this.deferRequest) this.requestData(); }, /** - * Hides the dropdown and resets its selected item. + * Hides the dropdown and resets its selected item and scroll position. */ closeDropdown() { - this.selectedItemIndex = -1; this.showDropdown = false; + this.selectedItemIndex = -1; + this.$scrollableContainer.scrollTop(0); }, /** * Increments the selectedItemIndex by one if there is a - * next item to select. + * next item to select. Lastly, scrolls item into view if needed. */ selectNextItem() { - if(this.selectedItemIndex < this.items.length - 1) this.selectedItemIndex++; + if(this.selectedItemIndex < this.items.length - 1) { + this.selectedItemIndex++; + this.scrollSelectedItemIntoView(); + } }, /** * Decrement the selectedItemIndex by one if there is a - * previous item to select. + * previous item to select. Lastly, scrolls item into view if needed. */ selectPrevItem() { - if(this.selectedItemIndex > -1) this.selectedItemIndex--; + if(this.selectedItemIndex > -1) { + this.selectedItemIndex--; + this.scrollSelectedItemIntoView(); + } }, /** * Simulates the clicking of an item by setting the model to the items * value. Lastly, it closes the dropdown. * TODO: Implement ability to click with mouse, this does not currently * work unless invoked directly from instance controller. + * This method may be able to be changed a lot depending on what happens */ clickCurrentItem() { - selectedItemKey = Object.keys(this.items)[this.selectedItemIndex]; - this.fieldModel = this.items[selectedItemKey][this.valueKey]; + this.fieldModel = this.items[this.selectedItemIndex][this.valueKey]; this.closeDropdown(); + }, + /** + * Scrolls the currently selected item into view. It queries the set of + * selectable items and selects the currently selected one using the + * selected item index. If the currently selected item is out of the + * bounds of the dropdown it will scroll to keep the item fully in view. + */ + scrollSelectedItemIntoView() { + const $item = $('.item', this.$scrollableContainer) + .eq(this.selectedItemIndex); + if (!$item.length) return; + + let newScrollTop; + if (this.selectedItemIndex === -1) { + newScrollTop = 0; + } else { + const scrollTop = this.$scrollableContainer.scrollTop(); + const containerTop = this.$scrollableContainer.offset().top; + const containerBottom = containerTop + this.$scrollableContainer.height(); + const itemHeight = $item.outerHeight(); + const itemTop = $item.offset().top; + const itemBottom = itemTop + itemHeight; + + if (itemBottom > containerBottom) { + newScrollTop = scrollTop + itemHeight; + } else if (itemTop < containerTop) { + newScrollTop = scrollTop - itemHeight; + } + } + + this.$scrollableContainer.scrollTop(newScrollTop); } }, computed: { @@ -234,7 +273,9 @@ * @return {Object} - An array of regular items. */ regularItems() { - return this.items.filter((item) => item.slotType === 'item' || !item.slotType); + return this.items.filter((item) => { + return item.slotType === 'item' || !item.slotType + }); }, /** * Filters an array of only footer items from the default item array. diff --git a/app/assets/javascripts/issuable_filter/issuable_filter.js.es6 b/app/assets/javascripts/issuable_filter/issuable_filter.js.es6 index 9cb4f90232f..aa1d1c83870 100644 --- a/app/assets/javascripts/issuable_filter/issuable_filter.js.es6 +++ b/app/assets/javascripts/issuable_filter/issuable_filter.js.es6 @@ -44,29 +44,27 @@ $(() => { * open dropdowns. * Lastly, initialise the field model observers. */ - openFilterDropdown() { - this.closeFilterDropdowns(); + openDefaultDropdown() { + this.closeDropdowns(); this.$refs[DEFAULT_DROPDOWN].openDropdown(); this.initFieldObservers(); }, /** * Closes all dropdowns and sets the current dropdown to default. */ - closeFilterDropdowns() { + closeDropdowns() { for (const dropdownReference in this.dropdown) { this.$refs[dropdownReference].closeDropdown(); } this.currentDropdown = DEFAULT_DROPDOWN; }, /** - * Handle a keyup event from the filter input. + * Handle a keydown event from the filter input. * On up: Select the previous item of the current dropdown. * On down: Select the next item of the current dropdown. - * On enter: Click the current item of the current dropdown. - * On esc: Close the dropdowns and open the default dropdown. - * @param {Event} event - A keyup event object from the filter input. + * @param {Event} event - A keydown event object from the filter input. */ - filterInputKeyup(event) { + filterInputKeydown(event) { const keycode = event.keyCode || event.which; switch (keycode) { case KEY.UP: @@ -75,11 +73,22 @@ $(() => { case KEY.DOWN: this.$refs[this.currentDropdown].selectNextItem(); break; + } + }, + /** + * Handle a keyup event from the filter input. + * On enter: Click the current item of the current dropdown. + * On esc: Close the dropdowns and open the default dropdown. + * @param {Event} event - A keyup event object from the filter input. + */ + filterInputKeyup(event) { + const keycode = event.keyCode || event.which; + switch (keycode) { case KEY.ENTER: this.$refs[this.currentDropdown].clickCurrentItem(); break; case KEY.ESC: - this.openFilterDropdown() + this.openDefaultDropdown() break; } }, @@ -91,6 +100,7 @@ $(() => { */ openSelectedDropdown(clickedDropdownItem) { if (!clickedDropdownItem) return; + this.closeDropdowns(); this.$refs[clickedDropdownItem].openDropdown(); this.currentDropdown = clickedDropdownItem; }, @@ -105,16 +115,17 @@ $(() => { addFilter(filterType, filterValue) { if (!filterValue) return; const { valueKey, titleKey } = this.$refs[this.currentDropdown]; - const filterItem = this.$refs[this.currentDropdown].items.find((item) => { - return item[valueKey] === filterValue; - }); + const filterItem = this.$refs[this.currentDropdown].items + .find((item) => { + return item[valueKey] === filterValue; + }); const filter = { type: filterType, value: filterValue, title: filterItem[titleKey] }; this.activeFilters.push(filter); - this.closeFilterDropdowns() + this.closeDropdowns() this.$refs[this.currentDropdown].openDropdown(); }, /** @@ -124,7 +135,7 @@ $(() => { clearFilters() { this.activeFilters = []; this.filterInput = ''; - this.closeFilterDropdowns(); + this.closeDropdowns(); }, /** * TODO: Implement submit method. @@ -154,12 +165,12 @@ $(() => { * @param {String} observerName - The unique name of the observer. * @param {Function} observable - A function returning an observable * entity. - * @param {Function} updateAction - A function to invoke when the + * @param {Function} action - A function to invoke when the * observable entity value updates. */ - registerFieldObserver(observerName, observable, updateAction) { + registerFieldObserver(observerName, observable, action) { if (this.modelObserver[observerName]) return; - this.modelObserver[observerName] = this.$watch(observable, updateAction); + this.modelObserver[observerName] = this.$watch(observable, action); } } }); diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 548a09240cf..59ecad9d58f 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -611,6 +611,12 @@ border-radius: $border-radius-default; border: 1px solid $dropdown-2-border-color; min-width: 280px; + padding: 6px 0; + + .scrollable-container { + max-height: 225px; + overflow: scroll; + } .items, .header-items, @@ -618,11 +624,21 @@ list-style: none; margin: 0; padding: 0; - padding: 6px 0; } .header-items { - border-bottom: 1px solid $dropdown-2-divider-color; + position: relative; + + &:after { + border-bottom: 1px solid $dropdown-2-divider-color; + bottom: 0; + content: ''; + display: block; + left: 50%; + margin-left: -130px; + position: absolute; + width: 260px; + } } .footer-items { @@ -630,8 +646,6 @@ } .item, - .header-item, - .footer-item, [slot="item"], [slot="header-item"], [slot="footer-item"] { @@ -639,22 +653,13 @@ padding: $gl-padding; } - .item, - .header-item, - .footer-item { + .item { cursor: pointer; &.active, &:hover { background-color: $dropdown-2-active-item-background-color; color: $white-light; } - - &.last-header-item { - border-bottom: 1px solid $dropdown-2-divider-color; - } - &.first-footer-item { - border-top: 1px solid $dropdown-2-divider-color; - } } } @@ -662,7 +667,6 @@ display: block; font-size: 32px; width: 32px; - // height: 32px; margin: 32px auto; } } diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 4009fbd805f..f92133a6283 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -5,7 +5,7 @@ = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left" - = render 'shared/issuable/issuable_filter' + = render 'shared/issuable/issuable_filter_input' - if @bulk_edit .issues_bulk_update.hide diff --git a/app/views/shared/issuable/_issuable_filter.html.haml b/app/views/shared/issuable/_issuable_filter_input.html.haml index 1753f6a1cd3..f67683e730d 100644 --- a/app/views/shared/issuable/_issuable_filter.html.haml +++ b/app/views/shared/issuable/_issuable_filter_input.html.haml @@ -8,8 +8,9 @@ .filter-badges .filter-badge{ 'v-for': 'filter in activeFilters' } {{ filter.type }}:{{ filter.title }} - = text_field_tag 'issuable-filter-input', nil, class: 'form-control issuable-filter-input', '@focus': 'openFilterDropdown', '@blur': 'closeFilterDropdowns', '@keyup.prevent': 'filterInputKeyup', 'v-model': 'filterInput' - = icon('close', class: 'clear-filters', '@click': 'clearFilters') + -# TODO: Add `closeDropdowns` to `@blur` when finishing dev + = text_field_tag 'issuable-filter-input', nil, class: 'form-control issuable-filter-input', '@focus': 'openDefaultDropdown', '@blur': '', '@keydown': 'filterInputKeydown', '@keyup.prevent': 'filterInputKeyup', 'v-model': 'filterInput' + = icon('close', class: 'clear-filters', '@click': 'clearFilters', 'v-show': 'filterInput || activeFilters.length') .dropdown-filters %gl-dropdown{ ':show-dropdown.sync': 'dropdown.filterDropdown.show', 'v-ref:filter-dropdown': 'true' } diff --git a/app/views/shared/vue_templates/_gl_dropdown.html.haml b/app/views/shared/vue_templates/_gl_dropdown.html.haml index d40eb0bc7cc..bae281d4927 100644 --- a/app/views/shared/vue_templates/_gl_dropdown.html.haml +++ b/app/views/shared/vue_templates/_gl_dropdown.html.haml @@ -4,20 +4,20 @@ %input{ 'type': 'hidden', 'v-model': 'fieldModel', 'v-if': 'fieldName', 'name': '{{ fieldName }}', 'id': '{{ fieldName }}' } .dropdown{ 'v-show': 'showDropdown' } + .scrollable-container + %ul.header-items{ 'v-show': 'headerItems.length' } + %slot{ name: 'header-item' } + %li.item{ 'v-for': 'headerItem in headerItems', value: '{{ headerItem[valueKey] }}', ':class': '[headerItem.slotType, { "active": isSelectedItem(headerItem) }]', '@click': 'clickCurrentItem()' } + {{{ headerItem[titleKey] }}} - %ul.header-items{ 'v-show': 'headerItems.length' } - %slot{ name: 'header-item' } - %li.header-item{ 'v-for': 'headerItem in headerItems', value: '{{ headerItem[valueKey] }}', ':class': '[headerItem.slotType, { "active": isSelectedItem(headerItem) }]', '@click': 'clickCurrentItem()' } - {{{ headerItem[titleKey] }}} + %ul.items{ 'v-show': 'regularItems.length' } + %slot{ name: 'item' } + %li.item{ 'v-for': 'regularItem in regularItems', value: '{{ regularItem[valueKey] }}', ':class': '[regularItem.slotType, { "active": isSelectedItem(regularItem) }]', '@click': 'clickCurrentItem()' } + {{{ regularItem[titleKey] }}} - %ul.items{ 'v-show': 'regularItems.length' } - %slot{ name: 'item' } - %li.item{ 'v-for': 'regularItem in regularItems', value: '{{ regularItem[valueKey] }}', ':class': '[regularItem.slotType, { "active": isSelectedItem(regularItem) }]', '@click': 'clickCurrentItem()' } - {{{ regularItem[titleKey] }}} - - %i.fa.fa-spinner.fa-spin.requesting-icon{ 'v-show': 'requestingData' } + %i.fa.fa-spinner.fa-spin.requesting-icon{ 'v-show': 'requestingData' } %ul.footer-items{ 'v-show': 'footerItems.length' } %slot{ name: 'footer-item' } - %li.footer-item{ 'v-for': 'footerItem in footerItems', value: '{{ footerItem[valueKey] }}', ':class': '[footerItem.slotType, { "active": isSelectedItem(footerItem) }]', '@click': 'clickCurrentItem()' } + %li.item{ 'v-for': 'footerItem in footerItems', value: '{{ footerItem[valueKey] }}', ':class': '[footerItem.slotType, { "active": isSelectedItem(footerItem) }]', '@click': 'clickCurrentItem()' } {{{ footerItem[titleKey] }}} |