@michaelyagi/shoji 0.1.0-alpha.6

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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/esm/core/EventBus.d.ts +16 -0
  4. package/dist/esm/core/FocusTrap.d.ts +9 -0
  5. package/dist/esm/core/Gallery.d.ts +221 -0
  6. package/dist/esm/core/GestureController.d.ts +50 -0
  7. package/dist/esm/core/LiveRegion.d.ts +6 -0
  8. package/dist/esm/core/SlideManager.d.ts +83 -0
  9. package/dist/esm/core/bodyScrollLock.d.ts +2 -0
  10. package/dist/esm/core/dom.d.ts +27 -0
  11. package/dist/esm/core/icons.d.ts +6 -0
  12. package/dist/esm/core/index.d.ts +5 -0
  13. package/dist/esm/core/index.js +1966 -0
  14. package/dist/esm/core/index.js.map +1 -0
  15. package/dist/esm/core/plugin.d.ts +54 -0
  16. package/dist/esm/core/rotateFlipNormalize.d.ts +19 -0
  17. package/dist/esm/core/scan.d.ts +8 -0
  18. package/dist/esm/core/types.d.ts +268 -0
  19. package/dist/esm/core/zoomTransition.d.ts +33 -0
  20. package/dist/esm/gestures/GestureEngine.d.ts +78 -0
  21. package/dist/esm/index.css +475 -0
  22. package/dist/esm/index.d.ts +32 -0
  23. package/dist/esm/index.js +24 -0
  24. package/dist/esm/index.js.map +1 -0
  25. package/dist/esm/index2.css +22 -0
  26. package/dist/esm/index3.css +166 -0
  27. package/dist/esm/index4.css +27 -0
  28. package/dist/esm/plugins/activeThumbnail/index.d.ts +26 -0
  29. package/dist/esm/plugins/activeThumbnail/index.js +48 -0
  30. package/dist/esm/plugins/activeThumbnail/index.js.map +1 -0
  31. package/dist/esm/plugins/autoplay/icons.d.ts +3 -0
  32. package/dist/esm/plugins/autoplay/index.d.ts +17 -0
  33. package/dist/esm/plugins/autoplay/index.js +177 -0
  34. package/dist/esm/plugins/autoplay/index.js.map +1 -0
  35. package/dist/esm/plugins/fullscreen/icons.d.ts +3 -0
  36. package/dist/esm/plugins/fullscreen/index.d.ts +17 -0
  37. package/dist/esm/plugins/fullscreen/index.js +74 -0
  38. package/dist/esm/plugins/fullscreen/index.js.map +1 -0
  39. package/dist/esm/plugins/layout/index.d.ts +191 -0
  40. package/dist/esm/plugins/layout/index.js +746 -0
  41. package/dist/esm/plugins/layout/index.js.map +1 -0
  42. package/dist/esm/plugins/layout/justified.d.ts +68 -0
  43. package/dist/esm/plugins/layout/masonry.d.ts +92 -0
  44. package/dist/esm/plugins/rotateFlip/icons.d.ts +5 -0
  45. package/dist/esm/plugins/rotateFlip/index.d.ts +17 -0
  46. package/dist/esm/plugins/rotateFlip/index.js +138 -0
  47. package/dist/esm/plugins/rotateFlip/index.js.map +1 -0
  48. package/dist/esm/plugins/video/index.d.ts +13 -0
  49. package/dist/esm/plugins/video/index.js +95 -0
  50. package/dist/esm/plugins/video/index.js.map +1 -0
  51. package/dist/esm/plugins/video/youtube.d.ts +61 -0
  52. package/dist/esm/plugins/zoom/icons.d.ts +4 -0
  53. package/dist/esm/plugins/zoom/index.d.ts +30 -0
  54. package/dist/esm/plugins/zoom/index.js +274 -0
  55. package/dist/esm/plugins/zoom/index.js.map +1 -0
  56. package/dist/esm/plugins/zoom/zoomMath.d.ts +24 -0
  57. package/dist/esm/transitions/SlideTransition.d.ts +25 -0
  58. package/dist/esm/transitions/presets.d.ts +21 -0
  59. package/dist/esm/zoomTransition-bbKHpVpA.js +110 -0
  60. package/dist/esm/zoomTransition-bbKHpVpA.js.map +1 -0
  61. package/dist/shoji.css +690 -0
  62. package/dist/shoji.js +3605 -0
  63. package/dist/shoji.js.map +1 -0
  64. package/dist/shoji.min.css +1 -0
  65. package/dist/shoji.min.js +2 -0
  66. package/dist/shoji.min.js.map +1 -0
  67. package/package.json +77 -0
@@ -0,0 +1,746 @@
1
+ function computeColumnCount(options) {
2
+ const { containerWidth, gutterX, columns } = options;
3
+ if (typeof columns === "number") return Math.max(1, Math.floor(columns));
4
+ const target = Math.max(
5
+ 1,
6
+ Math.floor((containerWidth + gutterX) / (options.columnWidth + gutterX))
7
+ );
8
+ const maxAllowedByMin = Math.max(
9
+ 1,
10
+ Math.floor((containerWidth + gutterX) / (options.minColumnWidth + gutterX))
11
+ );
12
+ const minRequiredByMax = Math.max(
13
+ 1,
14
+ Math.ceil((containerWidth + gutterX) / (options.maxColumnWidth + gutterX))
15
+ );
16
+ return Math.max(minRequiredByMax, Math.min(target, maxAllowedByMin));
17
+ }
18
+ function computeColumnWidth(containerWidth, gutterX, columnCount) {
19
+ return (containerWidth - gutterX * (columnCount - 1)) / columnCount;
20
+ }
21
+ function layoutMasonry(tiles, options, startHeights) {
22
+ const columnCount = computeColumnCount(options);
23
+ const colWidth = computeColumnWidth(options.containerWidth, options.gutterX, columnCount);
24
+ const columnHeights = startHeights && startHeights.length === columnCount ? [...startHeights] : new Array(columnCount).fill(0);
25
+ const positions = tiles.map((tile, i) => {
26
+ const colIndex = options.fill === "ordered" ? i % columnCount : indexOfShortest(columnHeights);
27
+ const tileHeight = tile.width > 0 ? colWidth * (tile.height / tile.width) : colWidth;
28
+ const x = colIndex * (colWidth + options.gutterX);
29
+ const y = columnHeights[colIndex];
30
+ columnHeights[colIndex] = y + tileHeight + options.gutterY;
31
+ return { x, y, width: colWidth, height: tileHeight };
32
+ });
33
+ const containerHeight = tiles.length === 0 ? 0 : Math.max(...columnHeights) - options.gutterY;
34
+ return { positions, columnCount, columnWidth: colWidth, columnHeights, containerHeight };
35
+ }
36
+ function indexOfShortest(heights) {
37
+ let shortest = 0;
38
+ for (let i = 1; i < heights.length; i++) {
39
+ if (heights[i] < heights[shortest]) shortest = i;
40
+ }
41
+ return shortest;
42
+ }
43
+ function layoutMasonryHorizontal(tiles, options) {
44
+ const { containerWidth, gutterX, gutterY, rowHeight } = options;
45
+ const positions = new Array(tiles.length);
46
+ let currentY = 0;
47
+ let rowStart = 0;
48
+ let rowWidth = 0;
49
+ let rowCount = 0;
50
+ function tileWidth(tile) {
51
+ return tile.height > 0 ? rowHeight * (tile.width / tile.height) : rowHeight;
52
+ }
53
+ function placeRow(start, endExclusive) {
54
+ let x = 0;
55
+ for (let i = start; i < endExclusive; i++) {
56
+ const w = tileWidth(tiles[i]);
57
+ positions[i] = { x, y: currentY, width: w, height: rowHeight };
58
+ x += w + gutterX;
59
+ }
60
+ rowCount++;
61
+ currentY += rowHeight + gutterY;
62
+ }
63
+ for (let i = 0; i < tiles.length; i++) {
64
+ const w = tileWidth(tiles[i]);
65
+ const countInRow = i - rowStart;
66
+ const widthIfIncluded = rowWidth + (countInRow > 0 ? gutterX : 0) + w;
67
+ if (countInRow > 0 && widthIfIncluded > containerWidth) {
68
+ placeRow(rowStart, i);
69
+ rowStart = i;
70
+ rowWidth = w;
71
+ } else {
72
+ rowWidth = widthIfIncluded;
73
+ }
74
+ }
75
+ if (rowStart < tiles.length) placeRow(rowStart, tiles.length);
76
+ const containerHeight = tiles.length === 0 ? 0 : currentY - gutterY;
77
+ return { positions, rowCount, containerHeight };
78
+ }
79
+ function aspectRatioOf(tile) {
80
+ return tile.width > 0 && tile.height > 0 ? tile.width / tile.height : 1;
81
+ }
82
+ function layoutJustified(tiles, options) {
83
+ const { containerWidth, gutterX, gutterY, rowHeight, minRowHeight, maxRowHeight, lastRow } = options;
84
+ const positions = new Array(tiles.length).fill(null);
85
+ const rows = [];
86
+ let currentY = 0;
87
+ let rowStart = 0;
88
+ let aspectSum = 0;
89
+ function naturalHeightOf(startIndex, endExclusive, sum) {
90
+ const availableWidth = containerWidth - gutterX * (endExclusive - startIndex - 1);
91
+ return availableWidth / sum;
92
+ }
93
+ function placeRow(startIndex, endExclusive, height) {
94
+ let x = 0;
95
+ for (let i = startIndex; i < endExclusive; i++) {
96
+ const width = height * aspectRatioOf(tiles[i]);
97
+ positions[i] = { x, y: currentY, width, height };
98
+ x += width + gutterX;
99
+ }
100
+ rows.push({ start: startIndex, end: endExclusive, y: currentY, height });
101
+ currentY += height + gutterY;
102
+ }
103
+ function resolveScaledRow(startIndex, endExclusive, sum) {
104
+ let end = endExclusive;
105
+ let rowSum = sum;
106
+ while (end - startIndex > 1 && naturalHeightOf(startIndex, end, rowSum) < minRowHeight) {
107
+ end--;
108
+ rowSum -= aspectRatioOf(tiles[end]);
109
+ }
110
+ const natural = naturalHeightOf(startIndex, end, rowSum);
111
+ const height = end - startIndex === 1 && natural < minRowHeight ? natural : Math.min(Math.max(natural, minRowHeight), maxRowHeight);
112
+ placeRow(startIndex, end, height);
113
+ return end;
114
+ }
115
+ for (let i = 0; i < tiles.length; i++) {
116
+ aspectSum += aspectRatioOf(tiles[i]);
117
+ const count = i - rowStart + 1;
118
+ const widthAtTargetHeight = rowHeight * aspectSum + gutterX * (count - 1);
119
+ if (widthAtTargetHeight >= containerWidth) {
120
+ const end = resolveScaledRow(rowStart, i + 1, aspectSum);
121
+ rowStart = end;
122
+ aspectSum = 0;
123
+ for (let j = end; j <= i; j++) aspectSum += aspectRatioOf(tiles[j]);
124
+ }
125
+ }
126
+ if (rowStart < tiles.length) {
127
+ if (lastRow === "justify") {
128
+ let start = rowStart;
129
+ while (start < tiles.length) {
130
+ let sum = 0;
131
+ for (let j = start; j < tiles.length; j++) sum += aspectRatioOf(tiles[j]);
132
+ start = resolveScaledRow(start, tiles.length, sum);
133
+ }
134
+ } else if (lastRow === "left") {
135
+ placeRow(rowStart, tiles.length, rowHeight);
136
+ }
137
+ }
138
+ const containerHeight = tiles.length === 0 ? 0 : Math.max(0, currentY - gutterY);
139
+ return { positions, containerHeight, rows };
140
+ }
141
+ const DEFAULT_ASPECT = { width: 4, height: 3 };
142
+ function resolveGutter(raw) {
143
+ if (raw && typeof raw === "object") {
144
+ const obj = raw;
145
+ return { x: Number(obj.x ?? 8), y: Number(obj.y ?? 8) };
146
+ }
147
+ const n = Number(raw ?? 8);
148
+ return { x: n, y: n };
149
+ }
150
+ function keyOf(item) {
151
+ return item.id ?? item.src;
152
+ }
153
+ function isPureAppend(oldItems, newItems) {
154
+ if (newItems.length <= oldItems.length) return false;
155
+ for (let i = 0; i < oldItems.length; i++) {
156
+ if (keyOf(oldItems[i]) !== keyOf(newItems[i])) return false;
157
+ }
158
+ return true;
159
+ }
160
+ const Layout = {
161
+ name: "layout",
162
+ defaults: {
163
+ type: "justified",
164
+ gutter: 8,
165
+ columns: "auto",
166
+ columnWidth: 240,
167
+ minColumnWidth: 160,
168
+ maxColumnWidth: 480,
169
+ fill: "shortest",
170
+ animate: true,
171
+ autoMeasure: true
172
+ },
173
+ init(ctx) {
174
+ const { gallery } = ctx;
175
+ const type = ctx.options.type ?? "justified";
176
+ const gutter = resolveGutter(ctx.options.gutter);
177
+ const aspectRatio = Number(ctx.options.aspectRatio ?? 1);
178
+ const orientation = ctx.options.orientation ?? "vertical";
179
+ const columns = ctx.options.columns ?? "auto";
180
+ const columnWidth = Number(ctx.options.columnWidth ?? 240);
181
+ const minColumnWidth = Number(ctx.options.minColumnWidth ?? 160);
182
+ const maxColumnWidth = Number(ctx.options.maxColumnWidth ?? 480);
183
+ const fill = ctx.options.fill ?? "shortest";
184
+ const rowHeight = Number(ctx.options.rowHeight ?? 220);
185
+ const minRowHeight = Number(ctx.options.minRowHeight ?? 120);
186
+ const maxRowHeight = Number(ctx.options.maxRowHeight ?? 360);
187
+ const lastRow = ctx.options.lastRow ?? "justify";
188
+ const animate = ctx.options.animate !== false;
189
+ const autoMeasureEnabled = ctx.options.autoMeasure !== false;
190
+ const horizontal = type === "masonry" && orientation === "horizontal";
191
+ const groupByFn = ctx.options.groupBy;
192
+ const renderHeadingFn = ctx.options.renderHeading;
193
+ const headingOverflow = ctx.options.headingOverflow ?? "show";
194
+ const stickyHeadingsRaw = ctx.options.stickyHeadings === true;
195
+ const stickyHeadings = stickyHeadingsRaw && type === "grid";
196
+ if (stickyHeadingsRaw && type !== "grid") {
197
+ console.warn(
198
+ "Shoji: layout plugin — stickyHeadings is only implemented for type: 'grid'; ignored (see the option's doc comment)."
199
+ );
200
+ }
201
+ if (groupByFn && horizontal) {
202
+ console.warn(
203
+ "Shoji: layout plugin — groupBy isn't supported with orientation: 'horizontal'; ignored."
204
+ );
205
+ }
206
+ const groupingEnabled = !!groupByFn && !horizontal;
207
+ const baseConfig = {
208
+ gutter,
209
+ columns,
210
+ columnWidth,
211
+ minColumnWidth,
212
+ maxColumnWidth,
213
+ fill,
214
+ rowHeight,
215
+ minRowHeight,
216
+ maxRowHeight,
217
+ lastRow
218
+ };
219
+ const breakpointsRaw = ctx.options.breakpoints;
220
+ const sortedBreakpoints = breakpointsRaw ? Object.entries(breakpointsRaw).map(([key, overrides]) => [Number(key), overrides]).sort((a, b) => a[0] - b[0]) : [];
221
+ function resolveConfig(width) {
222
+ const match = sortedBreakpoints.find(([maxWidth]) => width <= maxWidth);
223
+ if (!match) return baseConfig;
224
+ const overrides = match[1];
225
+ return {
226
+ gutter: overrides.gutter !== void 0 ? resolveGutter(overrides.gutter) : baseConfig.gutter,
227
+ columns: overrides.columns ?? baseConfig.columns,
228
+ columnWidth: overrides.columnWidth ?? baseConfig.columnWidth,
229
+ minColumnWidth: overrides.minColumnWidth ?? baseConfig.minColumnWidth,
230
+ maxColumnWidth: overrides.maxColumnWidth ?? baseConfig.maxColumnWidth,
231
+ fill: overrides.fill ?? baseConfig.fill,
232
+ rowHeight: overrides.rowHeight ?? baseConfig.rowHeight,
233
+ minRowHeight: overrides.minRowHeight ?? baseConfig.minRowHeight,
234
+ maxRowHeight: overrides.maxRowHeight ?? baseConfig.maxRowHeight,
235
+ lastRow: overrides.lastRow ?? baseConfig.lastRow
236
+ };
237
+ }
238
+ const container = gallery.element;
239
+ container.classList.add("shoji-layout", `shoji-layout--${type}`);
240
+ if (animate) container.classList.add("shoji-layout--animate");
241
+ let tiles = [];
242
+ let headings = [];
243
+ let columnHeights;
244
+ let previousItems = [];
245
+ let warnedMissingDims = false;
246
+ function computeSections(items) {
247
+ const sections = [];
248
+ for (let i = 0; i < items.length; i++) {
249
+ const key = groupByFn(items[i]);
250
+ const current = sections[sections.length - 1];
251
+ if (!current || current.key !== key) {
252
+ sections.push({ key, startIndex: i, endIndex: i + 1 });
253
+ } else {
254
+ current.endIndex = i + 1;
255
+ }
256
+ }
257
+ return sections;
258
+ }
259
+ function createHeadingRoot(key, sectionItems) {
260
+ const rendered = renderHeadingFn ? renderHeadingFn(key, sectionItems) : key;
261
+ let root;
262
+ let structured = null;
263
+ if (typeof rendered === "string") {
264
+ root = document.createElement("h2");
265
+ root.textContent = rendered;
266
+ } else if (rendered instanceof HTMLElement) {
267
+ root = rendered;
268
+ } else {
269
+ root = document.createElement("h2");
270
+ const titleEl = document.createElement("span");
271
+ titleEl.className = "shoji-layout-heading-title";
272
+ titleEl.textContent = rendered.title;
273
+ root.appendChild(titleEl);
274
+ let subtitleEl = null;
275
+ if (rendered.subtitle) {
276
+ subtitleEl = document.createElement("span");
277
+ subtitleEl.className = "shoji-layout-heading-subtitle";
278
+ subtitleEl.textContent = rendered.subtitle;
279
+ root.appendChild(subtitleEl);
280
+ }
281
+ structured = { titleEl, subtitleEl };
282
+ }
283
+ root.classList.add("shoji-layout-heading");
284
+ if (stickyHeadings) root.classList.add("shoji-layout-heading--sticky");
285
+ return { root, structured };
286
+ }
287
+ function headingSegments() {
288
+ return headings.map((heading, i) => ({
289
+ heading,
290
+ start: heading.tileStart,
291
+ end: i + 1 < headings.length ? headings[i + 1].tileStart : tiles.length
292
+ }));
293
+ }
294
+ function measurableSrc(item) {
295
+ return item.thumb ?? item.poster ?? (item.video ? void 0 : item.src);
296
+ }
297
+ function aspectOf(item) {
298
+ if ((item == null ? void 0 : item.width) && item.height) return { width: item.width, height: item.height };
299
+ const selfCorrecting = autoMeasureEnabled && !!item && !!measurableSrc(item);
300
+ if (!warnedMissingDims && !selfCorrecting) {
301
+ warnedMissingDims = true;
302
+ console.warn(
303
+ autoMeasureEnabled ? "Shoji: layout plugin — item(s) missing width/height with no measurable thumb/poster/src to auto-measure from either; falling back to a 4:3 placeholder ratio permanently for those." : "Shoji: layout plugin — item(s) missing width/height and autoMeasure is disabled. Masonry/justified positions are computed from aspect ratios up front, never by measuring loaded images (DESIGN.md §5.2 — that causes layout-jump), so items without dimensions fall back to a 4:3 placeholder ratio instead."
304
+ );
305
+ }
306
+ return DEFAULT_ASPECT;
307
+ }
308
+ function createTile(item, index) {
309
+ const root = document.createElement("a");
310
+ root.className = "shoji-layout-tile";
311
+ if (item.id) root.dataset.shojiId = item.id;
312
+ root.dataset.shojiIndex = String(index);
313
+ const img = document.createElement("img");
314
+ img.src = item.thumb ?? item.poster ?? item.src;
315
+ img.loading = "lazy";
316
+ img.alt = item.alt ?? "";
317
+ root.appendChild(img);
318
+ const tile = { index, root, img };
319
+ root.addEventListener("click", (event) => {
320
+ event.preventDefault();
321
+ gallery.open(tile.index);
322
+ });
323
+ return tile;
324
+ }
325
+ function applyGrid(target) {
326
+ const containerWidth = container.getBoundingClientRect().width;
327
+ const config = resolveConfig(containerWidth);
328
+ container.style.setProperty("--shoji-layout-column-width", `${config.columnWidth}px`);
329
+ container.style.setProperty(
330
+ "--shoji-layout-gutter",
331
+ `${config.gutter.y}px ${config.gutter.x}px`
332
+ );
333
+ for (const tile of target) {
334
+ tile.root.style.setProperty("--shoji-layout-tile-aspect", `${aspectRatio}`);
335
+ }
336
+ }
337
+ function applyMasonry(appendOnly) {
338
+ const containerWidth = container.getBoundingClientRect().width;
339
+ if (containerWidth <= 0) return;
340
+ const config = resolveConfig(containerWidth);
341
+ const masonryOptions = {
342
+ containerWidth,
343
+ gutterX: config.gutter.x,
344
+ gutterY: config.gutter.y,
345
+ columns: config.columns,
346
+ columnWidth: config.columnWidth,
347
+ minColumnWidth: config.minColumnWidth,
348
+ maxColumnWidth: config.maxColumnWidth,
349
+ fill: config.fill
350
+ };
351
+ if (groupingEnabled && headings.length > 0) {
352
+ const segments = headingSegments();
353
+ const headingHeights = segments.map((s) => s.heading.root.getBoundingClientRect().height);
354
+ let y = 0;
355
+ segments.forEach(({ heading, start, end }, i) => {
356
+ const segTiles = tiles.slice(start, end);
357
+ heading.root.style.transform = `translate(0px, ${y}px)`;
358
+ y += headingHeights[i] + config.gutter.y;
359
+ const segResult = layoutMasonry(
360
+ segTiles.map((t) => aspectOf(gallery.items[t.index])),
361
+ masonryOptions
362
+ );
363
+ segTiles.forEach((tile, j) => {
364
+ const pos = segResult.positions[j];
365
+ tile.root.style.width = `${pos.width}px`;
366
+ tile.root.style.height = `${pos.height}px`;
367
+ tile.root.style.visibility = "visible";
368
+ tile.root.style.transform = `translate(${pos.x}px, ${y + pos.y}px)`;
369
+ });
370
+ y += segResult.containerHeight + config.gutter.y;
371
+ });
372
+ columnHeights = void 0;
373
+ container.style.height = `${Math.max(0, y - config.gutter.y)}px`;
374
+ return;
375
+ }
376
+ const target = appendOnly ?? tiles;
377
+ const masonryTiles = target.map((t) => aspectOf(gallery.items[t.index]));
378
+ const startHeights = appendOnly ? columnHeights : void 0;
379
+ const result = layoutMasonry(masonryTiles, masonryOptions, startHeights);
380
+ target.forEach((tile, i) => {
381
+ const pos = result.positions[i];
382
+ tile.root.style.width = `${pos.width}px`;
383
+ tile.root.style.height = `${pos.height}px`;
384
+ tile.root.style.visibility = "visible";
385
+ tile.root.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
386
+ });
387
+ columnHeights = result.columnHeights;
388
+ container.style.height = `${result.containerHeight}px`;
389
+ }
390
+ function applyMasonryHorizontal() {
391
+ const containerWidth = container.getBoundingClientRect().width;
392
+ if (containerWidth <= 0) return;
393
+ const config = resolveConfig(containerWidth);
394
+ const masonryTiles = tiles.map((t) => aspectOf(gallery.items[t.index]));
395
+ const result = layoutMasonryHorizontal(masonryTiles, {
396
+ containerWidth,
397
+ gutterX: config.gutter.x,
398
+ gutterY: config.gutter.y,
399
+ rowHeight: config.rowHeight
400
+ });
401
+ tiles.forEach((tile, i) => {
402
+ const pos = result.positions[i];
403
+ tile.root.style.width = `${pos.width}px`;
404
+ tile.root.style.height = `${pos.height}px`;
405
+ tile.root.style.visibility = "visible";
406
+ tile.root.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
407
+ });
408
+ container.style.height = `${result.containerHeight}px`;
409
+ }
410
+ function applyJustifiedGrouped(justifiedOptions, config, containerWidth) {
411
+ const fullWidths = /* @__PURE__ */ new Map();
412
+ const titleOnlyWidths = /* @__PURE__ */ new Map();
413
+ if (headingOverflow === "fit") {
414
+ for (const heading of headings) {
415
+ if (!heading.structured) continue;
416
+ const { subtitleEl } = heading.structured;
417
+ heading.root.style.maxWidth = "none";
418
+ heading.root.style.whiteSpace = "nowrap";
419
+ if (subtitleEl) subtitleEl.hidden = false;
420
+ fullWidths.set(heading.tileStart, heading.root.getBoundingClientRect().width);
421
+ if (subtitleEl) {
422
+ subtitleEl.hidden = true;
423
+ titleOnlyWidths.set(heading.tileStart, heading.root.getBoundingClientRect().width);
424
+ subtitleEl.hidden = false;
425
+ } else {
426
+ titleOnlyWidths.set(heading.tileStart, fullWidths.get(heading.tileStart));
427
+ }
428
+ }
429
+ }
430
+ function tileAspects(tileStart, tileEnd) {
431
+ return tiles.slice(tileStart, tileEnd).map((t) => aspectOf(gallery.items[t.index]));
432
+ }
433
+ function tryFit(startSection, endSection) {
434
+ const tileStart = headings[startSection].tileStart;
435
+ const tileEnd = endSection < headings.length ? headings[endSection].tileStart : tiles.length;
436
+ const result = layoutJustified(tileAspects(tileStart, tileEnd), justifiedOptions);
437
+ for (let k = startSection; k < endSection; k++) {
438
+ const heading = headings[k];
439
+ if (!heading.structured) continue;
440
+ const pos = result.positions[heading.tileStart - tileStart];
441
+ if (!pos) continue;
442
+ const next = k + 1 < endSection ? headings[k + 1] : void 0;
443
+ const nextPos = next ? result.positions[next.tileStart - tileStart] : null;
444
+ const sameRow = !!nextPos && Math.abs(nextPos.y - pos.y) < 0.5;
445
+ const available = (sameRow ? nextPos.x : containerWidth) - pos.x - config.gutter.x;
446
+ const bestWidth = Math.min(
447
+ fullWidths.get(heading.tileStart) ?? Infinity,
448
+ titleOnlyWidths.get(heading.tileStart) ?? Infinity
449
+ );
450
+ if (bestWidth > available) return null;
451
+ }
452
+ return result;
453
+ }
454
+ function computeSegments() {
455
+ if (headingOverflow !== "fit") {
456
+ return [
457
+ {
458
+ startSection: 0,
459
+ endSection: headings.length,
460
+ tileStart: 0,
461
+ tileEnd: tiles.length,
462
+ result: layoutJustified(tileAspects(0, tiles.length), justifiedOptions)
463
+ }
464
+ ];
465
+ }
466
+ const segments2 = [];
467
+ let segStart = 0;
468
+ while (segStart < headings.length) {
469
+ const base = tryFit(segStart, segStart + 1);
470
+ const tileStart = headings[segStart].tileStart;
471
+ if (base === null) {
472
+ const tileEnd2 = segStart + 1 < headings.length ? headings[segStart + 1].tileStart : tiles.length;
473
+ segments2.push({
474
+ startSection: segStart,
475
+ endSection: segStart + 1,
476
+ tileStart,
477
+ tileEnd: tileEnd2,
478
+ result: layoutJustified(tileAspects(tileStart, tileEnd2), justifiedOptions)
479
+ });
480
+ segStart += 1;
481
+ continue;
482
+ }
483
+ let end = segStart + 1;
484
+ let best = base;
485
+ while (end < headings.length) {
486
+ const trial = tryFit(segStart, end + 1);
487
+ if (trial === null) break;
488
+ end += 1;
489
+ best = trial;
490
+ }
491
+ const tileEnd = end < headings.length ? headings[end].tileStart : tiles.length;
492
+ segments2.push({
493
+ startSection: segStart,
494
+ endSection: end,
495
+ tileStart,
496
+ tileEnd,
497
+ result: best
498
+ });
499
+ segStart = end;
500
+ }
501
+ return segments2;
502
+ }
503
+ function applyContentDecisions(segment) {
504
+ for (let k = segment.startSection; k < segment.endSection; k++) {
505
+ const heading = headings[k];
506
+ if (!heading.structured) continue;
507
+ const pos = segment.result.positions[heading.tileStart - segment.tileStart];
508
+ if (!pos) continue;
509
+ const next = k + 1 < segment.endSection ? headings[k + 1] : void 0;
510
+ const nextPos = next ? segment.result.positions[next.tileStart - segment.tileStart] : null;
511
+ const sameRow = !!nextPos && Math.abs(nextPos.y - pos.y) < 0.5;
512
+ const available = (sameRow ? nextPos.x : containerWidth) - pos.x - config.gutter.x;
513
+ const { subtitleEl } = heading.structured;
514
+ heading.root.style.whiteSpace = "nowrap";
515
+ heading.root.style.maxWidth = "none";
516
+ heading.root.style.overflow = "";
517
+ heading.root.style.textOverflow = "";
518
+ if (subtitleEl) subtitleEl.hidden = false;
519
+ const full = fullWidths.get(heading.tileStart) ?? 0;
520
+ if (full <= available) continue;
521
+ if (subtitleEl) {
522
+ subtitleEl.hidden = true;
523
+ const titleOnly = titleOnlyWidths.get(heading.tileStart) ?? 0;
524
+ if (titleOnly <= available) continue;
525
+ }
526
+ heading.root.style.maxWidth = `${Math.max(0, available)}px`;
527
+ heading.root.style.overflow = "hidden";
528
+ heading.root.style.textOverflow = "ellipsis";
529
+ }
530
+ }
531
+ function applyWrapDecisions(segment) {
532
+ for (let k = segment.startSection; k < segment.endSection; k++) {
533
+ const heading = headings[k];
534
+ if (!heading.structured) continue;
535
+ const pos = segment.result.positions[heading.tileStart - segment.tileStart];
536
+ if (!pos) continue;
537
+ const next = k + 1 < segment.endSection ? headings[k + 1] : void 0;
538
+ const nextPos = next ? segment.result.positions[next.tileStart - segment.tileStart] : null;
539
+ const sameRow = !!nextPos && Math.abs(nextPos.y - pos.y) < 0.5;
540
+ const available = (sameRow ? nextPos.x : containerWidth) - pos.x - config.gutter.x;
541
+ heading.root.style.whiteSpace = "normal";
542
+ heading.root.style.maxWidth = `${Math.max(0, available)}px`;
543
+ const style = getComputedStyle(heading.root);
544
+ const parsedLineHeight = parseFloat(style.lineHeight);
545
+ const lineHeight = Number.isFinite(parsedLineHeight) ? parsedLineHeight : parseFloat(style.fontSize) * 1.2;
546
+ const maxLines = Math.max(1, Math.floor(config.maxRowHeight / lineHeight));
547
+ heading.root.style.display = "-webkit-box";
548
+ heading.root.style.overflow = "hidden";
549
+ heading.root.style.setProperty("-webkit-box-orient", "vertical");
550
+ heading.root.style.setProperty("-webkit-line-clamp", `${maxLines}`);
551
+ }
552
+ }
553
+ const segments = computeSegments();
554
+ let cumulativeExtra = 0;
555
+ let headingPtr = 0;
556
+ for (const segment of segments) {
557
+ if (headingOverflow === "fit") applyContentDecisions(segment);
558
+ else if (headingOverflow === "wrap") applyWrapDecisions(segment);
559
+ const headingHeights = /* @__PURE__ */ new Map();
560
+ for (let k = segment.startSection; k < segment.endSection; k++) {
561
+ const heading = headings[k];
562
+ headingHeights.set(heading.tileStart, heading.root.getBoundingClientRect().height);
563
+ }
564
+ for (const row of segment.result.rows) {
565
+ const globalStart = row.start + segment.tileStart;
566
+ const globalEnd = row.end + segment.tileStart;
567
+ let bandHeight = 0;
568
+ while (headingPtr < segment.endSection && headings[headingPtr].tileStart < globalEnd) {
569
+ const heading = headings[headingPtr];
570
+ const pos = segment.result.positions[heading.tileStart - segment.tileStart];
571
+ if (pos) {
572
+ heading.root.hidden = false;
573
+ heading.root.style.transform = `translate(${pos.x}px, ${row.y + cumulativeExtra}px)`;
574
+ bandHeight = Math.max(bandHeight, headingHeights.get(heading.tileStart) ?? 0);
575
+ }
576
+ headingPtr++;
577
+ }
578
+ if (bandHeight > 0) cumulativeExtra += bandHeight + config.gutter.y;
579
+ for (let i = globalStart; i < globalEnd; i++) {
580
+ const pos = segment.result.positions[i - segment.tileStart];
581
+ const tile = tiles[i];
582
+ tile.root.hidden = pos === null;
583
+ if (!pos) continue;
584
+ tile.root.style.width = `${pos.width}px`;
585
+ tile.root.style.height = `${pos.height}px`;
586
+ tile.root.style.visibility = "visible";
587
+ tile.root.style.transform = `translate(${pos.x}px, ${pos.y + cumulativeExtra}px)`;
588
+ }
589
+ }
590
+ const segLastCovered = segment.result.rows.length > 0 ? segment.result.rows[segment.result.rows.length - 1].end + segment.tileStart : segment.tileStart;
591
+ for (let i = segLastCovered; i < segment.tileEnd; i++) {
592
+ tiles[i].root.hidden = true;
593
+ }
594
+ cumulativeExtra += segment.result.containerHeight + config.gutter.y;
595
+ }
596
+ while (headingPtr < headings.length) {
597
+ headings[headingPtr].root.hidden = true;
598
+ headingPtr++;
599
+ }
600
+ container.style.height = `${Math.max(0, cumulativeExtra - config.gutter.y)}px`;
601
+ }
602
+ function applyJustified() {
603
+ const containerWidth = container.getBoundingClientRect().width;
604
+ if (containerWidth <= 0) return;
605
+ const config = resolveConfig(containerWidth);
606
+ const justifiedOptions = {
607
+ containerWidth,
608
+ gutterX: config.gutter.x,
609
+ gutterY: config.gutter.y,
610
+ rowHeight: config.rowHeight,
611
+ minRowHeight: config.minRowHeight,
612
+ maxRowHeight: config.maxRowHeight,
613
+ lastRow: config.lastRow
614
+ };
615
+ if (groupingEnabled && headings.length > 0) {
616
+ applyJustifiedGrouped(justifiedOptions, config, containerWidth);
617
+ return;
618
+ }
619
+ const justifiedTiles = tiles.map((t) => aspectOf(gallery.items[t.index]));
620
+ const result = layoutJustified(justifiedTiles, justifiedOptions);
621
+ tiles.forEach((tile, i) => {
622
+ const pos = result.positions[i];
623
+ tile.root.hidden = pos === null;
624
+ if (!pos) return;
625
+ tile.root.style.width = `${pos.width}px`;
626
+ tile.root.style.height = `${pos.height}px`;
627
+ tile.root.style.visibility = "visible";
628
+ tile.root.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
629
+ });
630
+ container.style.height = `${result.containerHeight}px`;
631
+ }
632
+ function relayoutAll() {
633
+ if (horizontal) applyMasonryHorizontal();
634
+ else if (type === "masonry") applyMasonry(null);
635
+ else if (type === "justified") applyJustified();
636
+ else applyGrid(tiles);
637
+ }
638
+ let destroyed = false;
639
+ let measureFrame = null;
640
+ function scheduleMeasureRelayout() {
641
+ if (measureFrame !== null || destroyed) return;
642
+ measureFrame = requestAnimationFrame(() => {
643
+ measureFrame = null;
644
+ if (destroyed) return;
645
+ if (groupingEnabled) fullRender(gallery.items);
646
+ else relayoutAll();
647
+ });
648
+ }
649
+ function autoMeasureTiles(newTiles) {
650
+ if (!autoMeasureEnabled) return;
651
+ for (const tile of newTiles) {
652
+ const item = gallery.items[tile.index];
653
+ if (!item || item.width && item.height || !measurableSrc(item)) continue;
654
+ const onLoad = () => {
655
+ if (destroyed) return;
656
+ if (tile.img.naturalWidth > 0 && tile.img.naturalHeight > 0) {
657
+ item.width = tile.img.naturalWidth;
658
+ item.height = tile.img.naturalHeight;
659
+ scheduleMeasureRelayout();
660
+ }
661
+ };
662
+ if (tile.img.complete) onLoad();
663
+ else tile.img.addEventListener("load", onLoad, { once: true });
664
+ }
665
+ }
666
+ function emitLayoutRender(rendered) {
667
+ ctx.emit("layoutRender", {
668
+ tiles: rendered.map((tile) => ({ index: tile.index, element: tile.root }))
669
+ });
670
+ }
671
+ function fullRender(items) {
672
+ container.replaceChildren();
673
+ tiles = items.map((item, i) => createTile(item, i));
674
+ headings = [];
675
+ if (groupingEnabled) {
676
+ for (const section of computeSections(items)) {
677
+ const sectionItems = items.slice(section.startIndex, section.endIndex);
678
+ const { root: headingRoot, structured } = createHeadingRoot(section.key, sectionItems);
679
+ container.appendChild(headingRoot);
680
+ headings.push({ root: headingRoot, tileStart: section.startIndex, structured });
681
+ for (let i = section.startIndex; i < section.endIndex; i++) {
682
+ container.appendChild(tiles[i].root);
683
+ }
684
+ }
685
+ } else {
686
+ for (const tile of tiles) container.appendChild(tile.root);
687
+ }
688
+ columnHeights = void 0;
689
+ relayoutAll();
690
+ autoMeasureTiles(tiles);
691
+ emitLayoutRender(tiles);
692
+ }
693
+ function appendRender(newItems, startIndex) {
694
+ const newTiles = newItems.map((item, i) => createTile(item, startIndex + i));
695
+ for (const tile of newTiles) container.appendChild(tile.root);
696
+ tiles = [...tiles, ...newTiles];
697
+ if (horizontal)
698
+ applyMasonryHorizontal();
699
+ else if (type === "masonry") applyMasonry(newTiles);
700
+ else if (type === "justified")
701
+ applyJustified();
702
+ else applyGrid(newTiles);
703
+ autoMeasureTiles(newTiles);
704
+ emitLayoutRender(newTiles);
705
+ }
706
+ fullRender(gallery.items);
707
+ previousItems = gallery.items;
708
+ const offItemsUpdated = ctx.on("itemsUpdated", ({ items }) => {
709
+ if (groupingEnabled) {
710
+ fullRender(items);
711
+ } else if (isPureAppend(previousItems, items)) {
712
+ appendRender(items.slice(previousItems.length), previousItems.length);
713
+ } else {
714
+ fullRender(items);
715
+ }
716
+ previousItems = items;
717
+ });
718
+ let resizeFrame = null;
719
+ let resizeObserver;
720
+ if (type === "masonry" || type === "justified" || sortedBreakpoints.length > 0) {
721
+ resizeObserver = new ResizeObserver(() => {
722
+ if (resizeFrame !== null) return;
723
+ resizeFrame = requestAnimationFrame(() => {
724
+ resizeFrame = null;
725
+ relayoutAll();
726
+ });
727
+ });
728
+ resizeObserver.observe(container);
729
+ }
730
+ return () => {
731
+ destroyed = true;
732
+ if (measureFrame !== null) cancelAnimationFrame(measureFrame);
733
+ resizeObserver == null ? void 0 : resizeObserver.disconnect();
734
+ if (resizeFrame !== null) cancelAnimationFrame(resizeFrame);
735
+ offItemsUpdated();
736
+ container.classList.remove("shoji-layout", `shoji-layout--${type}`, "shoji-layout--animate");
737
+ container.style.removeProperty("height");
738
+ container.style.removeProperty("width");
739
+ container.replaceChildren();
740
+ };
741
+ }
742
+ };
743
+ export {
744
+ Layout
745
+ };
746
+ //# sourceMappingURL=index.js.map