diff --git a/README.md b/README.md index 3ff999b..a4ecad7 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Use this option to specify your target container element to apply Macy too. All Define the default amount of columns to work with. Use the `breakAt` option to specify breakpoints for this value. #### **trueOrder** -*Default: `true`* +*Default: `false`* Setting this to false will prioritise equalising the height of each column over the order of the items themselves. @@ -59,6 +59,13 @@ When declaring the default margin as an object it requires both and x and y valu *Default: `false`* If set to true, Macy will wait for all images on the page to load before running. Set to `false` by default, it will run every time an image loads. + +#### **useOwnImageLoader** + +*Default: `false`* + +Set this to true if you would prefer to use a different image loaded library. + #### **breakAt** *Default: `None`* @@ -184,6 +191,38 @@ Reinitialises the current macy instance; macyInstance.reInit(); ``` +#### **on** +*Parameters: {String} - Event key, {Function} the function to run when the event occurs* + + +This would console log when all images are loaded. +```javascript +macyInstance.on(macyInstance.constants.EVENT_IMAGE_COMPLETE. function (ctx) { + console.log('all images have loaded'); +}); +``` + +#### **emit** +*Parameters: {String} - Event key* + +Emit an event, although macy does not utilise most of these events, these are more to trigger your own functions. + +--- + +## *Constants* + +Macy now has some constants available to be used with in the events system. This is to make sure the functions are targetting the correct event as the naming may be subject to change +They are all accessible under `macyInstance.constants` + +Currently available constants + +| Key | Value | Description | +|----------------------|--------------------------|-----------------------------------------------------------------------| +| EVENT_INITIALIZED | `'macy.initialized'` | This is the event constant for when macy is initialized/reinitialized | +| EVENT_RECALCULATED | `'macy.recalculated'` | This is the event constant for every time the layout is recalculated | +| EVENT_IMAGE_LOAD | `'macy.images.load'` | This is the event constant for when an image loads | +| EVENT_IMAGE_COMPLETE | `'macy.images.complete'` | This is the event constant for when all images are complete | +| EVENT_RESIZE | `'macy.resize'` | This is the event constant for when the document is resized | --- ## *Notes* diff --git a/bower.json b/bower.json index bcc46b2..14f8cc3 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "macy", - "version": "2.1.1", + "version": "2.2.0", "homepage": "http://macyjs.com/", "author": { "name": "Big Bite Creative", diff --git a/demo/index.html b/demo/index.html index 34a2c18..5c6b8f0 100644 --- a/demo/index.html +++ b/demo/index.html @@ -55,41 +55,41 @@

Macy.js is a lightweight, dependency free, 2kb (gzipped)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -106,7 +106,8 @@

Macy.js is a lightweight, dependency free, 2kb (gzipped) var masonry = new Macy({ container: '#macy-container', trueOrder: false, - waitForImages: true, + waitForImages: false, + useOwnImageLoader: false, debug: true, margin: { x: 10, diff --git a/dist/macy.js b/dist/macy.js index 23982cb..37a22c9 100644 --- a/dist/macy.js +++ b/dist/macy.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Macy=e()}(this,function(){"use strict";function t(t){var e=document.body.clientWidth,n={columns:t.columns},o=void 0;u(t.margin)?n.margin={x:t.margin.x,y:t.margin.y}:n.margin={x:t.margin,y:t.margin};for(var r=Object.keys(t.breakAt),i=r.length-1;i>=0;i--){var s=parseInt(r[i],10);e<=s&&(o=t.breakAt[s],u(o)||(n.columns=o),u(o)&&o.columns&&(n.columns=o.columns),u(o)&&o.margin&&!u(o.margin)&&(n.margin={x:o.margin,y:o.margin}),u(o)&&o.margin&&u(o.margin)&&o.margin.x&&(n.margin.x=o.margin.x),u(o)&&o.margin&&u(o.margin)&&o.margin.y&&(n.margin.y=o.margin.y))}return n}function e(e){return t(e).columns}function n(e){return t(e).margin}function o(t){var o=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],r=e(t),i=n(t).x,s=100/r;return o?1===r?"100%":(i=(r-1)*i/r,"calc("+s+"% - "+i+"px)"):s}function r(t,r){var i=e(t.options),s=0,a=void 0,c=void 0;return 1===++r?0:(c=n(t.options).x,a=(c-(i-1)*c/i)*(r-1),s+=o(t.options,!1)*(r-1),"calc("+s+"% + "+a+"px)")}function i(t){for(var e=0,n=t.container,o=t.rows,r=o.length-1;r>=0;r--)e=o[r]>e?o[r]:e;n.style.height=e+"px"}function s(t,o){var r=arguments.length>2&&void 0!==arguments[2]&&arguments[2],s=!(arguments.length>3&&void 0!==arguments[3])||arguments[3],a=e(t.options),c=n(t.options).y;f(t,a,r),o.forEach(function(e){var n=0,o=parseInt(e.offsetHeight,10);isNaN(o)||(t.rows.forEach(function(e,o){e2&&void 0!==arguments[2]&&arguments[2],s=!(arguments.length>3&&void 0!==arguments[3])||arguments[3],a=e(t.options),c=n(t.options).y;f(t,a,r),o.forEach(function(e){t.lastcol===a&&(t.lastcol=0);var n=p(e,"height");n=parseInt(e.offsetHeight,10),isNaN(n)||(e.style.position="absolute",e.style.top=t.rows[t.lastcol]+"px",e.style.left=""+t.cols[t.lastcol],t.rows[t.lastcol]+=isNaN(n)?0:n+c,t.lastcol+=1,s&&(e.dataset.macyComplete=1))}),s&&(t.tmpRows=null),i(t)}function c(t,e){var n=void 0;return function(){n&&clearTimeout(n),n=setTimeout(t,e)}}Object.getOwnPropertyNames(Array.prototype).forEach(function(t){"length"!==t&&(NodeList.prototype[t]=Array.prototype[t],HTMLCollection.prototype[t]=Array.prototype[t])});var l=function t(e,n){if(!(this instanceof t))return new t(e,n);if(e=e.replace(/^\s*/,"").replace(/\s*$/,""),n)return this.byCss(e,n);for(var o in this.selectors)if(n=o.split("/"),new RegExp(n[1],n[2]).test(e))return this.selectors[o](e);return this.byCss(e)};l.prototype.byCss=function(t,e){return(e||document).querySelectorAll(t)},l.prototype.selectors={},l.prototype.selectors[/^\.[\w\-]+$/]=function(t){return document.getElementsByClassName(t.substring(1))},l.prototype.selectors[/^\w+$/]=function(t){return document.getElementsByTagName(t)},l.prototype.selectors[/^\#[\w\-]+$/]=function(t){return document.getElementById(t.substring(1))};var u=function(t){return t===Object(t)&&"[object Array]"!==Object.prototype.toString.call(t)},p=function(t,e){return window.getComputedStyle(t,null).getPropertyValue(e)},f=function(t,e){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];if(t.lastcol||(t.lastcol=0),n){t.rows=[],t.cols=[],t.lastcol=0;for(var o=e-1;o>=0;o--)t.rows[o]=0,t.cols[o]=r(t,o)}if(t.tmpRows){t.rows=[];for(var o=e-1;o>=0;o--)t.rows[o]=t.tmpRows[o]}else{t.tmpRows=[];for(var o=e-1;o>=0;o--)t.tmpRows[o]=t.rows[o]}},m=function(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=!(arguments.length>2&&void 0!==arguments[2])||arguments[2],r=e?t.container.children:l(':scope > *:not([data-macy-complete="1"])',t.container),i=o(t.options);return r.forEach(function(t){e&&(t.dataset.macyComplete=0),t.style.width=i}),t.options.trueOrder?a(t,r,e,n):s(t,r,e,n)},h=function(t,e){setTimeout(function(){var n=t();e&&e(n)},0)},d=function(t,e,n){t&&h(t),n.req===n.complete&&h(e)},g=function(t,e,n){var o=t.length,r=0;t.forEach(function(t){t.complete&&(r++,d(e,n,{req:o,complete:r})),t.addEventListener("load",function(){r++,d(e,n,{req:o,complete:r})})})},y=Object.assign||function(t){for(var e=1;e0&&void 0!==arguments[0]?arguments[0]:v;if(!(this instanceof t))return new t(e);if(this.options={},y(this.options,v,e),this.container=l(e.container),this.container instanceof l||!this.container)return!!e.debug&&console.error("Error: Container not found");delete this.options.container,this.container.length&&(this.container=this.container[0]),this.container.style.position="relative",this.rows=[];var n=this.recalculate.bind(this,!1,!1),o=this.recalculate.bind(this,!0,!0),r=l("img",this.container);if(this.resizer=c(function(){o()},100),window.addEventListener("resize",this.resizer),e.waitForImages)return g(r,null,o);this.recalculate(!0,!1),g(r,n,o)};return w.init=function(t){return console.warn("Depreciated: Macy.init will be removed in v3.0.0 opt to use Macy directly like so Macy({ /*options here*/ }) "),new w(t)},w.prototype.recalculateOnImageLoad=function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0],e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=l("img",this.container),o=this.recalculate.bind(this,e,!1),r=this.recalculate.bind(this,e,!0);return t?g(n,null,r):(o(),g(n,o,r))},w.prototype.runOnImageLoad=function(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=l("img",this.container);return e?g(n,t,t):g(n,null,t)},w.prototype.recalculate=function(){return m(this,arguments.length>0&&void 0!==arguments[0]&&arguments[0],!(arguments.length>1&&void 0!==arguments[1])||arguments[1])},w.prototype.remove=function(){window.removeEventListener("resize",this.resizer),this.container.children.forEach(function(t){t.removeAttribute("data-macy-complete"),t.removeAttribute("style")}),this.container.removeAttribute("style")},w.prototype.reInit=function(){this.recalculate(!0,!0),window.addEventListener("resize",this.resizer)},w}); +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):t.Macy=n()}(this,function(){"use strict";function t(t,n){var e=void 0;return function(){e&&clearTimeout(e),e=setTimeout(t,n)}}function n(t,n){for(var e=t.length,o=e,r=[];e--;)r.push(n(t[o-e-1]));return r}function e(t,n){E(t,n,arguments.length>2&&void 0!==arguments[2]&&arguments[2])}function o(t){var n=document.body.clientWidth,e={columns:t.columns},o=void 0;_(t.margin)?e.margin={x:t.margin.x,y:t.margin.y}:e.margin={x:t.margin,y:t.margin};for(var r=Object.keys(t.breakAt),i=r.length-1;i>=0;i--){var s=parseInt(r[i],10);n<=s&&(o=t.breakAt[s],_(o)||(e.columns=o),_(o)&&o.columns&&(e.columns=o.columns),_(o)&&o.margin&&!_(o.margin)&&(e.margin={x:o.margin,y:o.margin}),_(o)&&o.margin&&_(o.margin)&&o.margin.x&&(e.margin.x=o.margin.x),_(o)&&o.margin&&_(o.margin)&&o.margin.y&&(e.margin.y=o.margin.y))}return e}function r(t){return o(t).columns}function i(t){return o(t).margin}function s(t){var n=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],e=r(t),o=i(t).x,s=100/e;return n?1===e?"100%":(o=(e-1)*o/e,"calc("+s+"% - "+o+"px)"):s}function a(t,n){var e=r(t.options),o=0,a=void 0,c=void 0;return 1===++n?0:(c=i(t.options).x,a=(c-(e-1)*c/e)*(n-1),o+=s(t.options,!1)*(n-1),"calc("+o+"% + "+a+"px)")}function c(t){var n=0,e=t.container;p(t.rows,function(t){n=t>n?t:n}),e.style.height=n+"px"}function u(t,n){var e=arguments.length>2&&void 0!==arguments[2]&&arguments[2],o=!(arguments.length>3&&void 0!==arguments[3])||arguments[3],s=r(t.options),a=i(t.options).y;b(t,s,e),p(n,function(n){var e=0,r=parseInt(n.offsetHeight,10);isNaN(r)||(t.rows.forEach(function(n,o){n2&&void 0!==arguments[2]&&arguments[2],o=!(arguments.length>3&&void 0!==arguments[3])||arguments[3],s=r(t.options),a=i(t.options).y;b(t,s,e),p(n,function(n){t.lastcol===s&&(t.lastcol=0);var e=L(n,"height");e=parseInt(n.offsetHeight,10),isNaN(e)||(n.style.position="absolute",n.style.top=t.rows[t.lastcol]+"px",n.style.left=""+t.cols[t.lastcol],t.rows[t.lastcol]+=isNaN(e)?0:e+a,t.lastcol+=1,o&&(n.dataset.macyComplete=1))}),o&&(t.tmpRows=null),c(t)}var h=function t(n,e){if(!(this instanceof t))return new t(n,e);if(n=n.replace(/^\s*/,"").replace(/\s*$/,""),e)return this.byCss(n,e);for(var o in this.selectors)if(e=o.split("/"),new RegExp(e[1],e[2]).test(n))return this.selectors[o](n);return this.byCss(n)};h.prototype.byCss=function(t,n){return(n||document).querySelectorAll(t)},h.prototype.selectors={},h.prototype.selectors[/^\.[\w\-]+$/]=function(t){return document.getElementsByClassName(t.substring(1))},h.prototype.selectors[/^\w+$/]=function(t){return document.getElementsByTagName(t)},h.prototype.selectors[/^\#[\w\-]+$/]=function(t){return document.getElementById(t.substring(1))};var p=function(t,n){for(var e=t.length,o=e;e--;)n(t[o-e-1])},f=function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.running=!1,this.events=[],this.add(t)};f.prototype.run=function(){if(!this.running&&this.events.length>0){var t=this.events.shift();this.running=!0,t(),this.running=!1,this.run()}},f.prototype.add=function(){var t=this,n=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return!!n&&(Array.isArray(n)?p(n,function(n){return t.add(n)}):(this.events.push(n),void this.run()))},f.prototype.clear=function(){this.events=[]};var m=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return this.instance=t,this.data=n,this},v=function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];this.events={},this.instance=t};v.prototype.on=function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0],n=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return!(!t||!n)&&(Array.isArray(this.events[t])||(this.events[t]=[]),this.events[t].push(n))},v.prototype.emit=function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0],n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(!t||!Array.isArray(this.events[t]))return!1;var e=new m(this.instance,n);p(this.events[t],function(t){return t(e)})};var d=function(t){return!("naturalHeight"in t&&t.naturalHeight+t.naturalWidth===0)||t.width+t.height!==0},g=function(t,n){var e=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return new Promise(function(t,e){if(n.complete)return d(n)?t(n):e(n);n.addEventListener("load",function(){return d(n)?t(n):e(n)}),n.addEventListener("error",function(){return e(n)})}).then(function(n){e&&t.emit(t.constants.EVENT_IMAGE_LOAD,{img:n})}).catch(function(n){return t.emit(t.constants.EVENT_IMAGE_ERROR,{img:n})})},y=function(t,e){var o=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return n(e,function(n){return g(t,n,o)})},E=function(t,n){var e=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return Promise.all(y(t,n,e)).then(function(){t.emit(t.constants.EVENT_IMAGE_COMPLETE)})},w=function(n){return t(function(){n.emit(n.constants.EVENT_RESIZE),n.queue.add(function(){return n.recalculate(!0,!0)})},100)},A=function(t){if(t.container=h(t.options.container),t.container instanceof h||!t.container)return!!t.options.debug&&console.error("Error: Container not found");delete t.options.container,t.container.length&&(t.container=t.container[0]),t.container.style.position="relative"},I=function(t){t.queue=new f,t.events=new v(t),t.rows=[],t.resizer=w(t)},N=function(t){var n=h("img",t.container);window.addEventListener("resize",t.resizer),t.on(t.constants.EVENT_IMAGE_LOAD,function(){return t.recalculate(!1,!1)}),t.on(t.constants.EVENT_IMAGE_COMPLETE,function(){return t.recalculate(!0,!0)}),t.options.useOwnImageLoader||e(t,n,!t.options.waitForImages),t.emit(t.constants.EVENT_INITIALIZED)},T=function(t){A(t),I(t),N(t)},_=function(t){return t===Object(t)&&"[object Array]"!==Object.prototype.toString.call(t)},L=function(t,n){return window.getComputedStyle(t,null).getPropertyValue(n)},b=function(t,n){var e=arguments.length>2&&void 0!==arguments[2]&&arguments[2];if(t.lastcol||(t.lastcol=0),t.rows.length<1&&(e=!0),e){t.rows=[],t.cols=[],t.lastcol=0;for(var o=n-1;o>=0;o--)t.rows[o]=0,t.cols[o]=a(t,o)}else if(t.tmpRows){t.rows=[];for(var o=n-1;o>=0;o--)t.rows[o]=t.tmpRows[o]}else{t.tmpRows=[];for(var o=n-1;o>=0;o--)t.tmpRows[o]=t.rows[o]}},O=function(t){var n=arguments.length>1&&void 0!==arguments[1]&&arguments[1],e=!(arguments.length>2&&void 0!==arguments[2])||arguments[2],o=n?t.container.children:h(':scope > *:not([data-macy-complete="1"])',t.container),r=s(t.options);return p(o,function(t){n&&(t.dataset.macyComplete=0),t.style.width=r}),t.options.trueOrder?(l(t,o,n,e),t.emit(t.constants.EVENT_RECALCULATED)):(u(t,o,n,e),t.emit(t.constants.EVENT_RECALCULATED))},M=Object.assign||function(t){for(var n=1;n0&&void 0!==arguments[0]?arguments[0]:C;if(!(this instanceof t))return new t(n);this.options={},M(this.options,C,n),T(this)};return V.init=function(t){return console.warn("Depreciated: Macy.init will be removed in v3.0.0 opt to use Macy directly like so Macy({ /*options here*/ }) "),new V(t)},V.prototype.recalculateOnImageLoad=function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return e(this,h("img",this.container),!t)},V.prototype.runOnImageLoad=function(t){var n=arguments.length>1&&void 0!==arguments[1]&&arguments[1],o=h("img",this.container);return this.on(this.constants.EVENT_IMAGE_COMPLETE,t),n&&this.on(this.constants.EVENT_IMAGE_LOAD,t),e(this,o,n)},V.prototype.recalculate=function(){var t=this,n=arguments.length>0&&void 0!==arguments[0]&&arguments[0],e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return e&&this.queue.clear(),this.queue.add(function(){return O(t,n,e)})},V.prototype.remove=function(){window.removeEventListener("resize",this.resizer),p(this.container.children,function(t){t.removeAttribute("data-macy-complete"),t.removeAttribute("style")}),this.container.removeAttribute("style")},V.prototype.reInit=function(){this.recalculate(!0,!0),this.emit(this.constants.EVENT_INITIALIZED),window.addEventListener("resize",this.resizer),this.container.style.position="relative"},V.prototype.on=function(t,n){this.events.on(t,n)},V.prototype.emit=function(t,n){this.events.emit(t,n)},V.constants={EVENT_INITIALIZED:"macy.initialized",EVENT_RECALCULATED:"macy.recalculated",EVENT_IMAGE_LOAD:"macy.image.load",EVENT_IMAGE_ERROR:"macy.image.error",EVENT_IMAGE_COMPLETE:"macy.images.complete",EVENT_RESIZE:"macy.resize"},V.prototype.constants=V.constants,V}); diff --git a/package.json b/package.json index 2966f03..a4e6b1b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "macy", "description": "Macy is a lightweight, dependency free, masonry layout library", - "version": "2.1.1", + "version": "2.2.0", "author": { "name": "Big Bite Creative", "url": "http://bigbitecreative.com", diff --git a/rollup.config.js b/rollup.config.js index 887da52..6f45bbb 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -7,7 +7,7 @@ let buildObj = { entry: 'src/macy.js', format: 'umd', moduleName: 'Macy', - banner: '/* Macy.js - v2.1.1 */', + banner: '/* Macy.js - v2.2.0 */', plugins: [ eslint(), babel(), diff --git a/src/helpers/NodeListFix.js b/src/helpers/NodeListFix.js deleted file mode 100644 index de8befb..0000000 --- a/src/helpers/NodeListFix.js +++ /dev/null @@ -1,8 +0,0 @@ -let methods = Object.getOwnPropertyNames(Array.prototype); - -methods.forEach((methodName) => { - if (methodName !== 'length') { - NodeList.prototype[methodName] = Array.prototype[methodName]; - HTMLCollection.prototype[methodName] = Array.prototype[methodName]; - } -}); diff --git a/src/helpers/async.js b/src/helpers/async.js index cb4d516..d49005e 100644 --- a/src/helpers/async.js +++ b/src/helpers/async.js @@ -3,7 +3,7 @@ * @param {Function} fn - The Function to be ran asynchronously * @param {Function} cb - A optional function that runs after fn */ -let async = (fn, cb) => { +const async = (fn, cb) => { setTimeout(() => { let x = fn(); if (cb) { diff --git a/src/helpers/foreach.js b/src/helpers/foreach.js new file mode 100644 index 0000000..eb0e9dc --- /dev/null +++ b/src/helpers/foreach.js @@ -0,0 +1,21 @@ +const foreach = (iterable, callback) => { + let i = iterable.length, len = i; + + while (i--) { + callback(iterable[len - i - 1]) + } +}; + +export function map (iterable, callback) { + let i = iterable.length, len = i; + let returns = []; + + + while (i--) { + returns.push(callback(iterable[len - i - 1])); + } + + return returns; +} + +export default foreach; diff --git a/src/helpers/imagesLoaded.js b/src/helpers/imagesLoaded.js index f5025c1..0e8f890 100644 --- a/src/helpers/imagesLoaded.js +++ b/src/helpers/imagesLoaded.js @@ -1,49 +1,74 @@ -import async from './async'; +import { map } from './foreach'; /** - * This function calls the during and after function - * @param {Function} during - A function to be ran on each image load - * @param {Function} after - A function to be ran once all images are loaded - * @param {Object} data - An object containing number of complete images and number of loaded + * Checks if an image has loaded by checking the height and width + * @param img {Node} - Image element */ -let imagesComplete = (during, after, data) => { - if (during) { - async(during); - } - - if (data.req === data.complete) { - async(after); - } -} +const imageHasLoaded = (img) => !('naturalHeight' in img && img.naturalHeight + img.naturalWidth === 0) || img.width + img.height !== 0; /** - * Checks through all images and runs the function on complete images - * @param {NodeList} imgs - Image Elements - * @param {Function} during - Function to on each image load - * @param {Function} after - Function to run once all images loaded + * Returns a promise the emits events to macy context if loaded/errors + * @param ctx {Object} - Macy instance + * @param image {Node} - Image Element + * @param emitOnLoad {Boolean} - Should promise fire image load event + * @returns {Promise} */ -let imagesLoaded = (imgs, during, after) => { - let imgLen = imgs.length; - let imgComplete = 0; - - imgs.forEach((img) => { - if (img.complete) { - imgComplete++; - imagesComplete(during, after, { - req: imgLen, - complete: imgComplete - }); +const promise = (ctx, image, emitOnLoad = false) => { + return new Promise((resolve, reject) => { + if (image.complete) { + + if (!imageHasLoaded(image)) { + return reject(image); + } + + return resolve(image); } - img.addEventListener('load', () => { - imgComplete++; - imagesComplete(during, after, { - req: imgLen, - complete: imgComplete - }); + image.addEventListener('load', () => { + if (imageHasLoaded(image)) { + return resolve(image); + } + + return reject(image); }); - }); + + image.addEventListener('error', () => { + return reject(image); + }); + }).then((img) => { + if (emitOnLoad) { + ctx.emit(ctx.constants.EVENT_IMAGE_LOAD, { img }) + } + }).catch((img) => ctx.emit(ctx.constants.EVENT_IMAGE_ERROR, { img })); +}; + +/** + * Returns an array of promises for images loaded + * @param ctx {Object} - Macy instance + * @param images {NodeList} + * @param emitOnLoad {Boolean} - Should promise fire image load event + * @returns {Array} + */ +const getImagePromises = (ctx, images, emitOnLoad = false) => { + return map(images, (image) => promise(ctx, image, emitOnLoad)); }; +/** + * Returns a promise that emits an images complete event when all images are loaded. + * @param ctx {Object} - Macy instance + * @param images {NodeList} + * @param emitOnLoad {Boolean} - Should promise fire image load event + */ +const imageLoaderPromise = (ctx, images, emitOnLoad = false) => Promise.all(getImagePromises(ctx, images, emitOnLoad)).then(() => { + ctx.emit(ctx.constants.EVENT_IMAGE_COMPLETE); +}); -export default imagesLoaded; +/** + * Sets up the image loading promise. + * @param ctx {Object} - Macy instance + * @param imgs {NodeList} + * @param during {Boolean} - Should promise fire image load event + */ +export function imagesLoadedNew (ctx, imgs, during = false) { + imageLoaderPromise(ctx, imgs, during); +} diff --git a/src/helpers/isObject.js b/src/helpers/isObject.js index 71ead4c..9bddb96 100644 --- a/src/helpers/isObject.js +++ b/src/helpers/isObject.js @@ -1,3 +1,7 @@ +/** + * Checks if item is an object + * @param obj {Mixed} + */ const isObject = obj => obj === Object(obj) && Object.prototype.toString.call(obj) !== '[object Array]'; export default isObject; diff --git a/src/helpers/scopeshim.js b/src/helpers/scopeshim.js new file mode 100644 index 0000000..c628332 --- /dev/null +++ b/src/helpers/scopeshim.js @@ -0,0 +1,69 @@ +/** + * Polyfill from https://github.com/jonathantneal/element-qsa-scope + */ +/* eslint-disable */ +const init = () => { + try { + // test for scope support + document.createElement('a').querySelector(':scope *'); + } catch (error) { + (function() { + // scope regex + var scope = /:scope\b/gi; + + // polyfilled .querySelector + var querySelectorWithScope = polyfill(Element.prototype.querySelector); + + Element.prototype.querySelector = function querySelector(selectors) { + return querySelectorWithScope.apply(this, arguments); + }; + + // polyfilled .querySelectorAll + var querySelectorAllWithScope = polyfill(Element.prototype.querySelectorAll); + + Element.prototype.querySelectorAll = function querySelectorAll(selectors) { + return querySelectorAllWithScope.apply(this, arguments); + }; + + function polyfill(originalQuerySelector) { + return function(selectors) { + // whether selectors contain :scope + var hasScope = selectors && scope.test(selectors); + + if (hasScope) { + // element id + var id = this.getAttribute('id'); + + if (!id) { + // update id if falsey or missing + this.id = 'q' + Math.floor(Math.random() * 9000000) + 1000000; + } + + // modify arguments + arguments[0] = selectors.replace(scope, '#' + this.id); + + // result of the original query selector + var elementOrNodeList = originalQuerySelector.apply(this, arguments); + + if (id === null) { + // remove id if missing + this.removeAttribute('id'); + } else if (!id) { + // restore id if falsey + this.id = id; + } + + return elementOrNodeList; + } else { + // result of the original query sleector + return originalQuerySelector.apply(this, arguments); + } + }; + } + })(); + } +}; + +/* eslint-enable */ + +export default init; diff --git a/src/macy.js b/src/macy.js index 6fdf490..edbf6a7 100644 --- a/src/macy.js +++ b/src/macy.js @@ -1,91 +1,58 @@ -import './helpers/NodeListFix'; - import $e from './modules/$e'; +import setup from './modules/setup.js' import calculate from './modules/calculate'; -import imagesLoaded from './helpers/imagesLoaded'; -import { wait } from './helpers/wait'; +import { imagesLoadedNew } from './helpers/imagesLoaded'; +import scopeShim from './helpers/scopeshim'; + +import foreach from './helpers/foreach'; const defaults = { columns: 4, margin: 2, - trueOrder: true, + trueOrder: false, waitForImages: false, - breakAt: {} + useImageLoader: true, + breakAt: {}, + useOwnImageLoader: false, + onInit: false, }; +scopeShim(); + /** * Masonary Factory * @param {Object} opts - The configuration object for macy. */ -let Macy = function (opts = defaults) { +const Macy = function (opts = defaults) { /** - * Create instance of macy if not instatiated with new Macy + * Create instance of macy if not instantiated with new Macy */ if (!(this instanceof Macy)) { return new Macy(opts) } + this.options = {}; Object.assign(this.options, defaults, opts); - // this.options = opts; - this.container = $e(opts.container); - - // Checks if container element exists - if (this.container instanceof $e || !this.container) { - return opts.debug ? console.error('Error: Container not found') : false; - } - - // Remove container selector from the options - delete this.options.container; - - if (this.container.length) { - this.container = this.container[0]; - } - - this.container.style.position = 'relative'; - this.rows = []; - let loadingEvent = this.recalculate.bind(this, false, false); - let finishedLoading = this.recalculate.bind(this, true, true); - - let imgs = $e('img', this.container); - - this.resizer = wait(() => { - finishedLoading(); - }, 100); - - window.addEventListener('resize', this.resizer); - - if (opts.waitForImages) { - return imagesLoaded(imgs, null, finishedLoading); - } + setup(this); +}; - this.recalculate(true, false); - imagesLoaded(imgs, loadingEvent, finishedLoading); -} Macy.init = function (options) { - console.warn('Depreciated: Macy.init will be removed in v3.0.0 opt to use Macy directly like so Macy({ /*options here*/ }) ') + console.warn('Depreciated: Macy.init will be removed in v3.0.0 opt to use Macy directly like so Macy({ /*options here*/ }) '); return new Macy(options); -} +}; /** * Public method for recalculating image positions when the images have loaded. * @param {Boolean} waitUntilFinish - if true it will not recalculate until all images are finished loading * @param {Boolean} refresh - If true it will recalculate the entire container instead of just new elements. */ -Macy.prototype.recalculateOnImageLoad = function (waitUntilFinish = false, refresh = false) { +Macy.prototype.recalculateOnImageLoad = function (waitUntilFinish = false) { let imgs = $e('img', this.container); - let loadingEvent = this.recalculate.bind(this, refresh, false); - let finalEvent = this.recalculate.bind(this, refresh, true); - - if (waitUntilFinish) { - return imagesLoaded(imgs, null, finalEvent); - } - - loadingEvent(); - return imagesLoaded(imgs, loadingEvent, finalEvent); -} + return imagesLoadedNew(this, imgs, !waitUntilFinish); +}; /** * Run a function on every image load or once all images are loaded @@ -94,13 +61,14 @@ Macy.prototype.recalculateOnImageLoad = function (waitUntilFinish = false, refre */ Macy.prototype.runOnImageLoad = function (func, everyLoad = false) { let imgs = $e('img', this.container); + this.on(this.constants.EVENT_IMAGE_COMPLETE, func); if (everyLoad) { - return imagesLoaded(imgs, func, func); + this.on(this.constants.EVENT_IMAGE_LOAD, func); } - return imagesLoaded(imgs, null, func); -} + return imagesLoadedNew(this, imgs, everyLoad); +}; /** * Recalculates masonory positions @@ -108,8 +76,12 @@ Macy.prototype.runOnImageLoad = function (func, everyLoad = false) { * @param {Boolean} loaded - When true it sets the recalculated elements to be marked as complete */ Macy.prototype.recalculate = function (refresh = false, loaded = true) { - return calculate(this, refresh, loaded); -} + if (loaded) { + this.queue.clear(); + } + + return this.queue.add(() => calculate(this, refresh, loaded)); +}; /** * Destroys macy instance @@ -117,21 +89,56 @@ Macy.prototype.recalculate = function (refresh = false, loaded = true) { Macy.prototype.remove = function () { window.removeEventListener('resize', this.resizer); - this.container.children.forEach((child) => { + foreach(this.container.children, (child) => { child.removeAttribute('data-macy-complete'); child.removeAttribute('style'); }); this.container.removeAttribute('style'); -} +}; /** * ReInitializes the macy instance using the already defined options */ Macy.prototype.reInit = function () { this.recalculate(true, true); + this.emit(this.constants.EVENT_INITIALIZED); window.addEventListener('resize', this.resizer); -} + this.container.style.position = 'relative'; +}; + +/** + * Event listener for macy events + * @param key {String} - Event name to listen to + * @param func {Function} - Function to be called when event happens + */ +Macy.prototype.on = function (key, func) { + this.events.on(key, func); +}; + +/** + * Emit an event to macy. + * @param key {String} - Event name to listen to + * @param data {Object} - Extra data to be passed to the event object that is passed to the event listener. + */ +Macy.prototype.emit = function (key, data) { + this.events.emit(key, data); +}; + +/** + * Macy constants + * @type {{EVENT_INITIALIZED: string, EVENT_RECALCULATED: string, EVENT_IMAGE_LOAD: string, EVENT_IMAGE_ERROR: string, EVENT_IMAGE_COMPLETE: string, EVENT_RESIZE: string}} + */ +Macy.constants = { + EVENT_INITIALIZED: 'macy.initialized', + EVENT_RECALCULATED: 'macy.recalculated', + EVENT_IMAGE_LOAD: 'macy.image.load', + EVENT_IMAGE_ERROR: 'macy.image.error', + EVENT_IMAGE_COMPLETE: 'macy.images.complete', + EVENT_RESIZE: 'macy.resize', +}; + +Macy.prototype.constants = Macy.constants; /** * Export Macy diff --git a/src/modules/$e.js b/src/modules/$e.js index a5a2887..2a5e4b4 100644 --- a/src/modules/$e.js +++ b/src/modules/$e.js @@ -4,7 +4,7 @@ * @param {HTMLElement} context - The parent to find the selector in * @return {HTMLElement/HTMLCollection} */ -let $e = function (parameter, context) { +const $e = function (parameter, context) { if (!(this instanceof $e)) { return new $e(parameter, context); } diff --git a/src/modules/calculate.js b/src/modules/calculate.js index fb1a118..223795d 100644 --- a/src/modules/calculate.js +++ b/src/modules/calculate.js @@ -1,18 +1,19 @@ import $e from './$e'; import {getWidths} from './calculations'; import * as cols from './columns'; +import foreach from '../helpers/foreach'; /** - * Calculates the column widths and postitions dependant on options. + * Calculates the column widths and positions dependant on options. * @param {Macy} ctx - Macy instance * @param {Boolean} refresh - Should calculate recalculate all elements - * @param {Boolean} loaded - Should all elements be marked as compelete + * @param {Boolean} loaded - Should all elements be marked as complete */ const calculate = (ctx, refresh = false, loaded = true) => { let children = refresh ? ctx.container.children : $e(':scope > *:not([data-macy-complete="1"])', ctx.container); let eleWidth = getWidths(ctx.options); - children.forEach((child) => { + foreach(children, (child) => { if (refresh) { child.dataset.macyComplete = 0; } @@ -20,10 +21,12 @@ const calculate = (ctx, refresh = false, loaded = true) => { }); if (ctx.options.trueOrder) { - return cols.sort(ctx, children, refresh, loaded); + cols.sort(ctx, children, refresh, loaded); + return ctx.emit(ctx.constants.EVENT_RECALCULATED);//cols.sort(ctx, children, refresh, loaded); } - return cols.shuffle(ctx, children, refresh, loaded) + cols.shuffle(ctx, children, refresh, loaded); + return ctx.emit(ctx.constants.EVENT_RECALCULATED); //cols.shuffle(ctx, children, refresh, loaded); }; export default calculate; diff --git a/src/modules/calculations.js b/src/modules/calculations.js index c29867b..2324e6e 100644 --- a/src/modules/calculations.js +++ b/src/modules/calculations.js @@ -1,4 +1,5 @@ import isObject from '../helpers/isObject'; +import foreach from '../helpers/foreach'; /** * Return the current spacing options based on document size. @@ -139,9 +140,9 @@ export function setContainerHeight (ctx) { let largest = 0; let {container, rows} = ctx; - for (var i = rows.length - 1; i >= 0; i--) { - largest = rows[i] > largest ? rows[i] : largest; - } + foreach(rows, (row) => { + largest = row > largest ? row : largest; + }); container.style.height = `${largest}px`; } diff --git a/src/modules/columns.js b/src/modules/columns.js index 159c941..e342522 100644 --- a/src/modules/columns.js +++ b/src/modules/columns.js @@ -1,5 +1,6 @@ import {getLeftPosition, getCurrentColumns, getCurrentMargin, setContainerHeight} from './calculations'; import prop from '../helpers/prop'; +import foreach from '../helpers/foreach'; /** * Sets up the required data for the shuffle and sort method @@ -8,11 +9,14 @@ import prop from '../helpers/prop'; * @param {Boolean} refresh - Should columns and rows be reset */ const setUpRows = (ctx, cols, refresh = false) => { - if (!ctx.lastcol) { ctx.lastcol = 0; } + if (ctx.rows.length < 1) { + refresh = true; + } + // Reset rows if (refresh) { ctx.rows = []; @@ -23,6 +27,8 @@ const setUpRows = (ctx, cols, refresh = false) => { ctx.rows[i] = 0; ctx.cols[i] = getLeftPosition(ctx, i); } + + return; } if (ctx.tmpRows) { @@ -52,7 +58,7 @@ export function shuffle (ctx, $eles, refresh = false, markasComplete = true) { let margin = getCurrentMargin(ctx.options).y; setUpRows(ctx, cols, refresh); - $eles.forEach((ele) => { + foreach($eles, (ele) => { let smallest = 0; let eleHeight = parseInt(ele.offsetHeight, 10); @@ -93,7 +99,7 @@ export function sort (ctx, $eles, refresh = false, markasComplete = true) { let margin = getCurrentMargin(ctx.options).y; setUpRows(ctx, cols, refresh); - $eles.forEach((ele) => { + foreach($eles, (ele) => { if (ctx.lastcol === cols) { ctx.lastcol = 0; diff --git a/src/modules/events.js b/src/modules/events.js new file mode 100644 index 0000000..587b8e1 --- /dev/null +++ b/src/modules/events.js @@ -0,0 +1,58 @@ +import foreach from '../helpers/foreach'; + +/** + * Event object that will be passed to callbacks + * @param instance {Macy} - Macy instance + * @param data {Object} + * @returns {Event} + * @constructor + */ +const Event = function (instance, data = {}) { + this.instance = instance; + this.data = data; + + return this; +}; + +/** + * Event manager + * @param instance {Function/boolean} + * @constructor + */ +const EventManager = function (instance = false) { + this.events = {}; + this.instance = instance; +}; + +/** + * Event listener for macy events + * @param key {String/boolean} - Event name to listen to + * @param func {Function/boolean} - Function to be called when event happens + */ +EventManager.prototype.on = function (key = false, func = false) { + if (!key || !func) { + return false; + } + + if (!Array.isArray(this.events[key])) { + this.events[key] = []; + } + + return this.events[key].push(func); +}; + +/** + * Emit an event to macy. + * @param key {String/boolean} - Event name to listen to + * @param data {Object} - Extra data to be passed to the event object that is passed to the event listener. + */ +EventManager.prototype.emit = function (key = false, data = {}) { + if (!key || !Array.isArray(this.events[key])) { + return false; + } + + const evt = new Event(this.instance, data); + foreach(this.events[key], (fn) => fn(evt)); +}; + +export default EventManager; diff --git a/src/modules/queue.js b/src/modules/queue.js new file mode 100644 index 0000000..20b5bd5 --- /dev/null +++ b/src/modules/queue.js @@ -0,0 +1,52 @@ +import foreach from '../helpers/foreach'; + +/** + * The queue function allows to for recalculate to run one at a time to avoid conflicts. + * @param events {Mixed} a single function or an array of functions + * @constructor + */ +const Queue = function (events = false) { + this.running = false; + this.events = []; + this.add(events); +}; + +/** + * Run all the events one after the other. + */ +Queue.prototype.run = function () { + if (!this.running && this.events.length > 0) { + const fn = this.events.shift(); + this.running = true; + fn(); + this.running = false; + + this.run(); + } +}; + +/** + * Add a event to the queue and try to run the que + * @param event {Mixed} a single function or an array of functions + */ +Queue.prototype.add = function (event = false) { + if (!event) { + return false; + } + + if (Array.isArray(event)) { + return foreach(event, (evt) => this.add(evt)); + } + + this.events.push(event); + this.run(); +}; + +/** + * Clear all events from the queue + */ +Queue.prototype.clear = function () { + this.events = []; +}; + +export default Queue; diff --git a/src/modules/setup.js b/src/modules/setup.js new file mode 100644 index 0000000..c261ddd --- /dev/null +++ b/src/modules/setup.js @@ -0,0 +1,74 @@ +import { wait } from '../helpers/wait'; +import Queue from './queue'; +import EventsManager from './events'; +import $e from './$e'; +import { imagesLoadedNew } from '../helpers/imagesLoaded'; + +/** + * create a resize event that adds recalculate to the event queue; + * @param ctx {Object} - Macy instance + */ +const createResizeEvent = (ctx) => wait(() => { + ctx.emit(ctx.constants.EVENT_RESIZE); + ctx.queue.add(() => ctx.recalculate(true, true)); +}, 100); + +/** + * Setup the containing element with the correct styles and attaches it to the macy instance + * @param ctx {Object} - Macy instance + */ +const setupContainer = (ctx) => { + // this.options = opts; + ctx.container = $e(ctx.options.container); + + // Checks if container element exists + if (ctx.container instanceof $e || !ctx.container) { + return ctx.options.debug ? console.error('Error: Container not found') : false; + } + + // Remove container selector from the options + delete ctx.options.container; + + if (ctx.container.length) { + ctx.container = ctx.container[0]; + } + + ctx.container.style.position = 'relative'; +}; + +/** + * Generates the basic state objects for macy to run + * @param ctx {Object} - Macy instance + */ +const setupState = (ctx) => { + ctx.queue = new Queue(); + ctx.events = new EventsManager(ctx); + ctx.rows = []; + ctx.resizer = createResizeEvent(ctx); +}; + +/** + * Sets up event listeners for resize and image loading (if required) + * @param ctx {Object} - Macy instance + */ +const setupEventListeners = (ctx) => { + let imgs = $e('img', ctx.container); + + window.addEventListener('resize', ctx.resizer); + ctx.on(ctx.constants.EVENT_IMAGE_LOAD, () => ctx.recalculate(false, false)); + ctx.on(ctx.constants.EVENT_IMAGE_COMPLETE, () => ctx.recalculate(true, true)); + + if (!ctx.options.useOwnImageLoader) { + imagesLoadedNew(ctx, imgs, !ctx.options.waitForImages); + } + + ctx.emit(ctx.constants.EVENT_INITIALIZED); +}; + +const setup = (ctx) => { + setupContainer(ctx); + setupState(ctx); + setupEventListeners(ctx); +}; + +export default setup;