1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-13 08:41:46 +00:00
bramw_baserow/web-frontend/modules/database/components/view/gallery/GalleryView.vue
2021-12-17 14:02:54 +00:00

322 lines
9.3 KiB
Vue

<template>
<div class="gallery-view">
<a
v-if="!readOnly"
class="gallery-view__add"
@click="$refs.rowCreateModal.show()"
>
<i class="fas fa-plus"></i>
</a>
<div ref="scroll" class="gallery-view__scroll">
<div
class="gallery-view__cards"
:style="{
height: height + 'px',
}"
>
<RowCard
v-for="slot in buffer"
v-show="slot.left != -1"
:key="'card-' + slot.id"
:fields="cardFields"
:row="slot.row === null ? {} : slot.row"
:loading="slot.row === null"
class="gallery-view__card"
:style="{
width: cardWidth + 'px',
height: slot.row === null ? cardHeight + 'px' : undefined,
transform: `translateX(${slot.left}px) translateY(${slot.top}px)`,
}"
></RowCard>
</div>
</div>
<RowCreateModal
v-if="!readOnly"
ref="rowCreateModal"
:table="table"
:fields="fields"
:primary="primary"
@created="createRow"
@field-updated="$emit('refresh', $event)"
@field-deleted="$emit('refresh')"
></RowCreateModal>
</div>
</template>
<script>
import debounce from 'lodash/debounce'
import { mapGetters } from 'vuex'
import ResizeObserver from 'resize-observer-polyfill'
import { getCardHeight } from '@baserow/modules/database/utils/card'
import { maxPossibleOrderValue } from '@baserow/modules/database/viewTypes'
import RowCard from '@baserow/modules/database/components/card/RowCard'
import RowCreateModal from '@baserow/modules/database/components/row/RowCreateModal'
export default {
name: 'GalleryView',
components: { RowCard, RowCreateModal },
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,
},
readOnly: {
type: Boolean,
required: true,
},
storePrefix: {
type: String,
required: true,
},
},
data() {
return {
gutterSize: 30,
minimumCardWidth: 280,
height: 0,
cardWidth: 0,
buffer: [],
}
},
computed: {
firstRows() {
return this.allRows.slice(0, 200)
},
/**
* In order for the virtual scrolling to work, we need to know what the height of
* the card is to correctly position it.
*/
cardHeight() {
return getCardHeight(this.cardFields, this.$registry)
},
/**
* Returns the visible field objects in the right order.
*/
cardFields() {
return [this.primary]
.concat(this.fields)
.filter((field) => {
const exists = Object.prototype.hasOwnProperty.call(
this.fieldOptions,
field.id
)
return !exists || (exists && !this.fieldOptions[field.id].hidden)
})
.sort((a, b) => {
const orderA = this.fieldOptions[a.id]
? this.fieldOptions[a.id].order
: maxPossibleOrderValue
const orderB = this.fieldOptions[b.id]
? this.fieldOptions[b.id].order
: maxPossibleOrderValue
// First by order.
if (orderA > orderB) {
return 1
} else if (orderA < orderB) {
return -1
}
// Then by id.
if (a.id < b.id) {
return -1
} else if (a.id > b.id) {
return 1
} else {
return 0
}
})
},
},
watch: {
cardHeight() {
this.$nextTick(() => {
this.updateBuffer()
})
},
allRows() {
this.$nextTick(() => {
this.updateBuffer()
})
},
},
mounted() {
this.updateBuffer()
this.$el.resizeObserver = new ResizeObserver(() => {
this.updateBuffer()
})
this.$el.resizeObserver.observe(this.$el)
const fireUpdateBuffer = {
last: Date.now(),
distance: 0,
}
// Debounce function that's called when the user scrolls really fast. This is to
// make sure that the `updateBuffer` method is called with the
// `dispatchVisibleRows` parameter to true when the user immediately stops
// scrolling fast.
const updateBufferDebounced = debounce(() => {
this.updateBuffer(true)
}, 100)
this.$el.scrollEvent = (event) => {
const now = Date.now()
const { scrollTop } = event.target
const distance = Math.abs(scrollTop - fireUpdateBuffer.distance)
const timeDelta = now - fireUpdateBuffer.last
if (timeDelta > 100) {
const velocity = distance / timeDelta
fireUpdateBuffer.last = now
fireUpdateBuffer.distance = scrollTop
if (velocity < 2.5) {
// When scrolling "slow", the dispatchVisibleRows parameter is true so that
// the visible rows are fetched if needed.
updateBufferDebounced.cancel()
this.updateBuffer(true)
} else {
// Check if the user is scrolling super fast because in that case we don't
// fetch the rows when they're not needed.
updateBufferDebounced()
this.updateBuffer(false)
}
} else {
// If scroll stopped within the 100ms we still want to have a last
// updateBuffer(true) call.
updateBufferDebounced()
this.updateBuffer(false)
}
}
this.$refs.scroll.addEventListener('scroll', this.$el.scrollEvent)
},
beforeDestroy() {
this.$el.resizeObserver.unobserve(this.$el)
this.$refs.scroll.removeEventListener('scroll', this.$el.scrollEvent)
},
beforeCreate() {
this.$options.computed = {
...(this.$options.computed || {}),
...mapGetters({
allRows: this.$options.propsData.storePrefix + 'view/gallery/getRows',
fieldOptions:
this.$options.propsData.storePrefix +
'view/gallery/getAllFieldOptions',
}),
}
},
methods: {
/**
* This method makes sure that the correct cards/rows are shown based on the
* scroll offset, viewport width, viewport height and card height. Based on these
* values we can calculate which how many rows should be visible, which ones are
* visible and what their position is without rendering all the rows in the store
* at once.
*
* @TODO make this really virtual scrolling by letting the row persist in the same
* slot, even when it changes position. Currently, it updates all the cards when
* a new row of cards must be displayed.
*
* @param dispatchVisibleRows Indicates whether we want to dispatch the visibleRows
* action in the store. In some cases, when scrolling really fast through data we
* might want to wait a small moment before calling the action, which will make a
* request to the backend if needed.
*/
updateBuffer(dispatchVisibleRows = true) {
const el = this.$refs.scroll
const gutterSize = this.gutterSize
const containerWidth = el.clientWidth
const containerHeight = el.clientHeight
const cardsPerRow = Math.min(
Math.max(Math.floor(containerWidth / this.minimumCardWidth), 1),
20
)
const cardHeight = this.cardHeight
const cardWidth = (containerWidth - gutterSize) / cardsPerRow - gutterSize
const totalRows = Math.ceil(this.allRows.length / cardsPerRow)
const height = totalRows * (cardHeight + gutterSize) + gutterSize
this.cardWidth = cardWidth
this.height = height
const scrollTop = el.scrollTop
const minimumCardsToRender =
(Math.ceil(containerHeight / (cardHeight + gutterSize)) + 1) *
cardsPerRow
const startIndex =
Math.floor(scrollTop / (cardHeight + gutterSize)) * cardsPerRow
const endIndex = startIndex + minimumCardsToRender
const visibleRows = this.allRows.slice(startIndex, endIndex)
// Calculate an array containing only the rows that must be displayed and their
// position in the gallery as if all the rows are there.
this.buffer = visibleRows.map((row, positionInVisible) => {
const positionInAll = startIndex + positionInVisible
const left =
gutterSize + (positionInAll % cardsPerRow) * (gutterSize + cardWidth)
const top =
gutterSize +
Math.floor(positionInAll / cardsPerRow) * (gutterSize + cardHeight)
return {
id: positionInVisible,
row,
left,
top,
}
})
if (dispatchVisibleRows) {
// Tell the store which rows/cards are visible so that it can fetch the missing
// ones if needed.
this.$store.dispatch(
this.storePrefix + 'view/gallery/fetchMissingRowsInNewRange',
{
startIndex,
endIndex,
}
)
}
},
async createRow({ row, callback }) {
try {
await this.$store.dispatch(
this.storePrefix + 'view/gallery/createNewRow',
{
view: this.view,
table: this.table,
fields: this.fields,
primary: this.primary,
values: row,
}
)
callback()
} catch (error) {
callback(error)
}
},
},
}
</script>