/** * AKSRT Waterfall — 瀑布流/异步列表 * * 提供纯 CSS column-based 瀑布流布局和无限滚动加载能力。 * 无外部依赖。 * * @package AKSRT */ (function () { 'use strict'; /** * Initialize waterfall layout on a container. * * @param {HTMLElement} container - The container element * @param {Object} options * @param {number} options.columns - Number of columns (default: auto from CSS) * @param {string} options.itemSelector - Child item selector (default: '.waterfall-item') * @param {number} options.gap - Gap between items in px (default: 16) */ function Waterfall(container, options) { if (!container) return; this.container = container; this.options = Object.assign({ columns: 0, itemSelector: '.waterfall-item', gap: 16, }, options || {}); this._resizeHandler = this.layout.bind(this); window.addEventListener('resize', this._resizeHandler); this.layout(); } Waterfall.prototype.layout = function () { var items = this.container.querySelectorAll(this.options.itemSelector); if (items.length === 0) return; var cols = this.options.columns; if (cols <= 0) { var containerWidth = this.container.offsetWidth; cols = Math.max(1, Math.floor(containerWidth / 280)); } var gap = this.options.gap; var colWidth = (this.container.offsetWidth - gap * (cols - 1)) / cols; var colHeights = new Array(cols).fill(0); for (var i = 0; i < items.length; i++) { var item = items[i]; item.style.width = colWidth + 'px'; item.style.position = 'absolute'; // Find shortest column var minHeight = Infinity; var minCol = 0; for (var c = 0; c < cols; c++) { if (colHeights[c] < minHeight) { minHeight = colHeights[c]; minCol = c; } } item.style.left = (minCol * (colWidth + gap)) + 'px'; item.style.top = colHeights[minCol] + 'px'; colHeights[minCol] += item.offsetHeight + gap; } // Set container height to tallest column var maxHeight = Math.max.apply(null, colHeights); this.container.style.height = maxHeight + 'px'; this.container.style.position = 'relative'; }; Waterfall.prototype.relayout = function () { this.layout(); }; Waterfall.prototype.destroy = function () { window.removeEventListener('resize', this._resizeHandler); }; /** * Infinite scroll loader. * * @param {Object} options * @param {string} options.containerSelector - List container selector * @param {string} options.loadMoreSelector - Load-more button selector * @param {Function} options.loadMore - Async function that loads more items * @param {number} options.threshold - Distance from bottom to trigger (default: 200) */ function InfiniteScroll(options) { this.options = Object.assign({ containerSelector: '.aksrt-infinite-list', loadMoreSelector: '.aksrt-load-more', loadMore: null, threshold: 200, }, options || {}); this.loading = false; this.hasMore = true; this._scrollHandler = this._check.bind(this); window.addEventListener('scroll', this._scrollHandler); this._check(); } InfiniteScroll.prototype._check = function () { if (this.loading || !this.hasMore) return; var loadMoreBtn = document.querySelector(this.options.loadMoreSelector); if (!loadMoreBtn) { this.hasMore = false; return; } var rect = loadMoreBtn.getBoundingClientRect(); if (rect.top < window.innerHeight + this.options.threshold) { this._load(); } }; InfiniteScroll.prototype._load = function () { if (!this.options.loadMore) return; this.loading = true; var self = this; this.options.loadMore().then(function (hasMore) { self.hasMore = hasMore !== false; self.loading = false; if (self.hasMore) { self._check(); } }).catch(function () { self.loading = false; }); }; InfiniteScroll.prototype.destroy = function () { window.removeEventListener('scroll', this._scrollHandler); }; // Auto-init var waterfallInstances = []; function relayoutAll() { waterfallInstances.forEach(function (instance) { if (instance && typeof instance.relayout === 'function') { instance.relayout(); } }); } function autoInit(root) { var elements = []; if (root && root.nodeType === 1 && root.matches('[data-aksrt-waterfall]')) elements.push(root); Array.prototype.push.apply(elements, (root || document).querySelectorAll('[data-aksrt-waterfall]')); elements.forEach(function (el) { if (el._aksrtWaterfall) return; var cols = parseInt(el.dataset.aksrtWaterfallColumns) || 0; var gap = parseInt(el.dataset.aksrtWaterfallGap) || 16; el._aksrtWaterfall = new Waterfall(el, { columns: cols, gap: gap }); waterfallInstances.push(el._aksrtWaterfall); }); } function bindGlobalEvents() { if (document.__aksrtWaterfallEventsBound) return; document.__aksrtWaterfallEventsBound = true; document.addEventListener('post_ajax.ed', relayoutAll); document.addEventListener('aksrt:posts:rendered', relayoutAll); document.addEventListener('lazyloaded', relayoutAll, true); } window.AksrtFrontend.register('aksrt-waterfall', function (root) { bindGlobalEvents(); autoInit(root); }); // Export to global window.AksrtWaterfall = Waterfall; window.AksrtInfiniteScroll = InfiniteScroll; })();