mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #1256 from Human-Connection/add-masonry-grid
Add masonry layout grid
This commit is contained in:
commit
af5772db49
@ -1,27 +1,24 @@
|
||||
<template>
|
||||
<ds-card v-show="hashtag" class="filter-menu-card">
|
||||
<div>
|
||||
<ds-space margin-bottom="x-small" />
|
||||
<ds-flex>
|
||||
<ds-flex-item>
|
||||
<ds-heading size="h3">{{ $t('filter-menu.hashtag-search', { hashtag }) }}</ds-heading>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item>
|
||||
<div class="filter-menu-buttons">
|
||||
<ds-button
|
||||
v-tooltip="{
|
||||
content: this.$t('filter-menu.clearSearch'),
|
||||
placement: 'left',
|
||||
delay: { show: 500 },
|
||||
}"
|
||||
name="clear-search-button"
|
||||
icon="close"
|
||||
@click="clearSearch"
|
||||
/>
|
||||
</div>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</div>
|
||||
<ds-card class="filter-menu-card">
|
||||
<ds-flex class="filter-menu-content">
|
||||
<ds-flex-item>
|
||||
<ds-heading size="h3">{{ $t('filter-menu.hashtag-search', { hashtag }) }}</ds-heading>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item>
|
||||
<div class="filter-menu-buttons">
|
||||
<ds-button
|
||||
v-tooltip="{
|
||||
content: this.$t('filter-menu.clearSearch'),
|
||||
placement: 'left',
|
||||
delay: { show: 500 },
|
||||
}"
|
||||
name="clear-search-button"
|
||||
icon="close"
|
||||
@click="clearSearch"
|
||||
/>
|
||||
</div>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-card>
|
||||
</template>
|
||||
|
||||
@ -43,6 +40,11 @@ export default {
|
||||
background-color: $background-color-soft;
|
||||
}
|
||||
|
||||
.filter-menu-content {
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-menu-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
23
webapp/components/MasonryGrid/MasonryGrid.spec.js
Normal file
23
webapp/components/MasonryGrid/MasonryGrid.spec.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import MasonryGrid from './MasonryGrid'
|
||||
|
||||
describe('MasonryGrid', () => {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(MasonryGrid)
|
||||
})
|
||||
|
||||
it('adds the "reset-grid-height" class when one or more children are updating', () => {
|
||||
wrapper.trigger('calculating-item-height')
|
||||
|
||||
expect(wrapper.classes()).toContain('reset-grid-height')
|
||||
})
|
||||
|
||||
it('removes the "reset-grid-height" class when all children have completed updating', () => {
|
||||
wrapper.setData({ itemsCalculating: 1 })
|
||||
wrapper.trigger('finished-calculating-item-height')
|
||||
|
||||
expect(wrapper.classes()).not.toContain('reset-grid-height')
|
||||
})
|
||||
})
|
||||
35
webapp/components/MasonryGrid/MasonryGrid.vue
Normal file
35
webapp/components/MasonryGrid/MasonryGrid.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<ds-grid
|
||||
:min-column-width="300"
|
||||
v-on:calculating-item-height="startCalculation"
|
||||
v-on:finished-calculating-item-height="endCalculation"
|
||||
:class="[itemsCalculating ? 'reset-grid-height' : '']"
|
||||
>
|
||||
<slot></slot>
|
||||
</ds-grid>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
itemsCalculating: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startCalculation() {
|
||||
this.itemsCalculating += 1
|
||||
},
|
||||
endCalculation() {
|
||||
this.itemsCalculating -= 1
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.reset-grid-height {
|
||||
grid-auto-rows: auto !important;
|
||||
align-items: self-start;
|
||||
}
|
||||
</style>
|
||||
27
webapp/components/MasonryGrid/MasonryGridItem.spec.js
Normal file
27
webapp/components/MasonryGrid/MasonryGridItem.spec.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import MasonryGridItem from './MasonryGridItem'
|
||||
|
||||
describe('MasonryGridItem', () => {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(MasonryGridItem)
|
||||
wrapper.vm.$parent.$emit = jest.fn()
|
||||
})
|
||||
|
||||
it('emits "calculating-item-height" when starting calculation', async () => {
|
||||
wrapper.vm.calculateItemHeight()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const firstCallArgument = wrapper.vm.$parent.$emit.mock.calls[0][0]
|
||||
expect(firstCallArgument).toBe('calculating-item-height')
|
||||
})
|
||||
|
||||
it('emits "finished-calculating-item-height" after the calculation', async () => {
|
||||
wrapper.vm.calculateItemHeight()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const secondCallArgument = wrapper.vm.$parent.$emit.mock.calls[1][0]
|
||||
expect(secondCallArgument).toBe('finished-calculating-item-height')
|
||||
})
|
||||
})
|
||||
39
webapp/components/MasonryGrid/MasonryGridItem.vue
Normal file
39
webapp/components/MasonryGrid/MasonryGridItem.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<ds-grid-item :rowSpan="rowSpan">
|
||||
<slot></slot>
|
||||
</ds-grid-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
rowSpan: 10,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
calculateItemHeight() {
|
||||
this.$parent.$emit('calculating-item-height')
|
||||
this.$nextTick(() => {
|
||||
const gridStyle = this.$parent.$el.style
|
||||
const rowHeight = parseInt(gridStyle.gridAutoRows)
|
||||
const rowGapValue = gridStyle.rowGap || gridStyle.gridRowGap
|
||||
const rowGap = parseInt(rowGapValue)
|
||||
const itemHeight = this.$el.clientHeight
|
||||
|
||||
this.rowSpan = Math.ceil((itemHeight + rowGap) / (rowHeight + rowGap))
|
||||
this.$parent.$emit('finished-calculating-item-height')
|
||||
})
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const image = this.$el.querySelector('img')
|
||||
if (image) {
|
||||
image.onload = () => this.calculateItemHeight()
|
||||
} else {
|
||||
// use timeout to make sure layout is set up before calculation
|
||||
setTimeout(() => this.calculateItemHeight(), 0)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,69 +1,71 @@
|
||||
<template>
|
||||
<ds-flex-item :width="width">
|
||||
<ds-card
|
||||
:image="post.image | proxyApiUrl"
|
||||
:class="{ 'post-card': true, 'disabled-content': post.disabled }"
|
||||
<ds-card
|
||||
:image="post.image | proxyApiUrl"
|
||||
:class="{ 'post-card': true, 'disabled-content': post.disabled }"
|
||||
>
|
||||
<!-- Post Link Target -->
|
||||
<nuxt-link
|
||||
class="post-link"
|
||||
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
|
||||
>
|
||||
<!-- Post Link Target -->
|
||||
<nuxt-link
|
||||
class="post-link"
|
||||
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
|
||||
>
|
||||
{{ post.title }}
|
||||
</nuxt-link>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Username, Image & Date of Post -->
|
||||
<div>
|
||||
<no-ssr>
|
||||
<hc-user :user="post.author" :trunc="35" :date-time="post.createdAt" />
|
||||
</no-ssr>
|
||||
<hc-ribbon :text="$t('post.name')" />
|
||||
{{ post.title }}
|
||||
</nuxt-link>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Username, Image & Date of Post -->
|
||||
<div>
|
||||
<no-ssr>
|
||||
<hc-user :user="post.author" :trunc="35" :date-time="post.createdAt" />
|
||||
</no-ssr>
|
||||
<hc-ribbon :text="$t('post.name')" />
|
||||
</div>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Post Title -->
|
||||
<ds-heading tag="h3" no-margin>{{ post.title }}</ds-heading>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Post Content Excerpt -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<!-- TODO: replace editor content with tiptap render view -->
|
||||
<div class="hc-editor-content" v-html="excerpt" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<!-- Footer o the Post -->
|
||||
<template slot="footer">
|
||||
<div style="display: inline-block; opacity: .5;">
|
||||
<!-- Categories -->
|
||||
<hc-category
|
||||
v-for="category in post.categories"
|
||||
:key="category.id"
|
||||
v-tooltip="{
|
||||
content: category.name,
|
||||
placement: 'bottom-start',
|
||||
delay: { show: 500 },
|
||||
}"
|
||||
:icon="category.icon"
|
||||
/>
|
||||
</div>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Post Title -->
|
||||
<ds-heading tag="h3" no-margin>{{ post.title }}</ds-heading>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Post Content Excerpt -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<!-- TODO: replace editor content with tiptap render view -->
|
||||
<div class="hc-editor-content" v-html="excerpt" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<!-- Footer o the Post -->
|
||||
<template slot="footer">
|
||||
<div style="display: inline-block; opacity: .5;">
|
||||
<!-- Categories -->
|
||||
<hc-category
|
||||
v-for="category in post.categories"
|
||||
:key="category.id"
|
||||
v-tooltip="{ content: category.name, placement: 'bottom-start', delay: { show: 500 } }"
|
||||
:icon="category.icon"
|
||||
<no-ssr>
|
||||
<div style="display: inline-block; float: right">
|
||||
<!-- Shouts Count -->
|
||||
<span :style="{ opacity: post.shoutedCount ? 1 : 0.5 }">
|
||||
<ds-icon name="bullhorn" />
|
||||
<small>{{ post.shoutedCount }}</small>
|
||||
</span>
|
||||
|
||||
<!-- Comments Count -->
|
||||
<span :style="{ opacity: post.commentedCount ? 1 : 0.5 }">
|
||||
<ds-icon name="comments" />
|
||||
<small>{{ post.commentedCount }}</small>
|
||||
</span>
|
||||
<!-- Menu -->
|
||||
<content-menu
|
||||
resource-type="contribution"
|
||||
:resource="post"
|
||||
:modalsData="menuModalsData"
|
||||
:is-owner="isAuthor"
|
||||
/>
|
||||
</div>
|
||||
<no-ssr>
|
||||
<div style="display: inline-block; float: right">
|
||||
<!-- Shouts Count -->
|
||||
<span :style="{ opacity: post.shoutedCount ? 1 : 0.5 }">
|
||||
<ds-icon name="bullhorn" />
|
||||
<small>{{ post.shoutedCount }}</small>
|
||||
</span>
|
||||
|
||||
<!-- Comments Count -->
|
||||
<span :style="{ opacity: post.commentedCount ? 1 : 0.5 }">
|
||||
<ds-icon name="comments" />
|
||||
<small>{{ post.commentedCount }}</small>
|
||||
</span>
|
||||
<!-- Menu -->
|
||||
<content-menu
|
||||
resource-type="contribution"
|
||||
:resource="post"
|
||||
:modalsData="menuModalsData"
|
||||
:is-owner="isAuthor"
|
||||
/>
|
||||
</div>
|
||||
</no-ssr>
|
||||
</template>
|
||||
</ds-card>
|
||||
</ds-flex-item>
|
||||
</no-ssr>
|
||||
</template>
|
||||
</ds-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<ds-flex :width="{ base: '100%' }" gutter="base">
|
||||
<ds-flex-item>
|
||||
<masonry-grid>
|
||||
<ds-grid-item v-show="hashtag" :row-span="2" column-span="fullWidth">
|
||||
<filter-menu :hashtag="hashtag" @clearSearch="clearSearch" />
|
||||
</ds-flex-item>
|
||||
<ds-flex-item>
|
||||
</ds-grid-item>
|
||||
<ds-grid-item :row-span="2" column-span="fullWidth">
|
||||
<div class="sorting-dropdown">
|
||||
<ds-select
|
||||
v-model="selected"
|
||||
@ -14,15 +14,15 @@
|
||||
@input="toggleOnlySorting"
|
||||
></ds-select>
|
||||
</div>
|
||||
</ds-flex-item>
|
||||
<hc-post-card
|
||||
v-for="post in posts"
|
||||
:key="post.id"
|
||||
:post="post"
|
||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||
@removePostFromList="deletePost(index, post.id)"
|
||||
/>
|
||||
</ds-flex>
|
||||
</ds-grid-item>
|
||||
<masonry-grid-item v-for="post in posts" :key="post.id">
|
||||
<hc-post-card
|
||||
:post="post"
|
||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||
@removePostFromList="deletePost(index, post.id)"
|
||||
/>
|
||||
</masonry-grid-item>
|
||||
</masonry-grid>
|
||||
<no-ssr>
|
||||
<ds-button
|
||||
v-tooltip="{ content: 'Create a new Post', placement: 'left', delay: { show: 500 } }"
|
||||
@ -51,6 +51,8 @@ import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
import HcPostCard from '~/components/PostCard'
|
||||
import HcLoadMore from '~/components/LoadMore.vue'
|
||||
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
|
||||
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { filterPosts } from '~/graphql/PostQuery.js'
|
||||
|
||||
@ -59,6 +61,8 @@ export default {
|
||||
FilterMenu,
|
||||
HcPostCard,
|
||||
HcLoadMore,
|
||||
MasonryGrid,
|
||||
MasonryGridItem,
|
||||
},
|
||||
data() {
|
||||
const { hashtag = null } = this.$route.query
|
||||
@ -177,6 +181,21 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.masonry-grid {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
grid-auto-rows: 20px;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
grid-row-end: span 2;
|
||||
|
||||
&--full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.post-add-button {
|
||||
z-index: 100;
|
||||
position: fixed;
|
||||
@ -191,5 +210,6 @@ export default {
|
||||
position: relative;
|
||||
float: right;
|
||||
padding: 0 18px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -153,8 +153,8 @@
|
||||
</ds-flex-item>
|
||||
|
||||
<ds-flex-item :width="{ base: '100%', sm: 3, md: 5, lg: 3 }">
|
||||
<ds-flex class="user-profile-posts-list" :width="{ base: '100%' }" gutter="small">
|
||||
<ds-flex-item class="profile-top-navigation">
|
||||
<masonry-grid class="user-profile-posts-list">
|
||||
<ds-grid-item class="profile-top-navigation" :row-span="3" column-span="fullWidth">
|
||||
<ds-card class="ds-tab-nav">
|
||||
<ul class="Tabs">
|
||||
<li class="Tabs__tab Tab pointer" :class="{ active: tabActive === 'post' }">
|
||||
@ -193,9 +193,9 @@
|
||||
<li class="Tabs__presentation-slider" role="presentation"></li>
|
||||
</ul>
|
||||
</ds-card>
|
||||
</ds-flex-item>
|
||||
</ds-grid-item>
|
||||
|
||||
<ds-flex-item style="text-align: center">
|
||||
<ds-grid-item :row-span="2" column-span="fullWidth" class="create-button">
|
||||
<ds-button
|
||||
v-if="myProfile"
|
||||
v-tooltip="{ content: 'Create a new Post', placement: 'left', delay: { show: 500 } }"
|
||||
@ -205,30 +205,30 @@
|
||||
size="large"
|
||||
primary
|
||||
/>
|
||||
</ds-flex-item>
|
||||
</ds-grid-item>
|
||||
|
||||
<template v-if="activePosts.length">
|
||||
<hc-post-card
|
||||
v-for="(post, index) in activePosts"
|
||||
:key="post.id"
|
||||
:post="post"
|
||||
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
||||
@removePostFromList="removePostFromList(index)"
|
||||
/>
|
||||
<masonry-grid-item v-for="(post, index) in activePosts" :key="post.id">
|
||||
<hc-post-card
|
||||
:post="post"
|
||||
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
||||
@removePostFromList="removePostFromList(index)"
|
||||
/>
|
||||
</masonry-grid-item>
|
||||
</template>
|
||||
<template v-else-if="$apollo.loading">
|
||||
<ds-flex-item>
|
||||
<ds-grid-item>
|
||||
<ds-section centered>
|
||||
<ds-spinner size="base"></ds-spinner>
|
||||
</ds-section>
|
||||
</ds-flex-item>
|
||||
</ds-grid-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ds-flex-item :width="{ base: '100%' }">
|
||||
<ds-grid-item column-span="fullWidth">
|
||||
<hc-empty margin="xx-large" icon="file" />
|
||||
</ds-flex-item>
|
||||
</ds-grid-item>
|
||||
</template>
|
||||
</ds-flex>
|
||||
</masonry-grid>
|
||||
<div
|
||||
v-if="hasMore"
|
||||
v-infinite-scroll="showMoreContributions"
|
||||
@ -254,6 +254,8 @@ import HcEmpty from '~/components/Empty.vue'
|
||||
import ContentMenu from '~/components/ContentMenu'
|
||||
import HcUpload from '~/components/Upload'
|
||||
import HcAvatar from '~/components/Avatar/Avatar.vue'
|
||||
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
|
||||
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
||||
import { filterPosts } from '~/graphql/PostQuery'
|
||||
import UserQuery from '~/graphql/User'
|
||||
import { Block, Unblock } from '~/graphql/settings/BlockedUsers'
|
||||
@ -279,6 +281,8 @@ export default {
|
||||
HcAvatar,
|
||||
ContentMenu,
|
||||
HcUpload,
|
||||
MasonryGrid,
|
||||
MasonryGridItem,
|
||||
},
|
||||
transition: {
|
||||
name: 'slide-up',
|
||||
@ -425,6 +429,10 @@ export default {
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.create-button {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
}
|
||||
.Tab {
|
||||
border-collapse: collapse;
|
||||
padding-bottom: 5px;
|
||||
@ -435,6 +443,8 @@ export default {
|
||||
.Tabs {
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
height: 100%;
|
||||
|
||||
&:after {
|
||||
content: ' ';
|
||||
display: table;
|
||||
@ -443,10 +453,13 @@ export default {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
&__tab {
|
||||
float: left;
|
||||
width: 33.333%;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
|
||||
&:first-child.active ~ .Tabs__presentation-slider {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user