mirror of
https://gitlab.com/bramw/baserow.git
synced 2024-11-25 00:46:46 +00:00
285 lines
9.3 KiB
Vue
285 lines
9.3 KiB
Vue
<template>
|
|
<div v-show="dragging && moved">
|
|
<div
|
|
class="grid-view__field-dragging"
|
|
:style="{ width: draggingWidth + 'px', left: draggingLeft + 'px' }"
|
|
></div>
|
|
<div
|
|
class="grid-view__field-target"
|
|
:style="{ left: targetLeft + 'px' }"
|
|
></div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { notifyIf } from '@baserow/modules/core/utils/error'
|
|
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
|
|
|
|
export default {
|
|
name: 'GridViewFieldDragging',
|
|
mixins: [gridViewHelpers],
|
|
props: {
|
|
view: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
fields: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
offset: {
|
|
type: Number,
|
|
required: false,
|
|
default: 0,
|
|
},
|
|
containerWidth: {
|
|
type: Number,
|
|
required: true,
|
|
},
|
|
readOnly: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
// Indicates if the user is dragging a field to another position.
|
|
dragging: false,
|
|
// Indicates whether the user has moved the mouse more than the 3px threshold.
|
|
moved: false,
|
|
// The field object that is being dragged.
|
|
field: null,
|
|
// The id of the field where the dragged field must be placed after.
|
|
targetFieldId: null,
|
|
// The horizontal starting position of the mouse.
|
|
mouseStartX: 0,
|
|
// The vertical starting position of the mouse.
|
|
mouseStartY: 0,
|
|
// The horizontal scrollbar offset starting position.
|
|
scrollStart: 0,
|
|
// The width of the dragging animation, this is equal to the width of the field.
|
|
draggingWidth: 0,
|
|
// The position of the dragging animation.
|
|
draggingLeft: 0,
|
|
// The position of the target indicator where the field is going to be moved to.
|
|
targetLeft: 0,
|
|
// The mouse move event.
|
|
lastMoveEvent: null,
|
|
// Indicates if the user is auto scrolling at the moment.
|
|
autoScrolling: false,
|
|
}
|
|
},
|
|
beforeDestroy() {
|
|
this.cancel()
|
|
},
|
|
methods: {
|
|
getFieldLeft(id) {
|
|
let left = 0
|
|
for (let i = 0; i < this.fields.length; i++) {
|
|
if (this.fields[i].id === id) {
|
|
break
|
|
}
|
|
left += this.getFieldWidth(this.fields[i])
|
|
}
|
|
return left
|
|
},
|
|
/**
|
|
* Called when the field dragging must start. It will register the global mouse
|
|
* move, mouse up events and keyup events so that the user can drag the field to
|
|
* the correct position.
|
|
*/
|
|
start(field, event) {
|
|
this.field = field
|
|
this.targetFieldId = field.id
|
|
this.dragging = true
|
|
this.moved = false
|
|
this.mouseStartX = event.clientX
|
|
this.mouseStartY = event.clientY
|
|
this.scrollStart = this.$parent.$el.scrollLeft
|
|
this.draggingLeft = 0
|
|
this.targetLeft = 0
|
|
|
|
this.$el.moveEvent = (event) => this.move(event)
|
|
window.addEventListener('mousemove', this.$el.moveEvent)
|
|
|
|
this.$el.upEvent = (event) => this.up(event)
|
|
window.addEventListener('mouseup', this.$el.upEvent)
|
|
|
|
this.$el.keydownEvent = (event) => {
|
|
if (event.key === 'Escape') {
|
|
// When the user presses the escape key we want to cancel the action
|
|
this.cancel(event)
|
|
}
|
|
}
|
|
document.body.addEventListener('keydown', this.$el.keydownEvent)
|
|
},
|
|
/**
|
|
* The move method is called when every time the user moves the mouse while
|
|
* dragging a field. It can also be called while auto scrolling.
|
|
*/
|
|
move(event = null, startAutoScroll = true) {
|
|
if (event !== null) {
|
|
event.preventDefault()
|
|
this.lastMoveEvent = event
|
|
} else {
|
|
event = this.lastMoveEvent
|
|
}
|
|
|
|
// Sometimes the user could accidentally drag the element one or two pixels while
|
|
// clicking it. Because it could be annoying that the click doesn't work because
|
|
// the moving state started, we check here if the user has at least dragged
|
|
// the element 3 pixels vertically or horizontally before starting the moved
|
|
// state.
|
|
if (!this.moved) {
|
|
if (
|
|
Math.abs(event.clientX - this.mouseStartX) > 3 ||
|
|
Math.abs(event.clientY - this.mouseStartY) > 3
|
|
) {
|
|
this.moved = true
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
|
|
// This is the horizontally scrollable element.
|
|
const element = this.$parent.$el
|
|
|
|
this.draggingWidth = this.getFieldWidth(this.field)
|
|
|
|
// Calculate the left position of the dragging animation. This is the transparent
|
|
// overlay that has the same width as the field.
|
|
this.draggingLeft =
|
|
this.offset +
|
|
Math.min(
|
|
this.getFieldLeft(this.field.id) +
|
|
event.clientX -
|
|
this.mouseStartX +
|
|
this.$parent.$el.scrollLeft -
|
|
this.scrollStart,
|
|
this.containerWidth - this.draggingWidth
|
|
)
|
|
|
|
// Calculate which after which field we want to place the field that is currently
|
|
// being dragged. This is named the target. We also calculate what position the
|
|
// field would have for visualisation purposes.
|
|
const mouseLeft =
|
|
event.clientX -
|
|
element.getBoundingClientRect().left +
|
|
element.scrollLeft
|
|
let left = this.offset
|
|
for (let i = 0; i < this.fields.length; i++) {
|
|
const width = this.getFieldWidth(this.fields[i])
|
|
const nextWidth =
|
|
i + 1 < this.fields.length
|
|
? this.getFieldWidth(this.fields[i + 1])
|
|
: width
|
|
const leftHalf = left + Math.floor(width / 2)
|
|
const rightHalf = left + width + Math.floor(nextWidth / 2)
|
|
if (i === 0 && mouseLeft < leftHalf) {
|
|
this.targetFieldId = 0
|
|
// The value 1 makes sure it is visible instead of falling outside of the
|
|
// view port.
|
|
this.targetLeft = Math.max(this.offset, 1)
|
|
break
|
|
}
|
|
if (mouseLeft > leftHalf && mouseLeft < rightHalf) {
|
|
this.targetFieldId = this.fields[i].id
|
|
this.targetLeft = left + width
|
|
break
|
|
}
|
|
left += width
|
|
}
|
|
|
|
// If the user is not already auto scrolling, which happens while dragging and
|
|
// moving the element outside of the view port at the left or right side, we
|
|
// might need to initiate that process.
|
|
if (!this.autoScrolling || !startAutoScroll) {
|
|
const relativeLeft = this.draggingLeft - element.scrollLeft
|
|
const relativeRight = relativeLeft + this.getFieldWidth(this.field)
|
|
const maxScrollLeft = element.scrollWidth - element.clientWidth
|
|
let speed = 0
|
|
|
|
if (relativeLeft < 0 && element.scrollLeft > 0) {
|
|
// If the dragging animation falls out of the left side of the viewport we
|
|
// need to auto scroll to the left.
|
|
speed = -Math.ceil(Math.min(Math.abs(relativeLeft), 100) / 20)
|
|
} else if (
|
|
relativeRight > element.clientWidth &&
|
|
element.scrollLeft < maxScrollLeft
|
|
) {
|
|
// If the dragging animation falls out of the right side of the viewport we
|
|
// need to auto scroll to the right.
|
|
speed = Math.ceil(
|
|
Math.min(relativeRight - element.clientWidth, 100) / 20
|
|
)
|
|
}
|
|
|
|
// If the speed is either a position or negative, so not 0, we know that we
|
|
// need to start auto scrolling.
|
|
if (speed !== 0) {
|
|
this.autoScrolling = true
|
|
this.$emit('scroll', { pixelY: 0, pixelX: speed })
|
|
this.$el.scrollTimeout = setTimeout(() => {
|
|
this.move(null, false)
|
|
}, 1)
|
|
} else {
|
|
this.autoScrolling = false
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Can be called when the current dragging state needs to be stopped. It will
|
|
* remove all the created event listeners and timeouts.
|
|
*/
|
|
cancel() {
|
|
this.dragging = false
|
|
this.mouseStartX = 0
|
|
this.mouseStartY = 0
|
|
window.removeEventListener('mousemove', this.$el.moveEvent)
|
|
window.removeEventListener('mouseup', this.$el.upEvent)
|
|
document.body.addEventListener('keydown', this.$el.keydownEvent)
|
|
clearTimeout(this.$el.scrollTimeout)
|
|
},
|
|
/**
|
|
* Called when the user releases the mouse on a the desired position. It will
|
|
* calculate the new position of the field in the list and if it has changed
|
|
* position, then the order in the field options is updated accordingly.
|
|
*/
|
|
async up(event) {
|
|
event.preventDefault()
|
|
this.cancel()
|
|
|
|
if (!this.moved) {
|
|
return
|
|
}
|
|
|
|
// We don't need to do anything if the field needs to be placed after itself
|
|
// because that wouldn't change the position.
|
|
if (this.field.id === this.targetFieldId) {
|
|
return
|
|
}
|
|
|
|
// If targetfieldId is 0 then the field should be moved to the left of the
|
|
// first field, otherwise it should be moved at the right of the target field
|
|
const position = this.targetFieldId === 0 ? 'left' : 'right'
|
|
const fromField = {
|
|
id: this.targetFieldId === 0 ? this.fields[0].id : this.targetFieldId,
|
|
}
|
|
try {
|
|
await this.$store.dispatch(
|
|
`${this.storePrefix}view/grid/updateSingleFieldOptionOrder`,
|
|
{
|
|
fieldToMove: this.field,
|
|
position,
|
|
fromField,
|
|
readOnly: this.readOnly,
|
|
}
|
|
)
|
|
} catch (error) {
|
|
notifyIf(error, 'view')
|
|
}
|
|
},
|
|
},
|
|
}
|
|
</script>
|