1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-12 16:28:06 +00:00
bramw_baserow/web-frontend/modules/database/components/view/grid/GridView.vue
2020-05-11 17:27:35 +00:00

582 lines
19 KiB
Vue

<template>
<div v-scroll="scroll" class="grid-view">
<Scrollbars
ref="scrollbars"
horizontal="right"
vertical="rightBody"
:style="{ left: widths.left + 'px' }"
@vertical="verticalScroll"
@horizontal="horizontalScroll"
></Scrollbars>
<div class="grid-view-left" :style="{ width: widths.left + 'px' }">
<div class="grid-view-inner" :style="{ width: widths.left + 'px' }">
<div class="grid-view-head">
<div
class="grid-view-column"
:style="{ width: widths.leftReserved + 'px' }"
></div>
<GridViewFieldType
v-if="primary !== null"
:field="primary"
:style="{ width: widths.fields[primary.id] + 'px' }"
></GridViewFieldType>
</div>
<div ref="leftBody" class="grid-view-body">
<div class="grid-view-body-inner">
<div
class="grid-view-placeholder"
:style="{
height: placeholderHeight + 'px',
width: widths.left + 'px',
}"
>
<div
class="grid-view-placeholder-column"
:style="{
width: widths.left + 'px',
}"
></div>
</div>
<div
class="grid-view-rows"
:style="{ transform: `translateY(${rowsTop}px)` }"
>
<div
v-for="row in rows"
:key="'left-row-' + view.id + '-' + row.id"
class="grid-view-row"
:class="{
'grid-view-row-loading': row._.loading,
'grid-view-row-hover': row._.hover,
}"
@mouseover="setRowHover(row, true)"
@mouseleave="setRowHover(row, false)"
@contextmenu.prevent="showRowContext($event, row)"
>
<div
class="grid-view-column"
:style="{ width: widths.leftReserved + 'px' }"
>
<div class="grid-view-row-info">
<div class="grid-view-row-count">{{ row.id }}</div>
<a
class="grid-view-row-more"
@click="$refs.rowEditModal.show(row)"
>
<i class="fas fa-expand"></i>
</a>
</div>
</div>
<GridViewField
v-if="primary !== null"
:ref="'row-' + row.id + '-field-' + primary.id"
:field="primary"
:row="row"
:table="table"
:style="{ width: widths.fields[primary.id] + 'px' }"
@selected="selectedField(primary, $event.component)"
@selectNext="selectNextField(row, primary, fields, primary)"
@update="updateValue"
></GridViewField>
</div>
</div>
<div class="grid-view-row">
<div
class="grid-view-column"
:style="{ width: widths.left + 'px' }"
>
<a
class="grid-view-add-row"
:class="{ hover: addHover }"
@mouseover="addHover = true"
@mouseleave="addHover = false"
@click="addRow()"
>
<i class="fas fa-plus"></i>
</a>
</div>
</div>
</div>
</div>
<div class="grid-view-foot">
<div class="grid-view-column" :style="{ width: widths.left + 'px' }">
<div class="grid-view-foot-info">{{ count }} rows</div>
</div>
</div>
</div>
</div>
<div
ref="divider"
class="grid-view-divider"
:style="{ left: widths.left + 'px' }"
></div>
<GridViewFieldWidthHandle
class="grid-view-divider-width"
:style="{ left: widths.left + 'px' }"
:grid="view"
:field="primary"
:width="widths.fields[primary.id]"
></GridViewFieldWidthHandle>
<div
ref="right"
class="grid-view-right"
:style="{ left: widths.left + 'px' }"
>
<div
class="grid-view-inner"
:style="{ 'min-width': widths.right + 'px' }"
>
<div class="grid-view-head">
<GridViewFieldType
v-for="field in fields"
:key="'right-head-field-' + view.id + '-' + field.id"
:field="field"
:style="{ width: widths.fields[field.id] + 'px' }"
>
<GridViewFieldWidthHandle
class="grid-view-description-width"
:grid="view"
:field="field"
:width="widths.fields[field.id]"
></GridViewFieldWidthHandle>
</GridViewFieldType>
<div
class="grid-view-column"
:style="{ width: widths.rightAdd + 'px' }"
>
<a
ref="createFieldContextLink"
class="grid-view-add-column"
@click="
$refs.createFieldContext.toggle($refs.createFieldContextLink)
"
>
<i class="fas fa-plus"></i>
</a>
<CreateFieldContext
ref="createFieldContext"
:table="table"
></CreateFieldContext>
</div>
</div>
<div ref="rightBody" class="grid-view-body">
<div class="grid-view-body-inner">
<div
class="grid-view-placeholder"
:style="{
height: placeholderHeight + 'px',
width: widths.rightFieldsOnly + 'px',
}"
>
<div
v-for="(value, id) in widths.placeholderPositions"
:key="'right-placeholder-column-' + view.id + '-' + id"
class="grid-view-placeholder-column"
:style="{ left: value - 1 + 'px' }"
></div>
</div>
<div
class="grid-view-rows"
:style="{ transform: `translateY(${rowsTop}px)` }"
>
<!-- @TODO figure out a faster way to render the rows on scroll. -->
<div
v-for="row in rows"
:key="'right-row-' + view.id + '-' + row.id"
class="grid-view-row"
:class="{
'grid-view-row-loading': row._.loading,
'grid-view-row-hover': row._.hover,
}"
@mouseover="setRowHover(row, true)"
@mouseleave="setRowHover(row, false)"
@contextmenu.prevent="showRowContext($event, row)"
>
<GridViewField
v-for="field in fields"
:ref="'row-' + row.id + '-field-' + field.id"
:key="
'right-row-field-' + view.id + '-' + row.id + '-' + field.id
"
:field="field"
:row="row"
:table="table"
:style="{ width: widths.fields[field.id] + 'px' }"
@selected="selectedField(field, $event.component)"
@selectPrevious="
selectNextField(row, field, fields, primary, true)
"
@selectNext="selectNextField(row, field, fields, primary)"
@update="updateValue"
></GridViewField>
</div>
</div>
<div class="grid-view-row">
<div
class="grid-view-column"
:style="{ width: widths.rightFieldsOnly + 'px' }"
>
<a
class="grid-view-add-row"
:class="{ hover: addHover }"
@mouseover="addHover = true"
@mouseleave="addHover = false"
@click="addRow()"
></a>
</div>
</div>
</div>
</div>
<div class="grid-view-foot"></div>
</div>
</div>
<Context ref="rowContext">
<ul class="context-menu">
<li>
<a @click="deleteRow(selectedRow)">
<i class="context-menu-icon fas fa-fw fa-trash"></i>
Delete row
</a>
</li>
</ul>
</Context>
<RowEditModal
ref="rowEditModal"
:table="table"
:primary="primary"
:fields="fields"
@update="updateValue"
></RowEditModal>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import CreateFieldContext from '@baserow/modules/database/components/field/CreateFieldContext'
import GridViewFieldType from '@baserow/modules/database/components/view/grid/GridViewFieldType'
import GridViewField from '@baserow/modules/database/components/view/grid/GridViewField'
import GridViewFieldWidthHandle from '@baserow/modules/database/components/view/grid/GridViewFieldWidthHandle'
import RowEditModal from '@baserow/modules/database/components/row/RowEditModal'
import { notifyIf } from '@baserow/modules/core/utils/error'
import _ from 'lodash'
export default {
name: 'GridView',
components: {
CreateFieldContext,
GridViewFieldType,
GridViewField,
GridViewFieldWidthHandle,
RowEditModal,
},
props: {
primary: {
type: Object,
required: true,
},
fields: {
type: Array,
required: true,
},
view: {
type: Object,
required: true,
},
table: {
type: Object,
required: true,
},
database: {
type: Object,
required: true,
},
},
data() {
return {
addHover: false,
loading: true,
selectedRow: null,
lastHoveredRow: null,
widths: {
fields: {},
},
}
},
computed: {
...mapGetters({
rows: 'view/grid/getRows',
count: 'view/grid/getCount',
rowHeight: 'view/grid/getRowHeight',
rowsTop: 'view/grid/getRowsTop',
placeholderHeight: 'view/grid/getPlaceholderHeight',
fieldOptions: 'view/grid/getAllFieldOptions',
}),
},
watch: {
// The field options contain the widths of the field. Every time one of the values
// changes we need to recalculate all the widths.
fieldOptions: {
deep: true,
handler(value) {
this.calculateWidths(this.primary, this.fields, value)
},
},
// If a field is added or removed we need to recalculate all the widths.
fields(value) {
this.calculateWidths(this.primary, this.fields, this.fieldOptions)
},
},
created() {
// We have to calculate the widths when the component is created so that we can
// render the page properly on the server side.
this.calculateWidths(this.primary, this.fields, this.fieldOptions)
},
methods: {
async updateValue({ field, row, value, oldValue }) {
try {
await this.$store.dispatch('view/grid/updateValue', {
table: this.table,
row,
field,
value,
oldValue,
})
} catch (error) {
notifyIf(error, 'field')
}
},
scroll(pixelY, pixelX) {
const $rightBody = this.$refs.rightBody
const $right = this.$refs.right
const top = $rightBody.scrollTop + pixelY
const left = $right.scrollLeft + pixelX
this.verticalScroll(top)
this.horizontalScroll(left)
this.$refs.scrollbars.update()
},
verticalScroll(top) {
this.$refs.leftBody.scrollTop = top
this.$refs.rightBody.scrollTop = top
this.$store.dispatch('view/grid/fetchByScrollTopDelayed', {
gridId: this.view.id,
scrollTop: this.$refs.rightBody.scrollTop,
windowHeight: this.$refs.rightBody.clientHeight,
})
},
horizontalScroll(left) {
const $right = this.$refs.right
const $divider = this.$refs.divider
const canScroll = $right.scrollWidth > $right.clientWidth
$divider.classList.toggle('shadow', canScroll && left > 0)
$right.scrollLeft = left
},
/**
* Calculates the widths of all fields, left side, right side and place holder
* positions and returns the values in an object.
*/
getCalculatedWidths(primary, fields, fieldOptions) {
const getFieldWidth = (fieldId) => {
return Object.prototype.hasOwnProperty.call(fieldOptions, fieldId)
? fieldOptions[fieldId].width
: 200
}
// Calculate the widths left side of the grid view. This is the sticky side that
// contains the primary field and ids.
const leftReserved = 60
const leftFieldsOnly = getFieldWidth(primary.id)
const left = leftFieldsOnly + leftReserved
// Calculate the widths of the right side that contains all the other fields.
const rightAdd = 100
const rightReserved = 100
const rightFieldsOnly = fields.reduce(
(value, field) => getFieldWidth(field.id) + value,
0
)
const right = rightFieldsOnly + rightAdd + rightReserved
// Calculate the left positions of the placeholder columns. These are the gray
// vertical lines that are always visible, even when the data hasn't loaded yet.
let last = 0
const placeholderPositions = {}
fields.forEach((field) => {
last += getFieldWidth(field.id)
placeholderPositions[field.id] = last
})
const fieldWidths = {}
fieldWidths[primary.id] = getFieldWidth(primary.id)
fields.forEach((field) => {
fieldWidths[field.id] = getFieldWidth(field.id)
})
return {
left,
leftReserved,
leftFieldsOnly,
right,
rightReserved,
rightAdd,
rightFieldsOnly,
placeholderPositions,
fields: fieldWidths,
}
},
/**
* This method is called when the fieldOptions or fields changes. The reason why we
* don't have smaller methods that are called from the template to calculate the
* widths is that because that would quickly result in thousands of function calls
* when the smallest things change in the data. This is a speed improving
* workaround.
*/
calculateWidths(primary, fields, fieldOptions) {
_.assign(
this.widths,
this.getCalculatedWidths(primary, fields, fieldOptions)
)
if (this.$refs.scrollbars) {
this.$refs.scrollbars.update()
}
},
async addRow() {
try {
await this.$store.dispatch('view/grid/create', {
table: this.table,
// We need a list of all fields including the primary one here.
fields: [this.primary].concat(...this.fields),
values: {},
})
} catch (error) {
notifyIf(error, 'row')
}
},
showRowContext(event, row) {
this.selectedRow = row
this.$refs.rowContext.toggle(
{
top: event.clientY,
left: event.clientX,
},
'bottom',
'right',
0
)
},
async deleteRow(row) {
try {
this.$refs.rowContext.hide()
// We need a small helper function that calculates the current scrollTop because
// the delete action will recalculate the visible scroll range and buffer.
const getScrollTop = () => this.$refs.leftBody.scrollTop
await this.$store.dispatch('view/grid/delete', {
table: this.table,
grid: this.view,
row,
getScrollTop,
})
} catch (error) {
notifyIf(error, 'row')
}
},
/**
* When a field is selected we want to make sure it is visible so we might need to
* scroll a little bit. Because the primary fields are always visible we don't have
* to do anything when those are selected.
*/
selectedField(field, component) {
if (field.primary) {
return
}
const container = this.$refs.right
const containerLeft = container.scrollLeft
const containerWidth = container.clientWidth
const containerRight = containerLeft + container.clientWidth
const element = component.$el
const elementLeft = element.offsetLeft
const elementWidth = element.offsetWidth
const elementRight = element.offsetLeft + element.offsetWidth
if (elementWidth >= containerWidth || elementLeft < containerLeft) {
// If the element doesn't fit in the viewport we just want to make sure the
// beginning is visible.
container.scrollLeft = elementLeft - 20
this.$refs.scrollbars.updateHorizontal()
} else if (elementRight > containerRight) {
// If the right side if the element isn't visible within the viewport.
// The +10 is a small padding.
container.scrollLeft = elementRight - containerWidth + 20
this.$refs.scrollbars.updateHorizontal()
}
},
/**
* This method is called when the next field must be selected. This can for example
* happen when the tab key is pressed. It tries to find the next field and will
* select that one.
*/
selectNextField(row, field, fields, primary, previous = false) {
let nextFieldId = -1
if (field.primary && fields.length > 0 && !previous) {
// If the currently selected field is the primary we can just select the first
// field of the fields if there are any.
nextFieldId = fields[0].id
} else if (!field.primary) {
// First we need to know which index the currently selected field has in the
// fields list.
const index = fields.findIndex((f) => f.id === field.id)
if (!previous && fields.length > index + 1) {
// If we want to select the next field we can just check if the next index
// exists and read the id from there.
nextFieldId = fields[index + 1].id
} else if (previous && index > 0) {
// If we want to select the previous field we can just check if aren't already
// the first and read the id from the previous.
nextFieldId = fields[index - 1].id
} else if (previous && index === 0) {
// If we want to select the previous field and we already are the first index
// we just select the primary.
nextFieldId = primary.id
}
}
if (nextFieldId === -1) {
return
}
const current = this.$refs['row-' + row.id + '-field-' + field.id]
const next = this.$refs['row-' + row.id + '-field-' + nextFieldId]
if (next.length === 0 || current.length === 0) {
return
}
current[0].unselect()
next[0].select()
},
setRowHover(row, value) {
// Sometimes the mouseleave is not triggered, but because you can hover only one
// row at a time we can remember which was hovered last and set the hover state to
// false if it differs.
if (this.lastHoveredRow !== null && this.lastHoveredRow.id !== row.id) {
this.$store.dispatch('view/grid/setRowHover', {
row: this.lastHoveredRow,
value: false,
})
this.lastHoveredRow = true
}
this.$store.dispatch('view/grid/setRowHover', { row, value })
this.lastHoveredRow = row
},
},
}
</script>