<template>
<!-- pictures grid container -->
<div id="pictures-grid-container" ref="pictureGrid">
<!-- columns -->
<template v-for="(column, columnIndex) in columns">
<div :key="columnIndex" :style="columnWidth" class="column">
<!-- render each image in column -->
<template v-for="(element, index) in column">
<!-- desktop -->
<!-- click opens preview, as overlay for desktop -->
<div
v-if="!isMobile"
:key="index"
class="picture-container"
@click="emitClick(element.hash)"
>
<!--suppress JSUnusedGlobalSymbols -->
<picture-frame
:id="`picture-id-${index}`"
v-observe-visibility="{
callback: (isVisible, entry) => imageVisibilityChanged(isVisible, entry, index),
threshold: 0.1,
}"
class="picture"
:path="element.relativeThumbnailPath"
:width="element.thumbnailWidth"
:height="element.thumbnailHeight"
:columns-count="columnsCount"
:frame="true"
:class="{vertical: element.height > element.width}"
@loading-start="addToLoading(columnIndex, index)"
@loading-completed="removeFromLoading(columnIndex, index)"
/>
<div class="picture-overlay">
<div class="picture-overlay-content">
{{ stringTable.clickToGetImageDetails }}
</div>
</div>
</div>
<!-- mobile -->
<!-- tap will redirect to '/picture/:hash' -->
<router-link
v-else
:key="element"
:to="`/image/${element.hash}`"
class="picture-container"
>
<picture-frame
:id="`picture-id-${index}`"
v-observe-visibility="{
callback: (isVisible, entry) => imageVisibilityChanged(isVisible, entry, index),
threshold: 0.1,
}"
class="picture"
:path="element.relativeThumbnailPath"
:width="element.thumbnailWidth"
:height="element.thumbnailHeight"
:columns-count="columnsCount"
:frame="true"
:class="{vertical: element.height > element.width}"
@loading-start="addToLoading(columnIndex, index)"
@loading-completed="removeFromLoading(columnIndex, index)"
/>
<div class="picture-overlay">
<div class="picture-overlay-content">
{{ stringTable.clickToGetImageDetails }}
</div>
</div>
</router-link>
</template>
</div>
</template>
</div>
</template>
<!--suppress JSUnusedGlobalSymbols -->
<script>
import stringTable from '../constants/stringTable'
import PictureFrame from '../components/pictureFrame'
import galleryData from '../../public/galleryData.json'
import { isMobile } from 'mobile-device-detect'
import { debounce } from 'debounce'
export default {
name: "PicturesGrid",
components: {
PictureFrame
},
props: {
imagesToLoadPerColumn: {
type: Number,
required: false,
default: 4
}
},
data: function() {
return {
//helpers
stringTable: stringTable,
isMobile: isMobile,
columns: {
columns: [],
},
columnsCount: 0,
columnOffset: 0,
columnWidth: {width: '100%'},
loading: [],
loaded: [],
imageRenderOffset: 0,
renderTotal: 0,
renderStep: null
}
},
created: function () {
this.renderTotal = Object.keys(galleryData.files).length - 1
this.calculateColumns()
this.updateColumns()
},
mounted: function () {
this.$nextTick(() => {
window.addEventListener('resize', debounce(this.onResize.bind(this), 200))
this.requestRender(this.renderStep)
})
},
beforeDestroy: function() {
window.removeEventListener('resize', this.onResize)
},
methods: {
// -- component state management
calculateColumns: function() {
if(window.matchMedia("(min-width: 2000px)").matches) {
this.columnsCount = 4
} else if(window.matchMedia("(min-width: 1400px)").matches) {
this.columnsCount = 3 // huge screens
} else if(window.matchMedia("(min-width: 700px)").matches) {
this.columnsCount = 2 // desktops, laptops, tablets
} else {
this.columnsCount = 1 // mobile devices
}
this.renderStep = this.imagesToLoadPerColumn * this.columnsCount
},
updateColumns: function() {
this.columns = []
for(let i = 0; i < this.columnsCount; i++) {
this.columns.push([])
}
this.columnWidth.width = Math.round(100 / this.columnsCount) + '%'
},
relocateImages: function() {
const oldRenderOffset = this.imageRenderOffset
this.calculateColumns()
this.updateColumns()
//reset current render progress
this.imageRenderOffset = 0
this.columnOffset = 0
//rerender images, but with updated columns
this.requestRender(oldRenderOffset)
},
// -- grid items rendering
//add images to load
insert: function() {
//make sure we can render
if(this.imageRenderOffset + 1 > this.renderTotal) {
this.emitAllElementsRendered()
return
}
//reset placement counter
if(this.columnOffset + 1 > this.columnsCount) {
this.columnOffset = 1
//increment
} else {
this.columnOffset++
}
//update offset
this.imageRenderOffset++
//add element to current column, columnOffset isn't starting with 0,
//so we have to fix this
this.columns[this.columnOffset - 1].push(
galleryData.files[this.imageRenderOffset]
)
//this.rendering[this.imageRenderOffset] = galleryData.files[this.imageRenderOffset]
},
requestRender: function(step = null) {
// console.log(`requesting render`)
// this._debugRender()
if(step === null)
step = this.renderStep
for(let i = 0; i < step; i++)
this.insert()
},
imageVisibilityChanged: function(isVisible, entry, index) {
if(index === undefined || index == null)
return
if((index + 2) > (this.imageRenderOffset / this.columnsCount)) {
//don't load if more then half of dynamically loaded images
//are still loading
if(this.loading.length < (this.renderStep / 2))
this.requestRender(this.renderStep)
}
},
// -- component events (emit)
emitClick: function (hash) {
this.$emit('clicked', hash)
},
emitAllElementsRendered: function () {
this.$emit('all-items-rendered')
},
// -- images states handling
addToLoading: function (column, index) {
const element = [column, index]
if(this.loaded.includes(element))
return
this.loading.push(element)
},
removeFromLoading: function (column, index) {
const element = [column, index]
this.loaded.push(element)
const loadingIndex = this.loading.findIndex(e => e === element)
this.loading.splice(loadingIndex, 1)
},
// -- DOM events handlers
onResize: function() {
const oldColumnCount = this.columnsCount
this.calculateColumns()
//adjust columns to new view size and relocate images to
//specified columns
if(oldColumnCount !== this.columnsCount)
this.relocateImages()
},
_debugRender: function () {
console.log(`can: ${this.loading.length < 3}, loading: ${this.loading.length}, offset: ${this.imageRenderOffset}, total: ${this.renderTotal}`)
}
}
}
</script>
<style scoped lang="less">
@import "../styles/common";
@import "../styles/fonts";
@import "../styles/contentAnimations";
#pictures-grid-container {
margin: 0;
padding: 0;
width: 100%;
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-direction: row;
flex-wrap: wrap;
.column:first-child {
margin-top: 5%;
}
}
.column {
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
flex-wrap: wrap;
/*width: 100%;*/
height: auto;
}
.picture-container {
position: relative;
margin: 5% 2.5%;
transition: all 0.30s;
cursor: pointer;
&:hover {
.custom-rules-pc({
box-shadow: 0 73px 300px -10px rgba(186,186,186,0.20);
transform: translateY(-2%);
});
}
}
.picture-overlay {
z-index: 100;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: transparent;
transition: all 0.30s;
&:hover {
.custom-rules-pc({
background-color: fade(@color-background, 65%);
});
.picture-overlay-content {
opacity: 1;
}
}
.picture-overlay-content {
position: absolute;
color: @color-foreground-primary;
opacity: 0;
top: 50%;
left: 50%;
width: 75%;
height: 30%;
text-align: center;
font-size: 28pt;
transform: translate(-50%, -50%);
transition: all 0.30s;
}
}
</style>