1
0
mirror of https://gitlab.com/bramw/baserow.git synced 2024-11-21 23:37:55 +00:00
bramw_baserow/web-frontend/modules/database/mixins/gridField.js
2024-09-20 11:06:34 +00:00

303 lines
10 KiB
JavaScript

import { onClickOutside } from '@baserow/modules/core/utils/dom'
import baseField from '@baserow/modules/database/mixins/baseField'
import copyPasteHelper from '@baserow/modules/database/mixins/copyPasteHelper'
/**
* A mixin that can be used by a field grid component. It introduces the props that
* will be passed by the GridViewField component and it created methods that are
* going to be called.
*/
export default {
mixins: [baseField, copyPasteHelper],
props: {
/**
* Indicates if the grid field is in a selected state.
*/
selected: {
type: Boolean,
required: true,
},
readOnly: {
type: Boolean,
required: true,
},
storePrefix: {
type: String,
required: true,
},
},
watch: {
/**
* It could happen that the cell is not select, but still being kept alive to
* finish a task. This for example happens when the user selects another cell
* while still uploading a file. When the selected state changes, we do want to
* add and remove the event listeners to prevent conflicts.
*/
selected(value) {
if (value) {
this._select()
} else {
this._beforeUnSelect()
}
},
},
mounted() {
if (this.selected) {
this._select()
}
},
beforeDestroy() {
// It could be that the cell has already been unselected, in that case we don't
// have to before unselect twice.
if (this.selected) {
this._beforeUnSelect()
}
},
methods: {
/**
* This method adds an event listener to the given element. It also
* automatically removes the event listeners when the cell is unselected
* (emitted from `_beforeUnSelect`) so there's no need to do that manually.
*/
addEventListenerWithAutoRemove(el, event, eventHandler) {
el.addEventListener(event, eventHandler)
this.$once('unselected', () => {
el.removeEventListener(event, eventHandler)
})
},
/**
* This method is called when the cell is selected to add all the event
* listeners needed to handle the user interaction.
* It uses the `addEventListenerWithAutoRemove` method to automatically
* remove the event listeners when the cell is unselected.
*/
setupAllEventListenersOnCellSelected() {
this.addEventListenerWithAutoRemove(
this.$el,
'dblclick',
this.doubleClick
)
// Register a body click event listener so that we can detect if a user has
// clicked outside the field. If that happens we want to unselect the field and
// possibly save the value.
const clickOutsideEventCancel = onClickOutside(
this.$el,
(target, event) => {
if (
// Check if the event has the 'preventFieldCellUnselect' attribute which
// if true should prevent the field from being unselected.
!(
'preventFieldCellUnselect' in event &&
event.preventFieldCellUnselect
) &&
// If the child field allows to unselect when clicked outside.
this.canUnselectByClickingOutside(event)
) {
this.$emit('unselect')
}
}
)
this.$once('unselected', clickOutsideEventCancel)
// Event that is called when a key is pressed while the field is selected.
const keyDownEventListener = (event) => {
// When for example a related modal is open all the key combinations must be
// ignored because the focus is not in the cell.
if (!this.canKeyDown(event)) {
return
}
// If the tab or arrow keys are pressed we want to select the next field. This
// is however out of the scope of this component so we emit the selectNext
// event that the GridView can handle.
const { key, shiftKey } = event
const arrowKeysMapping = {
ArrowLeft: 'selectPrevious',
ArrowUp: 'selectAbove',
ArrowRight: 'selectNext',
ArrowDown: 'selectBelow',
}
if (this.canSelectNext(event)) {
if (Object.keys(arrowKeysMapping).includes(key) && !shiftKey) {
event.preventDefault()
this.$emit(arrowKeysMapping[key])
} else if (key === 'Tab') {
event.preventDefault()
this.$emit(shiftKey ? 'selectPrevious' : 'selectNext')
} else if (key === 'Enter' && shiftKey && !this.readOnly) {
event.preventDefault()
event.preventFieldCellUnselect = true
this.$emit('add-row-after')
this.$emit('selectBelow')
return
}
}
// Removes the value if the backspace/delete key is pressed.
if (
(key === 'Delete' || key === 'Backspace') &&
this.canKeyboardShortcut(event)
) {
event.preventDefault()
const value = this.$registry
.get('field', this.field.type)
.getEmptyValue(this.field)
const oldValue = this.value
if (
value !== oldValue &&
!this.readOnly &&
!this.field._.type.isReadOnly
) {
this.$emit('update', value, oldValue)
}
}
// Space bar should enlarge the row.
if (key === ' ' && this.canKeyboardShortcut(event)) {
event.preventDefault()
this.$emit('edit-modal')
}
}
this.addEventListenerWithAutoRemove(
document.body,
'keydown',
keyDownEventListener
)
const copyEventListener = async (event) => {
if (!this.canKeyDown(event) || !this.canKeyboardShortcut(event)) return
await this.copySelectionToClipboard(
Promise.resolve([
[this.field],
[{ [`field_${this.field.id}`]: this.value }],
])
)
// prevent Safari from beeping since the window.getSelection() is empty
event.preventDefault()
}
this.addEventListenerWithAutoRemove(window, 'copy', copyEventListener)
// Updates the value of the field when a user pastes something in the field.
const pasteEventListener = async (event) => {
if (!this.canKeyboardShortcut(event)) {
return
}
// Try to call the field handler if one exists
if (this.onPaste) {
// If the return value of onPaste is true then we must stop event handling
// here. It means the event has already been handled.
if (this.onPaste(event)) {
return
}
}
try {
const [data, jsonData] = await this.extractClipboardData(event)
// A grid field cell can only handle one single value. We try to extract
// that from the clipboard and update the cell, otherwise we emit the
// paste event up.
if (data.length === 1 && data[0].length === 1) {
const value = this.$registry
.get('field', this.field.type)
.prepareValueForPaste(
this.field,
data[0][0],
jsonData !== null ? jsonData[0][0] : undefined
)
const oldValue = this.value
if (
value !== undefined &&
value !== oldValue &&
!this.readOnly &&
!this.field._.type.isReadOnly
) {
this.$emit('update', value, oldValue)
}
} else {
// This is a multi cell paste
event.stopPropagation()
this.$emit('paste', { textData: data, jsonData })
}
} catch (e) {}
}
this.addEventListenerWithAutoRemove(document, 'paste', pasteEventListener)
},
/**
* Adds all the event listeners related to all the field types, for example when a
* user presses the one of the arrow keys, tab, backspace, double clicks etc. This
* method is not meant to be overwritten.
*/
_select() {
this.setupAllEventListenersOnCellSelected()
this.select()
// Emit the selected event so that the parent component can take an action like
// making sure that the element fits in the viewport.
this.$emit('selected', { component: this })
},
/**
* Removes all the listeners related to all field types.
*/
_beforeUnSelect() {
this.beforeUnSelect()
this.$emit('unselected', { component: this })
},
/**
* Method that is called when the column is selected. For example when clicked
* on the field. This is the moment to register event listeners if they are needed.
*/
select() {},
/**
* Method that is called when the column is unselected. For example when clicked
* outside the field. This is the moment to remove any event listeners.
*/
beforeUnSelect() {},
/**
* Method that is called when the column is double clicked. Some grid fields want
* to do something here apart from triggering the selected state. A boolean
* toggles its value for example.
*/
doubleClick() {},
/**
* There are keyboard shortcuts to select the next or previous field. For
* example when the arrow or tab keys are pressed. The GridViewField component
* first asks if this is allowed by calling this function. If false is returned
* the next field is not going to be selected.
*/
canSelectNext() {
return true
},
/**
* If the user clicks outside the cell, the cell is automatically unselected. In
* some cases, for example when you have a context menu as helper, you might not
* want to unselect when the user clicks in the context menu. The can be
* prevented by returned false here. The context menu lives at the root of the
* body element and not inside the cell.
*/
canUnselectByClickingOutside() {
return true
},
/**
* It must be possible for a field to ignore all key combinations. For example when
* it is possible for a field to open a modal to select some data, the
* backspace/delete key should not empty the field at that moment.
*/
canKeyDown() {
return true
},
/**
* If the user presses a keyboard shortcut like ctrl/cmd + v or spacebar while a
* field is selected In some cases, for example when the user is editing the
* value, we do not want to allow the keyboard shortcut to work. If false it will
* be disabled.
*/
canKeyboardShortcut() {
return true
},
},
}