eddiemf / vue-scrollactive

Lightweight and simple to use vue component that highlights menu items as you scroll the page, also scrolling to target section when clicked.
MIT License
542 stars 70 forks source link

Scroll a container #14

Closed henrikruscon closed 6 years ago

henrikruscon commented 6 years ago

Is it possible to target scroll a container instead of body? ✌️

eddiemf commented 6 years ago

I'm not really sure what you mean. What exactly do you want to achieve?

henrikruscon commented 6 years ago

Well everything has position absolute and overflow hidden except one container that allows overflow-y scrolling. Similar to how https://stripe.com/docs/api does it. Meaning being able to define which container that gets scrolled. For example https://www.npmjs.com/package/vue-scrollto provides a container option. Right now vue-scrollactive doesn't work since it doesn't target my container ✌️

Simply put I think providing a container means codewise that it doesn't look for scroll data from window/document/body but instead the provided container.

eddiemf commented 6 years ago

Ohh, now I think I understand. You mean for the click to scroll functionality, right? If you click on an item it will try to scroll the window, and you want to scroll a specific element acting as a scroll container, is that correct?

You cannot do that currently, but I think it's a quick add. I'll try to work on this tonight and I'll come back with a response.

henrikruscon commented 6 years ago

Yeah that's exactly what I mean. Thanks for looking into it!

henrikruscon commented 6 years ago

Got it working in a local version, want a PR? ✌️

eddiemf commented 6 years ago

I would appreciate it :)

Still didn't have time to look into it. Thanks!

damienroche commented 6 years ago

@henrikdahl @eddiemf any news about the PR ? need this too !

henrikruscon commented 6 years ago

@damienroche I ended up coding my own local ScrollSpy. It's different enough that doing a PR would require some time. Here's the code, specifically the container prop is what enables it.

<template>
    <nav role="navigation">
        <slot/>
    </nav>
</template>

<script>
export default {
    name: 'ScrollSpy',

    props: {
        container: {
            type: String,
            default: '.content'
        },

        activeClass: {
            type: String,
            default: 'is-active'
        },

        offset: {
            type: Number,
            default: 1
        }
    },

    data() {
        return {
            items: [],
            currentItem: null,
            lastActiveItem: null
        }
    },

    computed: {
        scrollContainer() {
            return document.querySelector(this.container)
        }
    },

    mounted() {
        this.initItems()

        this.scrollContainer.addEventListener('scroll', this.onScroll)
    },

    updated() {
        this.initItems()
    },

    beforeDestroy() {
        this.scrollContainer.removeEventListener('scroll', this.onScroll)
    },

    methods: {
        onScroll(event) {
            this.currentItem = this.getItemInsideWindow()

            if (this.currentItem) {
                if (this.currentItem !== this.lastActiveItem) {
                    this.removeActiveClass()
                    this.lastActiveItem = this.currentItem
                }

                this.currentItem.classList.add(this.activeClass)
            }
        },

        getItemInsideWindow() {
            let currentItem

            this.items.forEach(item => {
                const target = document.getElementById(item.hash.substr(1))
                const isScreenPastSection = this.scrollContainer.scrollTop >= this.getOffsetTop(target) - this.offset

                if (isScreenPastSection) currentItem = item
            })

            return currentItem
        },

        initItems() {
            this.items = this.$el.querySelectorAll('.sidebar__navigation__item')

            this.items.forEach(item => {
                item.addEventListener('click', this.handleClick)
            })

            this.removeActiveClass()
            this.currentItem = this.getItemInsideWindow()

            if (this.currentItem) this.currentItem.classList.add(this.activeClass)
        },

        handleClick(event) {
            event.preventDefault()

            const { hash } = event.currentTarget
            const target = document.getElementById(hash.substr(1))

            this.scrollContainer.removeEventListener('scroll', this.onScroll)

            this.removeActiveClass()
            event.currentTarget.classList.add(this.activeClass)

            target.scrollIntoView()

            this.scrollContainer.addEventListener('scroll', this.onScroll)

            if (window.history.replaceState) {
                window.history.replaceState(null, null, hash)
            } else {
                window.location.hash = hash
            }
        },

        getOffsetTop(element) {
            let yPosition = 0
            let nextElement = element

            while (nextElement) {
                yPosition += (nextElement.offsetTop)
                nextElement = nextElement.offsetParent
            }

            return yPosition
        },

        removeActiveClass() {
            this.items.forEach(item => {
                item.classList.remove(this.activeClass)
            })
        }
    }
}
</script>
damienroche commented 6 years ago

@henrikdahl thanks for sharing this, it works like a charm.

Notice for other people, don't forget to a sidebar__navigation__item class to links or add a custom prop. Also target.scrollIntoView() causes some displaying bugs because i'm scrolling into an overflowed div, I replaced this line by

const top = target.offsetTop
this.scrollContainer.scrollTop = top
henrikruscon commented 6 years ago

@damienroche Glad it helped you out!

Since none of the scroll spies did what I needed, I decided to simply maintain my own local version. I'm not really interested in maintaining such a plugin publicly and I think my needs differ too much from vue-scrollactive for it to be a useful PR in its entirety.

With that said. I'm going to add some additional features to my local ScrollSpy such as expandable groups. Additionally I need the sidebar navigation to scroll if the active item is outside of the view (overflowing) while you scroll the actual container.

Similar to how https://stripe.com/docs/api handles their ScrollSpy.

If you need any of those features, I could provide you the code here and either @eddiemf or someone else could add it to vue-scrollactive as a PR ✌️

eddiemf commented 6 years ago

Hello guys, thanks for sharing a workaround for this. Last weeks I've been working on updating another plugin that I maintain, this weekend I'll update this one with refactors and try to add some of the functionalities you mentioned. I really liked how stripe does it in the API docs, I'll use it as a reference and aim to implement all of its functionalities.

Thanks for the support!

damienroche commented 6 years ago

@henrikdahl interesting... I have to deal with sidebar scrolling too. I also added some lines of code to allow scroll to current section if hash is present in url

henrikruscon commented 6 years ago

@damienroche

This is how I handle scroll into current section based upon hash ✌️

async beforeCreate() {
    const { hash } = window.location

    if (hash) {
        const target = document.getElementById(hash.substr(1))

        target.scrollIntoView()
    }
},

What's your approach?

eddiemf commented 6 years ago

@henrikdahl @damienroche As mentioned in the commit, I'll still work on the features you guys proposed and will implement them very soon. You can open a new issue for them if you want to, but I'll basically just follow the stripe API documentation sidebar as a reference and make sure you can have the same results with this plugin.

Thanks again for the help.

damienroche commented 6 years ago

@eddiemf looking forward for it !