2021-03-09 20:53:39 +00:00
|
|
|
<template>
|
|
|
|
<div>
|
|
|
|
<div class="grid-view__inner" :style="{ 'min-width': width + 'px' }">
|
|
|
|
<GridViewHead
|
|
|
|
:table="table"
|
|
|
|
:view="view"
|
2021-04-06 12:23:17 +00:00
|
|
|
:fields="fields"
|
2021-03-09 20:53:39 +00:00
|
|
|
:include-field-width-handles="includeFieldWidthHandles"
|
|
|
|
:include-row-details="includeRowDetails"
|
|
|
|
:include-add-field="includeAddField"
|
2021-04-08 14:30:26 +00:00
|
|
|
:read-only="readOnly"
|
|
|
|
:store-prefix="storePrefix"
|
2021-03-09 20:53:39 +00:00
|
|
|
@refresh="$emit('refresh', $event)"
|
2021-03-12 13:46:22 +00:00
|
|
|
@dragging="
|
|
|
|
canOrderFields &&
|
|
|
|
$refs.fieldDragging.start($event.field, $event.event)
|
|
|
|
"
|
2021-03-09 20:53:39 +00:00
|
|
|
></GridViewHead>
|
|
|
|
<div ref="body" class="grid-view__body">
|
|
|
|
<div class="grid-view__body-inner">
|
|
|
|
<GridViewPlaceholder
|
2021-04-06 12:23:17 +00:00
|
|
|
:fields="fields"
|
2021-03-09 20:53:39 +00:00
|
|
|
:include-row-details="includeRowDetails"
|
2021-04-08 14:30:26 +00:00
|
|
|
:store-prefix="storePrefix"
|
2021-03-09 20:53:39 +00:00
|
|
|
></GridViewPlaceholder>
|
|
|
|
<GridViewRows
|
2022-01-06 13:02:26 +00:00
|
|
|
ref="rows"
|
2021-03-09 20:53:39 +00:00
|
|
|
:table="table"
|
|
|
|
:view="view"
|
2022-01-06 13:02:26 +00:00
|
|
|
:fields="fieldsToRender"
|
|
|
|
:left-offset="fieldsLeftOffset"
|
2021-03-09 20:53:39 +00:00
|
|
|
:include-row-details="includeRowDetails"
|
2021-04-08 14:30:26 +00:00
|
|
|
:read-only="readOnly"
|
|
|
|
:store-prefix="storePrefix"
|
2021-03-09 20:53:39 +00:00
|
|
|
v-on="$listeners"
|
|
|
|
></GridViewRows>
|
|
|
|
<GridViewRowAdd
|
2021-04-08 14:30:26 +00:00
|
|
|
v-if="!readOnly"
|
2021-04-06 12:23:17 +00:00
|
|
|
:fields="fields"
|
2021-03-09 20:53:39 +00:00
|
|
|
:include-row-details="includeRowDetails"
|
2021-04-08 14:30:26 +00:00
|
|
|
:store-prefix="storePrefix"
|
2021-03-09 20:53:39 +00:00
|
|
|
v-on="$listeners"
|
|
|
|
></GridViewRowAdd>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="grid-view__foot">
|
|
|
|
<slot name="foot"></slot>
|
|
|
|
</div>
|
|
|
|
</div>
|
2021-03-12 13:46:22 +00:00
|
|
|
<GridViewFieldDragging
|
|
|
|
ref="fieldDragging"
|
|
|
|
:view="view"
|
2021-04-06 12:23:17 +00:00
|
|
|
:fields="fields"
|
2021-03-12 13:46:22 +00:00
|
|
|
:container-width="width"
|
2021-04-08 14:30:26 +00:00
|
|
|
:store-prefix="storePrefix"
|
2021-03-12 13:46:22 +00:00
|
|
|
@scroll="$emit('scroll', $event)"
|
|
|
|
></GridViewFieldDragging>
|
2021-03-09 20:53:39 +00:00
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
2022-01-06 13:02:26 +00:00
|
|
|
import debounce from 'lodash/debounce'
|
|
|
|
import ResizeObserver from 'resize-observer-polyfill'
|
|
|
|
|
2021-03-09 20:53:39 +00:00
|
|
|
import GridViewHead from '@baserow/modules/database/components/view/grid/GridViewHead'
|
|
|
|
import GridViewPlaceholder from '@baserow/modules/database/components/view/grid/GridViewPlaceholder'
|
|
|
|
import GridViewRows from '@baserow/modules/database/components/view/grid/GridViewRows'
|
|
|
|
import GridViewRowAdd from '@baserow/modules/database/components/view/grid/GridViewRowAdd'
|
2021-03-12 13:46:22 +00:00
|
|
|
import GridViewFieldDragging from '@baserow/modules/database/components/view/grid/GridViewFieldDragging'
|
2021-03-09 20:53:39 +00:00
|
|
|
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
|
|
|
|
|
|
|
|
export default {
|
|
|
|
name: 'GridViewSection',
|
|
|
|
components: {
|
|
|
|
GridViewHead,
|
|
|
|
GridViewPlaceholder,
|
|
|
|
GridViewRows,
|
|
|
|
GridViewRowAdd,
|
2021-03-12 13:46:22 +00:00
|
|
|
GridViewFieldDragging,
|
2021-03-09 20:53:39 +00:00
|
|
|
},
|
|
|
|
mixins: [gridViewHelpers],
|
|
|
|
props: {
|
|
|
|
fields: {
|
|
|
|
type: Array,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
table: {
|
|
|
|
type: Object,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
view: {
|
|
|
|
type: Object,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
includeFieldWidthHandles: {
|
|
|
|
type: Boolean,
|
|
|
|
required: false,
|
|
|
|
default: () => true,
|
|
|
|
},
|
|
|
|
includeRowDetails: {
|
|
|
|
type: Boolean,
|
|
|
|
required: false,
|
|
|
|
default: () => false,
|
|
|
|
},
|
|
|
|
includeAddField: {
|
|
|
|
type: Boolean,
|
|
|
|
required: false,
|
|
|
|
default: () => false,
|
|
|
|
},
|
2021-03-12 13:46:22 +00:00
|
|
|
canOrderFields: {
|
|
|
|
type: Boolean,
|
|
|
|
required: false,
|
|
|
|
default: () => false,
|
|
|
|
},
|
2021-04-08 14:30:26 +00:00
|
|
|
readOnly: {
|
|
|
|
type: Boolean,
|
|
|
|
required: true,
|
|
|
|
},
|
2021-03-09 20:53:39 +00:00
|
|
|
},
|
2022-01-06 13:02:26 +00:00
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
// Render the first 20 fields by default so that there's at least some data when
|
|
|
|
// the page is server side rendered.
|
|
|
|
fieldsToRender: this.fields.slice(0, 20),
|
|
|
|
// Indicates the offset
|
|
|
|
fieldsLeftOffset: 0,
|
|
|
|
}
|
|
|
|
},
|
2021-03-09 20:53:39 +00:00
|
|
|
computed: {
|
|
|
|
/**
|
|
|
|
* Calculates the total width of the whole section based on the fields and the
|
|
|
|
* given options.
|
|
|
|
*/
|
|
|
|
width() {
|
2021-04-06 12:23:17 +00:00
|
|
|
let width = Object.values(this.fields).reduce(
|
2021-03-09 20:53:39 +00:00
|
|
|
(value, field) => this.getFieldWidth(field.id) + value,
|
|
|
|
0
|
|
|
|
)
|
|
|
|
|
|
|
|
if (this.includeRowDetails) {
|
|
|
|
width += this.gridViewRowDetailsWidth
|
|
|
|
}
|
|
|
|
|
|
|
|
// The add button has a width of 100 and we reserve 100 at the right side.
|
|
|
|
if (this.includeAddField) {
|
|
|
|
width += 100 + 100
|
|
|
|
}
|
|
|
|
|
|
|
|
return width
|
|
|
|
},
|
|
|
|
},
|
2022-01-06 13:02:26 +00:00
|
|
|
watch: {
|
|
|
|
fieldOptions: {
|
|
|
|
deep: true,
|
|
|
|
handler() {
|
|
|
|
this.updateVisibleFieldsInRow()
|
|
|
|
},
|
|
|
|
},
|
|
|
|
fields() {
|
|
|
|
this.updateVisibleFieldsInRow()
|
|
|
|
},
|
|
|
|
},
|
|
|
|
mounted() {
|
|
|
|
// When the component first loads, we need to check
|
|
|
|
this.updateVisibleFieldsInRow()
|
|
|
|
|
|
|
|
const updateDebounced = debounce(() => {
|
|
|
|
this.updateVisibleFieldsInRow()
|
|
|
|
}, 50)
|
|
|
|
|
|
|
|
// When the viewport resizes, we need to check if there are fields that must be
|
|
|
|
// rendered.
|
|
|
|
this.$el.resizeObserver = new ResizeObserver(() => {
|
|
|
|
updateDebounced()
|
|
|
|
})
|
|
|
|
this.$el.resizeObserver.observe(this.$el)
|
|
|
|
|
|
|
|
// When the user scrolls horizontally, we need to check if there fields/cells that
|
|
|
|
// have moved into the viewport and must be rendered.
|
|
|
|
const fireUpdateBuffer = {
|
|
|
|
last: Date.now(),
|
|
|
|
distance: 0,
|
|
|
|
}
|
|
|
|
this.$el.horizontalScrollEvent = (event) => {
|
|
|
|
// Call the update order debounce function to simulate a stop scrolling event.
|
|
|
|
updateDebounced()
|
|
|
|
|
|
|
|
const now = Date.now()
|
|
|
|
const { scrollLeft } = event.target
|
|
|
|
|
|
|
|
const distance = Math.abs(scrollLeft - fireUpdateBuffer.distance)
|
|
|
|
const timeDelta = now - fireUpdateBuffer.last
|
|
|
|
|
|
|
|
if (timeDelta > 100) {
|
|
|
|
const velocity = distance / timeDelta
|
|
|
|
|
|
|
|
fireUpdateBuffer.last = now
|
|
|
|
fireUpdateBuffer.distance = scrollLeft
|
|
|
|
|
|
|
|
if (velocity < 2.5) {
|
|
|
|
updateDebounced.cancel()
|
|
|
|
this.updateVisibleFieldsInRow()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.$el.addEventListener('scroll', this.$el.horizontalScrollEvent)
|
|
|
|
},
|
|
|
|
beforeDestroy() {
|
|
|
|
this.$el.resizeObserver.unobserve(this.$el)
|
|
|
|
this.$el.removeEventListener('scroll', this.$el.horizontalScrollEvent)
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
/**
|
|
|
|
* For performance reasons we only want to render the cells are visible in the
|
|
|
|
* viewport. This method makes sure that the right cells/fields are visible. It's
|
|
|
|
* for example called when the user scrolls, when the window is resized or when a
|
|
|
|
* field changes.
|
|
|
|
*/
|
|
|
|
updateVisibleFieldsInRow() {
|
|
|
|
const width = this.$el.clientWidth
|
|
|
|
const scrollLeft = this.$el.scrollLeft
|
|
|
|
// The padding is added to the start and end of the viewport to make sure that
|
|
|
|
// cells nearby will always be ready to be displayed.
|
|
|
|
const padding = 200
|
|
|
|
const viewportStart = scrollLeft - padding
|
|
|
|
const viewportEnd = scrollLeft + width + padding
|
|
|
|
let leftOffset = null
|
|
|
|
let left = 0
|
|
|
|
|
|
|
|
// Create an array containing the fields that are currently visible in the
|
|
|
|
// viewport and must be rendered.
|
|
|
|
const fieldsToRender = this.fields.filter((field) => {
|
|
|
|
const width = this.getFieldWidth(field.id)
|
|
|
|
const right = left + width
|
|
|
|
const visible = right >= viewportStart && left <= viewportEnd
|
|
|
|
if (visible && leftOffset === null) {
|
|
|
|
leftOffset = left
|
|
|
|
}
|
|
|
|
left = right
|
|
|
|
return visible
|
|
|
|
})
|
|
|
|
|
|
|
|
// A simple way to compare if there are new fields that must be rendered. We want
|
|
|
|
// to do this to avoid unnecessary vue updates.
|
|
|
|
const existingIds = this.fieldsToRender.map((f) => f.id)
|
|
|
|
const newIds = fieldsToRender.map((f) => f.id)
|
|
|
|
if (JSON.stringify(existingIds) !== JSON.stringify(newIds)) {
|
|
|
|
this.fieldsToRender = fieldsToRender
|
|
|
|
}
|
|
|
|
|
|
|
|
if (leftOffset !== this.fieldsLeftOffset) {
|
|
|
|
this.fieldsLeftOffset = leftOffset
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
2021-03-09 20:53:39 +00:00
|
|
|
}
|
|
|
|
</script>
|