social-masonry 1.0.0 → 1.0.1
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 +97 -308
- package/dist/index.d.ts +62 -437
- package/dist/index.esm.js +93 -1354
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +101 -1366
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +35 -141
- package/dist/react/index.esm.js +280 -651
- package/dist/react/index.esm.js.map +1 -1
- package/dist/react/index.js +278 -649
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,44 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Social Masonry - Utility Functions
|
|
3
3
|
*/
|
|
4
|
+
// ============================================
|
|
5
|
+
// URL Parsing Utilities
|
|
6
|
+
// ============================================
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
8
|
+
* Extract tweet ID from Twitter/X URL
|
|
6
9
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
clearTimeout(timeoutId);
|
|
13
|
-
}
|
|
14
|
-
timeoutId = setTimeout(() => {
|
|
15
|
-
fn.apply(this, args);
|
|
16
|
-
timeoutId = null;
|
|
17
|
-
}, delay);
|
|
18
|
-
};
|
|
10
|
+
function extractTweetId(url) {
|
|
11
|
+
if (!url)
|
|
12
|
+
return null;
|
|
13
|
+
const match = url.match(/(?:twitter\.com|x\.com)\/\w+\/status\/(\d+)/);
|
|
14
|
+
return match ? match[1] : null;
|
|
19
15
|
}
|
|
20
16
|
/**
|
|
21
|
-
*
|
|
17
|
+
* Extract post ID from Instagram URL
|
|
22
18
|
*/
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
19
|
+
function extractInstagramId(url) {
|
|
20
|
+
if (!url)
|
|
21
|
+
return null;
|
|
22
|
+
const match = url.match(/instagram\.com\/(?:p|reel)\/([A-Za-z0-9_-]+)/);
|
|
23
|
+
return match ? match[1] : null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Detect platform from URL
|
|
27
|
+
*/
|
|
28
|
+
function detectPlatform(url) {
|
|
29
|
+
if (/(?:twitter\.com|x\.com)\/\w+\/status\/\d+/.test(url)) {
|
|
30
|
+
return 'twitter';
|
|
31
|
+
}
|
|
32
|
+
if (/instagram\.com\/(?:p|reel)\/[A-Za-z0-9_-]+/.test(url)) {
|
|
33
|
+
return 'instagram';
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Generate embed URL for Twitter
|
|
39
|
+
*/
|
|
40
|
+
function getTwitterEmbedUrl(tweetId, options = {}) {
|
|
41
|
+
const params = new URLSearchParams();
|
|
42
|
+
params.set('id', tweetId);
|
|
43
|
+
if (options.theme)
|
|
44
|
+
params.set('theme', options.theme);
|
|
45
|
+
if (options.hideCard)
|
|
46
|
+
params.set('cards', 'hidden');
|
|
47
|
+
if (options.hideThread)
|
|
48
|
+
params.set('conversation', 'none');
|
|
49
|
+
return `https://platform.twitter.com/embed/Tweet.html?${params.toString()}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Generate embed URL for Instagram
|
|
53
|
+
*/
|
|
54
|
+
function getInstagramEmbedUrl(postId) {
|
|
55
|
+
return `https://www.instagram.com/p/${postId}/embed/`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Generate unique ID from post URL
|
|
59
|
+
*/
|
|
60
|
+
function generatePostId(post) {
|
|
61
|
+
if (post.id)
|
|
62
|
+
return post.id;
|
|
63
|
+
if (post.platform === 'twitter') {
|
|
64
|
+
const tweetId = extractTweetId(post.url);
|
|
65
|
+
return tweetId ? `tw-${tweetId}` : `tw-${hashString(post.url)}`;
|
|
66
|
+
}
|
|
67
|
+
if (post.platform === 'instagram') {
|
|
68
|
+
const igId = extractInstagramId(post.url);
|
|
69
|
+
return igId ? `ig-${igId}` : `ig-${hashString(post.url)}`;
|
|
70
|
+
}
|
|
71
|
+
return `post-${hashString(post.url)}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Simple string hash function
|
|
75
|
+
*/
|
|
76
|
+
function hashString(str) {
|
|
77
|
+
let hash = 0;
|
|
78
|
+
for (let i = 0; i < str.length; i++) {
|
|
79
|
+
const char = str.charCodeAt(i);
|
|
80
|
+
hash = ((hash << 5) - hash) + char;
|
|
81
|
+
hash = hash & hash;
|
|
82
|
+
}
|
|
83
|
+
return Math.abs(hash).toString(36);
|
|
42
84
|
}
|
|
43
85
|
/**
|
|
44
86
|
* Get number of columns based on viewport width
|
|
@@ -67,137 +109,10 @@ const defaultColumnConfig = [
|
|
|
67
109
|
{ columns: 1, minWidth: 0 },
|
|
68
110
|
];
|
|
69
111
|
/**
|
|
70
|
-
*
|
|
71
|
-
*/
|
|
72
|
-
function formatNumber(num) {
|
|
73
|
-
if (num >= 1000000) {
|
|
74
|
-
return `${(num / 1000000).toFixed(1).replace(/\.0$/, '')}M`;
|
|
75
|
-
}
|
|
76
|
-
if (num >= 1000) {
|
|
77
|
-
return `${(num / 1000).toFixed(1).replace(/\.0$/, '')}K`;
|
|
78
|
-
}
|
|
79
|
-
return num.toString();
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Format relative time
|
|
112
|
+
* Default embed heights
|
|
83
113
|
*/
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
87
|
-
if (diffInSeconds < 60) {
|
|
88
|
-
return 'now';
|
|
89
|
-
}
|
|
90
|
-
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
|
91
|
-
if (diffInMinutes < 60) {
|
|
92
|
-
return `${diffInMinutes}m`;
|
|
93
|
-
}
|
|
94
|
-
const diffInHours = Math.floor(diffInMinutes / 60);
|
|
95
|
-
if (diffInHours < 24) {
|
|
96
|
-
return `${diffInHours}h`;
|
|
97
|
-
}
|
|
98
|
-
const diffInDays = Math.floor(diffInHours / 24);
|
|
99
|
-
if (diffInDays < 7) {
|
|
100
|
-
return `${diffInDays}d`;
|
|
101
|
-
}
|
|
102
|
-
const diffInWeeks = Math.floor(diffInDays / 7);
|
|
103
|
-
if (diffInWeeks < 4) {
|
|
104
|
-
return `${diffInWeeks}w`;
|
|
105
|
-
}
|
|
106
|
-
// Format as date for older posts
|
|
107
|
-
return date.toLocaleDateString('en-US', {
|
|
108
|
-
month: 'short',
|
|
109
|
-
day: 'numeric',
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Parse CSS value to pixels
|
|
114
|
-
*/
|
|
115
|
-
function parseCSSValue(value, containerWidth) {
|
|
116
|
-
if (typeof value === 'number') {
|
|
117
|
-
return value;
|
|
118
|
-
}
|
|
119
|
-
const numMatch = value.match(/^([\d.]+)(px|rem|em|%|vw)?$/);
|
|
120
|
-
if (!numMatch) {
|
|
121
|
-
return 0;
|
|
122
|
-
}
|
|
123
|
-
const num = parseFloat(numMatch[1]);
|
|
124
|
-
const unit = numMatch[2] || 'px';
|
|
125
|
-
switch (unit) {
|
|
126
|
-
case 'px':
|
|
127
|
-
return num;
|
|
128
|
-
case 'rem':
|
|
129
|
-
return num * 16; // Assume 16px base
|
|
130
|
-
case 'em':
|
|
131
|
-
return num * 16;
|
|
132
|
-
case '%':
|
|
133
|
-
return 0;
|
|
134
|
-
case 'vw':
|
|
135
|
-
return (num / 100) * window.innerWidth;
|
|
136
|
-
default:
|
|
137
|
-
return num;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Generate unique ID
|
|
142
|
-
*/
|
|
143
|
-
function generateId() {
|
|
144
|
-
return `sm-${Math.random().toString(36).substring(2, 9)}`;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Check if element is in viewport
|
|
148
|
-
*/
|
|
149
|
-
function isInViewport(element, scrollTop, viewportHeight, overscan = 0) {
|
|
150
|
-
const expandedTop = scrollTop - overscan;
|
|
151
|
-
const expandedBottom = scrollTop + viewportHeight + overscan;
|
|
152
|
-
return element.bottom >= expandedTop && element.top <= expandedBottom;
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Get scroll position
|
|
156
|
-
*/
|
|
157
|
-
function getScrollPosition(scrollContainer) {
|
|
158
|
-
if (!scrollContainer || scrollContainer === window) {
|
|
159
|
-
return {
|
|
160
|
-
scrollTop: window.scrollY || document.documentElement.scrollTop,
|
|
161
|
-
viewportHeight: window.innerHeight,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
return {
|
|
165
|
-
scrollTop: scrollContainer.scrollTop,
|
|
166
|
-
viewportHeight: scrollContainer.clientHeight,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Type guard for Twitter posts
|
|
171
|
-
*/
|
|
172
|
-
function isTwitterPost(post) {
|
|
173
|
-
return post.platform === 'twitter';
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Type guard for Instagram posts
|
|
177
|
-
*/
|
|
178
|
-
function isInstagramPost(post) {
|
|
179
|
-
return post.platform === 'instagram';
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Request animation frame with fallback
|
|
183
|
-
*/
|
|
184
|
-
const raf = typeof requestAnimationFrame !== 'undefined'
|
|
185
|
-
? requestAnimationFrame
|
|
186
|
-
: (callback) => setTimeout(callback, 16);
|
|
187
|
-
/**
|
|
188
|
-
* Cancel animation frame with fallback
|
|
189
|
-
*/
|
|
190
|
-
const cancelRaf = typeof cancelAnimationFrame !== 'undefined'
|
|
191
|
-
? cancelAnimationFrame
|
|
192
|
-
: (id) => clearTimeout(id);
|
|
193
|
-
/**
|
|
194
|
-
* Check if we're in a browser environment
|
|
195
|
-
*/
|
|
196
|
-
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
197
|
-
/**
|
|
198
|
-
* Check if ResizeObserver is supported
|
|
199
|
-
*/
|
|
200
|
-
const supportsResizeObserver = isBrowser && typeof ResizeObserver !== 'undefined';
|
|
114
|
+
const DEFAULT_TWITTER_HEIGHT = 500;
|
|
115
|
+
const DEFAULT_INSTAGRAM_HEIGHT = 600;
|
|
201
116
|
|
|
202
117
|
/**
|
|
203
118
|
* Social Masonry - Layout Engine
|
|
@@ -208,11 +123,11 @@ class LayoutEngine {
|
|
|
208
123
|
this.options = {
|
|
209
124
|
gap: 16,
|
|
210
125
|
columns: defaultColumnConfig,
|
|
211
|
-
defaultColumns: 3,
|
|
212
126
|
padding: 0,
|
|
213
127
|
animationDuration: 300,
|
|
214
128
|
animate: true,
|
|
215
|
-
|
|
129
|
+
twitterHeight: DEFAULT_TWITTER_HEIGHT,
|
|
130
|
+
instagramHeight: DEFAULT_INSTAGRAM_HEIGHT,
|
|
216
131
|
...options,
|
|
217
132
|
};
|
|
218
133
|
this.state = {
|
|
@@ -226,9 +141,7 @@ class LayoutEngine {
|
|
|
226
141
|
* Calculate layout for all posts
|
|
227
142
|
*/
|
|
228
143
|
calculate(posts) {
|
|
229
|
-
const { containerWidth, itemHeights } = this.options;
|
|
230
|
-
const gap = parseCSSValue(this.options.gap);
|
|
231
|
-
const padding = parseCSSValue(this.options.padding);
|
|
144
|
+
const { containerWidth, itemHeights, gap, padding } = this.options;
|
|
232
145
|
// Calculate column count and width
|
|
233
146
|
const columnCount = getColumnCount(this.options.columns, containerWidth);
|
|
234
147
|
const availableWidth = containerWidth - padding * 2;
|
|
@@ -239,14 +152,15 @@ class LayoutEngine {
|
|
|
239
152
|
const positions = new Map();
|
|
240
153
|
// Place each item in the shortest column
|
|
241
154
|
for (const post of posts) {
|
|
242
|
-
const
|
|
155
|
+
const postId = generatePostId(post);
|
|
156
|
+
const itemHeight = itemHeights.get(postId) ?? this.getDefaultHeight(post);
|
|
243
157
|
// Find shortest column
|
|
244
158
|
const shortestColumn = columnHeights.indexOf(Math.min(...columnHeights));
|
|
245
159
|
// Calculate position
|
|
246
160
|
const x = padding + shortestColumn * (columnWidth + gap);
|
|
247
161
|
const y = columnHeights[shortestColumn];
|
|
248
|
-
positions.set(
|
|
249
|
-
id:
|
|
162
|
+
positions.set(postId, {
|
|
163
|
+
id: postId,
|
|
250
164
|
x,
|
|
251
165
|
y,
|
|
252
166
|
width: columnWidth,
|
|
@@ -267,43 +181,19 @@ class LayoutEngine {
|
|
|
267
181
|
return this.state;
|
|
268
182
|
}
|
|
269
183
|
/**
|
|
270
|
-
*
|
|
184
|
+
* Get default height for embed based on platform
|
|
271
185
|
*/
|
|
272
|
-
|
|
273
|
-
let height = 0;
|
|
274
|
-
// Header (avatar, name, etc.)
|
|
275
|
-
height += 56;
|
|
276
|
-
// Text content
|
|
186
|
+
getDefaultHeight(post) {
|
|
277
187
|
if (post.platform === 'twitter') {
|
|
278
|
-
|
|
279
|
-
const avgCharsPerLine = Math.floor(columnWidth / 8); // ~8px per char
|
|
280
|
-
const lines = Math.ceil(textLength / avgCharsPerLine);
|
|
281
|
-
height += lines * 24; // ~24px per line
|
|
282
|
-
}
|
|
283
|
-
else if (post.content.caption) {
|
|
284
|
-
const captionLength = post.content.caption.length;
|
|
285
|
-
const avgCharsPerLine = Math.floor(columnWidth / 8);
|
|
286
|
-
const lines = Math.min(Math.ceil(captionLength / avgCharsPerLine), 4); // Max 4 lines
|
|
287
|
-
height += lines * 20;
|
|
288
|
-
}
|
|
289
|
-
// Media
|
|
290
|
-
if (post.platform === 'twitter' && post.media?.length) {
|
|
291
|
-
const media = post.media[0];
|
|
292
|
-
const aspectRatio = media.aspectRatio ?? 16 / 9;
|
|
293
|
-
height += columnWidth / aspectRatio;
|
|
188
|
+
return this.options.twitterHeight;
|
|
294
189
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
height += columnWidth / aspectRatio;
|
|
190
|
+
if (post.platform === 'instagram') {
|
|
191
|
+
return this.options.instagramHeight;
|
|
298
192
|
}
|
|
299
|
-
|
|
300
|
-
height += 44;
|
|
301
|
-
// Padding
|
|
302
|
-
height += 24;
|
|
303
|
-
return Math.round(height);
|
|
193
|
+
return DEFAULT_TWITTER_HEIGHT;
|
|
304
194
|
}
|
|
305
195
|
/**
|
|
306
|
-
* Update single item height
|
|
196
|
+
* Update single item height
|
|
307
197
|
*/
|
|
308
198
|
updateItemHeight(id, height) {
|
|
309
199
|
this.options.itemHeights.set(id, height);
|
|
@@ -332,20 +222,6 @@ class LayoutEngine {
|
|
|
332
222
|
getColumnCount() {
|
|
333
223
|
return getColumnCount(this.options.columns, this.options.containerWidth);
|
|
334
224
|
}
|
|
335
|
-
/**
|
|
336
|
-
* Get CSS variables for animations
|
|
337
|
-
*/
|
|
338
|
-
getCSSVariables() {
|
|
339
|
-
return {
|
|
340
|
-
'--sm-animation-duration': `${this.options.animationDuration}ms`,
|
|
341
|
-
'--sm-easing': this.options.easing,
|
|
342
|
-
'--sm-gap': typeof this.options.gap === 'number'
|
|
343
|
-
? `${this.options.gap}px`
|
|
344
|
-
: this.options.gap,
|
|
345
|
-
'--sm-column-width': `${this.state.columnWidth}px`,
|
|
346
|
-
'--sm-container-height': `${this.state.containerHeight}px`,
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
225
|
}
|
|
350
226
|
/**
|
|
351
227
|
* Create a new layout engine instance
|
|
@@ -354,1142 +230,5 @@ function createLayoutEngine(options) {
|
|
|
354
230
|
return new LayoutEngine(options);
|
|
355
231
|
}
|
|
356
232
|
|
|
357
|
-
|
|
358
|
-
* Social Masonry - Virtualization Engine
|
|
359
|
-
* Handles virtual scrolling for large lists
|
|
360
|
-
*/
|
|
361
|
-
class VirtualizationEngine {
|
|
362
|
-
constructor(options) {
|
|
363
|
-
this.visibleItems = [];
|
|
364
|
-
this.rafId = null;
|
|
365
|
-
this.lastScrollTop = 0;
|
|
366
|
-
this.isScrolling = false;
|
|
367
|
-
this.scrollEndTimeout = null;
|
|
368
|
-
this.options = {
|
|
369
|
-
enabled: true,
|
|
370
|
-
overscan: 3,
|
|
371
|
-
estimatedItemHeight: 400,
|
|
372
|
-
scrollContainer: null,
|
|
373
|
-
...options,
|
|
374
|
-
};
|
|
375
|
-
this.posts = options.posts;
|
|
376
|
-
this.positions = options.positions;
|
|
377
|
-
this.onVisibleItemsChange = options.onVisibleItemsChange;
|
|
378
|
-
this.scrollHandler = throttle(this.handleScroll.bind(this), 16);
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Initialize scroll listener
|
|
382
|
-
*/
|
|
383
|
-
init() {
|
|
384
|
-
if (!this.options.enabled)
|
|
385
|
-
return;
|
|
386
|
-
const container = this.options.scrollContainer ?? window;
|
|
387
|
-
container.addEventListener('scroll', this.scrollHandler, { passive: true });
|
|
388
|
-
// Initial calculation
|
|
389
|
-
this.calculateVisibleItems();
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Destroy and cleanup
|
|
393
|
-
*/
|
|
394
|
-
destroy() {
|
|
395
|
-
const container = this.options.scrollContainer ?? window;
|
|
396
|
-
container.removeEventListener('scroll', this.scrollHandler);
|
|
397
|
-
if (this.rafId !== null) {
|
|
398
|
-
cancelRaf(this.rafId);
|
|
399
|
-
}
|
|
400
|
-
if (this.scrollEndTimeout) {
|
|
401
|
-
clearTimeout(this.scrollEndTimeout);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Handle scroll event
|
|
406
|
-
*/
|
|
407
|
-
handleScroll() {
|
|
408
|
-
if (this.rafId !== null)
|
|
409
|
-
return;
|
|
410
|
-
this.isScrolling = true;
|
|
411
|
-
// Clear existing scroll end timeout
|
|
412
|
-
if (this.scrollEndTimeout) {
|
|
413
|
-
clearTimeout(this.scrollEndTimeout);
|
|
414
|
-
}
|
|
415
|
-
// Set scroll end detection
|
|
416
|
-
this.scrollEndTimeout = setTimeout(() => {
|
|
417
|
-
this.isScrolling = false;
|
|
418
|
-
}, 150);
|
|
419
|
-
this.rafId = raf(() => {
|
|
420
|
-
this.rafId = null;
|
|
421
|
-
this.calculateVisibleItems();
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Calculate which items are visible
|
|
426
|
-
*/
|
|
427
|
-
calculateVisibleItems() {
|
|
428
|
-
const { scrollTop, viewportHeight } = getScrollPosition(this.options.scrollContainer);
|
|
429
|
-
const overscanPx = this.options.overscan * this.options.estimatedItemHeight;
|
|
430
|
-
this.lastScrollTop = scrollTop;
|
|
431
|
-
const newVisibleItems = [];
|
|
432
|
-
for (let i = 0; i < this.posts.length; i++) {
|
|
433
|
-
const post = this.posts[i];
|
|
434
|
-
const position = this.positions.get(post.id);
|
|
435
|
-
if (!position)
|
|
436
|
-
continue;
|
|
437
|
-
const isVisible = isInViewport({
|
|
438
|
-
top: position.y,
|
|
439
|
-
bottom: position.y + position.height,
|
|
440
|
-
}, scrollTop, viewportHeight, overscanPx);
|
|
441
|
-
if (isVisible) {
|
|
442
|
-
newVisibleItems.push({
|
|
443
|
-
index: i,
|
|
444
|
-
post,
|
|
445
|
-
position,
|
|
446
|
-
isVisible: true,
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
// Check if visible items changed
|
|
451
|
-
const hasChanged = this.hasVisibleItemsChanged(newVisibleItems);
|
|
452
|
-
if (hasChanged) {
|
|
453
|
-
this.visibleItems = newVisibleItems;
|
|
454
|
-
this.onVisibleItemsChange?.(newVisibleItems);
|
|
455
|
-
}
|
|
456
|
-
return this.visibleItems;
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* Check if visible items have changed
|
|
460
|
-
*/
|
|
461
|
-
hasVisibleItemsChanged(newItems) {
|
|
462
|
-
if (newItems.length !== this.visibleItems.length) {
|
|
463
|
-
return true;
|
|
464
|
-
}
|
|
465
|
-
for (let i = 0; i < newItems.length; i++) {
|
|
466
|
-
if (newItems[i].post.id !== this.visibleItems[i].post.id) {
|
|
467
|
-
return true;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
return false;
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Update posts and positions
|
|
474
|
-
*/
|
|
475
|
-
update(posts, positions) {
|
|
476
|
-
this.posts = posts;
|
|
477
|
-
this.positions = positions;
|
|
478
|
-
this.calculateVisibleItems();
|
|
479
|
-
}
|
|
480
|
-
/**
|
|
481
|
-
* Get visible items
|
|
482
|
-
*/
|
|
483
|
-
getVisibleItems() {
|
|
484
|
-
return this.visibleItems;
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Get all items (for non-virtualized mode)
|
|
488
|
-
*/
|
|
489
|
-
getAllItems() {
|
|
490
|
-
return this.posts.map((post, index) => ({
|
|
491
|
-
index,
|
|
492
|
-
post,
|
|
493
|
-
position: this.positions.get(post.id),
|
|
494
|
-
isVisible: true,
|
|
495
|
-
}));
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Check if scrolling
|
|
499
|
-
*/
|
|
500
|
-
getIsScrolling() {
|
|
501
|
-
return this.isScrolling;
|
|
502
|
-
}
|
|
503
|
-
/**
|
|
504
|
-
* Get scroll direction
|
|
505
|
-
*/
|
|
506
|
-
getScrollDirection() {
|
|
507
|
-
const { scrollTop } = getScrollPosition(this.options.scrollContainer);
|
|
508
|
-
if (scrollTop > this.lastScrollTop) {
|
|
509
|
-
return 'down';
|
|
510
|
-
}
|
|
511
|
-
else if (scrollTop < this.lastScrollTop) {
|
|
512
|
-
return 'up';
|
|
513
|
-
}
|
|
514
|
-
return 'none';
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Check if near bottom (for infinite scroll)
|
|
518
|
-
*/
|
|
519
|
-
isNearBottom(threshold = 500) {
|
|
520
|
-
const { scrollTop, viewportHeight } = getScrollPosition(this.options.scrollContainer);
|
|
521
|
-
// Get container height from positions
|
|
522
|
-
let maxBottom = 0;
|
|
523
|
-
for (const position of this.positions.values()) {
|
|
524
|
-
maxBottom = Math.max(maxBottom, position.y + position.height);
|
|
525
|
-
}
|
|
526
|
-
return scrollTop + viewportHeight >= maxBottom - threshold;
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* Scroll to item
|
|
530
|
-
*/
|
|
531
|
-
scrollToItem(id, behavior = 'smooth') {
|
|
532
|
-
const position = this.positions.get(id);
|
|
533
|
-
if (!position)
|
|
534
|
-
return;
|
|
535
|
-
const container = this.options.scrollContainer ?? window;
|
|
536
|
-
if (container === window) {
|
|
537
|
-
window.scrollTo({
|
|
538
|
-
top: position.y,
|
|
539
|
-
behavior,
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
container.scrollTo({
|
|
544
|
-
top: position.y,
|
|
545
|
-
behavior,
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Get render range for optimization
|
|
551
|
-
*/
|
|
552
|
-
getRenderRange() {
|
|
553
|
-
if (this.visibleItems.length === 0) {
|
|
554
|
-
return { start: 0, end: 0 };
|
|
555
|
-
}
|
|
556
|
-
const indices = this.visibleItems.map(item => item.index);
|
|
557
|
-
return {
|
|
558
|
-
start: Math.min(...indices),
|
|
559
|
-
end: Math.max(...indices) + 1,
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Create a new virtualization engine instance
|
|
565
|
-
*/
|
|
566
|
-
function createVirtualizationEngine(options) {
|
|
567
|
-
return new VirtualizationEngine(options);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Social Masonry - Card Renderer
|
|
572
|
-
* Renders social media post cards with proper styling
|
|
573
|
-
*/
|
|
574
|
-
const DEFAULT_OPTIONS$1 = {
|
|
575
|
-
variant: 'default',
|
|
576
|
-
theme: 'auto',
|
|
577
|
-
borderRadius: 12,
|
|
578
|
-
showPlatformIcon: true,
|
|
579
|
-
showAuthor: true,
|
|
580
|
-
showMetrics: true,
|
|
581
|
-
showTimestamp: true,
|
|
582
|
-
formatDate: (date) => formatRelativeTime(date),
|
|
583
|
-
formatNumber: formatNumber,
|
|
584
|
-
className: '',
|
|
585
|
-
hoverEffect: true,
|
|
586
|
-
imageLoading: 'lazy',
|
|
587
|
-
fallbackImage: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3Ctext fill="%23999" font-family="sans-serif" font-size="14" x="50%" y="50%" text-anchor="middle" dy=".3em"%3ENo Image%3C/text%3E%3C/svg%3E',
|
|
588
|
-
};
|
|
589
|
-
class CardRenderer {
|
|
590
|
-
constructor(options = {}) {
|
|
591
|
-
this.options = {
|
|
592
|
-
...DEFAULT_OPTIONS$1,
|
|
593
|
-
onPostClick: undefined,
|
|
594
|
-
onAuthorClick: undefined,
|
|
595
|
-
onMediaClick: undefined,
|
|
596
|
-
onImageError: undefined,
|
|
597
|
-
...options,
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* Render a single card
|
|
602
|
-
*/
|
|
603
|
-
render(post, position) {
|
|
604
|
-
const card = document.createElement('div');
|
|
605
|
-
card.className = this.getCardClasses(post);
|
|
606
|
-
card.setAttribute('data-post-id', post.id);
|
|
607
|
-
card.setAttribute('data-platform', post.platform);
|
|
608
|
-
// Apply position styles
|
|
609
|
-
Object.assign(card.style, {
|
|
610
|
-
position: 'absolute',
|
|
611
|
-
left: `${position.x}px`,
|
|
612
|
-
top: `${position.y}px`,
|
|
613
|
-
width: `${position.width}px`,
|
|
614
|
-
borderRadius: typeof this.options.borderRadius === 'number'
|
|
615
|
-
? `${this.options.borderRadius}px`
|
|
616
|
-
: this.options.borderRadius,
|
|
617
|
-
});
|
|
618
|
-
// Build card content
|
|
619
|
-
card.innerHTML = this.buildCardHTML(post);
|
|
620
|
-
// Attach event listeners
|
|
621
|
-
this.attachEventListeners(card, post);
|
|
622
|
-
return card;
|
|
623
|
-
}
|
|
624
|
-
/**
|
|
625
|
-
* Get CSS classes for card
|
|
626
|
-
*/
|
|
627
|
-
getCardClasses(post) {
|
|
628
|
-
const classes = [
|
|
629
|
-
'sm-card',
|
|
630
|
-
`sm-card--${post.platform}`,
|
|
631
|
-
`sm-card--${this.options.variant}`,
|
|
632
|
-
`sm-card--${this.options.theme}`,
|
|
633
|
-
];
|
|
634
|
-
if (this.options.hoverEffect) {
|
|
635
|
-
classes.push('sm-card--hover');
|
|
636
|
-
}
|
|
637
|
-
if (this.options.className) {
|
|
638
|
-
classes.push(this.options.className);
|
|
639
|
-
}
|
|
640
|
-
return classes.join(' ');
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* Build card HTML
|
|
644
|
-
*/
|
|
645
|
-
buildCardHTML(post) {
|
|
646
|
-
const parts = [];
|
|
647
|
-
// Platform icon
|
|
648
|
-
if (this.options.showPlatformIcon) {
|
|
649
|
-
parts.push(this.renderPlatformIcon(post.platform));
|
|
650
|
-
}
|
|
651
|
-
// Header with author
|
|
652
|
-
if (this.options.showAuthor) {
|
|
653
|
-
parts.push(this.renderHeader(post));
|
|
654
|
-
}
|
|
655
|
-
// Content
|
|
656
|
-
parts.push(this.renderContent(post));
|
|
657
|
-
// Media
|
|
658
|
-
parts.push(this.renderMedia(post));
|
|
659
|
-
// Footer with metrics
|
|
660
|
-
if (this.options.showMetrics || this.options.showTimestamp) {
|
|
661
|
-
parts.push(this.renderFooter(post));
|
|
662
|
-
}
|
|
663
|
-
return parts.join('');
|
|
664
|
-
}
|
|
665
|
-
/**
|
|
666
|
-
* Render platform icon
|
|
667
|
-
*/
|
|
668
|
-
renderPlatformIcon(platform) {
|
|
669
|
-
const icons = {
|
|
670
|
-
twitter: `<svg viewBox="0 0 24 24" class="sm-platform-icon sm-platform-icon--twitter"><path fill="currentColor" d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>`,
|
|
671
|
-
instagram: `<svg viewBox="0 0 24 24" class="sm-platform-icon sm-platform-icon--instagram"><path fill="currentColor" d="M12 2c2.717 0 3.056.01 4.122.06 1.065.05 1.79.217 2.428.465.66.254 1.216.598 1.772 1.153a4.908 4.908 0 0 1 1.153 1.772c.247.637.415 1.363.465 2.428.047 1.066.06 1.405.06 4.122 0 2.717-.01 3.056-.06 4.122-.05 1.065-.218 1.79-.465 2.428a4.883 4.883 0 0 1-1.153 1.772 4.915 4.915 0 0 1-1.772 1.153c-.637.247-1.363.415-2.428.465-1.066.047-1.405.06-4.122.06-2.717 0-3.056-.01-4.122-.06-1.065-.05-1.79-.218-2.428-.465a4.89 4.89 0 0 1-1.772-1.153 4.904 4.904 0 0 1-1.153-1.772c-.248-.637-.415-1.363-.465-2.428C2.013 15.056 2 14.717 2 12c0-2.717.01-3.056.06-4.122.05-1.066.217-1.79.465-2.428a4.88 4.88 0 0 1 1.153-1.772A4.897 4.897 0 0 1 5.45 2.525c.638-.248 1.362-.415 2.428-.465C8.944 2.013 9.283 2 12 2zm0 1.802c-2.67 0-2.986.01-4.04.058-.976.045-1.505.207-1.858.344-.466.182-.8.398-1.15.748-.35.35-.566.684-.748 1.15-.137.353-.3.882-.344 1.857-.048 1.055-.058 1.37-.058 4.041 0 2.67.01 2.986.058 4.04.045.976.207 1.505.344 1.858.182.466.399.8.748 1.15.35.35.684.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058 2.67 0 2.987-.01 4.04-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.684.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041 0-2.67-.01-2.986-.058-4.04-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 0 0-.748-1.15 3.098 3.098 0 0 0-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.055-.048-1.37-.058-4.041-.058zm0 3.063a5.135 5.135 0 1 1 0 10.27 5.135 5.135 0 0 1 0-10.27zm0 8.468a3.333 3.333 0 1 0 0-6.666 3.333 3.333 0 0 0 0 6.666zm6.538-8.671a1.2 1.2 0 1 1-2.4 0 1.2 1.2 0 0 1 2.4 0z"/></svg>`,
|
|
672
|
-
};
|
|
673
|
-
return `<div class="sm-card__platform-icon">${icons[platform] || ''}</div>`;
|
|
674
|
-
}
|
|
675
|
-
/**
|
|
676
|
-
* Render card header
|
|
677
|
-
*/
|
|
678
|
-
renderHeader(post) {
|
|
679
|
-
const author = post.author;
|
|
680
|
-
const avatarUrl = author.avatarUrl || this.options.fallbackImage;
|
|
681
|
-
const verifiedBadge = author.verified
|
|
682
|
-
? `<svg class="sm-card__verified" viewBox="0 0 24 24"><path fill="currentColor" d="M22.25 12c0-1.43-.88-2.67-2.19-3.34.46-1.39.2-2.9-.81-3.91s-2.52-1.27-3.91-.81c-.66-1.31-1.91-2.19-3.34-2.19s-2.67.88-3.34 2.19c-1.39-.46-2.9-.2-3.91.81s-1.27 2.52-.81 3.91C2.63 9.33 1.75 10.57 1.75 12s.88 2.67 2.19 3.34c-.46 1.39-.2 2.9.81 3.91s2.52 1.27 3.91.81c.66 1.31 1.91 2.19 3.34 2.19s2.67-.88 3.34-2.19c1.39.46 2.9.2 3.91-.81s1.27-2.52.81-3.91c1.31-.67 2.19-1.91 2.19-3.34zm-11.71 4.2L6.8 12.46l1.41-1.42 2.26 2.26 4.8-5.23 1.47 1.36-6.2 6.77z"/></svg>`
|
|
683
|
-
: '';
|
|
684
|
-
return `
|
|
685
|
-
<div class="sm-card__header">
|
|
686
|
-
<img
|
|
687
|
-
src="${avatarUrl}"
|
|
688
|
-
alt="${author.displayName || author.username}"
|
|
689
|
-
class="sm-card__avatar"
|
|
690
|
-
loading="${this.options.imageLoading}"
|
|
691
|
-
onerror="this.src='${this.options.fallbackImage}'"
|
|
692
|
-
/>
|
|
693
|
-
<div class="sm-card__author">
|
|
694
|
-
<span class="sm-card__author-name">
|
|
695
|
-
${author.displayName || author.username}
|
|
696
|
-
${verifiedBadge}
|
|
697
|
-
</span>
|
|
698
|
-
<span class="sm-card__author-handle">@${author.username}</span>
|
|
699
|
-
</div>
|
|
700
|
-
</div>
|
|
701
|
-
`;
|
|
702
|
-
}
|
|
703
|
-
/**
|
|
704
|
-
* Render card content
|
|
705
|
-
*/
|
|
706
|
-
renderContent(post) {
|
|
707
|
-
if (isTwitterPost(post)) {
|
|
708
|
-
return this.renderTwitterContent(post);
|
|
709
|
-
}
|
|
710
|
-
else if (isInstagramPost(post)) {
|
|
711
|
-
return this.renderInstagramContent(post);
|
|
712
|
-
}
|
|
713
|
-
return '';
|
|
714
|
-
}
|
|
715
|
-
/**
|
|
716
|
-
* Render Twitter content
|
|
717
|
-
*/
|
|
718
|
-
renderTwitterContent(post) {
|
|
719
|
-
const text = post.content.html || this.linkifyText(post.content.text);
|
|
720
|
-
let quotedPost = '';
|
|
721
|
-
if (post.quotedPost) {
|
|
722
|
-
quotedPost = `
|
|
723
|
-
<div class="sm-card__quoted">
|
|
724
|
-
<div class="sm-card__quoted-header">
|
|
725
|
-
<span class="sm-card__quoted-name">${post.quotedPost.author.displayName}</span>
|
|
726
|
-
<span class="sm-card__quoted-handle">@${post.quotedPost.author.username}</span>
|
|
727
|
-
</div>
|
|
728
|
-
<div class="sm-card__quoted-text">${post.quotedPost.content.text}</div>
|
|
729
|
-
</div>
|
|
730
|
-
`;
|
|
731
|
-
}
|
|
732
|
-
return `
|
|
733
|
-
<div class="sm-card__content">
|
|
734
|
-
<p class="sm-card__text">${text}</p>
|
|
735
|
-
${quotedPost}
|
|
736
|
-
</div>
|
|
737
|
-
`;
|
|
738
|
-
}
|
|
739
|
-
/**
|
|
740
|
-
* Render Instagram content
|
|
741
|
-
*/
|
|
742
|
-
renderInstagramContent(post) {
|
|
743
|
-
if (!post.content.caption)
|
|
744
|
-
return '';
|
|
745
|
-
// Truncate caption if too long
|
|
746
|
-
const maxLength = 150;
|
|
747
|
-
let caption = post.content.caption;
|
|
748
|
-
let showMore = false;
|
|
749
|
-
if (caption.length > maxLength) {
|
|
750
|
-
caption = caption.substring(0, maxLength);
|
|
751
|
-
showMore = true;
|
|
752
|
-
}
|
|
753
|
-
return `
|
|
754
|
-
<div class="sm-card__content">
|
|
755
|
-
<p class="sm-card__caption">
|
|
756
|
-
${this.linkifyText(caption)}${showMore ? '<span class="sm-card__more">... more</span>' : ''}
|
|
757
|
-
</p>
|
|
758
|
-
</div>
|
|
759
|
-
`;
|
|
760
|
-
}
|
|
761
|
-
/**
|
|
762
|
-
* Render media
|
|
763
|
-
*/
|
|
764
|
-
renderMedia(post) {
|
|
765
|
-
if (isTwitterPost(post)) {
|
|
766
|
-
return this.renderTwitterMedia(post);
|
|
767
|
-
}
|
|
768
|
-
else if (isInstagramPost(post)) {
|
|
769
|
-
return this.renderInstagramMedia(post);
|
|
770
|
-
}
|
|
771
|
-
return '';
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Render Twitter media
|
|
775
|
-
*/
|
|
776
|
-
renderTwitterMedia(post) {
|
|
777
|
-
if (!post.media?.length)
|
|
778
|
-
return '';
|
|
779
|
-
const mediaCount = post.media.length;
|
|
780
|
-
const gridClass = mediaCount > 1 ? `sm-card__media-grid sm-card__media-grid--${Math.min(mediaCount, 4)}` : '';
|
|
781
|
-
const mediaItems = post.media.slice(0, 4).map((media, index) => {
|
|
782
|
-
if (media.type === 'video' || media.type === 'gif') {
|
|
783
|
-
return `
|
|
784
|
-
<div class="sm-card__media-item sm-card__media-item--video" data-index="${index}">
|
|
785
|
-
<img
|
|
786
|
-
src="${media.thumbnailUrl || media.url}"
|
|
787
|
-
alt="Video thumbnail"
|
|
788
|
-
loading="${this.options.imageLoading}"
|
|
789
|
-
onerror="this.src='${this.options.fallbackImage}'"
|
|
790
|
-
/>
|
|
791
|
-
<div class="sm-card__play-button">
|
|
792
|
-
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>
|
|
793
|
-
</div>
|
|
794
|
-
</div>
|
|
795
|
-
`;
|
|
796
|
-
}
|
|
797
|
-
return `
|
|
798
|
-
<div class="sm-card__media-item" data-index="${index}">
|
|
799
|
-
<img
|
|
800
|
-
src="${media.url}"
|
|
801
|
-
alt="Post media"
|
|
802
|
-
loading="${this.options.imageLoading}"
|
|
803
|
-
onerror="this.src='${this.options.fallbackImage}'"
|
|
804
|
-
/>
|
|
805
|
-
</div>
|
|
806
|
-
`;
|
|
807
|
-
}).join('');
|
|
808
|
-
return `<div class="sm-card__media ${gridClass}">${mediaItems}</div>`;
|
|
809
|
-
}
|
|
810
|
-
/**
|
|
811
|
-
* Render Instagram media
|
|
812
|
-
*/
|
|
813
|
-
renderInstagramMedia(post) {
|
|
814
|
-
const media = post.media;
|
|
815
|
-
const aspectRatio = media.aspectRatio || 1;
|
|
816
|
-
if (media.type === 'carousel' && media.carouselItems?.length) {
|
|
817
|
-
return `
|
|
818
|
-
<div class="sm-card__media sm-card__carousel" style="aspect-ratio: ${aspectRatio}">
|
|
819
|
-
<img
|
|
820
|
-
src="${media.carouselItems[0].url}"
|
|
821
|
-
alt="Post media"
|
|
822
|
-
loading="${this.options.imageLoading}"
|
|
823
|
-
onerror="this.src='${this.options.fallbackImage}'"
|
|
824
|
-
/>
|
|
825
|
-
<div class="sm-card__carousel-indicator">
|
|
826
|
-
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M6 13c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm12 0c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm-6 0c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1z"/></svg>
|
|
827
|
-
</div>
|
|
828
|
-
</div>
|
|
829
|
-
`;
|
|
830
|
-
}
|
|
831
|
-
if (media.type === 'video') {
|
|
832
|
-
return `
|
|
833
|
-
<div class="sm-card__media sm-card__media--video" style="aspect-ratio: ${aspectRatio}">
|
|
834
|
-
<img
|
|
835
|
-
src="${media.thumbnailUrl || media.url}"
|
|
836
|
-
alt="Video thumbnail"
|
|
837
|
-
loading="${this.options.imageLoading}"
|
|
838
|
-
onerror="this.src='${this.options.fallbackImage}'"
|
|
839
|
-
/>
|
|
840
|
-
<div class="sm-card__play-button">
|
|
841
|
-
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>
|
|
842
|
-
</div>
|
|
843
|
-
</div>
|
|
844
|
-
`;
|
|
845
|
-
}
|
|
846
|
-
return `
|
|
847
|
-
<div class="sm-card__media" style="aspect-ratio: ${aspectRatio}">
|
|
848
|
-
<img
|
|
849
|
-
src="${media.url}"
|
|
850
|
-
alt="Post media"
|
|
851
|
-
loading="${this.options.imageLoading}"
|
|
852
|
-
onerror="this.src='${this.options.fallbackImage}'"
|
|
853
|
-
/>
|
|
854
|
-
</div>
|
|
855
|
-
`;
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* Render footer with metrics
|
|
859
|
-
*/
|
|
860
|
-
renderFooter(post) {
|
|
861
|
-
const parts = [];
|
|
862
|
-
if (this.options.showMetrics && post.metrics) {
|
|
863
|
-
parts.push(this.renderMetrics(post));
|
|
864
|
-
}
|
|
865
|
-
if (this.options.showTimestamp) {
|
|
866
|
-
const date = post.createdAt instanceof Date
|
|
867
|
-
? post.createdAt
|
|
868
|
-
: new Date(post.createdAt);
|
|
869
|
-
parts.push(`
|
|
870
|
-
<time class="sm-card__timestamp" datetime="${date.toISOString()}">
|
|
871
|
-
${this.options.formatDate(date)}
|
|
872
|
-
</time>
|
|
873
|
-
`);
|
|
874
|
-
}
|
|
875
|
-
return `<div class="sm-card__footer">${parts.join('')}</div>`;
|
|
876
|
-
}
|
|
877
|
-
/**
|
|
878
|
-
* Render metrics
|
|
879
|
-
*/
|
|
880
|
-
renderMetrics(post) {
|
|
881
|
-
if (!post.metrics)
|
|
882
|
-
return '';
|
|
883
|
-
const formatNum = this.options.formatNumber;
|
|
884
|
-
if (isTwitterPost(post)) {
|
|
885
|
-
const metrics = post.metrics;
|
|
886
|
-
return `
|
|
887
|
-
<div class="sm-card__metrics">
|
|
888
|
-
${metrics.replies !== undefined ? `
|
|
889
|
-
<span class="sm-card__metric">
|
|
890
|
-
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M1.751 10c0-4.42 3.584-8 8.005-8h4.366c4.49 0 8.129 3.64 8.129 8.13 0 2.96-1.607 5.68-4.196 7.11l-8.054 4.46v-3.69h-.067c-4.49.1-8.183-3.51-8.183-8.01zm8.005-6c-3.317 0-6.005 2.69-6.005 6 0 3.37 2.77 6.08 6.138 6.01l.351-.01h1.761v2.3l5.087-2.81c1.951-1.08 3.163-3.13 3.163-5.36 0-3.39-2.744-6.13-6.129-6.13H9.756z"/></svg>
|
|
891
|
-
${formatNum(metrics.replies)}
|
|
892
|
-
</span>
|
|
893
|
-
` : ''}
|
|
894
|
-
${metrics.retweets !== undefined ? `
|
|
895
|
-
<span class="sm-card__metric">
|
|
896
|
-
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M4.5 3.88l4.432 4.14-1.364 1.46L5.5 7.55V16c0 1.1.896 2 2 2H13v2H7.5c-2.209 0-4-1.79-4-4V7.55L1.432 9.48.068 8.02 4.5 3.88zM16.5 6H11V4h5.5c2.209 0 4 1.79 4 4v8.45l2.068-1.93 1.364 1.46-4.432 4.14-4.432-4.14 1.364-1.46 2.068 1.93V8c0-1.1-.896-2-2-2z"/></svg>
|
|
897
|
-
${formatNum(metrics.retweets)}
|
|
898
|
-
</span>
|
|
899
|
-
` : ''}
|
|
900
|
-
${metrics.likes !== undefined ? `
|
|
901
|
-
<span class="sm-card__metric">
|
|
902
|
-
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M16.697 5.5c-1.222-.06-2.679.51-3.89 2.16l-.805 1.09-.806-1.09C9.984 6.01 8.526 5.44 7.304 5.5c-1.243.07-2.349.78-2.91 1.91-.552 1.12-.633 2.78.479 4.82 1.074 1.97 3.257 4.27 7.129 6.61 3.87-2.34 6.052-4.64 7.126-6.61 1.111-2.04 1.03-3.7.477-4.82-.561-1.13-1.666-1.84-2.908-1.91zm4.187 7.69c-1.351 2.48-4.001 5.12-8.379 7.67l-.503.3-.504-.3c-4.379-2.55-7.029-5.19-8.382-7.67-1.36-2.5-1.41-4.86-.514-6.67.887-1.79 2.647-2.91 4.601-3.01 1.651-.09 3.368.56 4.798 2.01 1.429-1.45 3.146-2.1 4.796-2.01 1.954.1 3.714 1.22 4.601 3.01.896 1.81.846 4.17-.514 6.67z"/></svg>
|
|
903
|
-
${formatNum(metrics.likes)}
|
|
904
|
-
</span>
|
|
905
|
-
` : ''}
|
|
906
|
-
${metrics.views !== undefined ? `
|
|
907
|
-
<span class="sm-card__metric">
|
|
908
|
-
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M8.75 21V3h2v18h-2zM18 21V8.5h2V21h-2zM4 21l.004-10h2L6 21H4zm9.248 0v-7h2v7h-2z"/></svg>
|
|
909
|
-
${formatNum(metrics.views)}
|
|
910
|
-
</span>
|
|
911
|
-
` : ''}
|
|
912
|
-
</div>
|
|
913
|
-
`;
|
|
914
|
-
}
|
|
915
|
-
if (isInstagramPost(post)) {
|
|
916
|
-
const metrics = post.metrics;
|
|
917
|
-
return `
|
|
918
|
-
<div class="sm-card__metrics">
|
|
919
|
-
${metrics.likes !== undefined ? `
|
|
920
|
-
<span class="sm-card__metric">
|
|
921
|
-
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M16.792 3.904A4.989 4.989 0 0 1 21.5 9.122c0 3.072-2.652 4.959-5.197 7.222-2.512 2.243-3.865 3.469-4.303 3.752-.477-.309-2.143-1.823-4.303-3.752C5.141 14.072 2.5 12.167 2.5 9.122a4.989 4.989 0 0 1 4.708-5.218 4.21 4.21 0 0 1 3.675 1.941c.84 1.175.98 1.763 1.12 1.763s.278-.588 1.11-1.766a4.17 4.17 0 0 1 3.679-1.938m0-2a6.04 6.04 0 0 0-4.797 2.127 6.052 6.052 0 0 0-4.787-2.127A6.985 6.985 0 0 0 .5 9.122c0 3.61 2.55 5.827 5.015 7.97.283.246.569.494.853.747l1.027.918a44.998 44.998 0 0 0 3.518 3.018 2 2 0 0 0 2.174 0 45.263 45.263 0 0 0 3.626-3.115l.922-.824c.293-.26.59-.519.885-.774 2.334-2.025 4.98-4.32 4.98-7.94a6.985 6.985 0 0 0-6.708-7.218z"/></svg>
|
|
922
|
-
${formatNum(metrics.likes)}
|
|
923
|
-
</span>
|
|
924
|
-
` : ''}
|
|
925
|
-
${metrics.comments !== undefined ? `
|
|
926
|
-
<span class="sm-card__metric">
|
|
927
|
-
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M20.656 17.008a9.993 9.993 0 1 0-3.59 3.615L22 22l-1.344-4.992zM10 12a1 1 0 1 1 1 1 1 1 0 0 1-1-1zm-4 0a1 1 0 1 1 1 1 1 1 0 0 1-1-1zm8 0a1 1 0 1 1 1 1 1 1 0 0 1-1-1z"/></svg>
|
|
928
|
-
${formatNum(metrics.comments)}
|
|
929
|
-
</span>
|
|
930
|
-
` : ''}
|
|
931
|
-
</div>
|
|
932
|
-
`;
|
|
933
|
-
}
|
|
934
|
-
return '';
|
|
935
|
-
}
|
|
936
|
-
/**
|
|
937
|
-
* Linkify text (URLs, mentions, hashtags)
|
|
938
|
-
*/
|
|
939
|
-
linkifyText(text) {
|
|
940
|
-
// Escape HTML
|
|
941
|
-
let result = text
|
|
942
|
-
.replace(/&/g, '&')
|
|
943
|
-
.replace(/</g, '<')
|
|
944
|
-
.replace(/>/g, '>');
|
|
945
|
-
// URLs
|
|
946
|
-
result = result.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer" class="sm-card__link">$1</a>');
|
|
947
|
-
// Mentions
|
|
948
|
-
result = result.replace(/@(\w+)/g, '<a href="#" class="sm-card__mention" data-username="$1">@$1</a>');
|
|
949
|
-
// Hashtags
|
|
950
|
-
result = result.replace(/#(\w+)/g, '<a href="#" class="sm-card__hashtag" data-tag="$1">#$1</a>');
|
|
951
|
-
return result;
|
|
952
|
-
}
|
|
953
|
-
/**
|
|
954
|
-
* Attach event listeners
|
|
955
|
-
*/
|
|
956
|
-
attachEventListeners(card, post) {
|
|
957
|
-
// Post click
|
|
958
|
-
if (this.options.onPostClick) {
|
|
959
|
-
card.addEventListener('click', (e) => {
|
|
960
|
-
// Don't fire for links, buttons, etc.
|
|
961
|
-
const target = e.target;
|
|
962
|
-
if (target.tagName === 'A' || target.closest('a'))
|
|
963
|
-
return;
|
|
964
|
-
this.options.onPostClick?.(post, e);
|
|
965
|
-
});
|
|
966
|
-
card.style.cursor = 'pointer';
|
|
967
|
-
}
|
|
968
|
-
// Author click
|
|
969
|
-
if (this.options.onAuthorClick) {
|
|
970
|
-
const header = card.querySelector('.sm-card__header');
|
|
971
|
-
if (header) {
|
|
972
|
-
header.addEventListener('click', (e) => {
|
|
973
|
-
e.stopPropagation();
|
|
974
|
-
this.options.onAuthorClick?.(post, e);
|
|
975
|
-
});
|
|
976
|
-
header.style.cursor = 'pointer';
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
// Media click
|
|
980
|
-
if (this.options.onMediaClick) {
|
|
981
|
-
const mediaItems = card.querySelectorAll('.sm-card__media-item');
|
|
982
|
-
mediaItems.forEach((item) => {
|
|
983
|
-
item.addEventListener('click', (e) => {
|
|
984
|
-
e.stopPropagation();
|
|
985
|
-
const index = parseInt(item.dataset.index || '0', 10);
|
|
986
|
-
this.options.onMediaClick?.(post, index, e);
|
|
987
|
-
});
|
|
988
|
-
item.style.cursor = 'pointer';
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
// Image error handling
|
|
992
|
-
if (this.options.onImageError) {
|
|
993
|
-
const images = card.querySelectorAll('img');
|
|
994
|
-
images.forEach((img) => {
|
|
995
|
-
img.addEventListener('error', () => {
|
|
996
|
-
this.options.onImageError?.(post, new Error('Image failed to load'));
|
|
997
|
-
});
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
/**
|
|
1002
|
-
* Update card position
|
|
1003
|
-
*/
|
|
1004
|
-
updatePosition(card, position) {
|
|
1005
|
-
Object.assign(card.style, {
|
|
1006
|
-
left: `${position.x}px`,
|
|
1007
|
-
top: `${position.y}px`,
|
|
1008
|
-
width: `${position.width}px`,
|
|
1009
|
-
});
|
|
1010
|
-
}
|
|
1011
|
-
/**
|
|
1012
|
-
* Update options
|
|
1013
|
-
*/
|
|
1014
|
-
setOptions(options) {
|
|
1015
|
-
this.options = { ...this.options, ...options };
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
/**
|
|
1019
|
-
* Create a new card renderer instance
|
|
1020
|
-
*/
|
|
1021
|
-
function createCardRenderer(options) {
|
|
1022
|
-
return new CardRenderer(options);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
/**
|
|
1026
|
-
* Social Masonry - Main Class
|
|
1027
|
-
* Beautiful masonry layout for X (Twitter) and Instagram embeds
|
|
1028
|
-
*/
|
|
1029
|
-
const DEFAULT_OPTIONS = {
|
|
1030
|
-
// Layout
|
|
1031
|
-
gap: 16,
|
|
1032
|
-
columns: defaultColumnConfig,
|
|
1033
|
-
defaultColumns: 3,
|
|
1034
|
-
padding: 0,
|
|
1035
|
-
animationDuration: 300,
|
|
1036
|
-
animate: true,
|
|
1037
|
-
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
1038
|
-
// Card
|
|
1039
|
-
variant: 'default',
|
|
1040
|
-
theme: 'auto',
|
|
1041
|
-
borderRadius: 12,
|
|
1042
|
-
showPlatformIcon: true,
|
|
1043
|
-
showAuthor: true,
|
|
1044
|
-
showMetrics: true,
|
|
1045
|
-
showTimestamp: true,
|
|
1046
|
-
hoverEffect: true,
|
|
1047
|
-
imageLoading: 'lazy',
|
|
1048
|
-
// Virtualization
|
|
1049
|
-
virtualization: {
|
|
1050
|
-
enabled: false,
|
|
1051
|
-
overscan: 3,
|
|
1052
|
-
estimatedItemHeight: 400,
|
|
1053
|
-
scrollContainer: null,
|
|
1054
|
-
},
|
|
1055
|
-
// Infinite scroll
|
|
1056
|
-
loadMoreThreshold: 500,
|
|
1057
|
-
showLoading: true,
|
|
1058
|
-
// Debug
|
|
1059
|
-
debug: false,
|
|
1060
|
-
};
|
|
1061
|
-
class SocialMasonry {
|
|
1062
|
-
constructor(options) {
|
|
1063
|
-
this.posts = [];
|
|
1064
|
-
this.virtualizationEngine = null;
|
|
1065
|
-
this.cards = new Map();
|
|
1066
|
-
this.itemHeights = new Map();
|
|
1067
|
-
this.resizeObserver = null;
|
|
1068
|
-
this.isLoading = false;
|
|
1069
|
-
if (!isBrowser) {
|
|
1070
|
-
throw new Error('SocialMasonry requires a browser environment');
|
|
1071
|
-
}
|
|
1072
|
-
this.instanceId = generateId();
|
|
1073
|
-
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
1074
|
-
// Get container element
|
|
1075
|
-
const container = typeof options.container === 'string'
|
|
1076
|
-
? document.querySelector(options.container)
|
|
1077
|
-
: options.container;
|
|
1078
|
-
if (!container) {
|
|
1079
|
-
throw new Error('Container element not found');
|
|
1080
|
-
}
|
|
1081
|
-
this.container = container;
|
|
1082
|
-
this.posts = options.posts || [];
|
|
1083
|
-
// Initialize layout engine
|
|
1084
|
-
this.layoutEngine = new LayoutEngine({
|
|
1085
|
-
...this.options,
|
|
1086
|
-
containerWidth: this.container.clientWidth,
|
|
1087
|
-
itemHeights: this.itemHeights,
|
|
1088
|
-
});
|
|
1089
|
-
// Initialize card renderer
|
|
1090
|
-
this.cardRenderer = new CardRenderer({
|
|
1091
|
-
...this.options,
|
|
1092
|
-
});
|
|
1093
|
-
// Initialize virtualization if enabled
|
|
1094
|
-
if (this.options.virtualization?.enabled) {
|
|
1095
|
-
this.initVirtualization();
|
|
1096
|
-
}
|
|
1097
|
-
// Setup
|
|
1098
|
-
this.setupContainer();
|
|
1099
|
-
this.setupResizeObserver();
|
|
1100
|
-
this.render();
|
|
1101
|
-
if (this.options.debug) {
|
|
1102
|
-
console.log('[SocialMasonry] Initialized', {
|
|
1103
|
-
instanceId: this.instanceId,
|
|
1104
|
-
posts: this.posts.length,
|
|
1105
|
-
containerWidth: this.container.clientWidth,
|
|
1106
|
-
});
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
/**
|
|
1110
|
-
* Setup container styles
|
|
1111
|
-
*/
|
|
1112
|
-
setupContainer() {
|
|
1113
|
-
this.container.classList.add('sm-container');
|
|
1114
|
-
this.container.setAttribute('data-sm-instance', this.instanceId);
|
|
1115
|
-
Object.assign(this.container.style, {
|
|
1116
|
-
position: 'relative',
|
|
1117
|
-
width: '100%',
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
/**
|
|
1121
|
-
* Setup resize observer
|
|
1122
|
-
*/
|
|
1123
|
-
setupResizeObserver() {
|
|
1124
|
-
if (!supportsResizeObserver) {
|
|
1125
|
-
// Fallback to window resize
|
|
1126
|
-
window.addEventListener('resize', debounce(() => this.handleResize(), 150));
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
this.resizeObserver = new ResizeObserver(debounce((entries) => {
|
|
1130
|
-
for (const entry of entries) {
|
|
1131
|
-
if (entry.target === this.container) {
|
|
1132
|
-
this.handleResize();
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
}, 150));
|
|
1136
|
-
this.resizeObserver.observe(this.container);
|
|
1137
|
-
}
|
|
1138
|
-
/**
|
|
1139
|
-
* Handle container resize
|
|
1140
|
-
*/
|
|
1141
|
-
handleResize() {
|
|
1142
|
-
const newWidth = this.container.clientWidth;
|
|
1143
|
-
this.layoutEngine.setContainerWidth(newWidth);
|
|
1144
|
-
this.recalculateLayout();
|
|
1145
|
-
if (this.options.debug) {
|
|
1146
|
-
console.log('[SocialMasonry] Resize', { width: newWidth });
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
/**
|
|
1150
|
-
* Initialize virtualization
|
|
1151
|
-
*/
|
|
1152
|
-
initVirtualization() {
|
|
1153
|
-
const state = this.layoutEngine.getState();
|
|
1154
|
-
this.virtualizationEngine = new VirtualizationEngine({
|
|
1155
|
-
...this.options.virtualization,
|
|
1156
|
-
posts: this.posts,
|
|
1157
|
-
positions: state.positions,
|
|
1158
|
-
onVisibleItemsChange: (items) => this.handleVisibleItemsChange(items),
|
|
1159
|
-
});
|
|
1160
|
-
this.virtualizationEngine.init();
|
|
1161
|
-
// Setup infinite scroll
|
|
1162
|
-
if (this.options.onLoadMore) {
|
|
1163
|
-
this.setupInfiniteScroll();
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
/**
|
|
1167
|
-
* Handle visible items change (for virtualization)
|
|
1168
|
-
*/
|
|
1169
|
-
handleVisibleItemsChange(items) {
|
|
1170
|
-
const visibleIds = new Set(items.map(item => item.post.id));
|
|
1171
|
-
// Remove cards that are no longer visible
|
|
1172
|
-
for (const [id, card] of this.cards) {
|
|
1173
|
-
if (!visibleIds.has(id)) {
|
|
1174
|
-
card.remove();
|
|
1175
|
-
this.cards.delete(id);
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
// Add new visible cards
|
|
1179
|
-
for (const item of items) {
|
|
1180
|
-
if (!this.cards.has(item.post.id)) {
|
|
1181
|
-
this.renderCard(item.post, item.position);
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Setup infinite scroll
|
|
1187
|
-
*/
|
|
1188
|
-
setupInfiniteScroll() {
|
|
1189
|
-
const checkLoadMore = () => {
|
|
1190
|
-
if (!this.isLoading &&
|
|
1191
|
-
this.virtualizationEngine?.isNearBottom(this.options.loadMoreThreshold || 500)) {
|
|
1192
|
-
this.loadMore();
|
|
1193
|
-
}
|
|
1194
|
-
};
|
|
1195
|
-
const scrollContainer = this.options.virtualization?.scrollContainer ?? window;
|
|
1196
|
-
scrollContainer.addEventListener('scroll', checkLoadMore, { passive: true });
|
|
1197
|
-
}
|
|
1198
|
-
/**
|
|
1199
|
-
* Load more posts
|
|
1200
|
-
*/
|
|
1201
|
-
async loadMore() {
|
|
1202
|
-
if (this.isLoading || !this.options.onLoadMore)
|
|
1203
|
-
return;
|
|
1204
|
-
this.isLoading = true;
|
|
1205
|
-
this.showLoadingIndicator();
|
|
1206
|
-
try {
|
|
1207
|
-
await this.options.onLoadMore();
|
|
1208
|
-
}
|
|
1209
|
-
finally {
|
|
1210
|
-
this.isLoading = false;
|
|
1211
|
-
this.hideLoadingIndicator();
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
/**
|
|
1215
|
-
* Show loading indicator
|
|
1216
|
-
*/
|
|
1217
|
-
showLoadingIndicator() {
|
|
1218
|
-
if (!this.options.showLoading)
|
|
1219
|
-
return;
|
|
1220
|
-
let loader = this.container.querySelector('.sm-loader');
|
|
1221
|
-
if (!loader) {
|
|
1222
|
-
loader = document.createElement('div');
|
|
1223
|
-
loader.className = 'sm-loader';
|
|
1224
|
-
if (this.options.loadingElement) {
|
|
1225
|
-
if (typeof this.options.loadingElement === 'string') {
|
|
1226
|
-
loader.innerHTML = this.options.loadingElement;
|
|
1227
|
-
}
|
|
1228
|
-
else {
|
|
1229
|
-
loader.appendChild(this.options.loadingElement.cloneNode(true));
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
else {
|
|
1233
|
-
loader.innerHTML = `
|
|
1234
|
-
<div class="sm-loader__spinner">
|
|
1235
|
-
<div></div><div></div><div></div>
|
|
1236
|
-
</div>
|
|
1237
|
-
`;
|
|
1238
|
-
}
|
|
1239
|
-
this.container.appendChild(loader);
|
|
1240
|
-
}
|
|
1241
|
-
loader.style.display = 'flex';
|
|
1242
|
-
}
|
|
1243
|
-
/**
|
|
1244
|
-
* Hide loading indicator
|
|
1245
|
-
*/
|
|
1246
|
-
hideLoadingIndicator() {
|
|
1247
|
-
const loader = this.container.querySelector('.sm-loader');
|
|
1248
|
-
if (loader) {
|
|
1249
|
-
loader.style.display = 'none';
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
/**
|
|
1253
|
-
* Render all posts
|
|
1254
|
-
*/
|
|
1255
|
-
render() {
|
|
1256
|
-
// Calculate layout
|
|
1257
|
-
const state = this.layoutEngine.calculate(this.posts);
|
|
1258
|
-
// Update container height
|
|
1259
|
-
this.container.style.height = `${state.containerHeight}px`;
|
|
1260
|
-
// Apply CSS variables
|
|
1261
|
-
const cssVars = this.layoutEngine.getCSSVariables();
|
|
1262
|
-
for (const [key, value] of Object.entries(cssVars)) {
|
|
1263
|
-
this.container.style.setProperty(key, value);
|
|
1264
|
-
}
|
|
1265
|
-
// Render cards
|
|
1266
|
-
if (this.virtualizationEngine) {
|
|
1267
|
-
// Virtualized rendering
|
|
1268
|
-
this.virtualizationEngine.update(this.posts, state.positions);
|
|
1269
|
-
const visibleItems = this.virtualizationEngine.calculateVisibleItems();
|
|
1270
|
-
this.handleVisibleItemsChange(visibleItems);
|
|
1271
|
-
}
|
|
1272
|
-
else {
|
|
1273
|
-
// Full rendering
|
|
1274
|
-
for (const post of this.posts) {
|
|
1275
|
-
const position = state.positions.get(post.id);
|
|
1276
|
-
if (position) {
|
|
1277
|
-
this.renderCard(post, position);
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
// Show empty state if no posts
|
|
1282
|
-
if (this.posts.length === 0) {
|
|
1283
|
-
this.showEmptyState();
|
|
1284
|
-
}
|
|
1285
|
-
else {
|
|
1286
|
-
this.hideEmptyState();
|
|
1287
|
-
}
|
|
1288
|
-
// Callback
|
|
1289
|
-
this.options.onLayoutComplete?.(Array.from(state.positions.values()));
|
|
1290
|
-
}
|
|
1291
|
-
/**
|
|
1292
|
-
* Render a single card
|
|
1293
|
-
*/
|
|
1294
|
-
renderCard(post, position) {
|
|
1295
|
-
let card = this.cards.get(post.id);
|
|
1296
|
-
if (card) {
|
|
1297
|
-
// Update existing card position
|
|
1298
|
-
this.cardRenderer.updatePosition(card, position);
|
|
1299
|
-
}
|
|
1300
|
-
else {
|
|
1301
|
-
// Create new card
|
|
1302
|
-
card = this.cardRenderer.render(post, position);
|
|
1303
|
-
this.container.appendChild(card);
|
|
1304
|
-
this.cards.set(post.id, card);
|
|
1305
|
-
// Measure actual height after render
|
|
1306
|
-
requestAnimationFrame(() => {
|
|
1307
|
-
if (card) {
|
|
1308
|
-
const actualHeight = card.offsetHeight;
|
|
1309
|
-
if (actualHeight !== position.height) {
|
|
1310
|
-
this.itemHeights.set(post.id, actualHeight);
|
|
1311
|
-
this.recalculateLayout();
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
});
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
/**
|
|
1318
|
-
* Recalculate layout
|
|
1319
|
-
*/
|
|
1320
|
-
recalculateLayout() {
|
|
1321
|
-
const state = this.layoutEngine.calculate(this.posts);
|
|
1322
|
-
this.container.style.height = `${state.containerHeight}px`;
|
|
1323
|
-
// Update virtualization
|
|
1324
|
-
if (this.virtualizationEngine) {
|
|
1325
|
-
this.virtualizationEngine.update(this.posts, state.positions);
|
|
1326
|
-
}
|
|
1327
|
-
// Update card positions with animation
|
|
1328
|
-
for (const [id, card] of this.cards) {
|
|
1329
|
-
const position = state.positions.get(id);
|
|
1330
|
-
if (position) {
|
|
1331
|
-
if (this.options.animate) {
|
|
1332
|
-
card.style.transition = `left ${this.options.animationDuration}ms ${this.options.easing}, top ${this.options.animationDuration}ms ${this.options.easing}, width ${this.options.animationDuration}ms ${this.options.easing}`;
|
|
1333
|
-
}
|
|
1334
|
-
this.cardRenderer.updatePosition(card, position);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
this.options.onLayoutComplete?.(Array.from(state.positions.values()));
|
|
1338
|
-
}
|
|
1339
|
-
/**
|
|
1340
|
-
* Show empty state
|
|
1341
|
-
*/
|
|
1342
|
-
showEmptyState() {
|
|
1343
|
-
let empty = this.container.querySelector('.sm-empty');
|
|
1344
|
-
if (!empty) {
|
|
1345
|
-
empty = document.createElement('div');
|
|
1346
|
-
empty.className = 'sm-empty';
|
|
1347
|
-
if (this.options.emptyElement) {
|
|
1348
|
-
if (typeof this.options.emptyElement === 'string') {
|
|
1349
|
-
empty.innerHTML = this.options.emptyElement;
|
|
1350
|
-
}
|
|
1351
|
-
else {
|
|
1352
|
-
empty.appendChild(this.options.emptyElement.cloneNode(true));
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
else {
|
|
1356
|
-
empty.textContent = this.options.emptyMessage || 'No posts to display';
|
|
1357
|
-
}
|
|
1358
|
-
this.container.appendChild(empty);
|
|
1359
|
-
}
|
|
1360
|
-
empty.style.display = 'flex';
|
|
1361
|
-
}
|
|
1362
|
-
/**
|
|
1363
|
-
* Hide empty state
|
|
1364
|
-
*/
|
|
1365
|
-
hideEmptyState() {
|
|
1366
|
-
const empty = this.container.querySelector('.sm-empty');
|
|
1367
|
-
if (empty) {
|
|
1368
|
-
empty.style.display = 'none';
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
// ============================================
|
|
1372
|
-
// Public API
|
|
1373
|
-
// ============================================
|
|
1374
|
-
/**
|
|
1375
|
-
* Add posts
|
|
1376
|
-
*/
|
|
1377
|
-
addPosts(posts) {
|
|
1378
|
-
this.posts = [...this.posts, ...posts];
|
|
1379
|
-
this.render();
|
|
1380
|
-
if (this.options.debug) {
|
|
1381
|
-
console.log('[SocialMasonry] Added posts', { count: posts.length, total: this.posts.length });
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
/**
|
|
1385
|
-
* Set posts (replace all)
|
|
1386
|
-
*/
|
|
1387
|
-
setPosts(posts) {
|
|
1388
|
-
// Clear existing cards
|
|
1389
|
-
for (const card of this.cards.values()) {
|
|
1390
|
-
card.remove();
|
|
1391
|
-
}
|
|
1392
|
-
this.cards.clear();
|
|
1393
|
-
this.itemHeights.clear();
|
|
1394
|
-
this.posts = posts;
|
|
1395
|
-
this.render();
|
|
1396
|
-
if (this.options.debug) {
|
|
1397
|
-
console.log('[SocialMasonry] Set posts', { count: posts.length });
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
/**
|
|
1401
|
-
* Remove post
|
|
1402
|
-
*/
|
|
1403
|
-
removePost(id) {
|
|
1404
|
-
const card = this.cards.get(id);
|
|
1405
|
-
if (card) {
|
|
1406
|
-
card.remove();
|
|
1407
|
-
this.cards.delete(id);
|
|
1408
|
-
}
|
|
1409
|
-
this.itemHeights.delete(id);
|
|
1410
|
-
this.posts = this.posts.filter(p => p.id !== id);
|
|
1411
|
-
this.recalculateLayout();
|
|
1412
|
-
if (this.options.debug) {
|
|
1413
|
-
console.log('[SocialMasonry] Removed post', { id });
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
/**
|
|
1417
|
-
* Update options
|
|
1418
|
-
*/
|
|
1419
|
-
setOptions(options) {
|
|
1420
|
-
this.options = { ...this.options, ...options };
|
|
1421
|
-
this.cardRenderer.setOptions(options);
|
|
1422
|
-
this.recalculateLayout();
|
|
1423
|
-
}
|
|
1424
|
-
/**
|
|
1425
|
-
* Get layout state
|
|
1426
|
-
*/
|
|
1427
|
-
getLayoutState() {
|
|
1428
|
-
return this.layoutEngine.getState();
|
|
1429
|
-
}
|
|
1430
|
-
/**
|
|
1431
|
-
* Get posts
|
|
1432
|
-
*/
|
|
1433
|
-
getPosts() {
|
|
1434
|
-
return [...this.posts];
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* Scroll to post
|
|
1438
|
-
*/
|
|
1439
|
-
scrollToPost(id, behavior = 'smooth') {
|
|
1440
|
-
if (this.virtualizationEngine) {
|
|
1441
|
-
this.virtualizationEngine.scrollToItem(id, behavior);
|
|
1442
|
-
}
|
|
1443
|
-
else {
|
|
1444
|
-
const position = this.layoutEngine.getPosition(id);
|
|
1445
|
-
if (position) {
|
|
1446
|
-
window.scrollTo({
|
|
1447
|
-
top: position.y,
|
|
1448
|
-
behavior,
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
/**
|
|
1454
|
-
* Refresh layout
|
|
1455
|
-
*/
|
|
1456
|
-
refresh() {
|
|
1457
|
-
this.itemHeights.clear();
|
|
1458
|
-
this.recalculateLayout();
|
|
1459
|
-
}
|
|
1460
|
-
/**
|
|
1461
|
-
* Destroy instance
|
|
1462
|
-
*/
|
|
1463
|
-
destroy() {
|
|
1464
|
-
// Remove cards
|
|
1465
|
-
for (const card of this.cards.values()) {
|
|
1466
|
-
card.remove();
|
|
1467
|
-
}
|
|
1468
|
-
this.cards.clear();
|
|
1469
|
-
// Remove observers
|
|
1470
|
-
this.resizeObserver?.disconnect();
|
|
1471
|
-
// Destroy virtualization
|
|
1472
|
-
this.virtualizationEngine?.destroy();
|
|
1473
|
-
// Clean up container
|
|
1474
|
-
this.container.classList.remove('sm-container');
|
|
1475
|
-
this.container.removeAttribute('data-sm-instance');
|
|
1476
|
-
this.container.style.cssText = '';
|
|
1477
|
-
// Remove internal elements
|
|
1478
|
-
const loader = this.container.querySelector('.sm-loader');
|
|
1479
|
-
const empty = this.container.querySelector('.sm-empty');
|
|
1480
|
-
loader?.remove();
|
|
1481
|
-
empty?.remove();
|
|
1482
|
-
if (this.options.debug) {
|
|
1483
|
-
console.log('[SocialMasonry] Destroyed', { instanceId: this.instanceId });
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
/**
|
|
1488
|
-
* Create a new SocialMasonry instance
|
|
1489
|
-
*/
|
|
1490
|
-
function createSocialMasonry(options) {
|
|
1491
|
-
return new SocialMasonry(options);
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
export { CardRenderer, LayoutEngine, SocialMasonry, VirtualizationEngine, createCardRenderer, createLayoutEngine, createSocialMasonry, createVirtualizationEngine, SocialMasonry as default, defaultColumnConfig, formatNumber, formatRelativeTime, isInstagramPost, isTwitterPost };
|
|
233
|
+
export { DEFAULT_INSTAGRAM_HEIGHT, DEFAULT_TWITTER_HEIGHT, LayoutEngine, createLayoutEngine, defaultColumnConfig, detectPlatform, extractInstagramId, extractTweetId, generatePostId, getColumnCount, getInstagramEmbedUrl, getTwitterEmbedUrl };
|
|
1495
234
|
//# sourceMappingURL=index.esm.js.map
|