4
votes

I have a requirement, where I need to freeze the selected item from list of items in a container to top, when the selected item is in top fold of the container. and when the selected item is in bottom fold of the container, I need to stick it to the bottom.

If the selected item is in visible fold, nothing should happen. I mean the selected item should be in normal flow with other adjacent items.

I somehow managed to solve this to some extent. But when I scroll up, when the selected item is sticked above of the container, the selected item is hiding. This behavior is happening even when I scroll down, when the selected item is sticked to the bottom of the container.

Here is the Fiddle

enter image description here

$('.item').click(function () {
    $('.item').removeClass('select').removeClass('pAbsolute');
    $(this).addClass('select');
});

$('.parent').scroll(function () {
    var $selected = $('.item.select');
    var cTop = $selected.offset().top;
    var cHeight = $selected.height();
    var pHeight = $(this).height();
    if (cTop < 0) {
        $selected.css({
            'top': $(this).scrollTop(),
                'bottom': ''
        }).addClass('pAbsolute');
    } else if (cTop > pHeight - cHeight) {
        $selected.css({
            'bottom': -$(this).scrollTop(),
                'top': ''
        }).addClass('pAbsolute');
    } else {
        $selected.css({
            'top': '',
                'bottom': ''
        }).removeClass('pAbsolute');
    }
});
2
How many selected items can you have? Your fiddle shows two elements that are "selected" (1 and 22). - Jack
@JackPattishall only one at a time. I updated the fiddle (it was a bug) - Mr_Green

2 Answers

2
votes

You have to use a consistent value to keep the initial offset relative to the container when you select it.

Then, calculate the offset and scroll value,

  1. If cTop < 0, which means its top out of box, stick to top.

  2. If cTop + cHeight > pHeight, which means its view is out of bottom block, set to bottom.

  3. Else stay in position.

Edit:

When selecting a new Item, as the previous item may have .pAbsolute attr, the relative position of current item may change, but we can get the offset change by track the offset before and after those class add/remove actions.

Then we can add the missing height by change the scrollTop of the container manually.

var offset;
$('.item').click(function () {
    // This is the offset in container before class change.
    offset = this.offsetTop;
    $('.item').removeClass('select').removeClass('pAbsolute');
    $(this).addClass('select');
    // Calculate the difference
    var distortion = offset - this.offsetTop;
    
    // Remove the distortion by manual scroll.
    var $parent = $(this).parent();
    $parent.scrollTop($parent.scrollTop() - distortion);
    
    offset = this.offsetTop;
});


$('.parent').scroll(function () {
    var $selected = $('.item.select');
    var cTop = offset - $(this).scrollTop();
    var cHeight = $selected.height();
    var pHeight = $(this).height();
    if (cTop < 0) {
        $selected.css({
            'top': $(this).scrollTop(),
                'bottom': ''
        }).addClass('pAbsolute');
    } else if (cTop + cHeight > pHeight) {
        $selected.css({
            'bottom': -$(this).scrollTop(),
                'top': ''
        }).addClass('pAbsolute');
    } else {
        $selected.css({
            'top': '',
                'bottom': ''
        }).removeClass('pAbsolute');
    }
});
body, html {
    padding: 0;
    margin: 0;
}
.parent {
    overflow: auto;
    height: 200px;
    position: relative;
}
.item {
    padding: 10px 15px;
    background-color: tomato;
    width: 100%;
}
.item.select {
    background-color: beige;
}
.pAbsolute {
    position: absolute;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="parent">
    <div class="item select">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
    <div class="item">5</div>
    <div class="item">6</div>
    <div class="item">7</div>
    <div class="item">8</div>
    <div class="item">9</div>
    <div class="item">10</div>
    <div class="item">11</div>
    <div class="item">12</div>
    <div class="item">13</div>
    <div class="item">14</div>
    <div class="item">15</div>
    <div class="item">16</div>
    <div class="item">17</div>
    <div class="item">18</div>
    <div class="item">19</div>
    <div class="item">20</div>
    <div class="item">21</div>
    <div class="item">22</div>
    <div class="item">23</div>
    <div class="item">24</div>
    <div class="item">25</div>
    <div class="item">26</div>
    <div class="item">27</div>
    <div class="item">28</div>
    <div class="item">29</div>
    <div class="item">30</div>
    <div class="item">31</div>
    <div class="item">32</div>
    <div class="item">33</div>
    <div class="item">34</div>
    <div class="item">35</div>
    <div class="item">36</div>
    <div class="item">37</div>
    <div class="item">38</div>
    <div class="item">39</div>
    <div class="item">40</div>
    <div class="item">41</div>
</div>
1
votes

This solution uses a bottom and top header who are filled in with the selected values and showed/hidden when necessary:

Working Fiddle

Javascript:

function stickItems($parent, itemClass, selectClass) {
    // Attach dummy element items
    $parent.prepend('<div class="' + itemClass + ' sticky top"></div>');
    $parent.append('<div class="' + itemClass + ' sticky bottom"></div>');

    var $items = $('.' + itemClass),
        $stickyTop = $('.' + itemClass + '.sticky.top'),
        $stickyBottom = $('.' + itemClass + '.sticky.bottom');

    // Click event registering 
    $items.click(function (e) {
        if (!$(e.target).hasClass('sticky')) {
            $items.removeClass(selectClass);
            $stickyTop.css('display', 'none');
            $stickyBottom.css('display', 'none');
            $(this).addClass(selectClass);
        }
    });



    // Scroll event
    $parent.scroll(function () {
        var $self = $(this);
        var $selected = $('.' + itemClass + '.' + selectClass);
        var cTop = $selected.offset().top;
        var pTop = $self.offset().top;
        var cHeight = $selected.height();
        var pHeight = $self.height();
        if (cTop - pTop <= 0) {
            $stickyTop.html($selected.html()).css({
                'display': 'block',
                    'top': $(this).scrollTop()
            });
            $stickyBottom.css('display', 'none');
        } else if (cTop > pTop && cTop < pTop + pHeight) {
            $stickyTop.css('display', 'none');
            $stickyBottom.css('display', 'none');
        } else {
            $stickyTop.css('display', 'none');
            $stickyBottom.html($selected.html()).css({
                'display': 'block',
                    'bottom': -$(this).scrollTop()
            });
        }
    });
}

stickItems($('.parent'), 'item', 'select');

Css:

body, html {
    padding: 0;
    margin: 0;
}
body {
    padding-top: 200px;
}
.parent {
    overflow-x: hidden;
    overflow-y: auto;
    height: 200px;
    position: relative;
}
.item {
    padding: 10px 15px;
    background-color: tomato;
}
.item.select {
    background-color: beige;
}
.item.sticky {
    background-color: beige;
    display: none;
    position: absolute;
    left: 0;
    right: 0;
    z-index: 1;
}

Html:

<div class="parent">
    <div class="item sticky top"></div>
    <div class="item select">1</div>
    <div class="item">2</div>
    <!-- ... -->
    <div class="item">39</div>
    <div class="item">40</div>
    <div class="item">41</div>
    <div class="item sticky bottom"></div>
</div>