summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Bennett <lukeeeebennettplus@gmail.com>2016-10-04 23:52:10 +0100
committerLuke Bennett <lukeeeebennettplus@gmail.com>2016-10-04 23:52:10 +0100
commit6f92526ad5f34bd77cc80c1009c8fb43ed4f770d (patch)
tree601dd409ff2293b8a3b20cdf79021d49d18d2252
parented268bd1e6d8a96146edfbc5137a8ae1ed27ada1 (diff)
downloadgitlab-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.es659
-rw-r--r--app/assets/javascripts/issuable_filter/issuable_filter.js.es645
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss34
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-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.haml22
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] }}}