import { isPrintableUnicodeCharacterKeyPress } from '@baserow/modules/core/utils/events' /** * This mixin can be used with a grid view field if the field only needs an input. For * example for the text and number fields. It depends on the gridField mixin. */ export default { data() { return { /** * Indicates whether the cell is opened. */ opened: false, /** * Indicates whether the user is editing the value. */ editing: false, /** * A temporary copy of the value when editing. */ copy: null, } }, watch: { copy(value) { if (this.editing) { this.$emit('edit', value, this.value) } }, }, mounted() { const leftMouseDownListener = (event) => { // The `GridViewCell` component initiates the multiple selection when left // clicking on the element. This is something we want to prevent while editing // by stopping the propagation. if (event.button === 0 && this.editing) { event.stopPropagation() } } this.$el.addEventListener('mousedown', leftMouseDownListener) this.$once('hook:beforeDestroy', () => this.$el.removeEventListener('mousedown', leftMouseDownListener) ) }, methods: { /** * Event that is called when the column is selected. Here we will add an event * keydown event listener so we monitor if the enter key is pressed which should * start the editing or save the current changes. We also look check if other * characters are pressed because that should replace the value. */ select() { const keydownListener = (event) => { // If the tab or arrow keys are pressed we don't want to do anything because // the GridViewField component will select the next field. const ignoredKeys = [ 'Tab', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', ] if (ignoredKeys.includes(event.key)) { return } // If the space bar key is pressed, and not editing, we don't want to do // anything because it should open the row edit modal. if (event.key === ' ' && !this.editing) { return } // If the escape key is pressed while editing we want to cancel the current // input and undo the editing state. if (event.key === 'Escape' && this.editing) { this.cancel() return } // If the enter key is pressed. if (event.key === 'Enter') { if ( this.editing && this.isValid() && this.canSaveByPressingEnter(event) ) { // While editing we want to save the changes and select the cell below // to improve the spreadsheet like experience. this.save() this.$emit('selectBelow') } else if (!this.editing) { // If only selected we will start the editing mode. this.edit(null, event) } } // If F2 is pressed we want to switch into editing mode // with the current value of the cell if (event.key === 'F2' && !this.editing) { this.edit(null, event) } // If a printable key was pressed while not editing we want to replace the // exiting value with something new. if (!this.editing && isPrintableUnicodeCharacterKeyPress(event)) { this.edit('', event) } } document.body.addEventListener('keydown', keydownListener) this.$once('unselected', () => document.body.removeEventListener('keydown', keydownListener) ) }, /** * Event that is called wen the column is unselected, for example when clicked * outside. Here we will remove the added keydown event because we don't longer * need it. We will also save the changes if the user was editing. */ beforeUnSelect() { this.opened = false if (this.editing && this.isValid()) { this.save() } else { this.editing = false } }, /** * Event that is called when the user double clicks in the column. In this case * we want to initiate the editing mode. */ doubleClick(event = null) { if (!this.editing) { this.edit(null, event) } }, /** * Method that can be called to initiate the edit state. */ edit(value = null, event = null) { this.opened = true if (this.readOnly) { return } this.editing = true this.copy = value === null ? this.value : value this.afterEdit(event, value) }, /** * Method that can be called when in the editing state. It will bring the * component outside of the editing state and will emit an event which will * eventually save the changes. */ save() { this.opened = false this.editing = false const newValue = this.beforeSave(this.copy) // If the value hasn't changed we don't want to do anything. if (newValue === this.value) { return } this.$emit('update', newValue, this.value) this.afterSave() }, /** * Cancels the current editing state and reverts the copy to the old value * without saving. */ cancel() { this.opened = false this.editing = false this.copy = this.value this.$emit('edit', this.value, this.value) }, /** * Method that is called after initiating the edit state. This can be overridden * in the component. */ afterEdit() {}, /** * This method is called before saving the value. Optionally the value can be * changed or formatted here if necessary. */ beforeSave(value) { return value }, /** * Method that is called after saving the value. This can be overridden in the * component. */ afterSave() {}, /** * Small helper method that stops the propagation of the context menu when the * field is being edited. Can be used on the element like: * `@contextmenu="stopContextIfEditing($event)"`. */ stopContextIfEditing(event) { if (this.editing) { event.stopPropagation() } }, /** * While editing we want to disable the arrow keys to select the next of * previous field. The tab key stays enabled. */ canSelectNext(event) { return !this.editing || event.key === 'Tab' }, /** * If true the value can be saved by pressing the enter key. This could for * example be disabled for a long text field that can have multiple lines. */ canSaveByPressingEnter() { return true }, canKeyboardShortcut() { return !this.editing }, getError() { return this.getValidationError(this.editing ? this.copy : this.value) }, }, }