1/** 2* Filterizr is a jQuery plugin that sorts, shuffles and applies stunning filters over 3* responsive galleries using CSS3 transitions and custom CSS effects. 4* 5* @author Yiotis Kaltsikis 6* @see {@link http://yiotis.net/filterizr} 7* @version 1.2.1 8* @license MIT License 9*/ 10 11(function(global, $) { 12 13 'use strict'; 14 15 //Make sure jQuery exists 16 if (!$) throw new Error('Filterizr requires jQuery to work.'); 17 18 /** 19 * Modified version of Jake Gordon's Bin Packing algorithm used for Filterizr's 'packed' layout 20 * @see {@link https://github.com/jakesgordon/bin-packing} 21 */ 22 var Packer = function(w) { 23 this.init(w); 24 }; 25 26 Packer.prototype = { 27 init: function(w) { 28 this.root = { x: 0, y: 0, w: w }; 29 }, 30 fit: function(blocks) { 31 var n, node, block, len = blocks.length; 32 var h = len > 0 ? blocks[0].h : 0; 33 this.root.h = h; 34 for (n = 0; n < len ; n++) { 35 block = blocks[n]; 36 if ((node = this.findNode(this.root, block.w, block.h))) 37 block.fit = this.splitNode(node, block.w, block.h); 38 else 39 block.fit = this.growDown(block.w, block.h); 40 } 41 }, 42 findNode: function(root, w, h) { 43 if (root.used) 44 return this.findNode(root.right, w, h) || this.findNode(root.down, w, h); 45 else if ((w <= root.w) && (h <= root.h)) 46 return root; 47 else 48 return null; 49 }, 50 splitNode: function(node, w, h) { 51 node.used = true; 52 node.down = { x: node.x, y: node.y + h, w: node.w, h: node.h - h }; 53 node.right = { x: node.x + w, y: node.y, w: node.w - w, h: h }; 54 return node; 55 }, 56 growDown: function(w, h) { 57 var node; 58 this.root = { 59 used: true, 60 x: 0, 61 y: 0, 62 w: this.root.w, 63 h: this.root.h + h, 64 down: { x: 0, y: this.root.h, w: this.root.w, h: h }, 65 right: this.root 66 }; 67 if ((node = this.findNode(this.root, w, h))) 68 return this.splitNode(node, w, h); 69 else 70 return null; 71 } 72 }; 73 74 /** 75 * Only Filterizr method extracted on jQuery.fn. 76 * Instantiates a new Filterizr or calls any of the public Filterizr methods. 77 * @return {jQuery} this - to facilitate jQuery method chaining. 78 */ 79 $.fn.filterizr = function() { 80 var self = this, args = arguments; 81 //Create the Filterizr obj as an internal private property on the current object 82 //to serve as a private namespace 83 if (!self._fltr) { 84 self._fltr = $.fn.filterizr.prototype.init(self.selector, (typeof args[0] === 'object' ? args[0] : undefined)); 85 } 86 //Call all public Filterizr methods through the private Filterizr object 87 if (typeof args[0] === 'string') { 88 if (args[0].lastIndexOf('_') > -1) throw new Error('Filterizr error: You cannot call private methods'); 89 if (typeof self._fltr[args[0]] === 'function') { 90 self._fltr[args[0]](args[1], args[2]); 91 } 92 else throw new Error('Filterizr error: There is no such function'); 93 } 94 return self; 95 }; 96 97 /** 98 * Filterizr prototype 99 */ 100 $.fn.filterizr.prototype = { 101 102 /** 103 * Filterizr constructor. 104 * @param {string} [selector] - the selector of your container. 105 * @param {Object} [options] - user options to override defaults. 106 * @constructor 107 */ 108 init: function(selector, options) { 109 var self = $(selector).extend($.fn.filterizr.prototype); 110 //Default options 111 self.options = { 112 animationDuration: 0.5, 113 callbacks: { 114 onFilteringStart: function() { }, 115 onFilteringEnd : function() { } 116 }, 117 delay: 0, 118 delayMode: 'progressive', 119 easing: 'ease-out', 120 filter: 'all', 121 filterOutCss: { 122 'opacity': 0, 123 'transform': 'scale(0.5)' 124 }, 125 filterInCss: { 126 'opacity': 1, 127 'transform': 'scale(1)' 128 }, 129 layout: 'sameSize', 130 selector: (typeof selector === 'string') ? selector : '.filtr-container', 131 setupControls: true 132 }; 133 //No arguments constructor 134 if (arguments.length === 0) { 135 selector = self.options.selector; 136 options = self.options; 137 } 138 //One argument constructor (only options) 139 if (arguments.length === 1 && typeof arguments[0] === 'object') options = arguments[0]; 140 //If options argument provided, override defaults 141 if (options) { 142 self.setOptions(options); 143 } 144 //Private properties 145 self.css({ //Cache reference to container as jQuery obj and init its CSS 146 'padding' : 0, 147 'position': 'relative' 148 }); 149 self._lastCategory = 0; //Highest value in data-category of .filtr-items 150 self._isAnimating = false;
151 //.filtr-item collections 152 self._mainArray = self._getFiltrItems(); 153 self._subArrays = self._makeSubarrays(); 154 self._activeArray = self._getCollectionByFilter(self.options.filter); 155 //Used for multiple category filtering 156 self._toggledCategories = { }; 157 //Used for search feature 158 self._typedText = $('input[data-search]').val() || ''; 159 //Generate unique ID for resize events 160 self._uID = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 161 var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 162 return v.toString(16); 163 }); 164 //Set up Filterizr events 165 self._setupEvents(); 166 //Set up standard Filterizr controls (for multiple Filterizrs in your scene, set to false) 167 if (self.options.setupControls) self._setupControls(); 168 //Start Filterizr! 169 self.filter(self.options.filter); 170 return self; 171 }, 172 173 /*********************************** 174 * Public methods 175 ***********************************/ 176 /** 177 * Filters the items 178 * @param {number} targetFilter - the applied filter towards which items transition 179 */ 180 filter: function(targetFilter) { 181 var self = this, 182 target = self._getCollectionByFilter(targetFilter); 183 184 self.options.filter = targetFilter; 185 self.trigger('filteringStart'); 186 //Filter items 187 self._handleFiltering(target); 188 //Apply search filter on top if activated 189 if (self._isSearchActivated()) self.search(self._typedText); 190 }, 191 192 /** 193 * Toggles filters on/off and renders the new collection 194 * @param {number} toggledFilter - the filter to toggle 195 */ 196 toggleFilter: function(toggledFilter) { 197 var self = this, 198 target = [], i = 0; 199 200 self.trigger('filteringStart'); 201 //Toggle the toggledFilter in the active categories 202 if (!self._toggledCategories[toggledFilter]) 203 self._toggledCategories[toggledFilter] = true; 204 else 205 delete self._toggledCategories[toggledFilter]; 206 207 //If a filter is toggled on then display only items belonging to that category 208 if (self._multifilterModeOn()) { 209 target = self._makeMultifilterArray(); 210 //Filter items 211 self._handleFiltering(target); 212 //Apply search filter on top if activated 213 if (self._isSearchActivated()) self.search(self._typedText); 214 } 215 //If all filters toggled off then display unfiltered gallery 216 else { 217 //Filter items 218 self.filter('all'); 219 //Apply search filter on top if activated 220 if (self._isSearchActivated()) self.search(self._typedText); 221 } 222 }, 223 224 /** 225 * Searches the contents of .filtr-item elements, filters them and renders the results 226 * @param {string} text to search in contents of .filtr-item elements 227 */ 228 search: function(text) { 229 var self = this, 230 //get active category 231 array = self._multifilterModeOn() ? 232 self._makeMultifilterArray() : 233 self._getCollectionByFilter(self.options.filter), 234 target = [], i = 0; 235 236 if (self._isSearchActivated()) { 237 for (i = 0; i < array.length; i++) { 238 //Check if the text typed in the textbox is contained in the .filtr-item element 239 //and add it to the target array 240 var containsText = array[i].text().toLowerCase().indexOf(text.toLowerCase()) > -1; 241 if (containsText) { 242 target.push(array[i]); 243 } 244 } 245 } 246 //Show the results 247 if (target.length > 0) { 248 self._handleFiltering(target); 249 } 250 //If there are no results 251 else { 252 //and search is activated, show blank 253 if (self._isSearchActivated()) { 254 for (i = 0; i < self._activeArray.length; i++) { 255 self._activeArray[i]._filterOut(); 256 } 257 } 258 //if search is not activated display gallery with last applied filter 259 else { 260 self._handleFiltering(array); 261 } 262 } 263 }, 264 265 /** 266 * Shuffles the active collection and rearranges it on screen 267 */ 268 shuffle: function() { 269 var self = this; 270 271 self._mainArray = self._fisherYatesShuffle(self._mainArray); 272 self._subArrays = self._makeSubarrays(); 273 274 var target = self._multifilterModeOn() ? 275 self._makeMultifilterArray() :
276 self._getCollectionByFilter(self.options.filter); 277 278 if (self._isSearchActivated()) 279 self.search(self._typedText); 280 else 281 self._placeItems(target); 282 }, 283 284 /** 285 * Sorts the active collection and rearranges it on screen. 286 * @param {string} [attr] - attr based on which to sort (default: 'domIndex' / possible: 'domIndex', 'sortData', 'w', 'h'). 287 * @param {string} [sortOrder] - asc/desc (default: 'asc'). 288 */ 289 sort: function(attr, sortOrder) { 290 var self = this; 291 //Set defaults 292 attr = attr || 'domIndex'; 293 sortOrder = sortOrder || 'asc'; 294 295 //Register sort attr on all elements if it is a user-defined data-attribute 296 var isUserAttr = attr !== 'domIndex' && attr !== 'sortData' && attr !== 'w' && attr!== 'h'; 297 if (isUserAttr) { 298 for (var i = 0; i < self._mainArray.length; i++) { 299 self._mainArray[i][attr] = self._mainArray[i].data(attr); 300 } 301 } 302 //Sort items 303 self._mainArray.sort(self._comparator(attr, sortOrder)); 304 self._subArrays = self._makeSubarrays(); 305 //Place sorted collection to new positions 306 var target = self._multifilterModeOn() ? 307 self._makeMultifilterArray() : 308 self._getCollectionByFilter(self.options.filter); 309 310 if (self._isSearchActivated()) 311 self.search(self._typedText); 312 else 313 self._placeItems(target); 314 }, 315 316 /** 317 * Overrides the default options with the user-provided ones. 318 * @param {object} options - the user-provided options to override defaults. 319 */ 320 setOptions: function(options) { 321 var self = this, i = 0; 322 //Override options 323 for (var prop in options) { 324 self.options[prop] = options[prop]; 325 } 326 //If the user tries to override animationDuration, easing, delay or delayMode 327 if (self._mainArray && (options.animationDuration || options.delay || options.easing || options.delayMode)) { 328 for (i = 0; i < self._mainArray.length; i++) { 329 self._mainArray[i].css('transition', 'all ' + self.options.animationDuration + 's ' + self.options.easing + ' ' + self._mainArray[i]._calcDelay() + 'ms'); 330 } 331 } 332 //If the user has not defined a transform property in their CSS, add it 333 //while overriding, including translates for movement 334 if (!self.options.filterInCss.transform) self.options.filterInCss.transform = 'translate3d(0,0,0)'; 335 if (!self.options.filterOutCss.transform) self.options.filterOutCss.transform = 'translate3d(0,0,0)'; 336 }, 337 338 /*********************************** 339 * Private & helper methods 340 ***********************************/ 341 /** 342 * Finds all .filtr-item elements in the .filtr-container and sets them up before returning them in an array. 343 * @return {Object[]} all .filtr-item elements contained in Filterizr's container. 344 * @private 345 */ 346 _getFiltrItems: function() { 347 var self = this, 348 filtrItems = $(self.find('.filtr-item')), 349 itemsArray = []; 350 351 $.each(filtrItems, function(i, e) { 352 //Set item up as Filtr object & push to array 353 var item = $(e).extend(FiltrItemProto)._init(i, self); 354 itemsArray.push(item); 355 }); 356 return itemsArray; 357 }, 358 359 /** 360 * Divide .filtr-item elements into sub-arrays based on data-category attribute. 361 * @return {Object[Object[self._lastCategory]]} Array of arrays including items grouped by data-category. 362 * @private 363 */ 364 _makeSubarrays: function() { 365 var self = this, 366 subArrays = []; 367 368 for (var i = 0; i < self._lastCategory; i++) subArrays.push([]); 369 370 //Populate the sub-arrays 371 for (i = 0; i < self._mainArray.length; i++) { 372 //Multiple categories scenario 373 if (typeof self._mainArray[i]._category === 'object') { 374 for (var p in self._mainArray[i]._category) 375 subArrays[self._mainArray[i]._category[p] - 1].push(self._mainArray[i]); 376 } 377 //Single category 378 else subArrays[self._mainArray[i]._category - 1].push(self._mainArray[i]); 379 } 380 return subArrays; 381 }, 382 383 /** 384 * Make a .filtr-item array based on the activated filters 385 * @return {Object[]} array consisting of the .filtr-item elements belonging to active filters 386 * @private 387 */ 388 _makeMultifilterArray: function() { 389 var self = this, 390 target = [], addedMap = {}; 391 392 for (var i = 0; i < self._mainArray.length; i++) { 393 //If the item belongs to multiple categories 394 var item = self._mainArray[i], 395 belongsToCategory = false, 396 isUnique = item.domIndex in addedMap === false; 397 //Check if item belongs to categories whose filters are toggled on 398 if (Array.isArray(item._category)) { 399 for (var x = 0; x < item._category.length; x++) { 400 if (item._category[x] in self._toggledCategories) { 401 belongsToCategory = true; 402 break; 403 } 404 } 405 } 406 else { 407 if (item._category in self._toggledCategories) belongsToCategory = true; 408 } 409 //If the item is not already visible and belongs to a category 410 //of the toggled on filters push it to target collection 411 if (isUnique && belongsToCategory) { 412 addedMap[item.domIndex] = true; 413 target.push(item); 414 } 415 } 416 return target; 417 }, 418 419 /** 420 * Detect and set up preset controls. 421 * @private 422 */ 423 _setupControls: function() { 424 var self = this; 425 //Filter controls 426 $('*[data-filter]').click(function() { 427 var targetFilter = $(this).data('filter'); 428 //Exit case 429 if (self.options.filter === targetFilter) return; 430 self.filter(targetFilter); 431 }); 432 //Multiple filter controls 433 $('*[data-multifilter]').click(function() { 434 var targetFilter = $(this).data('multifilter'); 435 if (targetFilter === 'all') { 436 self._toggledCategories = { }; 437 self.filter('all'); 438 } 439 else { 440 self.toggleFilter(targetFilter); 441 } 442 }); 443 //Shuffle control 444 $('*[data-shuffle]').click(function() { 445 self.shuffle(); 446 }); 447 //Sort controls 448 $('*[data-sortAsc]').click(function() { 449 var sortAttr = $('*[data-sortOrder]').val(); 450 self.sort(sortAttr, 'asc'); 451 }); 452 $('*[data-sortDesc]').click(function() { 453 var sortAttr = $('*[data-sortOrder]').val(); 454 self.sort(sortAttr, 'desc'); 455 }); 456 //Search control 457 $('input[data-search]').keyup(function() { 458 self._typedText = $(this).val(); 459 self._delayEvent(function() { 460 self.search(self._typedText); 461 }, 250, self._uID); 462 }); 463 }, 464 465 /** 466 * Set up window and Filterizr events. 467 * @private 468 */ 469 _setupEvents: function() { 470 var self = this; 471 //Window resize event 472 $(global).resize(function() { 473 self._delayEvent(function() { 474 self.trigger('resizeFiltrContainer'); 475 }, 250, self._uID); 476 }); 477 //Filterizr events 478 self 479 //Container resize event 480 .on('resizeFiltrContainer', function() { 481 if (self._multifilterModeOn()) 482 self.toggleFilter(); 483 else 484 self.filter(self.options.filter); 485 }) 486 //onFilteringStart event 487 .on('filteringStart', function() { 488 self.options.callbacks.onFilteringStart(); 489 }) 490 //onFilteringEnd event 491 .on('filteringEnd', function() { 492 self.options.callbacks.onFilteringEnd(); 493 }); 494 }, 495 496 /** 497 * Calculates the final positions of items being filtered in, updates the height of .filtr-container. 498 * @return {Object[]} array of future item positions. 499 * @private 500 */ 501 _calcItemPositions: function() { 502 var self = this, 503 array = self._activeArray, 504 //Container data 505 containerHeight = 0, 506 cols = Math.round(self.width() / self.find('.filtr-item').outerWidth()), 507 rows = 0, 508 //Item data 509 itemWidth = array[0].outerWidth(), 510 itemHeight = 0, 511 //Position calculation vars 512 left = 0, top = 0, 513 //Loop vars 514 i = 0, x = 0, 515 //Array of positions to return 516 posArray = []; 517 518 //Layout for items of varying sizes 519 if (self.options.layout === 'packed') { 520 //Cache current item width/height 521 $.each(self._activeArray, function(i, e) { 522 e._updateDimensions(); 523 }); 524 //Instantiate new Packer, set up grid 525 var packer = new Packer(self.outerWidth()); 526 packer.fit(self._activeArray); 527 for (i = 0; i < array.length; i++) { 528 posArray.push({ 529 left: array[i].fit.x, 530 top: array[i].fit.y 531 }); 532 } 533 containerHeight = packer.root.h; 534 } 535 //Horizontal layout 536 if (self.options.layout === 'horizontal') { 537 rows = 1; 538 for (i = 1; i <= array.length; i++) { 539 itemWidth = array[i - 1].outerWidth(); 540 itemHeight = array[i - 1].outerHeight(); 541 posArray.push({ 542 left: left, 543 top: top 544 }); 545 left += itemWidth; 546 if (containerHeight < itemHeight) containerHeight = itemHeight; 547 } 548 } 549 //Vertical layout 550 else if (self.options.layout === 'vertical') { 551 for (i = 1; i <= array.length; i++) { 552 itemHeight = array[i - 1].outerHeight(); 553 posArray.push({ 554 left: left, 555 top: top 556 }); 557 top += itemHeight; 558 } 559 containerHeight = top; 560 } 561 //Layout of items for same height and varying width 562 else if (self.options.layout === 'sameHeight') { 563 rows = 1; 564 var rowWidth = self.outerWidth(); 565 for (i = 1; i <= array.length; i++) { 566 itemWidth = array[i - 1].width(); 567 var itemOuterWidth = array[i - 1].outerWidth(), 568 nextItemWidth = 0; 569 if (array[i]) nextItemWidth = array[i].width(); 570 posArray.push({ 571 left: left, 572 top: top 573 }); 574 x = left + itemWidth + nextItemWidth; 575 if (x > rowWidth) { 576 x = 0; 577 left = 0; 578 top += array[0].outerHeight(); 579 rows++; 580 } 581 else left += itemOuterWidth; 582 } 583 containerHeight = rows * array[0].outerHeight(); 584 } 585 //Layout for items of same width and varying height 586 else if (self.options.layout === 'sameWidth') { 587 //Get positions 588 for (i = 1; i <= array.length; i++) { 589 posArray.push({ 590 left: left, 591 top: top 592 }); 593 if (i % cols === 0) rows++; 594 left += itemWidth; 595 top = 0; 596 if (rows > 0) { 597 x = rows; 598 while (x > 0) { 599 top += array[i - (cols * x)].outerHeight(); 600 x--; 601 } 602 } 603 if (i % cols === 0) left = 0; 604 } 605 //Calculate containerHeight 606 for (i = 0; i < cols; i++) { 607 var columnHeight = 0, index = i; 608 while(array[index]) { 609 columnHeight += array[index].outerHeight(); 610 index += cols; 611 } 612 if (columnHeight > containerHeight) { 613 containerHeight = columnHeight; 614 columnHeight = 0; 615 } 616 else 617 columnHeight = 0; 618 } 619 } 620 //Layout for items of exactly same size 621 else if (self.options.layout === 'sameSize') { 622 for (i = 1; i <= array.length; i++) { 623 //Push first point at (left: 0, top: 0) 624 posArray.push({ 625 left: left, 626 top: top 627 }); 628 //Set left and top properties for next point before next iteration 629 left += itemWidth; 630 //On row change calc new top and reset left 631 if (i % cols === 0) { 632 top += array[0].outerHeight(); 633 left = 0; 634 } 635 }
636 rows = Math.ceil(array.length / cols); 637 containerHeight = rows * array[0].outerHeight(); 638 } 639 //Update the height of .filtr-container based on new positions 640 self.css('height', containerHeight); 641 return posArray; 642 }, 643 644 /** 645 * Handles filtering in/out and reposition items when transition between categories 646 * @param {Object[]} the target array towards which to filter 647 * @private 648 */ 649 _handleFiltering: function(target) { 650 var self = this, 651 toFilterOut = self._getArrayOfUniqueItems(self._activeArray, target); 652 //Minimize all .filtr-item elements that are not the same between categories 653 for (var i = 0; i < toFilterOut.length; i++) { 654 toFilterOut[i]._filterOut(); 655 } 656 self._activeArray = target; 657 //Reposition same items and filter in new 658 self._placeItems(target); 659 }, 660 661 /** 662 * Determines if the user is using data-multifilter controls or simple data-filter controls 663 * @return {boolean} indicating whether multiple filter mode is on 664 * @private 665 */ 666 _multifilterModeOn: function() { 667 var self = this; 668 return Object.keys(self._toggledCategories).length > 0; 669 }, 670 671 /** 672 * Determines if the user has something typed in the search box 673 * @return {boolean} indicating whether the user has searched 674 * @private 675 */ 676 _isSearchActivated: function() { 677 var self = this; 678 return self._typedText.length > 0; 679 }, 680 681 /** 682 * Places .filtr-item elements on the grid positions 683 * @param {Object[]} arr - an array consisting of .filtr-item elements 684 * @private 685 */ 686 _placeItems: function(arr) { 687 var self = this; 688 //Tag gallery state as animating 689 self._isAnimating = true; 690 //Recalculate positions and filter in items 691 self._itemPositions = self._calcItemPositions(); 692 for (var i = 0; i < arr.length; i++) { 693 arr[i]._filterIn(self._itemPositions[i]); 694 } 695 }, 696 697 /** 698 * Returns item collection based on a certain filter 699 * @param {string|number} filter of category to return 700 * @return {Object[]} one of the item collections based on filter 701 * @private 702 */ 703 _getCollectionByFilter: function(filter) { 704 var self = this; 705 return filter === 'all' ? self._mainArray : self._subArrays[filter - 1]; 706 }, 707 708 /** 709 * Used to make deep copies of the predefined filters 710 * in the options for the filterIn/Out methods of items. 711 * @see _filterIn and _filterOut methods in FiltrItemProto. 712 * @param {Object} obj - is the source object to make a deep copy from. 713 * @return {Object} Deep copy of the obj param. 714 * @private 715 */ 716 _makeDeepCopy: function(obj) { 717 var r = {}; 718 for (var p in obj) r[p] = obj[p]; 719 return r; 720 }, 721 722 /** 723 * Comparator factory used to produce camparers for sorting. 724 * @see Filterizr.prototype.sort. 725 * @param {string} prop - property based on which to sort ('domIndex', 'sortData', 'w', 'h') 726 * @param {string} sortOrder - 'asc'/'desc' 727 * @return {function} comparer which takes arguments 728 * @private 729 */ 730 _comparator: function(prop, sortOrder) { 731 return function(a, b) { 732 if (sortOrder === 'asc') { 733 if (a[prop] < b[prop]) 734 return -1; 735 else if (a[prop] > b[prop]) 736 return 1; 737 else 738 return 0; 739 } 740 else if (sortOrder === 'desc') { 741 if (b[prop] < a[prop]) 742 return -1; 743 else if (b[prop] > a[prop]) 744 return 1; 745 else 746 return 0; 747 } 748 }; 749 }, 750 751 /** 752 * Modified version of Jeffery To's array intersection method 753 * @see {@link http://www.falsepositives.com/index.php/2009/12/01/javascript-function-to-get-the-intersect-of-2-arrays/} 754 * @return {Object[]} a disjoint array containing the elements of the first array not found in the second 755 * @private 756 */ 757 _getArrayOfUniqueItems: function(arr1, arr2) { 758 var r = [], o = {}, l = arr2.length, i, v; 759 for (i = 0; i < l; i++) { 760 o[arr2[i].domIndex] = true; 761 } 762 l = arr1.length; 763 for (i = 0; i < l; i++) { 764 v = arr1[i]; 765 if (!(v.domIndex in o)) { 766 r.push(v); 767 } 768 } 769 return r; 770 }, 771 772 /** 773 * Brahn's take on CMS's solution to calling the window.resize event at set 774 * intervals in multiple places in the code using a Java-like UUID with a regexp 775 * @see {@link http://stackoverflow.com/questions/2854407/javascript-jquery-window-resize-how-to-fire-after-the-resize-is-completed} 776 * @return {function} which calls the callback or just clears the timer 777 * @private 778 */ 779 _delayEvent: (function () { 780 var self = this, timers = {}; 781 return function (callback, ms, uniqueId) { 782 if (uniqueId === null) { 783 throw Error("UniqueID needed"); 784 } 785 if (timers[uniqueId]) { 786 clearTimeout (timers[uniqueId]); 787 } 788 timers[uniqueId] = setTimeout(callback, ms); 789 }; 790 })(), 791 792 /** 793 * Fisher-Yates array shuffling algorithm implemented for JavaScript. 794 * @return {Object[]} shuffled array.
795 * @private 796 */ 797 _fisherYatesShuffle: function shuffle(array) { 798 var m = array.length, t, i; 799 // While there remain elements to shuffle⦠800 while (m) { 801 // Pick a remaining element⦠802 i = Math.floor(Math.random() * m--); 803 // And swap it with the current element. 804 t = array[m]; 805 array[m] = array[i]; 806 array[i] = t; 807 } 808 return array; 809 } 810 }; 811 812 /** 813 * FiltrItem Prototype 814 */ 815 var FiltrItemProto = { 816 817 /** 818 * Transforms a jQuery item with .filtr-item class into a FiltrItem. 819 * @param {number} index - initial item order based on position in DOM. 820 * @param {Filterizr} parent - reference to Filterizr container containing this Filtr Item. 821 * @return {jQuery} this - to facilitate method chaining. 822 * @constructor 823 */ 824 _init: function(index, parent) { 825 var self = this, delay = 0; 826 //Private item properties 827 self._parent = parent; //Ref to parent Filterizr object 828 self._category = self._getCategory(); //data-category values 829 self._lastPos = {}; //Used for animations 830 //Public properties - used for sorting 831 self.domIndex = index; 832 self.sortData = self.data('sort'); 833 //Item Dimensions used for Bin Packing algorithm (packed layout) and sorting. 834 self.w = 0; 835 self.h = 0; 836 //self states 837 self._isFilteredOut = true; 838 self._filteringOut = false; 839 self._filteringIn = false; 840 //Determine delay & set initial item styles 841 self.css(parent.options.filterOutCss) 842 .css({ 843 '-webkit-backface-visibility': 'hidden', 844 'perspective': '1000px', 845 '-webkit-perspective': '1000px', 846 '-webkit-transform-style': 'preserve-3d', 847 'position': 'absolute', 848 'transition': 'all ' + parent.options.animationDuration + 's ' + parent.options.easing + ' ' + self._calcDelay() + 'ms' 849 }); 850 //Events 851 self.on("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd", function(){ 852 self._onTransitionEnd(); 853 }); 854 return self; 855 }, 856 857 /** 858 * Updates the dimensions (width/height) of the item. 859 * @private 860 */ 861 _updateDimensions: function() { 862 var self = this; 863 self.w = self.outerWidth(); 864 self.h = self.outerHeight(); 865 }, 866 867 /** 868 * Calculates and returns the value of delay to apply to transition-delay in ms, depending on delayMode 869 * @return {number} value to apply to transition-delay in ms. 870 * @private 871 */ 872 _calcDelay: function() { 873 var self = this, r = 0; 874 if (self._parent.options.delayMode === 'progressive') 875 r = self._parent.options.delay * self.domIndex; 876 else 877 if (self.domIndex % 2 === 0) r = self._parent.options.delay; 878 return r; 879 }, 880 881 /** 882 * Determines which categories this items belongs to and updates the _lastCategory prop of Filterizr. 883 * @throws {InvalidArgumentException} data-category of .filtr-item elements must be integer or string of integers delimited by ', ' 884 * @return {Object[]|number} the categories this item belongs to. 885 * @private 886 */ 887 _getCategory: function() { 888 var self = this, 889 ret = self.data('category'); 890 //If more than one category provided 891 if (typeof ret === 'string') { 892 ret = ret.split(', '); 893 for (var n in ret) { 894 //Error checking: make sure data-category has an integer as its value 895 if (isNaN(parseInt(ret[n]))) { 896 throw new Error('Filterizr: the value of data-category must be a number, starting from value 1 and increasing.'); 897 } 898 if (ret[n] > self._parent._lastCategory) { 899 self._parent._lastCategory = ret[n]; 900 } 901 } 902 } 903 else { 904 if (ret > self._parent._lastCategory) 905 self._parent._lastCategory = ret; 906 } 907 return ret; 908 }, 909 910 /** 911 * Handles the transitionEnd event. 912 * @private 913 */ 914 _onTransitionEnd: function() { 915 var self = this; 916 //finished filtering out 917 if (self._filteringOut) { 918 $(self).addClass('filteredOut'); 919 self._isFilteredOut = true; 920 self._filteringOut = false;
921 } 922 //finished filtering in 923 else if (self._filteringIn) { 924 self._isFilteredOut = false; 925 self._filteringIn = false; 926 } 927 //if animating trigger filteringEnd event once on parent 928 if (self._parent._isAnimating) { 929 self._parent.trigger('filteringEnd'); 930 self._parent._isAnimating = false; 931 } 932 }, 933 934 /** 935 * Filters out the item. 936 * @private 937 */ 938 _filterOut: function() { 939 var self = this, 940 filterOutCss = self._parent._makeDeepCopy(self._parent.options.filterOutCss); 941 //Auto add translate to transform over user-defined filterOut styles 942 filterOutCss.transform += ' translate3d(' + self._lastPos.left + 'px,' + self._lastPos.top + 'px, 0)'; 943 //Play animation 944 self.css(filterOutCss); 945 //Tag as filteringOut for transitionend event 946 self._filteringOut = true; 947 }, 948 949 /** 950 * Filters in the item. 951 * @param {Object} targetPos - is the position to move to with transform-translate 952 * @private 953 */ 954 _filterIn: function(targetPos) { 955 var self = this, 956 filterInCss = self._parent._makeDeepCopy(self._parent.options.filterInCss); 957 //Remove the filteredOut class 958 $(self).removeClass('filteredOut'); 959 //Tag as filtering in for transitionend event 960 self._filteringIn = true; 961 self._lastPos = targetPos; 962 //Auto add translate to transform over user-defined filterIn styles 963 filterInCss.transform += ' translate3d(' + targetPos.left + 'px,' + targetPos.top + 'px, 0)'; 964 //Play animation 965 self.css(filterInCss); 966 } 967 }; 968 969})(this, jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.