mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #1701 from Human-Connection/1273-fix-post-page-nav
fix the bug with scrolling post comments into view
This commit is contained in:
commit
b31126c391
10
webapp/app/router.scrollBehavior.js
Normal file
10
webapp/app/router.scrollBehavior.js
Normal file
@ -0,0 +1,10 @@
|
||||
export default function(to, from, savedPosition) {
|
||||
if (savedPosition) return savedPosition
|
||||
|
||||
// Edge case: If you click on a notification from a comment and then on the
|
||||
// post page you click on 'comments', we avoid a "jumping" scroll behavior,
|
||||
// ie. jump to the top and scroll back from there
|
||||
if (to.path === from.path && to.hash !== from.hash) return false
|
||||
|
||||
return { x: 0, y: 0 }
|
||||
}
|
||||
@ -32,6 +32,7 @@ describe('Comment.vue', () => {
|
||||
truncate: a => a,
|
||||
removeHtml: a => a,
|
||||
},
|
||||
$scrollTo: jest.fn(),
|
||||
$apollo: {
|
||||
mutate: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
@ -51,6 +52,8 @@ describe('Comment.vue', () => {
|
||||
})
|
||||
|
||||
describe('shallowMount', () => {
|
||||
beforeEach(jest.useFakeTimers)
|
||||
|
||||
Wrapper = () => {
|
||||
const store = new Vuex.Store({
|
||||
getters,
|
||||
@ -117,7 +120,35 @@ describe('Comment.vue', () => {
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(jest.useFakeTimers)
|
||||
describe('scrollToAnchor mixin', () => {
|
||||
describe('$route.hash !== comment.id', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$route = {
|
||||
hash: '',
|
||||
}
|
||||
})
|
||||
|
||||
it('skips $scrollTo', () => {
|
||||
wrapper = Wrapper()
|
||||
jest.runAllTimers()
|
||||
expect(mocks.$scrollTo).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('$route.hash === comment.id', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$route = {
|
||||
hash: '#commentId-2',
|
||||
}
|
||||
})
|
||||
|
||||
it('calls $scrollTo', () => {
|
||||
wrapper = Wrapper()
|
||||
jest.runAllTimers()
|
||||
expect(mocks.$scrollTo).toHaveBeenCalledWith('#commentId-2')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('test callbacks', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
</ds-card>
|
||||
</div>
|
||||
<div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }">
|
||||
<ds-card :id="`commentId-${comment.id}`">
|
||||
<ds-card :id="anchor">
|
||||
<ds-space margin-bottom="small" margin-top="small">
|
||||
<hc-user :user="author" :date-time="comment.createdAt" />
|
||||
<!-- Content Menu (can open Modals) -->
|
||||
@ -80,8 +80,10 @@ import ContentMenu from '~/components/ContentMenu'
|
||||
import ContentViewer from '~/components/Editor/ContentViewer'
|
||||
import HcCommentForm from '~/components/CommentForm/CommentForm'
|
||||
import CommentMutations from '~/graphql/CommentMutations'
|
||||
import scrollToAnchor from '~/mixins/scrollToAnchor.js'
|
||||
|
||||
export default {
|
||||
mixins: [scrollToAnchor],
|
||||
data: function() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
@ -109,6 +111,9 @@ export default {
|
||||
user: 'auth/user',
|
||||
isModerator: 'auth/isModerator',
|
||||
}),
|
||||
anchor() {
|
||||
return `commentId-${this.comment.id}`
|
||||
},
|
||||
displaysComment() {
|
||||
return !this.unavailable || this.isModerator
|
||||
},
|
||||
@ -142,6 +147,9 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
checkAnchor(anchor) {
|
||||
return `#${this.anchor}` === anchor
|
||||
},
|
||||
isAuthor(id) {
|
||||
return this.user.id === id
|
||||
},
|
||||
|
||||
@ -42,6 +42,7 @@ describe('CommentList.vue', () => {
|
||||
truncate: a => a,
|
||||
removeHtml: a => a,
|
||||
},
|
||||
$scrollTo: jest.fn(),
|
||||
$apollo: {
|
||||
queries: {
|
||||
Post: {
|
||||
@ -65,12 +66,46 @@ describe('CommentList.vue', () => {
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
it('displays a comments counter', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('span.ds-tag').text()).toEqual('1')
|
||||
})
|
||||
|
||||
it('displays a comments counter', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('span.ds-tag').text()).toEqual('1')
|
||||
})
|
||||
|
||||
describe('scrollToAnchor mixin', () => {
|
||||
beforeEach(jest.useFakeTimers)
|
||||
|
||||
describe('$route.hash !== `#comments`', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$route = {
|
||||
hash: '',
|
||||
}
|
||||
})
|
||||
|
||||
it('skips $scrollTo', () => {
|
||||
wrapper = Wrapper()
|
||||
jest.runAllTimers()
|
||||
expect(mocks.$scrollTo).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('$route.hash === `#comments`', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$route = {
|
||||
hash: '#comments',
|
||||
}
|
||||
})
|
||||
|
||||
it('calls $scrollTo', () => {
|
||||
wrapper = Wrapper()
|
||||
jest.runAllTimers()
|
||||
expect(mocks.$scrollTo).toHaveBeenCalledWith('#comments')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -30,8 +30,10 @@
|
||||
</template>
|
||||
<script>
|
||||
import Comment from '~/components/Comment/Comment'
|
||||
import scrollToAnchor from '~/mixins/scrollToAnchor'
|
||||
|
||||
export default {
|
||||
mixins: [scrollToAnchor],
|
||||
components: {
|
||||
Comment,
|
||||
},
|
||||
@ -39,6 +41,9 @@ export default {
|
||||
post: { type: Object, default: () => {} },
|
||||
},
|
||||
methods: {
|
||||
checkAnchor(anchor) {
|
||||
return anchor === '#comments'
|
||||
},
|
||||
updateCommentList(updatedComment) {
|
||||
this.post.comments = this.post.comments.map(comment => {
|
||||
return comment.id === updatedComment.id ? updatedComment : comment
|
||||
|
||||
@ -75,7 +75,7 @@ export default {
|
||||
|
||||
const followedUser = follow ? data.followUser : data.unfollowUser
|
||||
this.$emit('update', followedUser)
|
||||
} catch {
|
||||
} catch (err) {
|
||||
optimisticResult.followedByCurrentUser = !follow
|
||||
this.$emit('optimistic', optimisticResult)
|
||||
}
|
||||
|
||||
23
webapp/mixins/scrollToAnchor.js
Normal file
23
webapp/mixins/scrollToAnchor.js
Normal file
@ -0,0 +1,23 @@
|
||||
function scrollToAnchor(anchor, { checkAnchor, $scrollTo }) {
|
||||
if (typeof checkAnchor !== 'function')
|
||||
throw new Error(
|
||||
'You must define `checkAnchor` on the component if you use scrollToAnchor mixin!',
|
||||
)
|
||||
if (!checkAnchor(anchor)) return
|
||||
setTimeout(() => {
|
||||
$scrollTo(anchor)
|
||||
}, 250)
|
||||
}
|
||||
|
||||
export default {
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
const anchor = to && to.hash
|
||||
scrollToAnchor(anchor, this)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const anchor = this.$route && this.$route.hash
|
||||
scrollToAnchor(anchor, this)
|
||||
},
|
||||
}
|
||||
@ -124,80 +124,6 @@ export default {
|
||||
middleware: ['authenticated', 'termsAndConditions'],
|
||||
linkActiveClass: 'router-link-active',
|
||||
linkExactActiveClass: 'router-link-exact-active',
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
let position = false
|
||||
// if no children detected and scrollToTop is not explicitly disabled
|
||||
if (
|
||||
to.matched.length < 2 &&
|
||||
to.matched.every(r => r.components.default.options.scrollToTop !== false)
|
||||
) {
|
||||
// scroll to the top of the page
|
||||
position = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
} else if (to.matched.some(r => r.components.default.options.scrollToTop)) {
|
||||
// if one of the children has scrollToTop option set to true
|
||||
position = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// savedPosition is only available for popstate navigations (back button)
|
||||
if (savedPosition) {
|
||||
position = savedPosition
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
// wait for the out transition to complete (if necessary)
|
||||
window.$nuxt.$once('triggerScroll', () => {
|
||||
let processInterval = null
|
||||
let processTime = 0
|
||||
const callInterval = 100
|
||||
const callIntervalLimit = 2000
|
||||
|
||||
// coords will be used if no selector is provided,
|
||||
// or if the selector didn't match any element.
|
||||
if (to.hash) {
|
||||
let hash = to.hash
|
||||
// CSS.escape() is not supported with IE and Edge.
|
||||
if (typeof window.CSS !== 'undefined' && typeof window.CSS.escape !== 'undefined') {
|
||||
hash = '#' + window.CSS.escape(hash.substr(1))
|
||||
}
|
||||
try {
|
||||
processInterval = setInterval(() => {
|
||||
const hashIsFound = document.querySelector(hash)
|
||||
|
||||
if (hashIsFound) {
|
||||
position = {
|
||||
selector: hash,
|
||||
offset: { x: 0, y: -500 },
|
||||
}
|
||||
}
|
||||
processTime += callInterval
|
||||
if (hashIsFound || processTime >= callIntervalLimit) {
|
||||
clearInterval(processInterval)
|
||||
processInterval = null
|
||||
}
|
||||
}, callInterval)
|
||||
} catch (e) {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.warn(
|
||||
'Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let resolveInterval = setInterval(() => {
|
||||
if (!processInterval) {
|
||||
clearInterval(resolveInterval)
|
||||
resolve(position)
|
||||
}
|
||||
}, callInterval)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
@ -216,6 +142,13 @@ export default {
|
||||
keys: envWhitelist,
|
||||
},
|
||||
],
|
||||
[
|
||||
'vue-scrollto/nuxt',
|
||||
{
|
||||
offset: -100, // to compensate fixed navbar height
|
||||
duration: 1000,
|
||||
},
|
||||
],
|
||||
'cookie-universal-nuxt',
|
||||
'@nuxtjs/apollo',
|
||||
'@nuxtjs/axios',
|
||||
|
||||
@ -83,6 +83,7 @@
|
||||
"vue-count-to": "~1.0.13",
|
||||
"vue-infinite-scroll": "^2.0.2",
|
||||
"vue-izitoast": "^1.2.1",
|
||||
"vue-scrollto": "^2.17.1",
|
||||
"vue-sweetalert-icons": "~4.2.0",
|
||||
"vuex-i18n": "~1.13.1",
|
||||
"xregexp": "^4.2.4",
|
||||
|
||||
@ -77,17 +77,6 @@ export default {
|
||||
]
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
if (to.hash === '#comments') {
|
||||
window.scroll({
|
||||
top: document.getElementById('comments').offsetTop,
|
||||
left: 0,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -197,7 +197,7 @@ export default {
|
||||
|
||||
.ds-card-image {
|
||||
img {
|
||||
max-height: 300px;
|
||||
height: 300px;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
@ -4235,6 +4235,11 @@ bcrypt-pbkdf@^1.0.0:
|
||||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
bezier-easing@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bezier-easing/-/bezier-easing-2.1.0.tgz#c04dfe8b926d6ecaca1813d69ff179b7c2025d86"
|
||||
integrity sha1-wE3+i5JtbsrKGBPWn/F5t8ICXYY=
|
||||
|
||||
bfj@^6.1.1:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/bfj/-/bfj-6.1.1.tgz#05a3b7784fbd72cfa3c22e56002ef99336516c48"
|
||||
@ -15307,6 +15312,13 @@ vue-router@~3.0.7:
|
||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.7.tgz#b36ca107b4acb8ff5bc4ff824584059c23fcb87b"
|
||||
integrity sha512-utJ+QR3YlIC/6x6xq17UMXeAfxEvXA0VKD3PiSio7hBOZNusA1jXcbxZxVEfJunLp48oonjTepY8ORoIlRx/EQ==
|
||||
|
||||
vue-scrollto@^2.17.1:
|
||||
version "2.17.1"
|
||||
resolved "https://registry.yarnpkg.com/vue-scrollto/-/vue-scrollto-2.17.1.tgz#cd62ee0b98cf7e2ba9fd94f029addcd093978a48"
|
||||
integrity sha512-uxOJXg6cZL88B+hTXRHDJMR+gHGiaS70ZTNk55fE5Z2TdwyIx9K/IHoNeTrtBrM6u3FASAIymKjZaQLmDf8Ykg==
|
||||
dependencies:
|
||||
bezier-easing "2.1.0"
|
||||
|
||||
vue-server-renderer@^2.6.10:
|
||||
version "2.6.10"
|
||||
resolved "https://registry.yarnpkg.com/vue-server-renderer/-/vue-server-renderer-2.6.10.tgz#cb2558842ead360ae2ec1f3719b75564a805b375"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user