mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-13 08:41:46 +00:00
322 lines
9.3 KiB
Vue
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>
|