virtual-image-layout 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # 图片瀑布流布局 - 虚拟列表
1
+ # 图片等高布局 - 虚拟列表
2
2
 
3
- 一个基于虚拟列表技术的高性能图片瀑布流布局项目,支持大量图片的流畅展示。
3
+ 一个基于虚拟列表技术的高性能图片等高布局项目,支持大量图片的流畅展示。
4
4
 
5
5
  ## 功能特性
6
6
 
package/package.json CHANGED
@@ -1,20 +1,32 @@
1
1
  {
2
2
  "name": "virtual-image-layout",
3
- "version": "1.0.5",
4
- "description": "A virtual scrolling justified image gallery layout library, framework-agnostic",
3
+ "version": "1.0.7",
4
+ "description": "A virtual scrolling image gallery layout library, framework-agnostic. Includes justified/equal-height layout and waterfall layout.",
5
5
  "main": "js/index.js",
6
6
  "module": "js/index.js",
7
+ "exports": {
8
+ ".": "./js/index.js",
9
+ "./waterfall": "./waterfall/waterfall.js",
10
+ "./WaterFall.vue": "./waterfall/WaterFall.vue",
11
+ "./css/index.css": "./css/index.css",
12
+ "./waterfall/index.css": "./waterfall/index.css"
13
+ },
7
14
  "author": "lukuihao <351973031@qq.com>",
8
15
  "keywords": [
9
16
  "virtual-scroll",
10
17
  "image-layout",
11
18
  "justified-gallery",
12
- "waterfall"
19
+ "waterfall",
20
+ "masonry",
21
+ "vue"
13
22
  ],
14
23
  "license": "MIT",
15
24
  "files": [
16
25
  "js/index.js",
17
- "css/index.css"
26
+ "css/index.css",
27
+ "waterfall/waterfall.js",
28
+ "waterfall/WaterFall.vue",
29
+ "waterfall/index.css"
18
30
  ],
19
31
  "scripts": {
20
32
  "build": "echo 'No build step required'"
@@ -0,0 +1,404 @@
1
+ <template>
2
+ <ul ref="scrollerRef" class="waterfall"></ul>
3
+ </template>
4
+
5
+ <script>
6
+ export default {
7
+ name: 'WaterFall',
8
+ props: {
9
+ // 列间距(px)
10
+ spacing: {
11
+ type: Number,
12
+ default: 16,
13
+ },
14
+ // 标题额外高度(px)
15
+ titleGap: {
16
+ type: Number,
17
+ default: 0,
18
+ },
19
+ // 自定义渲染函数,返回 HTML 字符串
20
+ onRenderItem: {
21
+ type: Function,
22
+ required: true,
23
+ },
24
+ // 曝光回调
25
+ onExposure: {
26
+ type: Function,
27
+ default: null,
28
+ },
29
+ },
30
+
31
+ data() {
32
+ return {
33
+ listData: [],
34
+ itemIncontainer: {},
35
+ allList_h: 0,
36
+ maxColumn: 16,
37
+ seeHeight: 0,
38
+ spacing_top: 0,
39
+ // 曝光相关
40
+ site_num: [],
41
+ BurialPoint: { res_detail: [] },
42
+ indexReportedArr: [],
43
+ maxNum: 30,
44
+ _observer: null,
45
+ _timer: null,
46
+ _resizeTimer: null,
47
+ _scrollTimer: null,
48
+ };
49
+ },
50
+
51
+ mounted() {
52
+ this.seeHeight = window.innerHeight;
53
+ this.spacing_top = this.$refs.scrollerRef.getBoundingClientRect().top + window.scrollY;
54
+
55
+ if (typeof this.onExposure === 'function') {
56
+ this._initObserver();
57
+ }
58
+
59
+ window.addEventListener('scroll', this._onScroll);
60
+ window.addEventListener('resize', this._onResize);
61
+ },
62
+
63
+ beforeUnmount() {
64
+ window.removeEventListener('scroll', this._onScroll);
65
+ window.removeEventListener('resize', this._onResize);
66
+ if (this._observer) this._observer.disconnect();
67
+ clearTimeout(this._timer);
68
+ clearTimeout(this._resizeTimer);
69
+ clearTimeout(this._scrollTimer);
70
+ },
71
+
72
+ methods: {
73
+ // ── 公开方法 ──────────────────────────────────────
74
+
75
+ // 添加图片数据
76
+ addImages(data) {
77
+ this.listData.push(...data);
78
+ const el = this.$refs.scrollerRef;
79
+ if (el && el.querySelectorAll('li').length !== 0) {
80
+ this._setPosition();
81
+ this._drawVirtualRows();
82
+ } else {
83
+ this._calculationListData();
84
+ }
85
+ },
86
+
87
+ // 清空重置
88
+ reset() {
89
+ if (typeof this.onExposure === 'function' && this.site_num.length > 0) {
90
+ clearTimeout(this._timer);
91
+ this.onExposure(this.BurialPoint);
92
+ }
93
+ this.listData = [];
94
+ this.itemIncontainer = {};
95
+ this.allList_h = 0;
96
+ this.site_num = [];
97
+ this.indexReportedArr = [];
98
+ this.BurialPoint = { res_detail: [] };
99
+ const el = this.$refs.scrollerRef;
100
+ if (el) {
101
+ el.innerHTML = '';
102
+ el.removeAttribute('style');
103
+ }
104
+ },
105
+
106
+ // ── 私有方法 ──────────────────────────────────────
107
+
108
+ _onScroll() {
109
+ if (!this.listData.length) return;
110
+ clearTimeout(this._scrollTimer);
111
+ this._scrollTimer = setTimeout(() => this._drawVirtualRows(), 100);
112
+ },
113
+
114
+ _onResize() {
115
+ if (!this.listData.length) return;
116
+ clearTimeout(this._resizeTimer);
117
+ this._resizeTimer = setTimeout(() => this._resizeWaterFall(), 500);
118
+ },
119
+
120
+ _getContainerWidth() {
121
+ const el = this.$refs.scrollerRef;
122
+ return el ? el.offsetWidth : 0;
123
+ },
124
+
125
+ _calculateProductWidth() {
126
+ const containerWith = this._getContainerWidth();
127
+ const spacing = this.spacing;
128
+ let itemNum = 0;
129
+ let _width = 0;
130
+ if (containerWith < 600) {
131
+ itemNum = 2;
132
+ _width = (containerWith - spacing) / 2;
133
+ } else if (containerWith < 800) {
134
+ itemNum = Math.floor((containerWith + spacing) / (200 + spacing));
135
+ _width = (containerWith - spacing * (itemNum - 1)) / itemNum;
136
+ } else if (containerWith < 1700) {
137
+ itemNum = Math.floor((containerWith + spacing) / (240 + spacing));
138
+ _width = (containerWith - spacing * (itemNum - 1)) / itemNum;
139
+ } else if (containerWith < 1900) {
140
+ itemNum = Math.floor((containerWith + spacing) / (260 + spacing));
141
+ _width = (containerWith - spacing * (itemNum - 1)) / itemNum;
142
+ } else {
143
+ itemNum = Math.floor((containerWith + spacing) / (280 + spacing));
144
+ _width = (containerWith - spacing * (itemNum - 1)) / itemNum;
145
+ }
146
+ return { itemsPerRow: itemNum, width: _width };
147
+ },
148
+
149
+ _getCols() {
150
+ return this._calculateProductWidth().itemsPerRow;
151
+ },
152
+
153
+ _getScaledHeight(originalWidth, originalHeight, newWidth) {
154
+ const scaleRatio = newWidth / originalWidth;
155
+ return { width: newWidth, height: Math.round(originalHeight * scaleRatio) };
156
+ },
157
+
158
+ _getShort(heightArr) {
159
+ let index = 0;
160
+ let min = heightArr[0];
161
+ for (let i = 0; i < heightArr.length; i++) {
162
+ if (min > heightArr[i]) { min = heightArr[i]; index = i; }
163
+ }
164
+ return index;
165
+ },
166
+
167
+ _setPosition() {
168
+ if (!this.listData.length) return;
169
+ const result = this._calculateProductWidth();
170
+ const cols = result.itemsPerRow;
171
+ const heightArr = [];
172
+ let totleSnm = 0;
173
+ let waterfallHeight = 500;
174
+
175
+ this.listData.forEach((item, index) => {
176
+ if (item.width == 0 || item.height == 0) { item.width = 4000; item.height = 3000; }
177
+ item.data_index = index + 1;
178
+ const item_w = result.width;
179
+ const scaledSize = this._getScaledHeight(item.width, item.height, item_w);
180
+ item.count_height = scaledSize.height + this.titleGap;
181
+ item.img_height = scaledSize.height;
182
+ item.item_width = item_w;
183
+
184
+ if (index < cols) {
185
+ heightArr.push(item.count_height);
186
+ item.top = 0;
187
+ item.left = index * (item_w + this.maxColumn);
188
+ } else {
189
+ const _index = this._getShort(heightArr);
190
+ const min = Math.min(...heightArr);
191
+ item.top = min + this.maxColumn;
192
+ item.left = _index * (item_w + this.maxColumn);
193
+ heightArr[_index] += item.count_height + this.maxColumn;
194
+ waterfallHeight = Math.max(...heightArr);
195
+ }
196
+ totleSnm += item.count_height + this.maxColumn;
197
+ });
198
+
199
+ this.allList_h = Math.floor(totleSnm / this.listData.length);
200
+ const el = this.$refs.scrollerRef;
201
+ if (el) el.style.height = waterfallHeight + 'px';
202
+ },
203
+
204
+ _calculationListData() {
205
+ this._setPosition();
206
+ for (let i = 0; i < this.listData.length; i++) {
207
+ this._insert(this.listData[i], i);
208
+ }
209
+ },
210
+
211
+ _resizeWaterFall() {
212
+ if (!this.listData.length) return;
213
+ const result = this._calculateProductWidth();
214
+ const cols = result.itemsPerRow;
215
+ const heightArr = [];
216
+ let totleSnm = 0;
217
+ let waterfallHeight = 500;
218
+ const el = this.$refs.scrollerRef;
219
+
220
+ this.listData.forEach((item, index) => {
221
+ const item_w = result.width;
222
+ const scaledSize = this._getScaledHeight(item.width, item.height, item_w);
223
+ item.count_height = scaledSize.height + this.titleGap;
224
+ item.img_height = scaledSize.height;
225
+ item.item_width = item_w;
226
+
227
+ if (index < cols) {
228
+ heightArr.push(item.count_height);
229
+ item.top = 0;
230
+ item.left = index * (item_w + this.maxColumn);
231
+ } else {
232
+ const _index = this._getShort(heightArr);
233
+ const min = Math.min(...heightArr);
234
+ item.top = min + this.maxColumn;
235
+ item.left = _index * (item_w + this.maxColumn);
236
+ heightArr[_index] += item.count_height + this.maxColumn;
237
+ waterfallHeight = Math.max(...heightArr);
238
+ }
239
+ totleSnm += item.count_height + this.maxColumn;
240
+
241
+ if (el) {
242
+ const li = el.querySelector(`li[data-index="${item.data_index}"]`);
243
+ if (li) {
244
+ li.style.transform = `translate(${item.left}px,${item.top}px)`;
245
+ li.style.width = item_w + 'px';
246
+ const boxImg = li.querySelector('.box-images');
247
+ if (boxImg) boxImg.style.height = item.img_height + 'px';
248
+ }
249
+ }
250
+ });
251
+
252
+ this.allList_h = Math.floor(totleSnm / this.listData.length);
253
+ if (el) el.style.height = waterfallHeight + 'px';
254
+ this._drawVirtualRows();
255
+ },
256
+
257
+ _insert(value, index) {
258
+ this.itemIncontainer[index] = { index };
259
+ const el = this.$refs.scrollerRef;
260
+ if (!el) return;
261
+ el.insertAdjacentHTML('beforeend', this.onRenderItem(value));
262
+ if (typeof this.onExposure === 'function') {
263
+ if (this.indexReportedArr.indexOf(value.data_index + '') === -1) {
264
+ const li = el.querySelector(`li[data-index="${value.data_index}"]`);
265
+ if (li) this._observeItem(li);
266
+ }
267
+ }
268
+ },
269
+
270
+ _removeItem(rows) {
271
+ const el = this.$refs.scrollerRef;
272
+ rows.forEach((item) => {
273
+ const data_index = parseInt(item) + 1;
274
+ if (el) {
275
+ const li = el.querySelector(`li[data-index="${data_index}"]`);
276
+ if (li) li.remove();
277
+ }
278
+ delete this.itemIncontainer[item];
279
+ });
280
+ },
281
+
282
+ _drawVirtualRows() {
283
+ if (!this.listData.length) return;
284
+ let topPixel = window.scrollY - this.spacing_top;
285
+ topPixel = topPixel < 0 ? 0 : topPixel;
286
+ const bottomPixel = topPixel + this.seeHeight;
287
+ const bufferZone = this.seeHeight * 2;
288
+ let firstRow = Math.floor((topPixel - bufferZone) / this.allList_h) * this._getCols();
289
+ let lastRow = Math.ceil((bottomPixel + bufferZone) / this.allList_h) * this._getCols();
290
+ firstRow = Math.max(0, firstRow);
291
+ this._ensureRowsRendered(firstRow, lastRow);
292
+ },
293
+
294
+ _ensureRowsRendered(start, finish) {
295
+ const rowsIndex = Object.keys(this.itemIncontainer);
296
+ const needToRemove = [...rowsIndex];
297
+ for (let rowIndex = start; rowIndex <= finish; rowIndex++) {
298
+ const rowIndexStr = rowIndex.toString();
299
+ const idx = needToRemove.indexOf(rowIndexStr);
300
+ if (idx > -1) { needToRemove.splice(idx, 1); continue; }
301
+ if (this.listData.length > rowIndex && rowIndex >= 0) {
302
+ this._insert(this.listData[rowIndex], rowIndex);
303
+ }
304
+ }
305
+ this._removeItem(needToRemove);
306
+ },
307
+
308
+ // ── 曝光观察 ──────────────────────────────────────
309
+
310
+ _initObserver() {
311
+ this._observer = new IntersectionObserver((entries) => {
312
+ entries.forEach(entry => {
313
+ if (!entry.isIntersecting) return;
314
+ const { index } = entry.target.dataset;
315
+ if (this.indexReportedArr.indexOf(index) !== -1) return;
316
+ clearTimeout(this._timer);
317
+ this.site_num.push(index);
318
+ const attributeNames = Array.from(entry.target.attributes).map(a => a.name);
319
+ const customAttributes = {};
320
+ attributeNames.forEach(name => {
321
+ if (name === 'style') return;
322
+ const key = name.startsWith('data-') ? name.replace('data-', '') : name;
323
+ customAttributes[key] = entry.target.getAttribute(name);
324
+ if (!this.BurialPoint[key]) {
325
+ this.BurialPoint[key] = [entry.target.getAttribute(name)];
326
+ } else {
327
+ this.BurialPoint[key].push(entry.target.getAttribute(name));
328
+ }
329
+ });
330
+ this.BurialPoint.res_detail.push(customAttributes);
331
+ this.indexReportedArr.push(index);
332
+ this._observer.unobserve(entry.target);
333
+
334
+ if (this.site_num.length >= this.maxNum) {
335
+ this.onExposure(this.BurialPoint);
336
+ this.site_num.splice(0, this.maxNum);
337
+ this.BurialPoint.res_detail.splice(0, this.maxNum);
338
+ } else {
339
+ this._timer = setTimeout(() => {
340
+ if (this.site_num.length > 0) {
341
+ this.onExposure(this.BurialPoint);
342
+ this.site_num.splice(0, this.maxNum);
343
+ this.BurialPoint.res_detail.splice(0, this.maxNum);
344
+ }
345
+ }, 1000);
346
+ }
347
+ });
348
+ }, { threshold: 0.1 });
349
+ },
350
+
351
+ _observeItem(el) {
352
+ if (this._observer) this._observer.observe(el);
353
+ },
354
+ },
355
+ };
356
+ </script>
357
+
358
+ <style scoped>
359
+ .waterfall {
360
+ position: relative;
361
+ min-height: 250px;
362
+ padding: 0;
363
+ margin: 0;
364
+ list-style: none;
365
+ }
366
+
367
+ :deep(.waterfall li) {
368
+ box-sizing: border-box;
369
+ position: absolute;
370
+ top: 0;
371
+ left: 0;
372
+ line-height: 1;
373
+ transition: transform 0.3s ease;
374
+ list-style-type: none;
375
+ }
376
+
377
+ :deep(.waterfall li .box-images) {
378
+ display: block;
379
+ position: relative;
380
+ background: rgba(17, 17, 17, 0.04);
381
+ border-radius: 8px;
382
+ width: 100%;
383
+ line-height: 1;
384
+ overflow: hidden;
385
+ }
386
+
387
+ :deep(.waterfall li .box-images::after) {
388
+ content: "";
389
+ position: absolute;
390
+ inset: 0;
391
+ background: rgba(0, 0, 0, 0.04);
392
+ border-radius: 12px;
393
+ }
394
+
395
+ :deep(.waterfall li img) {
396
+ max-width: 100%;
397
+ max-height: 100%;
398
+ border-radius: 8px;
399
+ margin: auto;
400
+ width: 100%;
401
+ height: 100%;
402
+ object-fit: cover;
403
+ }
404
+ </style>
@@ -0,0 +1,74 @@
1
+ .waterfall {
2
+ position: relative;
3
+ min-height: 250px;
4
+ padding: 0;
5
+ }
6
+
7
+ .waterfall li {
8
+ box-sizing: border-box;
9
+ position: absolute;
10
+ top: 0;
11
+ left: 0;
12
+ visibility: visible;
13
+ pointer-events: auto;
14
+ line-height: 1;
15
+ transition: top .3s ease;
16
+ list-style-type: none
17
+ }
18
+
19
+ .waterfall li[data-res_type="5"] .box-images::after {
20
+ background: rgba(0, 0, 0, .02);
21
+ }
22
+
23
+ .waterfall li .box-images {
24
+ display: block;
25
+ position: relative;
26
+ background: rgba(17, 17, 17, 0.04);
27
+ border-radius: 8px;
28
+ width: 100%;
29
+ line-height: 1;
30
+ overflow: hidden;
31
+ }
32
+
33
+ .waterfall li .box-images::after {
34
+ content: "";
35
+ position: absolute;
36
+ left: 0;
37
+ right: 0;
38
+ bottom: 0;
39
+ top: 0;
40
+ background: rgba(0, 0, 0, .04);
41
+ border-radius: 12px;
42
+ }
43
+
44
+ .waterfall li:hover .case-name-display,
45
+ .waterfall li:hover .list-model-collect,
46
+ .waterfall li:hover .not-like-search,
47
+ .waterfall li:hover .action-container .item-actions,
48
+ .waterfall li:hover .suspended-material-name {
49
+ opacity: 1;
50
+ }
51
+
52
+ .waterfall li:hover .leftTop-floating {
53
+ display: none;
54
+ }
55
+
56
+ .waterfall li.select-mask-collect .list-model-collect,
57
+ .waterfall li.select-mask-collect .not-like-search,
58
+ .waterfall li.select-mask-collect .action-container .item-actions {
59
+ opacity: 1;
60
+ }
61
+
62
+ .waterfall li.select-mask-collect .leftTop-floating {
63
+ display: none;
64
+ }
65
+
66
+ .waterfall li img {
67
+ max-width: 100%;
68
+ max-height: 100%;
69
+ border-radius: 8px;
70
+ margin: auto;
71
+ width: 100%;
72
+ height: 100%;
73
+ object-fit: cover;
74
+ }
@@ -0,0 +1,318 @@
1
+ /*
2
+ 瀑布流虚拟列表核心类(无 jQuery 依赖)
3
+ 用法:
4
+ const layout = new WaterFall({
5
+ element: document.getElementById('waterfall'), // 或 DOM 元素
6
+ spacing: 16,
7
+ titleGap: 0,
8
+ onRenderItem: (item) => `<li ...>...</li>`,
9
+ onExposure: (burialPoint) => {},
10
+ });
11
+ layout.addImages(list);
12
+ layout.reset();
13
+ layout.destroy();
14
+ */
15
+ class WaterFall {
16
+ constructor(param) {
17
+ if (typeof param.element === 'string') {
18
+ this.scroller = document.querySelector(param.element);
19
+ } else if (param.element instanceof HTMLElement) {
20
+ this.scroller = param.element;
21
+ }
22
+ if (!this.scroller) throw new Error('[WaterFall] element not found');
23
+
24
+ this.spacing = param.spacing ?? 16;
25
+ this.titleGap = param.titleGap ?? 0;
26
+ this.onRenderItem = param.onRenderItem;
27
+ this.onExposure = typeof param.onExposure === 'function' ? param.onExposure : null;
28
+
29
+ this.listData = [];
30
+ this.itemIncontainer = {};
31
+ this.allList_h = 0;
32
+ this.maxColumn = 16;
33
+ this.seeHeight = window.innerHeight;
34
+ this.spacing_top = this.scroller.getBoundingClientRect().top + window.scrollY;
35
+
36
+ // 曝光相关
37
+ this.site_num = [];
38
+ this.BurialPoint = { res_detail: [] };
39
+ this.indexReportedArr = [];
40
+ this.maxNum = 30;
41
+ this._observer = null;
42
+ this._timer = null;
43
+
44
+ if (this.onExposure) this._initObserver();
45
+
46
+ this._onScroll = this._debounce(() => this._drawVirtualRows(), 100);
47
+ this._onResize = this._debounce(() => this._resizeWaterFall(), 500);
48
+ window.addEventListener('scroll', this._onScroll);
49
+ window.addEventListener('resize', this._onResize);
50
+ }
51
+
52
+ // ── 公开方法 ──────────────────────────────────────
53
+
54
+ addImages(data) {
55
+ this.listData.push(...data);
56
+ if (this.scroller.querySelectorAll('li').length !== 0) {
57
+ this._setPosition();
58
+ this._drawVirtualRows();
59
+ } else {
60
+ this._calculationListData();
61
+ }
62
+ }
63
+
64
+ reset() {
65
+ if (this.onExposure && this.site_num.length > 0) {
66
+ clearTimeout(this._timer);
67
+ this.onExposure(this.BurialPoint);
68
+ }
69
+ this.listData = [];
70
+ this.itemIncontainer = {};
71
+ this.allList_h = 0;
72
+ this.site_num = [];
73
+ this.indexReportedArr = [];
74
+ this.BurialPoint = { res_detail: [] };
75
+ this.scroller.innerHTML = '';
76
+ this.scroller.removeAttribute('style');
77
+ }
78
+
79
+ destroy() {
80
+ this.reset();
81
+ window.removeEventListener('scroll', this._onScroll);
82
+ window.removeEventListener('resize', this._onResize);
83
+ if (this._observer) this._observer.disconnect();
84
+ }
85
+
86
+ // ── 私有方法 ──────────────────────────────────────
87
+
88
+ _debounce(fn, wait) {
89
+ let timer = null;
90
+ return function (...args) {
91
+ clearTimeout(timer);
92
+ timer = setTimeout(() => fn.apply(this, args), wait);
93
+ };
94
+ }
95
+
96
+ _getContainerWidth() {
97
+ return this.scroller.offsetWidth;
98
+ }
99
+
100
+ _calculateProductWidth() {
101
+ const containerWith = this._getContainerWidth();
102
+ const spacing = this.spacing;
103
+ let itemNum = 0;
104
+ let _width = 0;
105
+ if (containerWith < 600) {
106
+ itemNum = 2;
107
+ _width = (containerWith - spacing) / 2;
108
+ } else if (containerWith < 800) {
109
+ itemNum = Math.floor((containerWith + spacing) / (200 + spacing));
110
+ _width = (containerWith - spacing * (itemNum - 1)) / itemNum;
111
+ } else if (containerWith < 1700) {
112
+ itemNum = Math.floor((containerWith + spacing) / (240 + spacing));
113
+ _width = (containerWith - spacing * (itemNum - 1)) / itemNum;
114
+ } else if (containerWith < 1900) {
115
+ itemNum = Math.floor((containerWith + spacing) / (260 + spacing));
116
+ _width = (containerWith - spacing * (itemNum - 1)) / itemNum;
117
+ } else {
118
+ itemNum = Math.floor((containerWith + spacing) / (280 + spacing));
119
+ _width = (containerWith - spacing * (itemNum - 1)) / itemNum;
120
+ }
121
+ return { itemsPerRow: itemNum, width: _width };
122
+ }
123
+
124
+ _getCols() {
125
+ return this._calculateProductWidth().itemsPerRow;
126
+ }
127
+
128
+ _getScaledHeight(originalWidth, originalHeight, newWidth) {
129
+ return { width: newWidth, height: Math.round((originalHeight / originalWidth) * newWidth) };
130
+ }
131
+
132
+ _getShort(heightArr) {
133
+ let index = 0;
134
+ let min = heightArr[0];
135
+ for (let i = 0; i < heightArr.length; i++) {
136
+ if (heightArr[i] < min) { min = heightArr[i]; index = i; }
137
+ }
138
+ return index;
139
+ }
140
+
141
+ _setPosition() {
142
+ if (!this.listData.length) return;
143
+ const result = this._calculateProductWidth();
144
+ const cols = result.itemsPerRow;
145
+ const heightArr = [];
146
+ let totleSnm = 0;
147
+ let waterfallHeight = 500;
148
+
149
+ this.listData.forEach((item, index) => {
150
+ if (!item.width || !item.height) { item.width = 4000; item.height = 3000; }
151
+ item.data_index = index + 1;
152
+ const item_w = result.width;
153
+ const scaled = this._getScaledHeight(item.width, item.height, item_w);
154
+ item.count_height = scaled.height + this.titleGap;
155
+ item.img_height = scaled.height;
156
+ item.item_width = item_w;
157
+
158
+ if (index < cols) {
159
+ heightArr.push(item.count_height);
160
+ item.top = 0;
161
+ item.left = index * (item_w + this.maxColumn);
162
+ } else {
163
+ const _index = this._getShort(heightArr);
164
+ item.top = Math.min(...heightArr) + this.maxColumn;
165
+ item.left = _index * (item_w + this.maxColumn);
166
+ heightArr[_index] += item.count_height + this.maxColumn;
167
+ waterfallHeight = Math.max(...heightArr);
168
+ }
169
+ totleSnm += item.count_height + this.maxColumn;
170
+ });
171
+
172
+ this.allList_h = Math.floor(totleSnm / this.listData.length);
173
+ this.scroller.style.height = waterfallHeight + 'px';
174
+ }
175
+
176
+ _calculationListData() {
177
+ this._setPosition();
178
+ for (let i = 0; i < this.listData.length; i++) {
179
+ this._insert(this.listData[i], i);
180
+ }
181
+ }
182
+
183
+ _resizeWaterFall() {
184
+ if (!this.listData.length) return;
185
+ const result = this._calculateProductWidth();
186
+ const cols = result.itemsPerRow;
187
+ const heightArr = [];
188
+ let totleSnm = 0;
189
+ let waterfallHeight = 500;
190
+
191
+ this.listData.forEach((item, index) => {
192
+ const item_w = result.width;
193
+ const scaled = this._getScaledHeight(item.width, item.height, item_w);
194
+ item.count_height = scaled.height + this.titleGap;
195
+ item.img_height = scaled.height;
196
+ item.item_width = item_w;
197
+
198
+ if (index < cols) {
199
+ heightArr.push(item.count_height);
200
+ item.top = 0;
201
+ item.left = index * (item_w + this.maxColumn);
202
+ } else {
203
+ const _index = this._getShort(heightArr);
204
+ item.top = Math.min(...heightArr) + this.maxColumn;
205
+ item.left = _index * (item_w + this.maxColumn);
206
+ heightArr[_index] += item.count_height + this.maxColumn;
207
+ waterfallHeight = Math.max(...heightArr);
208
+ }
209
+ totleSnm += item.count_height + this.maxColumn;
210
+
211
+ const li = this.scroller.querySelector(`li[data-index="${item.data_index}"]`);
212
+ if (li) {
213
+ li.style.transform = `translate(${item.left}px,${item.top}px)`;
214
+ li.style.width = item_w + 'px';
215
+ const box = li.querySelector('.box-images');
216
+ if (box) box.style.height = item.img_height + 'px';
217
+ }
218
+ });
219
+
220
+ this.allList_h = Math.floor(totleSnm / this.listData.length);
221
+ this.scroller.style.height = waterfallHeight + 'px';
222
+ this._drawVirtualRows();
223
+ }
224
+
225
+ _insert(value, index) {
226
+ this.itemIncontainer[index] = { index };
227
+ this.scroller.insertAdjacentHTML('beforeend', this.onRenderItem(value));
228
+ if (this.onExposure && this.indexReportedArr.indexOf(value.data_index + '') === -1) {
229
+ const li = this.scroller.querySelector(`li[data-index="${value.data_index}"]`);
230
+ if (li) this._observer && this._observer.observe(li);
231
+ }
232
+ }
233
+
234
+ _removeItem(rows) {
235
+ rows.forEach(item => {
236
+ const li = this.scroller.querySelector(`li[data-index="${parseInt(item) + 1}"]`);
237
+ if (li) li.remove();
238
+ delete this.itemIncontainer[item];
239
+ });
240
+ }
241
+
242
+ _drawVirtualRows() {
243
+ if (!this.listData.length) return;
244
+ let topPixel = window.scrollY - this.spacing_top;
245
+ topPixel = topPixel < 0 ? 0 : topPixel;
246
+ const bottomPixel = topPixel + this.seeHeight;
247
+ const bufferZone = this.seeHeight * 2;
248
+ let firstRow = Math.floor((topPixel - bufferZone) / this.allList_h) * this._getCols();
249
+ let lastRow = Math.ceil((bottomPixel + bufferZone) / this.allList_h) * this._getCols();
250
+ firstRow = Math.max(0, firstRow);
251
+ this._ensureRowsRendered(firstRow, lastRow);
252
+ }
253
+
254
+ _ensureRowsRendered(start, finish) {
255
+ const rowsIndex = Object.keys(this.itemIncontainer);
256
+ const needToRemove = [...rowsIndex];
257
+ for (let rowIndex = start; rowIndex <= finish; rowIndex++) {
258
+ const str = rowIndex.toString();
259
+ const idx = needToRemove.indexOf(str);
260
+ if (idx > -1) { needToRemove.splice(idx, 1); continue; }
261
+ if (rowIndex >= 0 && rowIndex < this.listData.length) {
262
+ this._insert(this.listData[rowIndex], rowIndex);
263
+ }
264
+ }
265
+ this._removeItem(needToRemove);
266
+ }
267
+
268
+ _initObserver() {
269
+ this._observer = new IntersectionObserver((entries) => {
270
+ entries.forEach(entry => {
271
+ if (!entry.isIntersecting) return;
272
+ const { index } = entry.target.dataset;
273
+ if (this.indexReportedArr.indexOf(index) !== -1) return;
274
+ clearTimeout(this._timer);
275
+ this.site_num.push(index);
276
+
277
+ const attributeNames = Array.from(entry.target.attributes).map(a => a.name);
278
+ const customAttributes = {};
279
+ attributeNames.forEach(name => {
280
+ if (name === 'style') return;
281
+ const key = name.startsWith('data-') ? name.replace('data-', '') : name;
282
+ customAttributes[key] = entry.target.getAttribute(name);
283
+ if (!this.BurialPoint[key]) {
284
+ this.BurialPoint[key] = [customAttributes[key]];
285
+ } else {
286
+ this.BurialPoint[key].push(customAttributes[key]);
287
+ }
288
+ });
289
+ this.BurialPoint.res_detail.push(customAttributes);
290
+ this.indexReportedArr.push(index);
291
+ this._observer.unobserve(entry.target);
292
+
293
+ const flush = () => {
294
+ this.onExposure(this.BurialPoint);
295
+ this.site_num.splice(0, this.maxNum);
296
+ this.BurialPoint.res_detail.splice(0, this.maxNum);
297
+ };
298
+
299
+ if (this.site_num.length >= this.maxNum) {
300
+ flush();
301
+ } else {
302
+ this._timer = setTimeout(() => {
303
+ if (this.site_num.length > 0) flush();
304
+ }, 1000);
305
+ }
306
+ });
307
+ }, { threshold: 0.1 });
308
+ }
309
+ }
310
+
311
+ // UMD 导出
312
+ if (typeof module !== 'undefined' && module.exports) {
313
+ module.exports = WaterFall;
314
+ } else if (typeof define === 'function' && define.amd) {
315
+ define(function () { return WaterFall; });
316
+ } else if (typeof window !== 'undefined') {
317
+ window.WaterFall = WaterFall;
318
+ }