import { mapGetters } from 'vuex' import { notifyIf } from '@baserow/modules/core/utils/error' /** * This mixin can be used in combination with a view component that uses the * bufferedRows store mixin. It allows for drag and drop ordering of the items. It * can be used by adding the following listeners to the row objects. * * @mousedown="rowDown($event, row)" * @mousemove="rowMoveOver($event, row)" * @mouseenter="rowMoveOver($event, row)" * * Note that the parent component must implement the `getDragAndDropStoreName` method. */ export default { data() { return { // Must be overwritten by the parent component. It should hold the class name // of the clone of the dom element when the user starts dragging the row. dragAndDropCloneClass: '', // Indicates whether a transition effect is currently active. dragAndDropTransitioning: false, // The row object that the user clicked on. dragAndDropDownRow: null, // The initial horizontal position absolute client position of the card after // mousedown. dragAndDropRowClientX: 0, // The initial vertical position absolute client position of the card after // mousedown. dragAndDropRowClientY: 0, } }, beforeCreate() { this.$options.computed = { ...(this.$options.computed || {}), ...mapGetters({ dragAndDropDraggingRow: `${this.$options.methods.getDragAndDropStoreName( this.$options.propsData )}/getDraggingRow`, }), } }, methods: { /** * Should return the name of the store that's has the bufferedRows mixed in. * This can be different for each component, to this method must be overwritten. */ getDragAndDropStoreName(props) { throw new Error( 'The `getDragAndDropStoreName` method must be implemented.' ) }, /** * Called when a user presses the left mouse on a card. This method will prepare * the dragging if the user moves the mouse a bit. Otherwise, if the mouse is * release without moving, the edit modal is opened. */ rowDown(event, row) { // If it isn't a left click. if (event.button !== 0 || row === null || this.readOnly) { return } event.preventDefault() const rect = event.target.getBoundingClientRect() this.dragAndDropDownRow = row this.dragAndDropRowClientX = event.clientX this.dragAndDropRowClientY = event.clientY this.dragAndDropDownRowTop = event.clientY - rect.top this.dragAndDropDownRowLeft = event.clientX - rect.left this.clonedElement = document.createElement('div') this.clonedElement.innerHTML = event.target.outerHTML this.clonedElement.style = `position: absolute; left: 0; top: 0; width: ${rect.width}px; z-index: 10;` this.clonedElement.firstChild.classList.add(this.dragAndDropCloneClass) this.clonedWrapper = document.createElement('div') this.clonedWrapper.style = 'position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: hidden; pointer-event: none;' this.clonedWrapper.appendChild(this.clonedElement) this.$el.keydownEvent = (event) => { if (event.key === 'Escape') { if (this.dragAndDropDraggingRow !== null) { this.$store.dispatch( `${this.getDragAndDropStoreName(this)}/cancelRowDrag`, { view: this.view, fields: this.fields, row: this.dragAndDropDraggingRow, } ) } this.rowCancel(event) } } document.body.addEventListener('keydown', this.$el.keydownEvent) this.$el.mouseMoveEvent = (event) => this.rowMove(event) window.addEventListener('mousemove', this.$el.mouseMoveEvent) this.$el.mouseUpEvent = (event) => this.rowUp(event) window.addEventListener('mouseup', this.$el.mouseUpEvent) this.rowMove(event) }, /** * Called when moving the mouse after the down event on a row. If we're not in a * dragging state already, it must be started if the user moves more than 3 pixels. */ async rowMove(event) { if ( this.dragAndDropDraggingRow === null && // We only want to allow ordering by drag and drop if there aren't any // sortings because if there are, the ordering is not only based of the // `order` property and drag and dropping in the right position doesn't make // sense. this.view.sortings.length === 0 ) { if ( Math.abs(event.clientX - this.dragAndDropRowClientX) > 3 || Math.abs(event.clientY - this.dragAndDropRowClientY) > 3 ) { document.body.appendChild(this.clonedWrapper) await this.$store.dispatch( `${this.getDragAndDropStoreName(this)}/startRowDrag`, { row: this.dragAndDropDownRow, } ) } } this.clonedElement.style.top = event.clientY - this.dragAndDropDownRowTop + 'px' this.clonedElement.style.left = event.clientX - this.dragAndDropDownRowLeft + 'px' }, /** * Called when the user release the mouse after pressing the left mouse button * on the row. It will check if we're in a dragging state and if so, we need to * stop the dragging of the row and make the new position persistent. */ async rowUp() { if (this.dragAndDropDraggingRow !== null) { this.rowCancel() try { await this.$store.dispatch( `${this.getDragAndDropStoreName(this)}/stopRowDrag`, { table: this.table, view: this.view, fields: this.fields, primary: this.primary, } ) } catch (error) { notifyIf(error) } } else { // Call the `rowClick` method because in some cases, we might want to take // certain action after a "normal" click on the row. Like for example // opening a row edit modal. this.rowClick(this.dragAndDropDownRow) this.rowCancel() } }, rowCancel() { this.dragAndDropDownRow = null this.clonedWrapper.remove() document.body.removeEventListener('keydown', this.$el.keydownEvent) window.removeEventListener('mousemove', this.$el.mouseMoveEvent) window.removeEventListener('mouseup', this.$el.mouseUpEvent) }, /** * Must be called when the user hovers over another row. It will check if we're * currently in a dragging state and if so, the row is temporarily moved to the * new position. */ async rowMoveOver(event, row) { if ( row === null || this.dragAndDropDraggingRow === null || this.dragAndDropDraggingRow.id === row.id || this.dragAndDropTransitioning ) { return } const moved = await this.$store.dispatch( `${this.getDragAndDropStoreName(this)}/forceMoveRowBefore`, { row: this.dragAndDropDraggingRow, targetRow: row, } ) if (moved) { this.rowMoved() } }, /** * After a row has been moved, we need to temporarily need to set the transition * state to true. While it's true, it can't be moved to another position to avoid * strange transition effects of other cards. */ rowMoved() { this.dragAndDropTransitioning = true setTimeout( () => { this.dragAndDropTransitioning = false }, // Must be kept in sync with the transition-duration of // gallery.scss.gallery-view__cards--dragging 100 + 1 ) }, /** * Is called when the user clicks on a row without moving it to another position. */ rowClick() {}, }, }