isotope.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. /*!
  2. * Isotope v3.0.4
  3. *
  4. * Licensed GPLv3 for open source use
  5. * or Isotope Commercial License for commercial use
  6. *
  7. * http://isotope.metafizzy.co
  8. * Copyright 2017 Metafizzy
  9. */
  10. ( function( window, factory ) {
  11. // universal module definition
  12. /* jshint strict: false */ /*globals define, module, require */
  13. if ( typeof define == 'function' && define.amd ) {
  14. // AMD
  15. define( [
  16. 'outlayer/outlayer',
  17. 'get-size/get-size',
  18. 'desandro-matches-selector/matches-selector',
  19. 'fizzy-ui-utils/utils',
  20. './item',
  21. './layout-mode',
  22. // include default layout modes
  23. './layout-modes/masonry',
  24. './layout-modes/fit-rows',
  25. './layout-modes/vertical'
  26. ],
  27. function( Outlayer, getSize, matchesSelector, utils, Item, LayoutMode ) {
  28. return factory( window, Outlayer, getSize, matchesSelector, utils, Item, LayoutMode );
  29. });
  30. } else if ( typeof module == 'object' && module.exports ) {
  31. // CommonJS
  32. module.exports = factory(
  33. window,
  34. require('outlayer'),
  35. require('get-size'),
  36. require('desandro-matches-selector'),
  37. require('fizzy-ui-utils'),
  38. require('./item'),
  39. require('./layout-mode'),
  40. // include default layout modes
  41. require('./layout-modes/masonry'),
  42. require('./layout-modes/fit-rows'),
  43. require('./layout-modes/vertical')
  44. );
  45. } else {
  46. // browser global
  47. window.Isotope = factory(
  48. window,
  49. window.Outlayer,
  50. window.getSize,
  51. window.matchesSelector,
  52. window.fizzyUIUtils,
  53. window.Isotope.Item,
  54. window.Isotope.LayoutMode
  55. );
  56. }
  57. }( window, function factory( window, Outlayer, getSize, matchesSelector, utils,
  58. Item, LayoutMode ) {
  59. 'use strict';
  60. // -------------------------- vars -------------------------- //
  61. var jQuery = window.jQuery;
  62. // -------------------------- helpers -------------------------- //
  63. var trim = String.prototype.trim ?
  64. function( str ) {
  65. return str.trim();
  66. } :
  67. function( str ) {
  68. return str.replace( /^\s+|\s+$/g, '' );
  69. };
  70. // -------------------------- isotopeDefinition -------------------------- //
  71. // create an Outlayer layout class
  72. var Isotope = Outlayer.create( 'isotope', {
  73. layoutMode: 'masonry',
  74. isJQueryFiltering: true,
  75. sortAscending: true
  76. });
  77. Isotope.Item = Item;
  78. Isotope.LayoutMode = LayoutMode;
  79. var proto = Isotope.prototype;
  80. proto._create = function() {
  81. this.itemGUID = 0;
  82. // functions that sort items
  83. this._sorters = {};
  84. this._getSorters();
  85. // call super
  86. Outlayer.prototype._create.call( this );
  87. // create layout modes
  88. this.modes = {};
  89. // start filteredItems with all items
  90. this.filteredItems = this.items;
  91. // keep of track of sortBys
  92. this.sortHistory = [ 'original-order' ];
  93. // create from registered layout modes
  94. for ( var name in LayoutMode.modes ) {
  95. this._initLayoutMode( name );
  96. }
  97. };
  98. proto.reloadItems = function() {
  99. // reset item ID counter
  100. this.itemGUID = 0;
  101. // call super
  102. Outlayer.prototype.reloadItems.call( this );
  103. };
  104. proto._itemize = function() {
  105. var items = Outlayer.prototype._itemize.apply( this, arguments );
  106. // assign ID for original-order
  107. for ( var i=0; i < items.length; i++ ) {
  108. var item = items[i];
  109. item.id = this.itemGUID++;
  110. }
  111. this._updateItemsSortData( items );
  112. return items;
  113. };
  114. // -------------------------- layout -------------------------- //
  115. proto._initLayoutMode = function( name ) {
  116. var Mode = LayoutMode.modes[ name ];
  117. // set mode options
  118. // HACK extend initial options, back-fill in default options
  119. var initialOpts = this.options[ name ] || {};
  120. this.options[ name ] = Mode.options ?
  121. utils.extend( Mode.options, initialOpts ) : initialOpts;
  122. // init layout mode instance
  123. this.modes[ name ] = new Mode( this );
  124. };
  125. proto.layout = function() {
  126. // if first time doing layout, do all magic
  127. if ( !this._isLayoutInited && this._getOption('initLayout') ) {
  128. this.arrange();
  129. return;
  130. }
  131. this._layout();
  132. };
  133. // private method to be used in layout() & magic()
  134. proto._layout = function() {
  135. // don't animate first layout
  136. var isInstant = this._getIsInstant();
  137. // layout flow
  138. this._resetLayout();
  139. this._manageStamps();
  140. this.layoutItems( this.filteredItems, isInstant );
  141. // flag for initalized
  142. this._isLayoutInited = true;
  143. };
  144. // filter + sort + layout
  145. proto.arrange = function( opts ) {
  146. // set any options pass
  147. this.option( opts );
  148. this._getIsInstant();
  149. // filter, sort, and layout
  150. // filter
  151. var filtered = this._filter( this.items );
  152. this.filteredItems = filtered.matches;
  153. this._bindArrangeComplete();
  154. if ( this._isInstant ) {
  155. this._noTransition( this._hideReveal, [ filtered ] );
  156. } else {
  157. this._hideReveal( filtered );
  158. }
  159. this._sort();
  160. this._layout();
  161. };
  162. // alias to _init for main plugin method
  163. proto._init = proto.arrange;
  164. proto._hideReveal = function( filtered ) {
  165. this.reveal( filtered.needReveal );
  166. this.hide( filtered.needHide );
  167. };
  168. // HACK
  169. // Don't animate/transition first layout
  170. // Or don't animate/transition other layouts
  171. proto._getIsInstant = function() {
  172. var isLayoutInstant = this._getOption('layoutInstant');
  173. var isInstant = isLayoutInstant !== undefined ? isLayoutInstant :
  174. !this._isLayoutInited;
  175. this._isInstant = isInstant;
  176. return isInstant;
  177. };
  178. // listen for layoutComplete, hideComplete and revealComplete
  179. // to trigger arrangeComplete
  180. proto._bindArrangeComplete = function() {
  181. // listen for 3 events to trigger arrangeComplete
  182. var isLayoutComplete, isHideComplete, isRevealComplete;
  183. var _this = this;
  184. function arrangeParallelCallback() {
  185. if ( isLayoutComplete && isHideComplete && isRevealComplete ) {
  186. _this.dispatchEvent( 'arrangeComplete', null, [ _this.filteredItems ] );
  187. }
  188. }
  189. this.once( 'layoutComplete', function() {
  190. isLayoutComplete = true;
  191. arrangeParallelCallback();
  192. });
  193. this.once( 'hideComplete', function() {
  194. isHideComplete = true;
  195. arrangeParallelCallback();
  196. });
  197. this.once( 'revealComplete', function() {
  198. isRevealComplete = true;
  199. arrangeParallelCallback();
  200. });
  201. };
  202. // -------------------------- filter -------------------------- //
  203. proto._filter = function( items ) {
  204. var filter = this.options.filter;
  205. filter = filter || '*';
  206. var matches = [];
  207. var hiddenMatched = [];
  208. var visibleUnmatched = [];
  209. var test = this._getFilterTest( filter );
  210. // test each item
  211. for ( var i=0; i < items.length; i++ ) {
  212. var item = items[i];
  213. if ( item.isIgnored ) {
  214. continue;
  215. }
  216. // add item to either matched or unmatched group
  217. var isMatched = test( item );
  218. // item.isFilterMatched = isMatched;
  219. // add to matches if its a match
  220. if ( isMatched ) {
  221. matches.push( item );
  222. }
  223. // add to additional group if item needs to be hidden or revealed
  224. if ( isMatched && item.isHidden ) {
  225. hiddenMatched.push( item );
  226. } else if ( !isMatched && !item.isHidden ) {
  227. visibleUnmatched.push( item );
  228. }
  229. }
  230. // return collections of items to be manipulated
  231. return {
  232. matches: matches,
  233. needReveal: hiddenMatched,
  234. needHide: visibleUnmatched
  235. };
  236. };
  237. // get a jQuery, function, or a matchesSelector test given the filter
  238. proto._getFilterTest = function( filter ) {
  239. if ( jQuery && this.options.isJQueryFiltering ) {
  240. // use jQuery
  241. return function( item ) {
  242. return jQuery( item.element ).is( filter );
  243. };
  244. }
  245. if ( typeof filter == 'function' ) {
  246. // use filter as function
  247. return function( item ) {
  248. return filter( item.element );
  249. };
  250. }
  251. // default, use filter as selector string
  252. return function( item ) {
  253. return matchesSelector( item.element, filter );
  254. };
  255. };
  256. // -------------------------- sorting -------------------------- //
  257. /**
  258. * @params {Array} elems
  259. * @public
  260. */
  261. proto.updateSortData = function( elems ) {
  262. // get items
  263. var items;
  264. if ( elems ) {
  265. elems = utils.makeArray( elems );
  266. items = this.getItems( elems );
  267. } else {
  268. // update all items if no elems provided
  269. items = this.items;
  270. }
  271. this._getSorters();
  272. this._updateItemsSortData( items );
  273. };
  274. proto._getSorters = function() {
  275. var getSortData = this.options.getSortData;
  276. for ( var key in getSortData ) {
  277. var sorter = getSortData[ key ];
  278. this._sorters[ key ] = mungeSorter( sorter );
  279. }
  280. };
  281. /**
  282. * @params {Array} items - of Isotope.Items
  283. * @private
  284. */
  285. proto._updateItemsSortData = function( items ) {
  286. // do not update if no items
  287. var len = items && items.length;
  288. for ( var i=0; len && i < len; i++ ) {
  289. var item = items[i];
  290. item.updateSortData();
  291. }
  292. };
  293. // ----- munge sorter ----- //
  294. // encapsulate this, as we just need mungeSorter
  295. // other functions in here are just for munging
  296. var mungeSorter = ( function() {
  297. // add a magic layer to sorters for convienent shorthands
  298. // `.foo-bar` will use the text of .foo-bar querySelector
  299. // `[foo-bar]` will use attribute
  300. // you can also add parser
  301. // `.foo-bar parseInt` will parse that as a number
  302. function mungeSorter( sorter ) {
  303. // if not a string, return function or whatever it is
  304. if ( typeof sorter != 'string' ) {
  305. return sorter;
  306. }
  307. // parse the sorter string
  308. var args = trim( sorter ).split(' ');
  309. var query = args[0];
  310. // check if query looks like [an-attribute]
  311. var attrMatch = query.match( /^\[(.+)\]$/ );
  312. var attr = attrMatch && attrMatch[1];
  313. var getValue = getValueGetter( attr, query );
  314. // use second argument as a parser
  315. var parser = Isotope.sortDataParsers[ args[1] ];
  316. // parse the value, if there was a parser
  317. sorter = parser ? function( elem ) {
  318. return elem && parser( getValue( elem ) );
  319. } :
  320. // otherwise just return value
  321. function( elem ) {
  322. return elem && getValue( elem );
  323. };
  324. return sorter;
  325. }
  326. // get an attribute getter, or get text of the querySelector
  327. function getValueGetter( attr, query ) {
  328. // if query looks like [foo-bar], get attribute
  329. if ( attr ) {
  330. return function getAttribute( elem ) {
  331. return elem.getAttribute( attr );
  332. };
  333. }
  334. // otherwise, assume its a querySelector, and get its text
  335. return function getChildText( elem ) {
  336. var child = elem.querySelector( query );
  337. return child && child.textContent;
  338. };
  339. }
  340. return mungeSorter;
  341. })();
  342. // parsers used in getSortData shortcut strings
  343. Isotope.sortDataParsers = {
  344. 'parseInt': function( val ) {
  345. return parseInt( val, 10 );
  346. },
  347. 'parseFloat': function( val ) {
  348. return parseFloat( val );
  349. }
  350. };
  351. // ----- sort method ----- //
  352. // sort filteredItem order
  353. proto._sort = function() {
  354. if ( !this.options.sortBy ) {
  355. return;
  356. }
  357. // keep track of sortBy History
  358. var sortBys = utils.makeArray( this.options.sortBy );
  359. if ( !this._getIsSameSortBy( sortBys ) ) {
  360. // concat all sortBy and sortHistory, add to front, oldest goes in last
  361. this.sortHistory = sortBys.concat( this.sortHistory );
  362. }
  363. // sort magic
  364. var itemSorter = getItemSorter( this.sortHistory, this.options.sortAscending );
  365. this.filteredItems.sort( itemSorter );
  366. };
  367. // check if sortBys is same as start of sortHistory
  368. proto._getIsSameSortBy = function( sortBys ) {
  369. for ( var i=0; i < sortBys.length; i++ ) {
  370. if ( sortBys[i] != this.sortHistory[i] ) {
  371. return false;
  372. }
  373. }
  374. return true;
  375. };
  376. // returns a function used for sorting
  377. function getItemSorter( sortBys, sortAsc ) {
  378. return function sorter( itemA, itemB ) {
  379. // cycle through all sortKeys
  380. for ( var i = 0; i < sortBys.length; i++ ) {
  381. var sortBy = sortBys[i];
  382. var a = itemA.sortData[ sortBy ];
  383. var b = itemB.sortData[ sortBy ];
  384. if ( a > b || a < b ) {
  385. // if sortAsc is an object, use the value given the sortBy key
  386. var isAscending = sortAsc[ sortBy ] !== undefined ? sortAsc[ sortBy ] : sortAsc;
  387. var direction = isAscending ? 1 : -1;
  388. return ( a > b ? 1 : -1 ) * direction;
  389. }
  390. }
  391. return 0;
  392. };
  393. }
  394. // -------------------------- methods -------------------------- //
  395. // get layout mode
  396. proto._mode = function() {
  397. var layoutMode = this.options.layoutMode;
  398. var mode = this.modes[ layoutMode ];
  399. if ( !mode ) {
  400. // TODO console.error
  401. throw new Error( 'No layout mode: ' + layoutMode );
  402. }
  403. // HACK sync mode's options
  404. // any options set after init for layout mode need to be synced
  405. mode.options = this.options[ layoutMode ];
  406. return mode;
  407. };
  408. proto._resetLayout = function() {
  409. // trigger original reset layout
  410. Outlayer.prototype._resetLayout.call( this );
  411. this._mode()._resetLayout();
  412. };
  413. proto._getItemLayoutPosition = function( item ) {
  414. return this._mode()._getItemLayoutPosition( item );
  415. };
  416. proto._manageStamp = function( stamp ) {
  417. this._mode()._manageStamp( stamp );
  418. };
  419. proto._getContainerSize = function() {
  420. return this._mode()._getContainerSize();
  421. };
  422. proto.needsResizeLayout = function() {
  423. return this._mode().needsResizeLayout();
  424. };
  425. // -------------------------- adding & removing -------------------------- //
  426. // HEADS UP overwrites default Outlayer appended
  427. proto.appended = function( elems ) {
  428. var items = this.addItems( elems );
  429. if ( !items.length ) {
  430. return;
  431. }
  432. // filter, layout, reveal new items
  433. var filteredItems = this._filterRevealAdded( items );
  434. // add to filteredItems
  435. this.filteredItems = this.filteredItems.concat( filteredItems );
  436. };
  437. // HEADS UP overwrites default Outlayer prepended
  438. proto.prepended = function( elems ) {
  439. var items = this._itemize( elems );
  440. if ( !items.length ) {
  441. return;
  442. }
  443. // start new layout
  444. this._resetLayout();
  445. this._manageStamps();
  446. // filter, layout, reveal new items
  447. var filteredItems = this._filterRevealAdded( items );
  448. // layout previous items
  449. this.layoutItems( this.filteredItems );
  450. // add to items and filteredItems
  451. this.filteredItems = filteredItems.concat( this.filteredItems );
  452. this.items = items.concat( this.items );
  453. };
  454. proto._filterRevealAdded = function( items ) {
  455. var filtered = this._filter( items );
  456. this.hide( filtered.needHide );
  457. // reveal all new items
  458. this.reveal( filtered.matches );
  459. // layout new items, no transition
  460. this.layoutItems( filtered.matches, true );
  461. return filtered.matches;
  462. };
  463. /**
  464. * Filter, sort, and layout newly-appended item elements
  465. * @param {Array or NodeList or Element} elems
  466. */
  467. proto.insert = function( elems ) {
  468. var items = this.addItems( elems );
  469. if ( !items.length ) {
  470. return;
  471. }
  472. // append item elements
  473. var i, item;
  474. var len = items.length;
  475. for ( i=0; i < len; i++ ) {
  476. item = items[i];
  477. this.element.appendChild( item.element );
  478. }
  479. // filter new stuff
  480. var filteredInsertItems = this._filter( items ).matches;
  481. // set flag
  482. for ( i=0; i < len; i++ ) {
  483. items[i].isLayoutInstant = true;
  484. }
  485. this.arrange();
  486. // reset flag
  487. for ( i=0; i < len; i++ ) {
  488. delete items[i].isLayoutInstant;
  489. }
  490. this.reveal( filteredInsertItems );
  491. };
  492. var _remove = proto.remove;
  493. proto.remove = function( elems ) {
  494. elems = utils.makeArray( elems );
  495. var removeItems = this.getItems( elems );
  496. // do regular thing
  497. _remove.call( this, elems );
  498. // bail if no items to remove
  499. var len = removeItems && removeItems.length;
  500. // remove elems from filteredItems
  501. for ( var i=0; len && i < len; i++ ) {
  502. var item = removeItems[i];
  503. // remove item from collection
  504. utils.removeFrom( this.filteredItems, item );
  505. }
  506. };
  507. proto.shuffle = function() {
  508. // update random sortData
  509. for ( var i=0; i < this.items.length; i++ ) {
  510. var item = this.items[i];
  511. item.sortData.random = Math.random();
  512. }
  513. this.options.sortBy = 'random';
  514. this._sort();
  515. this._layout();
  516. };
  517. /**
  518. * trigger fn without transition
  519. * kind of hacky to have this in the first place
  520. * @param {Function} fn
  521. * @param {Array} args
  522. * @returns ret
  523. * @private
  524. */
  525. proto._noTransition = function( fn, args ) {
  526. // save transitionDuration before disabling
  527. var transitionDuration = this.options.transitionDuration;
  528. // disable transition
  529. this.options.transitionDuration = 0;
  530. // do it
  531. var returnValue = fn.apply( this, args );
  532. // re-enable transition for reveal
  533. this.options.transitionDuration = transitionDuration;
  534. return returnValue;
  535. };
  536. // ----- helper methods ----- //
  537. /**
  538. * getter method for getting filtered item elements
  539. * @returns {Array} elems - collection of item elements
  540. */
  541. proto.getFilteredItemElements = function() {
  542. return this.filteredItems.map( function( item ) {
  543. return item.element;
  544. });
  545. };
  546. // ----- ----- //
  547. return Isotope;
  548. }));