| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- /**
- * CustomIsotope - Custom implementation of filtering and layout
- * Replaces Isotope.js functionality - FIXED VERSION
- */
- class CustomIsotope {
- constructor(container, options = {}) {
- this.container = typeof container === 'string' ? document.querySelector(container) : container;
- this.options = {
- itemSelector: '.portfolio-item',
- layoutMode: 'masonry',
- columnWidth: null,
- gutter: 20,
- transitionDuration: 600,
- hiddenStyle: { opacity: 0, transform: 'scale(0.8)' },
- visibleStyle: { opacity: 1, transform: 'scale(1)' },
- ...options
- };
-
- this.items = [];
- this.filteredItems = [];
- this.currentFilter = '*';
- this.isLayoutComplete = false;
-
- this.init();
- }
-
- // Initialize the isotope instance
- init() {
- if (!this.container) {
- console.error('❌ Container not found');
- return;
- }
-
- // Get all items
- this.items = Array.from(this.container.querySelectorAll(this.options.itemSelector));
- this.filteredItems = [...this.items];
-
- // Setup initial styles
- this.setupContainer();
- this.setupItems();
-
- // Execute initial layout
- setTimeout(() => {
- this.layout();
- }, 100);
-
- // Listen for window resize
- this.setupResizeListener();
- }
-
- // Setup container styles
- setupContainer() {
- const container = this.container;
- container.style.position = 'relative';
- container.style.transition = `height ${this.options.transitionDuration}ms ease`;
- }
-
- // Setup individual item styles
- setupItems() {
- this.items.forEach((item, index) => {
- item.style.position = 'absolute';
- item.style.transition = `all ${this.options.transitionDuration}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`;
- item.style.left = '0';
- item.style.top = '0';
-
- // Apply default visible styles
- Object.assign(item.style, {
- opacity: this.options.visibleStyle.opacity,
- transform: this.options.visibleStyle.transform
- });
- });
- }
-
- // Setup window resize listener
- setupResizeListener() {
- let resizeTimeout;
- window.addEventListener('resize', () => {
- clearTimeout(resizeTimeout);
- resizeTimeout = setTimeout(() => {
- this.layout();
- }, 250);
- });
- }
-
- /**
- * Filter items by category
- */
- arrange(options = {}) {
- const filter = options.filter || '*';
- this.currentFilter = filter;
-
- // Filter items
- if (filter === '*') {
- this.filteredItems = [...this.items];
- } else {
- const filterClass = filter.replace('.', '');
- this.filteredItems = this.items.filter(item =>
- item.classList.contains(filterClass)
- );
- }
-
- // Animate hidden items out
- this.hideItems();
-
- // Layout after hide animation
- setTimeout(() => {
- this.layout();
- this.showItems();
- }, this.options.transitionDuration / 2);
- }
-
- // Hide items that don't match filter
- hideItems() {
- this.items.forEach(item => {
- if (!this.filteredItems.includes(item)) {
- Object.assign(item.style, {
- opacity: this.options.hiddenStyle.opacity,
- transform: this.options.hiddenStyle.transform,
- pointerEvents: 'none'
- });
- }
- });
- }
-
- // Show filtered items with staggered animation
- showItems() {
- this.filteredItems.forEach((item, index) => {
- setTimeout(() => {
- Object.assign(item.style, {
- opacity: this.options.visibleStyle.opacity,
- transform: this.options.visibleStyle.transform,
- pointerEvents: 'auto'
- });
- }, index * 50); // Staggered animation
- });
- }
-
- /**
- * Calculate and apply layout - FIXED VERSION
- */
- layout() {
- if (this.filteredItems.length === 0) {
- this.container.style.height = '0px';
- return;
- }
-
- const containerWidth = this.container.offsetWidth;
-
- // Calculate columns based on container width and item width
- const itemWidth = this.getItemWidth();
- const columns = Math.max(1, Math.floor((containerWidth + this.options.gutter) / (itemWidth + this.options.gutter)));
- const actualItemWidth = Math.floor((containerWidth - (columns - 1) * this.options.gutter) / columns);
-
- // Arrays to track each column height
- const columnHeights = new Array(columns).fill(0);
-
- // Process each filtered item
- this.filteredItems.forEach((item, index) => {
- // Set width first
- item.style.width = `${actualItemWidth}px`;
-
- // Force reflow to get accurate height
- item.offsetHeight;
-
- // Find shortest column
- const shortestColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));
-
- // Calculate position
- const x = shortestColumnIndex * (actualItemWidth + this.options.gutter);
- const y = columnHeights[shortestColumnIndex];
-
- // Apply position
- item.style.left = `${x}px`;
- item.style.top = `${y}px`;
-
- // Get item height and update column height
- const itemHeight = item.offsetHeight;
- columnHeights[shortestColumnIndex] += itemHeight + this.options.gutter;
- });
-
- // Update container height
- const maxHeight = Math.max(...columnHeights) - this.options.gutter;
- this.container.style.height = `${maxHeight}px`;
-
- this.isLayoutComplete = true;
- }
-
- // Get item width for calculations
- getItemWidth() {
- if (this.options.columnWidth) {
- return this.options.columnWidth;
- }
-
- // Use first item width as reference, but make it responsive
- const containerWidth = this.container.offsetWidth;
-
- // Responsive breakpoints
- if (containerWidth <= 640) { // sm (mobile)
- return containerWidth; // 1 column
- } else if (containerWidth <= 768) { // md (tablet)
- return Math.floor((containerWidth - this.options.gutter) / 2); // 2 columns
- } else { // lg and xl (desktop) - ALWAYS 3 COLUMNS
- return Math.floor((containerWidth - 2 * this.options.gutter) / 3); // 3 columns
- }
- }
-
- /**
- * Reload items and layout
- */
- reloadItems() {
- this.items = Array.from(this.container.querySelectorAll(this.options.itemSelector));
- this.setupItems();
- this.arrange({ filter: this.currentFilter });
- }
-
- /**
- * Destroy instance
- */
- destroy() {
- this.items.forEach(item => {
- item.style.position = '';
- item.style.left = '';
- item.style.top = '';
- item.style.width = '';
- item.style.transition = '';
- item.style.opacity = '';
- item.style.transform = '';
- item.style.pointerEvents = '';
- });
-
- this.container.style.height = '';
- this.container.style.position = '';
- this.container.style.transition = '';
- }
-
- /**
- * Get currently filtered items
- */
- getFilteredItems() {
- return [...this.filteredItems];
- }
-
- /**
- * Get current filter
- */
- getCurrentFilter() {
- return this.currentFilter;
- }
- }
- /**
- * Setup filter buttons
- */
- function setupFilterButtons(isotope, filterButtons) {
- filterButtons.forEach((button, index) => {
- button.addEventListener('click', function(e) {
- e.preventDefault();
-
- const filterValue = this.getAttribute('data-filter');
-
- // Update active button state
- filterButtons.forEach(btn => btn.classList.remove('active'));
- this.classList.add('active');
-
- // Apply filter
- isotope.arrange({ filter: filterValue });
-
- // Refresh AOS after filtering
- setTimeout(() => {
- if (typeof AOS !== 'undefined') {
- AOS.refresh();
- }
- }, 700);
- });
- });
- }
- /**
- * Wait for images to load
- */
- function waitForImages(container) {
- return new Promise((resolve, reject) => {
- const images = container.querySelectorAll('img');
-
- if (images.length === 0) {
- resolve();
- return;
- }
-
- let loadedCount = 0;
- let hasError = false;
-
- const checkComplete = () => {
- loadedCount++;
- if (loadedCount === images.length) {
- if (hasError) {
- reject(new Error('Some images failed to load'));
- } else {
- resolve();
- }
- }
- };
-
- images.forEach(img => {
- if (img.complete) {
- checkComplete();
- } else {
- img.addEventListener('load', checkComplete);
- img.addEventListener('error', () => {
- hasError = true;
- checkComplete();
- });
- }
- });
-
- // Safety timeout
- setTimeout(() => {
- if (loadedCount < images.length) {
- resolve();
- }
- }, 5000);
- });
- }
- // Updated global utilities
- window.PortfolioUtils = {
- refresh: function() {
- if (window.customIsotope) {
- window.customIsotope.layout();
- }
- },
-
- filter: function(category) {
- if (window.customIsotope) {
- window.customIsotope.arrange({ filter: category });
-
- // Update active button
- const filterBtn = document.querySelector(`[data-filter="${category}"]`);
- if (filterBtn) {
- document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
- filterBtn.classList.add('active');
- }
- }
- },
-
- getCurrentFilter: function() {
- return window.customIsotope ? window.customIsotope.getCurrentFilter() : '*';
- },
-
- getFilteredItems: function() {
- return window.customIsotope ? window.customIsotope.getFilteredItems() : [];
- },
-
- reload: function() {
- if (window.customIsotope) {
- window.customIsotope.reloadItems();
- }
- },
-
- destroy: function() {
- if (window.customIsotope) {
- window.customIsotope.destroy();
- window.customIsotope = null;
- window.portfolioIsotope = null;
- }
- },
-
- reinitialize: function() {
- this.destroy();
- setTimeout(() => {
- initPortfolioIsotope();
- }, 100);
- }
- };
|