blacklight-gallery 5.1.0 → 6.1.0
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.
- checksums.yaml +4 -4
- data/.env +2 -0
- data/.github/workflows/ruby.yml +10 -26
- data/Gemfile +5 -12
- data/README.md +3 -5
- data/Rakefile +53 -33
- data/app/assets/config/blacklight_gallery/manifest.js +1 -2
- data/app/assets/javascripts/blacklight_gallery/blacklight-gallery.esm.js +959 -484
- data/app/assets/javascripts/blacklight_gallery/blacklight-gallery.esm.js.map +1 -1
- data/app/assets/javascripts/blacklight_gallery/blacklight-gallery.js +959 -484
- data/app/assets/javascripts/blacklight_gallery/blacklight-gallery.js.map +1 -1
- data/app/assets/stylesheets/blacklight_gallery/{_gallery.scss → gallery.css} +8 -9
- data/app/assets/stylesheets/blacklight_gallery/masonry.css +104 -0
- data/app/assets/stylesheets/blacklight_gallery/{_osd_viewer.scss → osd_viewer.css} +1 -3
- data/app/assets/stylesheets/blacklight_gallery/{_slideshow.scss → slideshow.css} +19 -32
- data/app/components/blacklight/gallery/document_component.html.erb +2 -6
- data/app/components/blacklight/gallery/document_component.rb +0 -4
- data/app/components/blacklight/gallery/openseadragon_embed_component.rb +3 -4
- data/app/components/blacklight/gallery/slideshow_component.html.erb +1 -1
- data/app/components/blacklight/gallery/slideshow_component.rb +3 -6
- data/app/{views/catalog/_slideshow_modal.html.erb → components/blacklight/gallery/slideshow_modal_component.html.erb} +2 -3
- data/app/components/blacklight/gallery/slideshow_modal_component.rb +6 -0
- data/app/components/blacklight/gallery/slideshow_presenter_component.html.erb +14 -0
- data/app/components/blacklight/gallery/slideshow_presenter_component.rb +17 -0
- data/app/components/blacklight/gallery/slideshow_preview_component.html.erb +1 -5
- data/app/components/blacklight/gallery/slideshow_preview_component.rb +0 -13
- data/app/javascript/blacklight-gallery/grid-lanes-polyfill.js +1067 -0
- data/app/javascript/blacklight-gallery/index.js +18 -4
- data/app/views/catalog/_document_gallery.html.erb +1 -1
- data/app/views/catalog/_document_masonry.html.erb +1 -1
- data/app/views/catalog/_document_slideshow.html.erb +5 -25
- data/app/views/catalog/_openseadragon_default.html.erb +1 -1
- data/blacklight-gallery.gemspec +19 -20
- data/compose.yaml +18 -0
- data/config/importmap.rb +0 -1
- data/config/locales/blacklight-gallery.ar.yml +0 -3
- data/config/locales/blacklight-gallery.de.yml +3 -0
- data/config/locales/blacklight-gallery.en.yml +0 -3
- data/config/locales/blacklight-gallery.es.yml +3 -3
- data/config/locales/blacklight-gallery.fr.yml +3 -3
- data/config/locales/blacklight-gallery.it.yml +3 -3
- data/config/locales/blacklight-gallery.pt-BR.yml +3 -3
- data/config/locales/blacklight-gallery.zh.yml +3 -3
- data/lib/blacklight/gallery/version.rb +1 -1
- data/lib/generators/blacklight_gallery/install_generator.rb +24 -51
- data/package.json +4 -6
- data/rollup.config.js +13 -16
- data/spec/components/blacklight/gallery/document_component_spec.rb +2 -10
- data/spec/components/blacklight/gallery/openseadragon_embed_component_spec.rb +1 -6
- data/spec/components/blacklight/gallery/slideshow_component_spec.rb +28 -25
- data/spec/components/blacklight/gallery/slideshow_preview_component_spec.rb +3 -12
- data/spec/features/slideshow_spec.rb +23 -1
- data/spec/views/catalog/_document_slideshow.html.erb_spec.rb +6 -10
- metadata +48 -56
- data/app/assets/stylesheets/blacklight_gallery/_masonry.scss +0 -81
- data/app/assets/stylesheets/blacklight_gallery/default.scss +0 -7
- data/app/components/blacklight/gallery/icons/pause_slideshow_component.rb +0 -18
- data/app/components/blacklight/gallery/icons/start_slideshow_component.rb +0 -18
- data/app/javascript/blacklight-gallery/masonry.js +0 -12
- data/app/javascript/blacklight-gallery/slideshow.js +0 -122
- data/lib/generators/blacklight_gallery/templates/blacklight_gallery.css.scss +0 -3
- data/vendor/assets/javascripts/imagesloaded.pkgd.js +0 -441
- data/vendor/assets/javascripts/masonry.min.js +0 -9
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Grid Lanes Polyfill
|
|
3
|
+
*
|
|
4
|
+
* Polyfills the new `display: grid-lanes` CSS feature for browsers
|
|
5
|
+
* that don't support it natively. Based on the WebKit implementation
|
|
6
|
+
* described at: https://webkit.org/blog/17660/introducing-css-grid-lanes/
|
|
7
|
+
*
|
|
8
|
+
* Features supported:
|
|
9
|
+
* - display: grid-lanes
|
|
10
|
+
* - grid-template-columns / grid-template-rows for lane definition
|
|
11
|
+
* - gap, column-gap, row-gap
|
|
12
|
+
* - --flow-tolerance for placement sensitivity
|
|
13
|
+
* - Spanning items (grid-column: span N)
|
|
14
|
+
* - Explicit placement (grid-column: N / M)
|
|
15
|
+
* - Responsive auto-fill/auto-fit with minmax()
|
|
16
|
+
* - Both waterfall (columns) and brick (rows) layouts
|
|
17
|
+
*
|
|
18
|
+
* Features that do not work:
|
|
19
|
+
* - fr units with grid-template-rows
|
|
20
|
+
*
|
|
21
|
+
* @version 1.0.0
|
|
22
|
+
* @author Simon Willison
|
|
23
|
+
* @author ninjamar
|
|
24
|
+
* @license MIT
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// CONSTANTS & STATE
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
const POLYFILL_NAME = "GridLanesPolyfill"
|
|
32
|
+
const POLYFILL_ATTR = "data-grid-lanes-polyfilled"
|
|
33
|
+
const DEFAULT_TOLERANCE = 16 // ~1em in pixels
|
|
34
|
+
|
|
35
|
+
// Cache computed styles to reduce redundant calls
|
|
36
|
+
let styleMap = new WeakMap() // Not const because map has no clear meaning it has to be initialized later
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// FEATURE DETECTION
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if the browser natively supports display: grid-lanes
|
|
44
|
+
*/
|
|
45
|
+
function supportsGridLanes() {
|
|
46
|
+
if (typeof CSS === "undefined" || !CSS.supports) {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
return CSS.supports("display", "grid-lanes")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// PARSING UTILITIES
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse a CSS length value to pixels
|
|
58
|
+
*/
|
|
59
|
+
function parseLengthToPixels(
|
|
60
|
+
value,
|
|
61
|
+
containerSize,
|
|
62
|
+
fontSize = 16,
|
|
63
|
+
rootFontSize = 16
|
|
64
|
+
) {
|
|
65
|
+
if (!value || value === "auto" || value === "none") return null
|
|
66
|
+
|
|
67
|
+
const num = parseFloat(value)
|
|
68
|
+
if (isNaN(num)) return null
|
|
69
|
+
|
|
70
|
+
if (value.endsWith("px")) return num
|
|
71
|
+
if (value.endsWith("rem")) return num * rootFontSize
|
|
72
|
+
if (value.endsWith("em")) return num * fontSize
|
|
73
|
+
if (value.endsWith("ch")) return num * fontSize * 0.5 // Approximate
|
|
74
|
+
if (value.endsWith("lh")) return num * fontSize * 1.2 // Approximate line-height
|
|
75
|
+
if (value.endsWith("%")) return (num / 100) * containerSize
|
|
76
|
+
if (value.endsWith("vw")) return (num / 100) * window.innerWidth
|
|
77
|
+
if (value.endsWith("vh")) return (num / 100) * window.innerHeight
|
|
78
|
+
if (value.endsWith("vmin"))
|
|
79
|
+
return (num / 100) * Math.min(window.innerWidth, window.innerHeight)
|
|
80
|
+
if (value.endsWith("vmax"))
|
|
81
|
+
return (num / 100) * Math.max(window.innerWidth, window.innerHeight)
|
|
82
|
+
if (value.endsWith("fr")) return null // Handled separately
|
|
83
|
+
|
|
84
|
+
// Unitless number treated as pixels
|
|
85
|
+
if (!isNaN(num) && value === String(num)) return num
|
|
86
|
+
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse minmax() function
|
|
92
|
+
*/
|
|
93
|
+
function parseMinMax(value) {
|
|
94
|
+
const match = value.match(/minmax\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/)
|
|
95
|
+
if (!match) return null
|
|
96
|
+
return {
|
|
97
|
+
min: match[1].trim(),
|
|
98
|
+
max: match[2].trim()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parse repeat() function
|
|
104
|
+
*/
|
|
105
|
+
function parseRepeat(value) {
|
|
106
|
+
const match = value.match(/repeat\(\s*([^,]+)\s*,\s*(.+)\s*\)/)
|
|
107
|
+
if (!match) return null
|
|
108
|
+
return {
|
|
109
|
+
count: match[1].trim(),
|
|
110
|
+
pattern: match[2].trim()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Tokenize a grid template string
|
|
116
|
+
*/
|
|
117
|
+
function tokenizeTemplate(template) {
|
|
118
|
+
const tokens = []
|
|
119
|
+
let current = ""
|
|
120
|
+
let parenDepth = 0
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < template.length; i++) {
|
|
123
|
+
const char = template[i]
|
|
124
|
+
|
|
125
|
+
if (char === "(") {
|
|
126
|
+
parenDepth++
|
|
127
|
+
current += char
|
|
128
|
+
} else if (char === ")") {
|
|
129
|
+
parenDepth--
|
|
130
|
+
current += char
|
|
131
|
+
} else if (char === " " && parenDepth === 0) {
|
|
132
|
+
if (current.trim()) {
|
|
133
|
+
tokens.push(current.trim())
|
|
134
|
+
}
|
|
135
|
+
current = ""
|
|
136
|
+
} else {
|
|
137
|
+
current += char
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (current.trim()) {
|
|
142
|
+
tokens.push(current.trim())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return tokens
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Calculate lane sizes from grid-template-columns/rows
|
|
150
|
+
*/
|
|
151
|
+
function calculateLaneSizes(
|
|
152
|
+
template,
|
|
153
|
+
containerSize,
|
|
154
|
+
gap,
|
|
155
|
+
fontSize,
|
|
156
|
+
rootFontSize
|
|
157
|
+
) {
|
|
158
|
+
if (!template || template === "none" || template === "auto") {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const availableSpace = containerSize
|
|
163
|
+
let lanes = []
|
|
164
|
+
let totalFr = 0
|
|
165
|
+
let fixedSpace = 0
|
|
166
|
+
|
|
167
|
+
// Parse the template
|
|
168
|
+
const tokens = tokenizeTemplate(template)
|
|
169
|
+
|
|
170
|
+
for (const token of tokens) {
|
|
171
|
+
// Handle repeat()
|
|
172
|
+
const repeatInfo = parseRepeat(token)
|
|
173
|
+
if (repeatInfo) {
|
|
174
|
+
const { count, pattern } = repeatInfo
|
|
175
|
+
const patternTokens = tokenizeTemplate(pattern)
|
|
176
|
+
|
|
177
|
+
if (count === "auto-fill" || count === "auto-fit") {
|
|
178
|
+
// Calculate how many repetitions fit
|
|
179
|
+
let minSize = 0
|
|
180
|
+
|
|
181
|
+
for (const pt of patternTokens) {
|
|
182
|
+
const minmax = parseMinMax(pt)
|
|
183
|
+
if (minmax) {
|
|
184
|
+
const minVal = parseLengthToPixels(
|
|
185
|
+
minmax.min,
|
|
186
|
+
containerSize,
|
|
187
|
+
fontSize,
|
|
188
|
+
rootFontSize
|
|
189
|
+
)
|
|
190
|
+
if (minmax.min === "max-content" || minmax.min === "min-content") {
|
|
191
|
+
minSize += 100 // Fallback estimate
|
|
192
|
+
} else if (minVal !== null) {
|
|
193
|
+
minSize += minVal
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
const size = parseLengthToPixels(
|
|
197
|
+
pt,
|
|
198
|
+
containerSize,
|
|
199
|
+
fontSize,
|
|
200
|
+
rootFontSize
|
|
201
|
+
)
|
|
202
|
+
if (size !== null) {
|
|
203
|
+
minSize += size
|
|
204
|
+
} else if (pt.endsWith("fr")) {
|
|
205
|
+
minSize += 100 // Minimum fallback for fr units
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Calculate repetitions
|
|
211
|
+
const patternCount = patternTokens.length
|
|
212
|
+
const gapCount = patternCount - 1
|
|
213
|
+
const minPatternSize = minSize + gapCount * gap
|
|
214
|
+
|
|
215
|
+
let reps = Math.max(
|
|
216
|
+
1,
|
|
217
|
+
Math.floor((availableSpace + gap) / (minPatternSize + gap))
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
// Expand pattern
|
|
221
|
+
for (let i = 0; i < reps; i++) {
|
|
222
|
+
for (const pt of patternTokens) {
|
|
223
|
+
const minmax = parseMinMax(pt)
|
|
224
|
+
if (minmax) {
|
|
225
|
+
const minVal = parseLengthToPixels(
|
|
226
|
+
minmax.min,
|
|
227
|
+
containerSize,
|
|
228
|
+
fontSize,
|
|
229
|
+
rootFontSize
|
|
230
|
+
)
|
|
231
|
+
const maxVal = minmax.max.endsWith("fr")
|
|
232
|
+
? { fr: parseFloat(minmax.max) }
|
|
233
|
+
: parseLengthToPixels(
|
|
234
|
+
minmax.max,
|
|
235
|
+
containerSize,
|
|
236
|
+
fontSize,
|
|
237
|
+
rootFontSize
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
lanes.push({
|
|
241
|
+
min: minVal || 0,
|
|
242
|
+
max: maxVal,
|
|
243
|
+
size: 0
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
if (typeof maxVal === "object" && maxVal.fr) {
|
|
247
|
+
totalFr += maxVal.fr
|
|
248
|
+
} else {
|
|
249
|
+
// Only add to fixedSpace if max is NOT fr (non-flexible tracks)
|
|
250
|
+
fixedSpace += minVal || 0
|
|
251
|
+
}
|
|
252
|
+
} else if (pt.endsWith("fr")) {
|
|
253
|
+
const fr = parseFloat(pt)
|
|
254
|
+
lanes.push({ min: 0, max: { fr }, size: 0 })
|
|
255
|
+
totalFr += fr
|
|
256
|
+
} else {
|
|
257
|
+
const size =
|
|
258
|
+
parseLengthToPixels(
|
|
259
|
+
pt,
|
|
260
|
+
containerSize,
|
|
261
|
+
fontSize,
|
|
262
|
+
rootFontSize
|
|
263
|
+
) || 0
|
|
264
|
+
lanes.push({ min: size, max: size, size })
|
|
265
|
+
fixedSpace += size
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
// Fixed repeat count
|
|
271
|
+
const reps = parseInt(count, 10)
|
|
272
|
+
for (let i = 0; i < reps; i++) {
|
|
273
|
+
for (const pt of patternTokens) {
|
|
274
|
+
const size = parseLengthToPixels(
|
|
275
|
+
pt,
|
|
276
|
+
containerSize,
|
|
277
|
+
fontSize,
|
|
278
|
+
rootFontSize
|
|
279
|
+
)
|
|
280
|
+
if (pt.endsWith("fr")) {
|
|
281
|
+
const fr = parseFloat(pt)
|
|
282
|
+
lanes.push({ min: 0, max: { fr }, size: 0 })
|
|
283
|
+
totalFr += fr
|
|
284
|
+
} else if (size !== null) {
|
|
285
|
+
lanes.push({ min: size, max: size, size })
|
|
286
|
+
fixedSpace += size
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Handle minmax()
|
|
295
|
+
const minmax = parseMinMax(token)
|
|
296
|
+
if (minmax) {
|
|
297
|
+
const minVal = parseLengthToPixels(
|
|
298
|
+
minmax.min,
|
|
299
|
+
containerSize,
|
|
300
|
+
fontSize,
|
|
301
|
+
rootFontSize
|
|
302
|
+
)
|
|
303
|
+
const maxVal = minmax.max.endsWith("fr")
|
|
304
|
+
? { fr: parseFloat(minmax.max) }
|
|
305
|
+
: parseLengthToPixels(minmax.max, containerSize, fontSize, rootFontSize)
|
|
306
|
+
|
|
307
|
+
lanes.push({ min: minVal || 0, max: maxVal, size: 0 })
|
|
308
|
+
if (typeof maxVal === "object" && maxVal.fr) {
|
|
309
|
+
totalFr += maxVal.fr
|
|
310
|
+
} else {
|
|
311
|
+
// Only add to fixedSpace if max is NOT fr (non-flexible tracks)
|
|
312
|
+
fixedSpace += minVal || 0
|
|
313
|
+
}
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Handle fr units
|
|
318
|
+
if (token.endsWith("fr")) {
|
|
319
|
+
const fr = parseFloat(token)
|
|
320
|
+
lanes.push({ min: 0, max: { fr }, size: 0 })
|
|
321
|
+
totalFr += fr
|
|
322
|
+
continue
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Handle fixed sizes
|
|
326
|
+
const size = parseLengthToPixels(
|
|
327
|
+
token,
|
|
328
|
+
containerSize,
|
|
329
|
+
fontSize,
|
|
330
|
+
rootFontSize
|
|
331
|
+
)
|
|
332
|
+
if (size !== null) {
|
|
333
|
+
lanes.push({ min: size, max: size, size })
|
|
334
|
+
fixedSpace += size
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Calculate final sizes
|
|
339
|
+
const totalGaps = Math.max(0, lanes.length - 1) * gap
|
|
340
|
+
const flexSpace = Math.max(0, availableSpace - fixedSpace - totalGaps)
|
|
341
|
+
const frUnit = totalFr > 0 ? flexSpace / totalFr : 0
|
|
342
|
+
|
|
343
|
+
for (const lane of lanes) {
|
|
344
|
+
if (typeof lane.max === "object" && lane.max.fr) {
|
|
345
|
+
lane.size = lane.min + frUnit * lane.max.fr
|
|
346
|
+
} else {
|
|
347
|
+
lane.size = lane.min
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return lanes.map(l => l.size)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// STYLE & ITEM UTILITIES
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get computed styles for grid-lanes properties
|
|
360
|
+
*/
|
|
361
|
+
function getGridLanesStyles(element) {
|
|
362
|
+
const computed = window.getComputedStyle(element)
|
|
363
|
+
const fontSize = parseFloat(computed.getPropertyValue("font-size")) || 16
|
|
364
|
+
const rootFontSize =
|
|
365
|
+
parseFloat(
|
|
366
|
+
window
|
|
367
|
+
.getComputedStyle(document.documentElement)
|
|
368
|
+
.getPropertyValue("font-size")
|
|
369
|
+
) || 16
|
|
370
|
+
|
|
371
|
+
// Get gap values
|
|
372
|
+
let gap =
|
|
373
|
+
computed.getPropertyValue("gap") ||
|
|
374
|
+
computed.getPropertyValue("grid-gap") ||
|
|
375
|
+
"0px"
|
|
376
|
+
let columnGap =
|
|
377
|
+
computed.getPropertyValue("column-gap") ||
|
|
378
|
+
computed.getPropertyValue("grid-column-gap") ||
|
|
379
|
+
gap
|
|
380
|
+
let rowGap =
|
|
381
|
+
computed.getPropertyValue("row-gap") ||
|
|
382
|
+
computed.getPropertyValue("grid-row-gap") ||
|
|
383
|
+
gap
|
|
384
|
+
|
|
385
|
+
// Handle combined gap shorthand like "24px 16px"
|
|
386
|
+
if (gap.includes(" ")) {
|
|
387
|
+
const [rg, cg] = gap.split(/\s+/)
|
|
388
|
+
rowGap = rg
|
|
389
|
+
columnGap = cg
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Parse flow-tolerance
|
|
393
|
+
let tolerance = DEFAULT_TOLERANCE
|
|
394
|
+
const toleranceValue = computed.getPropertyValue("--flow-tolerance").trim()
|
|
395
|
+
|
|
396
|
+
if (toleranceValue) {
|
|
397
|
+
const parsed = parseLengthToPixels(
|
|
398
|
+
toleranceValue,
|
|
399
|
+
0,
|
|
400
|
+
fontSize,
|
|
401
|
+
rootFontSize
|
|
402
|
+
)
|
|
403
|
+
if (parsed !== null) tolerance = parsed
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
gridTemplateColumns: computed.getPropertyValue("grid-template-columns"),
|
|
408
|
+
gridTemplateRows: computed.getPropertyValue("grid-template-rows"),
|
|
409
|
+
columnGap:
|
|
410
|
+
parseLengthToPixels(
|
|
411
|
+
String(columnGap).split(" ")[0],
|
|
412
|
+
0,
|
|
413
|
+
fontSize,
|
|
414
|
+
rootFontSize
|
|
415
|
+
) || 0,
|
|
416
|
+
rowGap:
|
|
417
|
+
parseLengthToPixels(
|
|
418
|
+
String(rowGap).split(" ")[0],
|
|
419
|
+
0,
|
|
420
|
+
fontSize,
|
|
421
|
+
rootFontSize
|
|
422
|
+
) || 0,
|
|
423
|
+
fontSize,
|
|
424
|
+
rootFontSize,
|
|
425
|
+
tolerance
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get item placement properties
|
|
431
|
+
*/
|
|
432
|
+
function getItemStyles(element) {
|
|
433
|
+
const computed = window.getComputedStyle(element)
|
|
434
|
+
const gridColumn = computed.gridColumn || computed.gridColumnStart
|
|
435
|
+
const gridRow = computed.gridRow || computed.gridRowStart
|
|
436
|
+
|
|
437
|
+
let columnSpan = 1
|
|
438
|
+
let columnStart = null
|
|
439
|
+
let columnEnd = null
|
|
440
|
+
let rowSpan = 1
|
|
441
|
+
let rowStart = null
|
|
442
|
+
let rowEnd = null
|
|
443
|
+
|
|
444
|
+
// Parse grid-column
|
|
445
|
+
if (gridColumn && gridColumn !== "auto") {
|
|
446
|
+
const spanMatch = gridColumn.match(/span\s+(\d+)/)
|
|
447
|
+
if (spanMatch) {
|
|
448
|
+
columnSpan = parseInt(spanMatch[1], 10)
|
|
449
|
+
} else if (gridColumn.includes("/")) {
|
|
450
|
+
const [start, end] = gridColumn.split("/").map(s => s.trim())
|
|
451
|
+
columnStart = parseInt(start, 10)
|
|
452
|
+
columnEnd = parseInt(end, 10)
|
|
453
|
+
if (!isNaN(columnStart) && !isNaN(columnEnd)) {
|
|
454
|
+
columnSpan = Math.abs(columnEnd - columnStart)
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
const num = parseInt(gridColumn, 10)
|
|
458
|
+
if (!isNaN(num)) {
|
|
459
|
+
columnStart = num
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Parse grid-row
|
|
465
|
+
if (gridRow && gridRow !== "auto") {
|
|
466
|
+
const spanMatch = gridRow.match(/span\s+(\d+)/)
|
|
467
|
+
if (spanMatch) {
|
|
468
|
+
rowSpan = parseInt(spanMatch[1], 10)
|
|
469
|
+
} else if (gridRow.includes("/")) {
|
|
470
|
+
const [start, end] = gridRow.split("/").map(s => s.trim())
|
|
471
|
+
rowStart = parseInt(start, 10)
|
|
472
|
+
rowEnd = parseInt(end, 10)
|
|
473
|
+
if (!isNaN(rowStart) && !isNaN(rowEnd)) {
|
|
474
|
+
rowSpan = Math.abs(rowEnd - rowStart)
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
const num = parseInt(gridRow, 10)
|
|
478
|
+
if (!isNaN(num)) {
|
|
479
|
+
rowStart = num
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
columnSpan,
|
|
486
|
+
columnStart,
|
|
487
|
+
columnEnd,
|
|
488
|
+
rowSpan,
|
|
489
|
+
rowStart,
|
|
490
|
+
rowEnd
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ============================================================================
|
|
495
|
+
// STYLE CACHE MANAGEMENT
|
|
496
|
+
// ============================================================================
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Get computed style with caching to reduce redundant calls
|
|
500
|
+
*/
|
|
501
|
+
function catchedGetComputedStyle(elem, useCatch = true) {
|
|
502
|
+
if (useCatch && styleMap.has(elem)) {
|
|
503
|
+
return styleMap.get(elem)
|
|
504
|
+
}
|
|
505
|
+
const style = window.getComputedStyle(elem)
|
|
506
|
+
styleMap.set(elem, style)
|
|
507
|
+
return style
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Clear cached style for an element
|
|
512
|
+
*/
|
|
513
|
+
function removeCachedStyle(elem) {
|
|
514
|
+
styleMap.delete(elem)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ============================================================================
|
|
518
|
+
// DOM UTILITIES
|
|
519
|
+
// ============================================================================
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Check if element has the grid-lanes polyfill marker
|
|
523
|
+
*/
|
|
524
|
+
function hasGridLanesProperty(elem) {
|
|
525
|
+
return (
|
|
526
|
+
catchedGetComputedStyle(elem)
|
|
527
|
+
.getPropertyValue("--grid-lanes-polyfill")
|
|
528
|
+
.trim() === "1"
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Find all elements with --grid-lanes-polyfill down to the granular level.
|
|
534
|
+
* Prefers children over parent elements. Note: nested grid-lanes are not
|
|
535
|
+
* supported because the custom property cascades to all descendants.
|
|
536
|
+
*
|
|
537
|
+
* Uses TreeWalker for efficient traversal and routes through hasGridLanesProperty()
|
|
538
|
+
* to leverage the catchedGetComputedStyle cache. Correctly handles subtree skipping.
|
|
539
|
+
*/
|
|
540
|
+
function findElements(root = document.body) {
|
|
541
|
+
const results = []
|
|
542
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT)
|
|
543
|
+
|
|
544
|
+
let node = walker.nextNode() // start from first child, skip root itself
|
|
545
|
+
while (node) {
|
|
546
|
+
if (hasGridLanesProperty(node)) {
|
|
547
|
+
results.push(node)
|
|
548
|
+
// Skip the entire subtree of this match by finding the next
|
|
549
|
+
// node outside of it: try nextSibling, then bubble up ancestors.
|
|
550
|
+
let next = null
|
|
551
|
+
let ancestor = node
|
|
552
|
+
while (ancestor && ancestor !== root) {
|
|
553
|
+
walker.currentNode = ancestor
|
|
554
|
+
next = walker.nextSibling()
|
|
555
|
+
if (next) break
|
|
556
|
+
ancestor = ancestor.parentElement
|
|
557
|
+
}
|
|
558
|
+
node = next // null means no more nodes in tree
|
|
559
|
+
} else {
|
|
560
|
+
node = walker.nextNode()
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return results
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Process a subtree for grid-lanes containers
|
|
568
|
+
*/
|
|
569
|
+
function processSubtree(root, instances, options) {
|
|
570
|
+
const containers = findElements(root)
|
|
571
|
+
for (const container of containers) {
|
|
572
|
+
if (!instances.has(container) && !container.hasAttribute(POLYFILL_ATTR)) {
|
|
573
|
+
instances.set(container, new GridLanesLayout(container, options))
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ============================================================================
|
|
579
|
+
// GRID LANES LAYOUT CLASS
|
|
580
|
+
// ============================================================================
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Main Grid Lanes layout class
|
|
584
|
+
*/
|
|
585
|
+
class GridLanesLayout {
|
|
586
|
+
constructor(container, options = {}) {
|
|
587
|
+
this.container = container
|
|
588
|
+
this.options = options
|
|
589
|
+
this.isVertical = true // true = waterfall (columns), false = brick (rows)
|
|
590
|
+
this.lanes = []
|
|
591
|
+
this.laneHeights = []
|
|
592
|
+
this.resizeObserver = null
|
|
593
|
+
this.mutationObserver = null
|
|
594
|
+
|
|
595
|
+
this.init()
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
init() {
|
|
599
|
+
// Mark as polyfilled
|
|
600
|
+
this.container.setAttribute(POLYFILL_ATTR, "true")
|
|
601
|
+
|
|
602
|
+
// Set up container styles
|
|
603
|
+
this.container.style.position = "relative"
|
|
604
|
+
this.container.style.display = "block"
|
|
605
|
+
|
|
606
|
+
// Initial layout
|
|
607
|
+
this.layout()
|
|
608
|
+
|
|
609
|
+
// Set up observers
|
|
610
|
+
this.setupObservers()
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
setupObservers() {
|
|
614
|
+
// Debounce layout calls to avoid excessive recalculations
|
|
615
|
+
let layoutTimeout = null
|
|
616
|
+
const debouncedLayout = () => {
|
|
617
|
+
if (layoutTimeout) clearTimeout(layoutTimeout)
|
|
618
|
+
layoutTimeout = setTimeout(() => this.layout(), 16)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Resize observer for responsive layouts AND child size changes
|
|
622
|
+
this.resizeObserver = new ResizeObserver(entries => {
|
|
623
|
+
// Check if it's the container or a child that resized
|
|
624
|
+
for (const entry of entries) {
|
|
625
|
+
if (
|
|
626
|
+
entry.target === this.container ||
|
|
627
|
+
entry.target.parentElement === this.container
|
|
628
|
+
) {
|
|
629
|
+
debouncedLayout()
|
|
630
|
+
break
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
})
|
|
634
|
+
this.resizeObserver.observe(this.container)
|
|
635
|
+
|
|
636
|
+
// Observe all direct children for size changes
|
|
637
|
+
for (const child of this.container.children) {
|
|
638
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
639
|
+
this.resizeObserver.observe(child)
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Mutation observer for dynamic content
|
|
644
|
+
this.mutationObserver = new MutationObserver(mutations => {
|
|
645
|
+
let shouldRelayout = false
|
|
646
|
+
for (const mutation of mutations) {
|
|
647
|
+
if (mutation.type === "childList") {
|
|
648
|
+
// Observe new children
|
|
649
|
+
for (const node of mutation.addedNodes) {
|
|
650
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
651
|
+
this.resizeObserver.observe(node)
|
|
652
|
+
// Also watch for images in new nodes
|
|
653
|
+
this.observeImages(node)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
shouldRelayout = true
|
|
657
|
+
} else if (
|
|
658
|
+
mutation.type === "attributes" &&
|
|
659
|
+
mutation.attributeName === "style"
|
|
660
|
+
) {
|
|
661
|
+
shouldRelayout = true
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (shouldRelayout) {
|
|
665
|
+
debouncedLayout()
|
|
666
|
+
}
|
|
667
|
+
})
|
|
668
|
+
this.mutationObserver.observe(this.container, {
|
|
669
|
+
childList: true,
|
|
670
|
+
subtree: false,
|
|
671
|
+
attributes: true,
|
|
672
|
+
attributeFilter: ["style", "class"]
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
// Observe all images for load events
|
|
676
|
+
this.observeImages(this.container)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
observeImages(root) {
|
|
680
|
+
const images = root.querySelectorAll("img")
|
|
681
|
+
for (const img of images) {
|
|
682
|
+
if (!img.complete) {
|
|
683
|
+
img.addEventListener("load", () => this.layout(), { once: true })
|
|
684
|
+
img.addEventListener("error", () => this.layout(), { once: true })
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
layout() {
|
|
690
|
+
const styles = getGridLanesStyles(this.container)
|
|
691
|
+
const containerRect = this.container.getBoundingClientRect()
|
|
692
|
+
|
|
693
|
+
// Determine direction based on which template is defined
|
|
694
|
+
const hasColumns =
|
|
695
|
+
styles.gridTemplateColumns &&
|
|
696
|
+
styles.gridTemplateColumns !== "none" &&
|
|
697
|
+
!styles.gridTemplateColumns.startsWith("auto")
|
|
698
|
+
const hasRows =
|
|
699
|
+
styles.gridTemplateRows &&
|
|
700
|
+
styles.gridTemplateRows !== "none" &&
|
|
701
|
+
!styles.gridTemplateRows.startsWith("auto")
|
|
702
|
+
|
|
703
|
+
this.isVertical = hasColumns || !hasRows
|
|
704
|
+
|
|
705
|
+
// Calculate lane sizes
|
|
706
|
+
if (this.isVertical) {
|
|
707
|
+
this.lanes = calculateLaneSizes(
|
|
708
|
+
styles.gridTemplateColumns,
|
|
709
|
+
containerRect.width,
|
|
710
|
+
styles.columnGap,
|
|
711
|
+
styles.fontSize,
|
|
712
|
+
styles.rootFontSize
|
|
713
|
+
) || [containerRect.width]
|
|
714
|
+
} else {
|
|
715
|
+
this.lanes = calculateLaneSizes(
|
|
716
|
+
styles.gridTemplateRows,
|
|
717
|
+
containerRect.height,
|
|
718
|
+
styles.rowGap,
|
|
719
|
+
styles.fontSize,
|
|
720
|
+
styles.rootFontSize
|
|
721
|
+
) || [containerRect.height]
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Initialize lane positions (heights for vertical, widths for horizontal)
|
|
725
|
+
this.laneHeights = new Array(this.lanes.length).fill(0)
|
|
726
|
+
|
|
727
|
+
// Get all direct children
|
|
728
|
+
const items = Array.from(this.container.children).filter(
|
|
729
|
+
el =>
|
|
730
|
+
el.nodeType === Node.ELEMENT_NODE &&
|
|
731
|
+
window.getComputedStyle(el).display !== "none"
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
// Separate explicitly placed items from auto-placed items
|
|
735
|
+
const explicitItems = []
|
|
736
|
+
const autoItems = []
|
|
737
|
+
|
|
738
|
+
for (const item of items) {
|
|
739
|
+
const itemStyles = getItemStyles(item)
|
|
740
|
+
if (this.isVertical && itemStyles.columnStart !== null) {
|
|
741
|
+
explicitItems.push({ element: item, styles: itemStyles })
|
|
742
|
+
} else if (!this.isVertical && itemStyles.rowStart !== null) {
|
|
743
|
+
explicitItems.push({ element: item, styles: itemStyles })
|
|
744
|
+
} else {
|
|
745
|
+
autoItems.push({ element: item, styles: itemStyles })
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Place explicitly positioned items first
|
|
750
|
+
for (const { element, styles: itemStyles } of explicitItems) {
|
|
751
|
+
this.placeExplicitItem(element, itemStyles, styles)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Place auto-positioned items
|
|
755
|
+
for (const { element, styles: itemStyles } of autoItems) {
|
|
756
|
+
this.placeAutoItem(element, itemStyles, styles)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Set container height
|
|
760
|
+
const containerHeight = Math.max(...this.laneHeights)
|
|
761
|
+
this.container.style.minHeight = `${containerHeight}px`
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
placeExplicitItem(element, itemStyles, containerStyles) {
|
|
765
|
+
const gap = this.isVertical
|
|
766
|
+
? containerStyles.columnGap
|
|
767
|
+
: containerStyles.rowGap
|
|
768
|
+
const crossGap = this.isVertical
|
|
769
|
+
? containerStyles.rowGap
|
|
770
|
+
: containerStyles.columnGap
|
|
771
|
+
|
|
772
|
+
let laneIndex
|
|
773
|
+
let span
|
|
774
|
+
|
|
775
|
+
if (this.isVertical) {
|
|
776
|
+
// Handle negative indices
|
|
777
|
+
laneIndex = itemStyles.columnStart
|
|
778
|
+
if (laneIndex < 0) {
|
|
779
|
+
laneIndex = this.lanes.length + laneIndex + 1
|
|
780
|
+
}
|
|
781
|
+
laneIndex = Math.max(0, Math.min(laneIndex - 1, this.lanes.length - 1))
|
|
782
|
+
span = itemStyles.columnSpan
|
|
783
|
+
} else {
|
|
784
|
+
laneIndex = itemStyles.rowStart
|
|
785
|
+
if (laneIndex < 0) {
|
|
786
|
+
laneIndex = this.lanes.length + laneIndex + 1
|
|
787
|
+
}
|
|
788
|
+
laneIndex = Math.max(0, Math.min(laneIndex - 1, this.lanes.length - 1))
|
|
789
|
+
span = itemStyles.rowSpan
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Calculate position
|
|
793
|
+
let position = 0
|
|
794
|
+
for (let i = 0; i < laneIndex; i++) {
|
|
795
|
+
position += this.lanes[i] + gap
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Calculate width (for spanning)
|
|
799
|
+
let size = 0
|
|
800
|
+
const endLane = Math.min(laneIndex + span, this.lanes.length)
|
|
801
|
+
for (let i = laneIndex; i < endLane; i++) {
|
|
802
|
+
size += this.lanes[i]
|
|
803
|
+
if (i < endLane - 1) size += gap
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Get the tallest lane in the span
|
|
807
|
+
let maxHeight = 0
|
|
808
|
+
for (let i = laneIndex; i < endLane; i++) {
|
|
809
|
+
maxHeight = Math.max(maxHeight, this.laneHeights[i])
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Position the element
|
|
813
|
+
element.style.position = "absolute"
|
|
814
|
+
|
|
815
|
+
if (this.isVertical) {
|
|
816
|
+
element.style.left = `${position}px`
|
|
817
|
+
element.style.top = `${maxHeight > 0 ? maxHeight + crossGap : 0}px`
|
|
818
|
+
element.style.width = `${size}px`
|
|
819
|
+
} else {
|
|
820
|
+
element.style.top = `${position}px`
|
|
821
|
+
element.style.left = `${maxHeight > 0 ? maxHeight + crossGap : 0}px`
|
|
822
|
+
element.style.height = `${size}px`
|
|
823
|
+
element.style.width = ""
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Update lane heights
|
|
827
|
+
const itemRect = element.getBoundingClientRect()
|
|
828
|
+
const itemSize = this.isVertical ? itemRect.height : itemRect.width
|
|
829
|
+
const newHeight = maxHeight + (maxHeight > 0 ? crossGap : 0) + itemSize
|
|
830
|
+
|
|
831
|
+
for (let i = laneIndex; i < endLane; i++) {
|
|
832
|
+
this.laneHeights[i] = newHeight
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
placeAutoItem(element, itemStyles, containerStyles) {
|
|
837
|
+
const gap = this.isVertical
|
|
838
|
+
? containerStyles.columnGap
|
|
839
|
+
: containerStyles.rowGap
|
|
840
|
+
const crossGap = this.isVertical
|
|
841
|
+
? containerStyles.rowGap
|
|
842
|
+
: containerStyles.columnGap
|
|
843
|
+
const tolerance = containerStyles.tolerance
|
|
844
|
+
const span = Math.min(
|
|
845
|
+
this.isVertical ? itemStyles.columnSpan : itemStyles.rowSpan,
|
|
846
|
+
this.lanes.length
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
// Find the best lane(s) considering tolerance
|
|
850
|
+
let bestLane = 0
|
|
851
|
+
let bestHeight = Infinity
|
|
852
|
+
|
|
853
|
+
for (let i = 0; i <= this.lanes.length - span; i++) {
|
|
854
|
+
// Get the max height across the span
|
|
855
|
+
let maxHeight = 0
|
|
856
|
+
for (let j = i; j < i + span; j++) {
|
|
857
|
+
maxHeight = Math.max(maxHeight, this.laneHeights[j])
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Use tolerance to determine if this is meaningfully better
|
|
861
|
+
if (bestHeight - maxHeight > tolerance) {
|
|
862
|
+
bestHeight = maxHeight
|
|
863
|
+
bestLane = i
|
|
864
|
+
}
|
|
865
|
+
// Within tolerance: keep the current (earlier) bestLane — spec prefers earliest equivalent lane
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Calculate position
|
|
869
|
+
let position = 0
|
|
870
|
+
for (let i = 0; i < bestLane; i++) {
|
|
871
|
+
position += this.lanes[i] + gap
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Calculate size (for spanning)
|
|
875
|
+
let size = 0
|
|
876
|
+
const endLane = Math.min(bestLane + span, this.lanes.length)
|
|
877
|
+
for (let i = bestLane; i < endLane; i++) {
|
|
878
|
+
size += this.lanes[i]
|
|
879
|
+
if (i < endLane - 1) size += gap
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Position the element
|
|
883
|
+
element.style.position = "absolute"
|
|
884
|
+
|
|
885
|
+
if (this.isVertical) {
|
|
886
|
+
element.style.left = `${position}px`
|
|
887
|
+
element.style.top = `${bestHeight > 0 ? bestHeight + crossGap : 0}px`
|
|
888
|
+
element.style.width = `${size}px`
|
|
889
|
+
} else {
|
|
890
|
+
element.style.top = `${position}px`
|
|
891
|
+
element.style.left = `${bestHeight > 0 ? bestHeight + crossGap : 0}px`
|
|
892
|
+
element.style.height = `${size}px`
|
|
893
|
+
element.style.width = ""
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Update lane heights
|
|
897
|
+
const itemRect = element.getBoundingClientRect()
|
|
898
|
+
const itemSize = this.isVertical ? itemRect.height : itemRect.width
|
|
899
|
+
const newHeight = bestHeight + (bestHeight > 0 ? crossGap : 0) + itemSize
|
|
900
|
+
|
|
901
|
+
for (let i = bestLane; i < endLane; i++) {
|
|
902
|
+
this.laneHeights[i] = newHeight
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
destroy() {
|
|
907
|
+
if (this.resizeObserver) {
|
|
908
|
+
this.resizeObserver.disconnect()
|
|
909
|
+
}
|
|
910
|
+
if (this.mutationObserver) {
|
|
911
|
+
this.mutationObserver.disconnect()
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
this.container.removeAttribute(POLYFILL_ATTR)
|
|
915
|
+
this.container.style.position = ""
|
|
916
|
+
this.container.style.display = ""
|
|
917
|
+
this.container.style.minHeight = ""
|
|
918
|
+
|
|
919
|
+
// Reset item styles
|
|
920
|
+
for (const item of this.container.children) {
|
|
921
|
+
if (item.nodeType === Node.ELEMENT_NODE) {
|
|
922
|
+
item.style.position = ""
|
|
923
|
+
item.style.left = ""
|
|
924
|
+
item.style.top = ""
|
|
925
|
+
item.style.width = ""
|
|
926
|
+
item.style.height = ""
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
refresh() {
|
|
932
|
+
this.layout()
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// ============================================================================
|
|
937
|
+
// PUBLIC API
|
|
938
|
+
// ============================================================================
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Initialize the polyfill on all grid-lanes elements in the document
|
|
942
|
+
*/
|
|
943
|
+
function init(options = {}) {
|
|
944
|
+
// Check if native support exists
|
|
945
|
+
if (supportsGridLanes() && !options.force) {
|
|
946
|
+
console.log(
|
|
947
|
+
`${POLYFILL_NAME}: Native support detected, polyfill not needed.`
|
|
948
|
+
)
|
|
949
|
+
return { supported: true, instances: [] }
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const instances = new Map()
|
|
953
|
+
|
|
954
|
+
// Process existing containers
|
|
955
|
+
const containers = findElements()
|
|
956
|
+
|
|
957
|
+
for (const container of containers) {
|
|
958
|
+
if (!container.hasAttribute(POLYFILL_ATTR)) {
|
|
959
|
+
instances.set(container, new GridLanesLayout(container, options))
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Watch for new stylesheets and elements
|
|
964
|
+
// Debounce for ancestor attribute changes to avoid excessive relayouts
|
|
965
|
+
let ancestorLayoutTimeout = null
|
|
966
|
+
const pendingAncestorInstances = new Map()
|
|
967
|
+
const debouncedRelayoutAll = () => {
|
|
968
|
+
if (ancestorLayoutTimeout) clearTimeout(ancestorLayoutTimeout)
|
|
969
|
+
ancestorLayoutTimeout = setTimeout(() => {
|
|
970
|
+
for (const [container, instance] of pendingAncestorInstances) {
|
|
971
|
+
if (instances.has(container)) {
|
|
972
|
+
removeCachedStyle(container)
|
|
973
|
+
instance.refresh()
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
pendingAncestorInstances.clear()
|
|
977
|
+
}, 16)
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const observer = new MutationObserver(mutations => {
|
|
981
|
+
for (const mutation of mutations) {
|
|
982
|
+
if (mutation.type === "attributes") {
|
|
983
|
+
removeCachedStyle(mutation.target)
|
|
984
|
+
// If the changed node is an ancestor of (or is) a container,
|
|
985
|
+
// cascaded/inherited styles on the container may have changed.
|
|
986
|
+
for (const [container] of instances) {
|
|
987
|
+
if (mutation.target.contains(container)) {
|
|
988
|
+
pendingAncestorInstances.set(container, instances.get(container))
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (pendingAncestorInstances.size > 0) {
|
|
992
|
+
debouncedRelayoutAll()
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (mutation.type === "childList") {
|
|
997
|
+
for (const node of mutation.addedNodes) {
|
|
998
|
+
if (node.nodeType !== node.ELEMENT_NODE) continue
|
|
999
|
+
|
|
1000
|
+
processSubtree(node, instances, options)
|
|
1001
|
+
}
|
|
1002
|
+
for (const node of mutation.removedNodes) {
|
|
1003
|
+
if (node.nodeType !== node.ELEMENT_NODE) continue
|
|
1004
|
+
if (instances.has(node)) {
|
|
1005
|
+
instances.get(node).destroy()
|
|
1006
|
+
instances.delete(node)
|
|
1007
|
+
}
|
|
1008
|
+
styleMap.delete(node)
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
observer.observe(document.documentElement, {
|
|
1015
|
+
childList: true,
|
|
1016
|
+
subtree: true,
|
|
1017
|
+
attributes: true,
|
|
1018
|
+
attributeFilter: ["style", "class"]
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
console.log(
|
|
1022
|
+
`${POLYFILL_NAME}: Initialized, ${instances.size} container(s) found.`
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
return {
|
|
1026
|
+
supported: false,
|
|
1027
|
+
instances,
|
|
1028
|
+
observer,
|
|
1029
|
+
refresh() {
|
|
1030
|
+
for (const instance of instances.values()) {
|
|
1031
|
+
instance.refresh()
|
|
1032
|
+
}
|
|
1033
|
+
},
|
|
1034
|
+
destroy() {
|
|
1035
|
+
observer.disconnect()
|
|
1036
|
+
for (const instance of instances.values()) {
|
|
1037
|
+
instance.destroy()
|
|
1038
|
+
}
|
|
1039
|
+
instances.clear()
|
|
1040
|
+
// TODO: Ensure GC is triggered
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Apply layout to a specific element
|
|
1047
|
+
*/
|
|
1048
|
+
function apply(element, options = {}) {
|
|
1049
|
+
if (supportsGridLanes() && !options.force) {
|
|
1050
|
+
return null
|
|
1051
|
+
}
|
|
1052
|
+
return new GridLanesLayout(element, options)
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// ============================================================================
|
|
1056
|
+
// EXPORTS
|
|
1057
|
+
// ============================================================================
|
|
1058
|
+
|
|
1059
|
+
export { supportsGridLanes, init, apply, GridLanesLayout }
|
|
1060
|
+
|
|
1061
|
+
export default {
|
|
1062
|
+
supportsGridLanes,
|
|
1063
|
+
init,
|
|
1064
|
+
apply,
|
|
1065
|
+
GridLanesLayout,
|
|
1066
|
+
version: "1.1.0"
|
|
1067
|
+
}
|