jQuery: 'load more' masonry elements?

2020-03-24 03:08发布

问题:

I have a few elements on my page which I'm loading 5 at a time.

What I need to do is to animate the loaded elements using animate.css.

To explain this, I've created this fiddle:

https://jsfiddle.net/89v781rL/6/

Scroll down to see the Show more button and click on it to see it in action.

When i load more items, the elements are animated but because they are layed out in a Masonry style, the position of them keep changing which is not what i want. Basically I need to keep the elements in their first position and keep adding/loading more elements.

If you comment out the following CSS code and run the JSFIDDLE, you will see how nicely the elements are layed out and that is how i want them to be when they are loading 5 at a time too:

#grid li:nth-child(n+6) {
  /*display: none;*/
}

what I need to do is to bounce the elements UP using animate.css.

This is my full code:

$('#sales').click(function() {
$('#grid li').not(':visible').slice(0, 5).slideDown();

$('#grid li').not(':visible').slice(0, 5).addClass('animated bounceInUp');
});

Note: I don't want to use any plugins like this

Mainly because it is very big and bloated for my project and it doesn't function properly in my project either.

Could someone please advise on this issue?

EDIT:

Here is another failed attempt to do this:

https://jsfiddle.net/89v781rL/8/

And this one too:

https://jsfiddle.net/89v781rL/9/

回答1:

TL;DR: Proposed solution.


Simply put, what you want is not possible. At least not how you started it.

But, here's the thing: the reason I got into coding (coming from design background - and also the main reason I answered your question) is because a bunch of web developers told me about a particular design I made, that it's not possible. So I (re)searched and experimented until I found a way.

Ever since, whenever I come to this conclusion, I always translate it to:
It's possible, I'm just doing it wrong™.


In your particular case, you're using a "masonry" technique that's not exactly masonry. It's a column layout technique, called CSS columns. Here's what it does, from a technical point of view:

  • calculates the width of columns, considering parent total width, columns number and column gutter
  • calculates the total height of contents, considering the resulting column width
  • breaks the content into chunks fitting into columns, trying to distribute content equally, respecting any break-inside declarations. Note the suggestive name of the property value: avoid, it's not forbid or disallow. Based on contents, this rule doesn't always apply.

But, in short, here's how your 2 columns layout renders initially:

1 4
2 5
3 6

When you decide to add more content, it will go through the steps above again, considering the updated content height resulting in:

1 5
2 6
3 7
4

It will calculate possible breaking options and will go for the one that results in the smallest height for the parent element. If two or more result in the same height, most browsers will choose the one that makes the later/last column shorter.


If your content needs to stay into place once rendered, the CSS Columns technique is clearly not an option. So you're looking at techniques using absolute positioning. You have many options, but the notable ones (I'm probably subjective - but is anyone really objective?) are:

  • Masonry. It comes with the very handy stamp method, specifically designed for your requirement (stick existing elements into place, so adding more content will not re-position existing elements - which could happen occasionally, depending on contents and parent - for example, if you have a list of existing short items and you add a very tall one, it would result in a column much taller than the rest; if stamp wasn't called on existing items, they will be re-positioned so that resulting columns have minimal differences in height)
  • Isotope - I've never had the chance to use it, but I heard good things about it.
  • The so-called pinterest layout script. It was written by Evan Sharp, it relies entirely on javascript and absolute positioning (just like Masonry) but is incredibly efficient. He explains how he did it in this SO answer and here's a good tutorial on the technique. If you don't care much about the technical part and are only interested in the juice, I found a light plugin, called Bootstrap Waterfall which, apart from everything linked above, has a production ready usable version.

Everything I wrote so far is what a bit of time and decent search-fu would have gotten you.

The only step left (and the real answer) would be animating each item into view using the required animate.css effect. I chose Bootstrap-waterfall for the layout, but you could as well go with any of the other options. Here it is:

// Included waterfall script as it doesn't load from github for everyone
+function(t){"use strict";function i(i){this.$pins=i,this.tasks=[],this.timerId=null,this.deferred=new t.Deferred}function e(t){this.img=t,this.initialWidth=t.width,this.initialHeight=t.height}function n(i){return this.each(function(){var e=t(this),n=e.data("mystist.waterfall"),s="object"==typeof i&&i;n&&"string"!=typeof i&&n.destroy()&&(n=null),n||e.data("mystist.waterfall",n=new o(this,s)),"string"==typeof i&&n[i]()})}var s=s||{now:Date.now||function(){return(new Date).getTime()},throttle:function(t,i,e){var n,o,r,a=null,h=0;e||(e={});var l=function(){h=e.leading===!1?0:s.now(),a=null,r=t.apply(n,o),a||(n=o=null)};return function(){var u=s.now();h||e.leading!==!1||(h=u);var c=i-(u-h);return n=this,o=arguments,0>=c||c>i?(a&&(clearTimeout(a),a=null),h=u,r=t.apply(n,o),a||(n=o=null)):a||e.trailing===!1||(a=setTimeout(l,c)),r}},debounce:function(t,i,e){var n,o,r,a,h,l=function(){var u=s.now()-a;i>u&&u>=0?n=setTimeout(l,i-u):(n=null,e||(h=t.apply(r,o),n||(r=o=null)))};return function(){r=this,o=arguments,a=s.now();var u=e&&!n;return n||(n=setTimeout(l,i)),u&&(h=t.apply(r,o),r=o=null),h}}},o=function(i,e){this.$element=t(i),this.options=t.extend({},o.DEFAULTS,e),this.id=Math.random().toString().slice(2),this.$fakePin=null,this.$container=null,this.$pins=null,this.pinWidth=null,this.imgWidth=null,this.lefts=[],this.tops=[],this.init().calculateWidth().calculatePosition().sail(),t(window).on("resize.mystist.waterfall"+this.id,s.debounce(t.proxy(function(){t(window).off("scroll.mystist.waterfall"+this.id),this.calculateWidth().calculatePosition().ship(r.getLoadedPins.call(this))},this),777))};o.VERSION="0.2.4",o.DEFAULTS={},o.prototype.init=function(){return this.initPins().initAttributes(),this},o.prototype.initPins=function(){var i=this.$element.children().length>0?this.$element.children().remove():t(this.$element.data("bootstrap-waterfall-template"));return i.each(function(){var i=t(this).find("img:eq(0)");i.length>0&&(t(this).data("bootstrap-waterfall-src",i.attr("src")),i.attr("src","data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="))}),this.$pins=i,this},o.prototype.initAttributes=function(){return this.$fakePin=this.$pins.first().clone(),this.$container=t("<div />").css("position","relative"),this.$element.html(this.$container),this},o.prototype.calculateWidth=function(){var t=this.$fakePin.clone();return this.$container.append(t.css("opacity",0)),this.pinWidth=t.outerWidth(!0),this.imgWidth=t.find("img:eq(0)").css("width","100%").width(),t.remove(),this},o.prototype.calculatePosition=function(){for(var t=parseInt(this.$container.width()/this.pinWidth,10),i=[],e=[],n=0;t>n;n++)i.push(n*this.pinWidth),e.push(0);return this.lefts=i,this.tops=e,this},o.prototype.sail=function(){var e=r.getToLoadPins.call(this),n=new i(e);return n.load().run().deferred.done(t.proxy(function(){this.ship(e)},this)),this},o.prototype.ship=function(i){return this.render(i).updateHeight(),t(window).on("scroll.mystist.waterfall"+this.id,s.throttle(t.proxy(function(){r.isWantMore.call(this)&&(t(window).off("scroll.mystist.waterfall"+this.id),this.sail())},this),500)),this},o.prototype.render=function(i){var e=this;return i.each(function(){e.placePin(t(this))}),this},o.prototype.placePin=function(t){var i=a.indexOf(this.tops,Math.min.apply(null,this.tops)),e=r.getPosition.call(this,i);return t.css({position:"absolute",left:e.left,top:e.top}),t.data("bootstrap-waterfall-pin")&&r.setImageHeight.call(this,t),t.data("bootstrap-waterfall-src")&&(r.makeImageAvailable.call(this,t),t.removeData("bootstrap-waterfall-src")),this.$container.append(t),r.updatePosition.call(this,i,t),this},o.prototype.updateHeight=function(){var t=a.indexOf(this.tops,Math.max.apply(null,this.tops));return this.$container.height(this.tops[t]),this},o.prototype.destroy=function(){return t(window).off("scroll.mystist.waterfall"+this.id),t(window).off("resize.mystist.waterfall"+this.id),this.$element.empty().removeData("mystist.waterfall"),this};var r={getToLoadPins:function(){var i=parseInt(this.$container.width()/this.pinWidth,10),e=3*i,n=this.$pins.map(function(){return t(this).find("img").length>0&&t(this).data("bootstrap-waterfall-src")?t(this):void 0});return n.slice(0,e)},getLoadedPins:function(){var i=this.$pins.map(function(){return t(this).find("img").length>0&&!t(this).data("bootstrap-waterfall-src")?t(this):void 0});return i},isWantMore:function(){return t(window).scrollTop()+t(window).height()>a.getDocHeight()-377?!0:!1},getPosition:function(t){var i={left:this.lefts[t],top:this.tops[t]};return i},setImageHeight:function(t){var i=t.data("bootstrap-waterfall-pin"),e=this.imgWidth*i.img.height/i.img.width;t.find("img:eq(0)").css({height:e,width:"auto"})},makeImageAvailable:function(t){t.find("img:eq(0)").attr("src",t.data("bootstrap-waterfall-src"))},updatePosition:function(t,i){this.tops[t]+=i.outerHeight(!0)}};i.prototype.load=function(){var i=this;return this.$pins.each(function(){var n=new Image;n.src=t(this).data("bootstrap-waterfall-src");var s=new e(n);i.tasks.push(s),t(this).data("bootstrap-waterfall-pin",s)}),this},i.prototype.run=function(){return this.timerId=setInterval(t.proxy(function(){this.isDone()?this.stop():this.check()},this),40),this},i.prototype.isDone=function(){return 0===this.tasks.length?!0:!1},i.prototype.stop=function(){clearInterval(this.timerId),this.timerId=null,this.deferred.resolve()},i.prototype.check=function(){for(var t=0;t<this.tasks.length;t++){var i=this.tasks[t];i.isLoaded()&&this.tasks.splice(t--,1)}},e.prototype.isLoaded=function(){return this.img.width!==this.initialWidth||this.img.height!==this.initialHeight||this.img.width*this.img.height>1024?!0:!1};var a={getDocHeight:function(){var t=document;return Math.max(t.body.scrollHeight,t.documentElement.scrollHeight,t.body.offsetHeight,t.documentElement.offsetHeight,t.body.clientHeight,t.documentElement.clientHeight)},indexOf:function(t,i){if(null==t)return-1;for(var e=0,n=t.length;n>e;e++)if(t[e]===i)return e;return-1}},h=t.fn.waterfall;t.fn.waterfall=n,t.fn.waterfall.Constructor=o,t.fn.waterfall.noConflict=function(){return t.fn.waterfall=h,this}}(jQuery);
// note waterfall should normally be linked as resource in your project

$('#waterfall').data('bootstrap-waterfall-template', $('#waterfall-template').html());
let wf = $('#waterfall').waterfall();

let fader = {
  wh: $(window).height(),
  full: function() {
  	$('#waterfall .pin').each(function(i, e) {
      (function(i, e) {
        setTimeout(function() {
          fader.check(e)
        }, i * 150);
      })(i, e)
    })
  },
  check: function(e) {
    if (fader.wh > e.getBoundingClientRect().top + 60) {
      $(e).addClass('inView');
      setTimeout(function() { $(e).addClass('fix') }, 750)
    }
  },
  resize:function(){
    fader.wh = $(window).height();
    fader.full();
  },
  light: function() {
    let fst = $('#waterfall .pin:not(".inView")').eq(0);
    if (fst.is('.pin')) {
      fader.check(fst[0])
    }
  }
};

setTimeout(function() { fader.full() }, 210);
$(window)
  .on('scroll', fader.light)
  .on('resize', _.throttle(fader.resize, 
    500,{leading:false,trailing:true})
  );
#waterfall .pin {
  width: calc(50% - 4px);
  opacity: 0;
  animation-duration: 0.75s;
  animation-fill-mode: both; }
#waterfall .pin.inView {
  opacity: 1;
  animation-name: bounceInUp; }
#waterfall .pin.inView.fix {
  animation: none; }
#waterfall .pin a {
  display: block;
  padding: 4px 4px 8px; }

* {
  box-sizing: border-box; }

.container {
  text-align: center; }
.container #waterfall {
  max-width: 800px;
  margin: 0 auto; }

@keyframes bounceInUp {
  from, 60%, 75%, 90%, to {
    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);   }
  from {
    opacity: 0;
    transform: translate3d(0, 3000px, 0);   }
  60% {
    opacity: 1;
    transform: translate3d(0, -20px, 0);   }
  75% {
    transform: translate3d(0, 10px, 0);   }
  90% {
    transform: translate3d(0, -5px, 0);   }
  to {
    transform: translate3d(0, 0, 0);   }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js"></script>


<script id="waterfall-template" type="text/template">
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/1.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/2.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/3.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/4.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/5.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/6.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/7.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/8.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/9.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/10.png" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/11.png" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/12.png" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/2.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/6.jpg">
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/1.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/3.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/2.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/4.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/5.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/6.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/7.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/8.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/9.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/10.png" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/11.png" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/12.png" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/2.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/6.jpg">
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/1.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/2.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/3.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/4.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/5.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/6.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/7.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/8.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/9.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/10.png" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/11.png" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/12.png" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/2.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/6.jpg">
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/1.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/3.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/2.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/4.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/5.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/6.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/7.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/8.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/9.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/10.png" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/11.png" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/12.png" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/2.jpg" />
    </a>
  </div>
  <div class="pin">
    <a href="javascript:;">
      <img src="https://tympanus.net/Development/GridLoadingEffects/images/6.jpg">
    </a>
  </div>      
</script>
<div class="container">
  <div id="waterfall"></div>
</div>

Its main advantage (and why I chose this route) is it lazy-loads the images just before they scroll into view, which means you no longer need a button to load more. You can just put all the images in the template <script> tag (read the plugin docs - scroll to Q&A - to see why it recommends adding markup like that).

I've also singled out the BounceInUp animation from animate.css so you don't need to load it.

Note the CSS needs prefixing and, if you want it, the fiddle has the SCSS.

As a general rule I tried to keep it as light as possible, especially in terms of javascript listeners (on scroll I'm only listening to the position of the next item in the list, not to all).

Intended as proof of concept, for pointing in the right direction.


I know what I did is not exactly what you asked (in the sense it doesn't allow you to add more items after first calculation - unfortunately, Bootstrap Waterfall doesn't currently have an update() or addItems() method and I really think they should add one, without recalculating existing pins -
This is no longer true: Mystist, author of bootstrap-waterfall.js has responded to my github request and added the method. I now updated the jsFiddle by adding an add more button to it and it works as expected. Huge thumb up for Mystist from me). Of course, you will probably need to adapt the script to your project's particular case, to how you're bringing the new items in. In the jsFiddle I chose to make a function that generates a new <script> template on the fly and adds pins randomly to it.

Regardless, here are the principles to follow with this layout:

  1. Have an absolute positioning method for placing your items
  2. Hide all items on load. Animate the visible ones into view (staggered) with your desired effect.
  3. Place a lock on all visible ones, to cancel any animation effects on resize (I used fix class).
  4. Place a listener on scroll and check if the next not-visible item has entered view. Animate it when it does and .fix it after end of animation.


回答2:

I don't think you will be able to achieve this effect using the column-count css because of the way it calculates the columns. When you set 'display:none;' on an item the positioning of the items in columns behaves as if it didn't exist. It seems the columns are also taking into account the height of the element.

My suggestion is could you change your approach and rather use your own wrappers for columns and your own javascript/jquery?

Anyway looking at this problem and searching around I found that your question appears to be a duplicate on here and here is the answer (similar conclusion to mine but there's a clever workaround too): CSS column-count elements jumping across columns