mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-05-14 21:22:19 +00:00
351 lines
12 KiB
JavaScript
351 lines
12 KiB
JavaScript
import { isDomElement, isElement } from '@baserow/modules/core/utils/dom'
|
|
|
|
export default {
|
|
props: {
|
|
value: {
|
|
type: [String, Number, Boolean, Object],
|
|
required: false,
|
|
default: null,
|
|
},
|
|
searchText: {
|
|
type: String,
|
|
required: false,
|
|
default: 'Search',
|
|
},
|
|
showSearch: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true,
|
|
},
|
|
showInput: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true,
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
loaded: false,
|
|
open: false,
|
|
name: null,
|
|
icon: null,
|
|
query: '',
|
|
hasItems: true,
|
|
hover: null,
|
|
}
|
|
},
|
|
computed: {
|
|
selectedName() {
|
|
return this.getSelectedProperty(this.value, 'name')
|
|
},
|
|
selectedIcon() {
|
|
return this.getSelectedProperty(this.value, 'icon')
|
|
},
|
|
},
|
|
watch: {
|
|
value() {
|
|
this.$nextTick(() => {
|
|
// When the value changes we want to forcefully reload the selectName and
|
|
// selectedIcon a little bit later because the children might have changed.
|
|
this.forceRefreshSelectedValue()
|
|
})
|
|
},
|
|
},
|
|
mounted() {
|
|
// When the component is mounted we want to forcefully reload the selectedName and
|
|
// selectedIcon.
|
|
this.forceRefreshSelectedValue()
|
|
},
|
|
methods: {
|
|
/**
|
|
* Toggles the open state of the dropdown menu.
|
|
*/
|
|
toggle(target, value) {
|
|
if (value === undefined) {
|
|
value = !this.open
|
|
}
|
|
|
|
if (value) {
|
|
this.show(target)
|
|
} else {
|
|
this.hide()
|
|
}
|
|
},
|
|
/**
|
|
* Shows the lists of choices, so a user can change the value.
|
|
*/
|
|
show(target) {
|
|
if (this.disabled) {
|
|
return
|
|
}
|
|
|
|
const isElementOrigin = isDomElement(target)
|
|
|
|
this.open = true
|
|
this.hover = this.value
|
|
this.opener = isElementOrigin ? target : null
|
|
this.$emit('show')
|
|
|
|
this.$nextTick(() => {
|
|
// We have to wait for the input to be visible before we can focus.
|
|
this.showSearch && this.$refs.search.focus()
|
|
|
|
// Scroll to the selected child.
|
|
this.$children.forEach((child) => {
|
|
if (child.value === this.value) {
|
|
this.$refs.items.scrollTop =
|
|
child.$el.offsetTop -
|
|
child.$el.clientHeight -
|
|
Math.round(this.$refs.items.clientHeight / 2)
|
|
}
|
|
})
|
|
})
|
|
|
|
// If the user clicks outside the dropdown while the list of choices of open we
|
|
// have to hide them.
|
|
this.$el.clickOutsideEvent = (event) => {
|
|
if (
|
|
// Check if the context menu is still open
|
|
this.open &&
|
|
// If the click was outside the context element because we want to ignore
|
|
// clicks inside it.
|
|
!isElement(this.$el, event.target) &&
|
|
// If the click was not on the opener because he can trigger the toggle
|
|
// method.
|
|
!isElement(this.opener, event.target)
|
|
) {
|
|
this.hide()
|
|
}
|
|
}
|
|
document.body.addEventListener('click', this.$el.clickOutsideEvent)
|
|
|
|
this.$el.keydownEvent = (event) => {
|
|
if (
|
|
// Check if the context menu is still open
|
|
this.open &&
|
|
// Check if the user has hit either of the keys we care about. If not,
|
|
// ignore.
|
|
(event.code === 'ArrowUp' || event.code === 'ArrowDown')
|
|
) {
|
|
// Prevent scrolling up and down while pressing the up and down key.
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
this.handleUpAndDownArrowPress(event)
|
|
}
|
|
// Allow the Enter key to select the value that is currently being hovered
|
|
// over.
|
|
if (this.open && event.code === 'Enter') {
|
|
// Prevent submitting the whole form when pressing the enter key while the
|
|
// dropdown is open.
|
|
event.preventDefault()
|
|
this.select(this.hover)
|
|
}
|
|
}
|
|
document.body.addEventListener('keydown', this.$el.keydownEvent)
|
|
},
|
|
/**
|
|
* Hides the list of choices. If something change in this method, you might need
|
|
* to update the hide method of the `PaginatedDropdown` component because it
|
|
* contains a partial copy of this code.
|
|
*/
|
|
hide() {
|
|
this.open = false
|
|
this.$emit('hide')
|
|
|
|
// Make sure that all the items are visible the next time we open the dropdown.
|
|
this.query = ''
|
|
this.search(this.query)
|
|
|
|
document.body.removeEventListener('click', this.$el.clickOutsideEvent)
|
|
document.body.removeEventListener('keydown', this.$el.keydownEvent)
|
|
},
|
|
/**
|
|
* Selects a new value which will also be
|
|
*/
|
|
select(value) {
|
|
this.$emit('input', value)
|
|
this.$emit('change', value)
|
|
this.hide()
|
|
},
|
|
/**
|
|
* If not empty it will only show children that contain the given query.
|
|
*/
|
|
search(query) {
|
|
this.hasItems = query === ''
|
|
this.$children.forEach((item) => {
|
|
if (item.search(query)) {
|
|
this.hasItems = true
|
|
}
|
|
})
|
|
},
|
|
/**
|
|
* Loops over all children to see if any of the values match with given value. If
|
|
* so the requested property of the child is returned
|
|
*/
|
|
getSelectedProperty(value, property) {
|
|
for (const i in this.$children) {
|
|
const item = this.$children[i]
|
|
if (item.value === value) {
|
|
return item[property]
|
|
}
|
|
}
|
|
return ''
|
|
},
|
|
/**
|
|
* Returns true if there is a value.
|
|
* @return {boolean}
|
|
*/
|
|
hasValue() {
|
|
for (const i in this.$children) {
|
|
const item = this.$children[i]
|
|
if (item.value === this.value) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
},
|
|
/**
|
|
* A nasty hack, but in some cases the $children have not yet been loaded when the
|
|
* `selectName` and `selectIcon` are computed. This would result in an empty
|
|
* initial value of the Dropdown because the correct value can't be extracted from
|
|
* the DropdownItem. With this hack we force the computed properties to recompute
|
|
* when the component is mounted. At this moment the $children have been added.
|
|
*/
|
|
forceRefreshSelectedValue() {
|
|
this._computedWatchers.selectedName.run()
|
|
this._computedWatchers.selectedIcon.run()
|
|
this.$forceUpdate()
|
|
},
|
|
/**
|
|
* Method that is called when the arrow up or arrow down key is pressed. Based on
|
|
* the index of the current child, the next child enabled child is set as hover.
|
|
*/
|
|
handleUpAndDownArrowPress(event) {
|
|
const children = this.$children.filter(
|
|
(child) => !child.disabled && child.isVisible(this.query)
|
|
)
|
|
|
|
const isArrowUp = event.code === 'ArrowUp'
|
|
let index = children.findIndex((item) => item.value === this.hover)
|
|
index = isArrowUp ? index - 1 : index + 1
|
|
|
|
// Check if the new index is within the allowed range.
|
|
if (index < 0 || index > children.length - 1) {
|
|
return
|
|
}
|
|
|
|
const next = children[index]
|
|
this.hover = next.value
|
|
this.$refs.items.scrollTop = this.getScrollTopAmountForNextChild(
|
|
next,
|
|
isArrowUp
|
|
)
|
|
},
|
|
/**
|
|
* This method calculates the expected container scroll top offset if the next
|
|
* child is selected. This is for example used when navigating with the arrow keys.
|
|
* If the element to scroll to is below the current dropdown's bottom scroll
|
|
* position, then scroll so that the item to scroll to is the last visible item
|
|
* in the dropdown window. Conversely if the element to scroll to is above the
|
|
* current dropdown's top scroll position then scroll so that the item to scroll
|
|
* to is the first viewable item in the dropdown window.
|
|
*/
|
|
getScrollTopAmountForNextChild(itemToScrollTo, isArrowUp) {
|
|
// Styles of the itemToScroll to. Needed in order to get margins and height
|
|
const itemToScrollToStyles =
|
|
itemToScrollTo.$el.currentStyle ||
|
|
window.getComputedStyle(itemToScrollTo.$el)
|
|
|
|
// Styles of the ref items (the dropdown window). Needed in order to get
|
|
// ::before height and ::after height
|
|
const dropdownWindowBeforeStyles =
|
|
this.$refs.items.currentStyle ||
|
|
window.getComputedStyle(this.$refs.items, ':before')
|
|
|
|
const dropdownWindowAfterStyles =
|
|
this.$refs.items.currentStyle ||
|
|
window.getComputedStyle(this.$refs.items, ':after')
|
|
|
|
const dropdownWindowBeforeHeight = parseInt(
|
|
dropdownWindowBeforeStyles.height
|
|
)
|
|
const dropdownWindowAfterHeight = parseInt(
|
|
dropdownWindowAfterStyles.height
|
|
)
|
|
const dropdownWindowHeight = this.$refs.items.clientHeight
|
|
|
|
const itemHeight = parseInt(itemToScrollToStyles.height)
|
|
const itemMarginTop = parseInt(itemToScrollToStyles.marginTop)
|
|
const itemMarginBottom = parseInt(itemToScrollToStyles.marginBottom)
|
|
const itemHeightWithMargins =
|
|
itemHeight + itemMarginTop + itemMarginBottom
|
|
|
|
// Based on the values set in the SCSS files. The height of a dropdowns select
|
|
// item is set to 32px and the height of the select_items window is set to 4 *
|
|
// 36 (select item height plus margins) plus 20 (heights of before and after
|
|
// pseudo elements) so that there is room for four elements
|
|
const itemsInView =
|
|
(dropdownWindowHeight -
|
|
dropdownWindowBeforeHeight -
|
|
dropdownWindowAfterHeight) /
|
|
itemHeightWithMargins
|
|
|
|
// Get the direction of the scrolling.
|
|
const movingDownwards = !isArrowUp
|
|
const movingUpwards = isArrowUp
|
|
|
|
// nextItemOutOfView can be used if one wants to check if the item to scroll
|
|
// to is out of view of the current dropdowns bottom scroll position.
|
|
// This happens when the difference between the element to scroll to's
|
|
// offsetTop and the current scrollTop of the dropdown is smaller than height
|
|
// of the dropdown minus the full height of the element
|
|
const nextItemOutOfView =
|
|
itemToScrollTo.$el.offsetTop - this.$refs.items.scrollTop >
|
|
dropdownWindowHeight - itemHeightWithMargins
|
|
|
|
// prevItemOutOfView can be used if one wants to check if the item to scroll
|
|
// to is out of view of the current dropdowns top scroll position.
|
|
// This happens when the element to scroll to's offsetTop is smaller than the
|
|
// current scrollTop of the dropdown
|
|
const prevItemOutOfView =
|
|
itemToScrollTo.$el.offsetTop < this.$refs.items.scrollTop
|
|
|
|
// When the user is scrolling downwards (i.e. pressing key down)
|
|
// and the itemToScrollTo is out of view we want to add the height of the
|
|
// elements preceding the itemToScrollTo plus the dropdownWindowBeforeHeight.
|
|
// This can be achieved by removing said heights from the itemToScrollTo's
|
|
// offsetTop
|
|
if (nextItemOutOfView && movingDownwards) {
|
|
const elementsHeightBeforeItemToScrollTo =
|
|
itemHeightWithMargins * (itemsInView - 1)
|
|
|
|
return (
|
|
itemToScrollTo.$el.offsetTop -
|
|
elementsHeightBeforeItemToScrollTo -
|
|
dropdownWindowBeforeHeight
|
|
)
|
|
}
|
|
|
|
// When the user is scrolling upwards (i.e. pressing key up) and the
|
|
// itemToScrollTo is out of view we want to set the scrollPosition to be the
|
|
// offsetTop of the element minus it's top margin and the height of the
|
|
// ::after pseudo element of the ref items element
|
|
if (prevItemOutOfView && movingUpwards) {
|
|
return (
|
|
itemToScrollTo.$el.offsetTop -
|
|
itemMarginTop -
|
|
dropdownWindowAfterHeight
|
|
)
|
|
}
|
|
|
|
// In the case that the next item to scroll to is completely visible we simply
|
|
// return the current scroll position so that no scrolling happens
|
|
return this.$refs.items.scrollTop
|
|
},
|
|
},
|
|
}
|