AngularJS Masonry for Dynamically changing heights
Asked Answered
G

2

5

I have divs that expand and contract when clicked on. The Masonry library has worked great for initializing the page. The problem I am experiencing is that with the absolute positioning in place from Masonry and the directive below, when divs expand they overlap with the divs below. I need to have the divs below the expanding div move down to deal with the expansion.

My sources are: http://masonry.desandro.com/

and

https://github.com/passy/angular-masonry/blob/master/src/angular-masonry.js

/*!
* angular-masonry <%= pkg.version %>
* Pascal Hartig, weluse GmbH, http://weluse.de/
* License: MIT
*/
(function () {
  'use strict';

angular.module('wu.masonry', [])
.controller('MasonryCtrl', function controller($scope, $element, $timeout) {
  var bricks = {};
  var schedule = [];
  var destroyed = false;
  var self = this;
  var timeout = null;

  this.preserveOrder = false;
  this.loadImages = true;

  this.scheduleMasonryOnce = function scheduleMasonryOnce() {
    var args = arguments;
    var found = schedule.filter(function filterFn(item) {
      return item[0] === args[0];
    }).length > 0;

    if (!found) {
      this.scheduleMasonry.apply(null, arguments);
    }
  };

  // Make sure it's only executed once within a reasonable time-frame in
  // case multiple elements are removed or added at once.
  this.scheduleMasonry = function scheduleMasonry() {
    if (timeout) {
      $timeout.cancel(timeout);
    }

    schedule.push([].slice.call(arguments));

    timeout = $timeout(function runMasonry() {
      if (destroyed) {
        return;
      }
      schedule.forEach(function scheduleForEach(args) {
        $element.masonry.apply($element, args);
      });
      schedule = [];
    }, 30);
  };

  function defaultLoaded($element) {
    $element.addClass('loaded');
  }

  this.appendBrick = function appendBrick(element, id) {
    if (destroyed) {
      return;
    }

    function _append() {
      if (Object.keys(bricks).length === 0) {
        $element.masonry('resize');
      }
      if (bricks[id] === undefined) {
        // Keep track of added elements.
        bricks[id] = true;
        defaultLoaded(element);
        $element.masonry('appended', element, true);
      }
    }

    function _layout() {
      // I wanted to make this dynamic but ran into huuuge memory leaks
      // that I couldn't fix. If you know how to dynamically add a
      // callback so one could say <masonry loaded="callback($element)">
      // please submit a pull request!
      self.scheduleMasonryOnce('layout');
    }

    if (!self.loadImages){
      _append();
      _layout();
    } else if (self.preserveOrder) {
      _append();
      element.imagesLoaded(_layout);
    } else {
      element.imagesLoaded(function imagesLoaded() {
        _append();
        _layout();
      });
    }
  };

  this.removeBrick = function removeBrick(id, element) {
    if (destroyed) {
      return;
    }

    delete bricks[id];
    $element.masonry('remove', element);
    this.scheduleMasonryOnce('layout');
  };

  this.destroy = function destroy() {
    destroyed = true;

    if ($element.data('masonry')) {
      // Gently uninitialize if still present
      $element.masonry('destroy');
    }
    $scope.$emit('masonry.destroyed');

    bricks = [];
  };

  this.reload = function reload() {
    $element.masonry();
    $scope.$emit('masonry.reloaded');
  };


}).directive('masonry', function masonryDirective() {
  return {
    restrict: 'AE',
    controller: 'MasonryCtrl',
    link: {
      pre: function preLink(scope, element, attrs, ctrl) {
        var attrOptions = scope.$eval(attrs.masonry || attrs.masonryOptions);
        var options = angular.extend({
          itemSelector: attrs.itemSelector || '.masonry-brick',
          columnWidth: parseInt(attrs.columnWidth, 10) || attrs.columnWidth
        }, attrOptions || {});
        element.masonry(options);
        var loadImages = scope.$eval(attrs.loadImages);
        ctrl.loadImages = loadImages !== false;
        var preserveOrder = scope.$eval(attrs.preserveOrder);
        ctrl.preserveOrder = (preserveOrder !== false && attrs.preserveOrder !== undefined);

        scope.$emit('masonry.created', element);
        scope.$on('$destroy', ctrl.destroy);
      }
    }
  };
}).directive('masonryBrick', function masonryBrickDirective() {
  return {
    restrict: 'AC',
    require: '^masonry',
    scope: true,
    link: {
      pre: function preLink(scope, element, attrs, ctrl) {
        var id = scope.$id, index;

        ctrl.appendBrick(element, id);
        element.on('$destroy', function () {
          ctrl.removeBrick(id, element);
        });

        scope.$on('masonry.reload', function () {
          ctrl.scheduleMasonryOnce('reloadItems');
          ctrl.scheduleMasonryOnce('layout');
        });

        scope.$watch('$index', function () {
          if (index !== undefined && index !== scope.$index) {
            ctrl.scheduleMasonryOnce('reloadItems');
            ctrl.scheduleMasonryOnce('layout');
          }
          index = scope.$index;
        });
      }
    }
  };
});
}());
Grammatical answered 14/5, 2014 at 1:29 Comment(3)
The solution I believe is to run masonry again after one of these events but I haven't been able to work out how to do that from within the Angular controller that I'm using it in. I've added a bounty to this question.Irradiance
Please make a simple plunker demonstrating the problemDonovan
Or you can just write a simpler directive: plnkr.co/edit/UyRS0clrCwDpSrYgBsXS?p=previewLithe
D
1

Like with many non-Angular libraries, it appears the answer lies in wrapping the library in an Angular directive.

I haven't tried it out but it appears that is what this person did

Dinkins answered 3/6, 2014 at 0:19 Comment(1)
Yes, the OP included that link as one of the 2 libraries that he is using. The problem that we are trying to solve is how do you call the masonry function again from within your controller to tell it to "refresh" because you have made changes to the size of the components that are on the page.Irradiance
F
1

You can use angular's $emit, $broadcast, and $on functionality.

Inside your masonry directive link function:

scope.$on('$resizeMasonry', ctrl.scheduleMasonryOnce('layout'));

Inside your masonryBrick directive link function or any other child element:

scope.$emit('$resizeMasonry');

Use $emit to send an event up the scope tree and $broadcast to send an event down the scope tree.

Forelady answered 6/6, 2014 at 3:43 Comment(2)
Thanks Travis. Imagine you had included both masonry and angular-masonry in a project and in your controller on a page you added an element through say a click event. This element now overlaps another element. How would you now call "masonry refresh/reload" from within the page's controller?Irradiance
@Guy, that would depend where the controller's scope is in relation to MasonryCtrl. If it's a child scope you'll want to $emit to a parent scope. If it's a parent scope use $broadcast. As a last resort $rootScope.$broadcast will trickle down to all scopes.Forelady

© 2022 - 2024 — McMap. All rights reserved.