@asd20/ui-next 2.33.1 → 2.33.3

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 (23) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/package.json +1 -1
  3. package/src/components/atoms/Asd20RichBodyContent/index.vue +3 -2
  4. package/src/components/organisms/Asd20MediaSection/index.vue +26 -5
  5. package/src/components/organisms/Asd20SecondaryHeader/index.vue +38 -348
  6. package/src/components/templates/Asd20ArticleDigestCompactTemplate/index.vue +1 -0
  7. package/src/components/templates/Asd20ArticleDigestTemplate/index.vue +1 -0
  8. package/src/components/templates/Asd20ArticleListTemplate/index.vue +1 -0
  9. package/src/components/templates/Asd20ArticleTemplate/index.vue +3 -2
  10. package/src/components/templates/Asd20CampusDetailTemplate/index.vue +1 -0
  11. package/src/components/templates/Asd20ClubsTemplate/index.vue +1 -0
  12. package/src/components/templates/Asd20DetailAlternateTemplate/index.vue +1 -0
  13. package/src/components/templates/Asd20DetailImageFullTemplate/index.vue +1 -0
  14. package/src/components/templates/Asd20DetailImageTemplate/index.vue +1 -0
  15. package/src/components/templates/Asd20DetailTemplate/index.vue +1 -0
  16. package/src/components/templates/Asd20FileListPageTemplate/index.vue +1 -0
  17. package/src/components/templates/Asd20GroupFeatureTemplate/index.vue +1 -0
  18. package/src/components/templates/Asd20ProfileTemplate/index.vue +1 -0
  19. package/src/components/templates/Asd20WayfindingAlternateTemplate/index.vue +1 -0
  20. package/src/components/templates/Asd20WayfindingImageTemplate/index.vue +1 -0
  21. package/src/components/templates/Asd20WayfindingTemplate/index.vue +1 -0
  22. package/src/helpers/excludeBodyMedia.js +96 -0
  23. package/src/mixins/pageTemplateMixin.js +0 -44
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.33.3](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.33.2...ui-next-v2.33.3) (2026-10-27)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * limit size of videos in body content; exclude videos and images already in body content from media section ([aba90ce](https://github.com/academydistrict20/asd20-ui-next/commit/aba90ceec4c244a8317cc544972fa753615da982))
9
+
10
+ ## [2.33.2](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.33.1...ui-next-v2.33.2) (2026-10-26)
11
+
3
12
  ## [2.33.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.33.0...ui-next-v2.33.1) (2026-10-26)
4
13
 
5
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.33.1",
3
+ "version": "2.33.3",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -231,8 +231,9 @@ export default {
231
231
  }
232
232
 
233
233
  :deep(.asd20-message-video) {
234
- max-width: 100%;
235
- margin: 0.75rem 0;
234
+ width: 100%;
235
+ max-width: 1200px;
236
+ margin: 0.75rem auto;
236
237
  }
237
238
 
238
239
  :deep(.asd20-message-video__description) {
@@ -1,22 +1,22 @@
1
1
  <template>
2
2
  <section
3
- v-show="images.length || videos.length"
3
+ v-if="showImageGallery || remainingMedia.videos.length"
4
4
  class="asd20-media-section"
5
5
  role="complementary"
6
6
  >
7
7
  <slot name="before" />
8
8
 
9
9
  <asd20-image-gallery
10
- v-if="allowSingleImageGallery ? images.length > 0 : images.length > 1"
10
+ v-if="showImageGallery"
11
11
  title="Images"
12
- :images="images"
12
+ :images="galleryImages"
13
13
  >
14
14
  </asd20-image-gallery>
15
15
 
16
16
  <asd20-videos-section
17
- v-if="videos.length > 0"
17
+ v-if="remainingMedia.videos.length > 0"
18
18
  title="Videos"
19
- :videos="videos"
19
+ :videos="remainingMedia.videos"
20
20
  >
21
21
  </asd20-videos-section>
22
22
  <slot />
@@ -25,6 +25,7 @@
25
25
 
26
26
  <script>
27
27
  import { defineAsyncComponent } from 'vue'
28
+ import excludeBodyMedia from '../../../helpers/excludeBodyMedia'
28
29
 
29
30
  export default {
30
31
  name: 'Asd20MediaSection',
@@ -40,6 +41,26 @@ export default {
40
41
  allowSingleImageGallery: { type: Boolean, default: false },
41
42
  images: { type: Array, default: () => [] },
42
43
  videos: { type: Array, default: () => [] },
44
+ // Include only messages whose bodies are displayed on this page.
45
+ bodyMessages: { type: Array, default: () => [] },
46
+ },
47
+ computed: {
48
+ remainingMedia() {
49
+ return excludeBodyMedia({
50
+ images: this.images,
51
+ videos: this.videos,
52
+ bodyMessages: this.bodyMessages,
53
+ })
54
+ },
55
+ galleryImages() {
56
+ return this.remainingMedia.images.filter(image => !image.isCover)
57
+ },
58
+ showImageGallery() {
59
+ // Preserve galleries that have one image left after body duplicates
60
+ // are removed, while retaining the existing single-image opt-in.
61
+ return this.galleryImages.length > 0 &&
62
+ (this.allowSingleImageGallery || this.images.length > 1)
63
+ },
43
64
  },
44
65
  }
45
66
  </script>
@@ -97,26 +97,11 @@
97
97
  <div class="white-accent-band">
98
98
  <section
99
99
  ref="header"
100
- :class="headerClasses"
100
+ class="asd20-secondary-header"
101
101
  aria-label="Secondary message"
102
- :style="secondaryHeaderStyle"
103
102
  >
104
- <!-- Parallax background (masked by .asd20-secondary-header mask) -->
105
- <div
106
- class="secondary-parallax"
107
- aria-hidden="true"
108
- >
109
- <div class="secondary-parallax__layer secondary-parallax__base"></div>
110
- <div
111
- class="secondary-parallax__layer secondary-parallax__field"
112
- ></div>
113
- <div
114
- class="secondary-parallax__layer secondary-parallax__ribbons"
115
- ></div>
116
- </div>
117
-
118
103
  <asd20-messaging
119
- v-bind="secondaryMessageForDisplay"
104
+ v-bind="secondaryMessage"
120
105
  fullscreen
121
106
  ></asd20-messaging>
122
107
  </section>
@@ -141,181 +126,10 @@ export default {
141
126
  grid: { type: Boolean, default: false },
142
127
  forceDisplay: { type: Boolean, default: false },
143
128
  },
144
- data() {
145
- return {
146
- isTouchCapableDevice: false,
147
- parallaxEnabled: false,
148
- intersectionObserver: null,
149
- parallaxRaf: null,
150
- handleScroll: null,
151
- handleResize: null,
152
- }
153
- },
154
129
  computed: {
155
- shouldUseParallax() {
156
- return !this.isTouchCapableDevice
157
- },
158
- headerClasses() {
159
- return {
160
- 'asd20-secondary-header': true,
161
- 'asd20-secondary-header--desktop-background':
162
- this.hasSecondaryHeaderImage,
163
- 'asd20-secondary-header--parallax-enabled-background':
164
- this.hasSecondaryHeaderImage && this.shouldUseParallax,
165
- 'asd20-secondary-header--hide-desktop-image':
166
- !this.shouldShowDesktopInlineImage,
167
- }
168
- },
169
130
  secondaryMessage() {
170
131
  return Array.isArray(this.messages) ? this.messages[1] || {} : {}
171
132
  },
172
- secondaryMessageImages() {
173
- return Array.isArray(this.secondaryMessage.images)
174
- ? this.secondaryMessage.images
175
- : []
176
- },
177
- secondaryHeaderImageObject() {
178
- return (
179
- this.secondaryMessageImages.find(image => image && image.isCover) ||
180
- this.secondaryMessageImages[0] ||
181
- null
182
- )
183
- },
184
- secondaryHeaderImage() {
185
- return this.resolveMessageImageUrl(this.secondaryHeaderImageObject)
186
- },
187
- hasSecondaryHeaderImage() {
188
- return Boolean(this.secondaryHeaderImage)
189
- },
190
- shouldShowDesktopInlineImage() {
191
- return Boolean(
192
- this.secondaryHeaderImageObject && this.secondaryHeaderImageObject.isCover
193
- )
194
- },
195
- secondaryMessageForDisplay() {
196
- if (!this.secondaryHeaderImageObject) return this.secondaryMessage
197
-
198
- return {
199
- ...this.secondaryMessage,
200
- images: [
201
- {
202
- ...this.secondaryHeaderImageObject,
203
- isCover: true,
204
- },
205
- ],
206
- }
207
- },
208
- secondaryHeaderStyle() {
209
- if (!this.hasSecondaryHeaderImage) return {}
210
-
211
- return {
212
- '--secondary-header-image': `url("${this.secondaryHeaderImage}")`,
213
- }
214
- },
215
- },
216
- mounted() {
217
- if (typeof window !== 'undefined') {
218
- this.isTouchCapableDevice = Boolean(
219
- window.matchMedia?.('(pointer: coarse)').matches ||
220
- window.navigator?.maxTouchPoints > 0
221
- )
222
- }
223
-
224
- // Respect reduced motion
225
- const prefersReduced =
226
- typeof window !== 'undefined' &&
227
- window.matchMedia &&
228
- window.matchMedia('(prefers-reduced-motion: reduce)').matches
229
-
230
- if (prefersReduced || !this.shouldUseParallax) return
231
-
232
- this.handleScroll = () => this.scheduleParallaxUpdate()
233
- this.handleResize = () => this.scheduleParallaxUpdate()
234
-
235
- // Only animate when the header is in view
236
- if (typeof window !== 'undefined' && 'IntersectionObserver' in window) {
237
- this.intersectionObserver = new IntersectionObserver(
238
- entries => {
239
- const entry = entries && entries[0]
240
- if (!entry) return
241
- if (entry.isIntersecting) this.enableParallax()
242
- else this.disableParallax()
243
- },
244
- { root: null, threshold: 0.01 }
245
- )
246
-
247
- if (this.$refs.header) this.intersectionObserver.observe(this.$refs.header)
248
- } else {
249
- // Fallback: just enable
250
- this.enableParallax()
251
- }
252
-
253
- // Initial set
254
- this.scheduleParallaxUpdate()
255
- },
256
- beforeUnmount() {
257
- this.teardownParallax()
258
- },
259
- methods: {
260
- resolveMessageImageUrl(image) {
261
- if (!image) return ''
262
-
263
- const imageFiles = Array.isArray(image.files) ? image.files : []
264
- const fullImage = imageFiles.find(file => file.name === 'full')
265
- return fullImage ? fullImage.url : image.url || ''
266
- },
267
- teardownParallax() {
268
- this.disableParallax()
269
- if (this.intersectionObserver) this.intersectionObserver.disconnect()
270
- this.intersectionObserver = null
271
- },
272
- enableParallax() {
273
- if (this.parallaxEnabled) return
274
- this.parallaxEnabled = true
275
- window.addEventListener('scroll', this.handleScroll, { passive: true })
276
- window.addEventListener('resize', this.handleResize, { passive: true })
277
- this.scheduleParallaxUpdate()
278
- },
279
- disableParallax() {
280
- if (!this.parallaxEnabled) return
281
- this.parallaxEnabled = false
282
- window.removeEventListener('scroll', this.handleScroll)
283
- window.removeEventListener('resize', this.handleResize)
284
- if (this.parallaxRaf) cancelAnimationFrame(this.parallaxRaf)
285
- this.parallaxRaf = null
286
- },
287
- scheduleParallaxUpdate() {
288
- if (!this.parallaxEnabled) return
289
- if (this.parallaxRaf) return
290
- this.parallaxRaf = requestAnimationFrame(() => {
291
- this.parallaxRaf = null
292
- this.updateParallax()
293
- })
294
- },
295
- updateParallax() {
296
- const el = this.$refs.header
297
- if (!el) return
298
-
299
- const rect = el.getBoundingClientRect()
300
- const vh = window.innerHeight || 1
301
-
302
- // Progress through viewport: 0 (below) -> 1 (above)
303
- // We keep it gentle and clamp
304
- const mid = rect.top + rect.height * 0.25
305
- const tRaw = 1 - mid / vh
306
- const t = Math.max(0, Math.min(1, tRaw * 1.25))
307
- const t2 = Math.max(0, Math.min(1, tRaw * 1.25)) // field
308
- const t3 = Math.max(0, Math.min(1, tRaw * 1.6)) // ribbons
309
-
310
- // Subtle movement only (px)
311
- const y1 = (t - 0.5) * 22 // base
312
- const y2 = (t2 - 0.5) * 60 // field
313
- const y3 = (t3 - 0.5) * 40 // ribbons
314
-
315
- el.style.setProperty('--secondary-parallax-y1', `${y1}px`)
316
- el.style.setProperty('--secondary-parallax-y2', `${y2}px`)
317
- el.style.setProperty('--secondary-parallax-y3', `${y3}px`)
318
- },
319
133
  },
320
134
  }
321
135
  </script>
@@ -344,13 +158,14 @@ export default {
344
158
  }
345
159
 
346
160
  .asd20-secondary-header {
347
- --secondary-header-image: none;
348
161
  position: relative;
349
162
  display: flex;
350
163
  flex-shrink: 0;
351
164
  flex-direction: column;
352
165
  color: var(--website-homepage-header__secondary-foreground-color);
353
- background: var(--website-homepage-header__secondary-background-color);
166
+ // CommCenter supplies a full background shorthand (image, position, size, repeat).
167
+ background: var(--website-homepage-header__secondary-background-style, none);
168
+ background-color: var(--website-homepage-header__secondary-background-color);
354
169
  padding: space(1) 0 0 0;
355
170
 
356
171
  mask: url(#secondary-header-mask-mobile);
@@ -358,7 +173,6 @@ export default {
358
173
  mask-size: 100% 100%;
359
174
  -webkit-mask-size: 100% 100%;
360
175
 
361
- /* Ensure content stays above the parallax layers */
362
176
  .asd20-messaging {
363
177
  position: relative;
364
178
  z-index: 2;
@@ -367,94 +181,6 @@ export default {
367
181
  flex-direction: column;
368
182
  }
369
183
 
370
- /* Parallax background container */
371
- .secondary-parallax {
372
- position: absolute;
373
- inset: 0;
374
- z-index: 1;
375
- pointer-events: none;
376
- overflow: hidden; /* mask will clip, but this avoids any edge artifacts */
377
- }
378
-
379
- .secondary-parallax__layer {
380
- position: absolute;
381
- inset: -10% -10% -10% -10%;
382
- transform: translate3d(0, 0, 0);
383
- will-change: transform;
384
- }
385
-
386
- /* Layer 1: base gradient glow */
387
- .secondary-parallax__base {
388
- transform: translate3d(0, var(--secondary-parallax-y1, 0px), 0);
389
- background: radial-gradient(
390
- 800px 520px at 90% 90%,
391
- rgba(255, 255, 255, 0.55) 0%,
392
- rgba(255, 255, 255, 0.22) 0%,
393
- rgba(255, 255, 255, 0) 0%
394
- ),
395
- linear-gradient(
396
- 90deg,
397
- rgba(12, 36, 86, 0.95) 0%,
398
- rgba(12, 36, 86, 0.85) 35%,
399
- rgba(12, 36, 86, 0.25) 100%
400
- );
401
- /* linear-gradient(*/
402
- /* 90deg,*/
403
- /* rgba(12, 36, 86, 0.95) 0%,*/
404
- /* rgba(24, 74, 160, 0.6) 85%,*/
405
- /* rgba(112, 160, 230, 0.85) 100%*/
406
- /* );*/
407
- }
408
-
409
- /* Layer 2: soft wave fields (broad arcs) */
410
- .secondary-parallax__field {
411
- transform: translate3d(0px, var(--secondary-parallax-y2, 0px), 0)
412
- rotate(-5deg);
413
- background: radial-gradient(
414
- 280% 155% at 20% 110%,
415
- rgba(255, 255, 255, 0) 0%,
416
- rgba(255, 255, 255, 0) 62%,
417
- rgba(188, 220, 250, 0.95) 70%,
418
- rgba(255, 255, 255, 0) 72%
419
- );
420
- filter: none;
421
- opacity: 0.9;
422
- mask-image: linear-gradient(
423
- 90deg,
424
- rgba(0, 0, 0, 0.85) 0%,
425
- rgba(0, 0, 0, 1) 85%
426
- );
427
- -webkit-mask-image: linear-gradient(90deg, rgba(0,0,0,0.85) 0%, rgba(0,0,0,1) 85%);
428
- }
429
-
430
- /* Layer 3: highlight ribbons (brighter curves) */
431
- .secondary-parallax__ribbons {
432
- transform: translate3d(0, var(--secondary-parallax-y3, 0px), 0)
433
- rotate(-3deg);
434
- background: radial-gradient(
435
- 260% 160% at 50% 110%,
436
- rgba(255, 255, 255, 0) 0%,
437
- rgba(255, 255, 255, 0) 60%,
438
- rgba(255, 255, 255, 0.9) 64%,
439
- rgba(255, 255, 255, 0) 70%
440
- );
441
- opacity: 0.9;
442
- filter: none;
443
- mask-image: linear-gradient(
444
- 90deg,
445
- rgba(0, 0, 0, 0.3) 0%,
446
- rgba(0, 0, 0, 1) 70%
447
- );
448
- -webkit-mask-image: linear-gradient(90deg, rgba(0,0,0,0.3) 0%, rgba(0,0,0,1) 70%);
449
- }
450
-
451
- /* Reduced motion: lock the layers */
452
- @media (prefers-reduced-motion: reduce) {
453
- .secondary-parallax__layer {
454
- transform: none !important;
455
- }
456
- }
457
-
458
184
  & :deep(.message-image) {
459
185
  order: 2;
460
186
  border-top: none !important;
@@ -484,6 +210,15 @@ export default {
484
210
  padding: space(1) space(1) space(1) space(1);
485
211
  }
486
212
 
213
+ & :deep(.asd20-messaging__button-group) {
214
+ display: grid;
215
+ grid-template-columns: minmax(0, 1fr);
216
+ gap: space(0.5);
217
+ width: 70vw;
218
+ max-width: 100%;
219
+ margin: space(0.5) auto;
220
+ }
221
+
487
222
  & :deep(.asd20-button) {
488
223
  @include asd20-font(
489
224
  0.75,
@@ -492,8 +227,10 @@ export default {
492
227
  700
493
228
  );
494
229
  flex-direction: column;
495
- width: 70vw;
496
- margin: space(0.5) auto !important;
230
+ box-sizing: border-box;
231
+ min-width: 0;
232
+ width: 100%;
233
+ margin: 0 !important;
497
234
  color: var(--website-homepage-header__secondary-button-foreground-color);
498
235
  background: var(
499
236
  --website-homepage-header__secondary-button-background-color
@@ -505,6 +242,9 @@ export default {
505
242
  z-index: 2;
506
243
  overflow: hidden;
507
244
  }
245
+ & :deep(.asd20-button__label) {
246
+ overflow-wrap: anywhere;
247
+ }
508
248
  & :deep(.asd20-button)::before {
509
249
  content: '';
510
250
  position: absolute;
@@ -542,9 +282,9 @@ export default {
542
282
  & :deep(.lead) {
543
283
  width: 100%;
544
284
  }
545
- & :deep(.asd20-button) {
285
+ & :deep(.asd20-messaging__button-group) {
546
286
  width: 50vw;
547
- margin: space(0.5) space(0.5) 0 0 !important;
287
+ margin-inline: 0;
548
288
  }
549
289
  }
550
290
  }
@@ -575,14 +315,8 @@ export default {
575
315
  }
576
316
 
577
317
  @media (min-width: 1024px) {
578
- .asd20-secondary-header--desktop-background {
318
+ .asd20-secondary-header {
579
319
  padding: space(3) space(3) space(2) space(3);
580
- background-color: var(--website-homepage-header__secondary-background-color);
581
- background-image: var(--secondary-header-image);
582
- background-position: center;
583
- background-repeat: no-repeat;
584
- background-size: cover;
585
- background-attachment: scroll;
586
320
  padding-top: 70px;
587
321
 
588
322
  mask: url(#secondary-header-mask-desktop);
@@ -607,6 +341,11 @@ export default {
607
341
  border-bottom: none;
608
342
  }
609
343
 
344
+ & :deep(.asd20-messaging__button-group) {
345
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
346
+ width: 100%;
347
+ }
348
+
610
349
  & :deep(.asd20-button) {
611
350
  @include asd20-font(
612
351
  0.75,
@@ -615,8 +354,6 @@ export default {
615
354
  700
616
355
  );
617
356
  flex-direction: row;
618
- width: inherit;
619
- margin: space(0.5) space(0.5) 0 0 !important;
620
357
  padding: space(0.5) space(1.5);
621
358
  }
622
359
 
@@ -634,64 +371,17 @@ export default {
634
371
  }
635
372
  }
636
373
 
637
- .asd20-secondary-header--desktop-background .secondary-parallax {
638
- display: block;
639
- }
640
-
641
- .asd20-secondary-header--desktop-background .secondary-parallax__field,
642
- .asd20-secondary-header--desktop-background .secondary-parallax__ribbons {
643
- display: none;
644
- }
645
-
646
- .asd20-secondary-header--parallax-enabled-background {
647
- background-attachment: fixed;
648
- }
649
-
650
- .asd20-secondary-header--parallax-enabled-background .secondary-parallax__field,
651
- .asd20-secondary-header--parallax-enabled-background .secondary-parallax__ribbons {
652
- display: block;
653
- }
654
-
655
- .asd20-secondary-header--parallax-enabled-background .secondary-parallax__field {
656
- transform: translate3d(0px, var(--secondary-parallax-y2, 0px), 0)
657
- rotate(-5deg);
658
- filter: none;
659
- opacity: 0.9;
660
- mask-image: linear-gradient(
661
- 90deg,
662
- rgba(0, 0, 0, 0.85) 0%,
663
- rgba(0, 0, 0, 1) 85%
664
- );
665
- -webkit-mask-image: linear-gradient(90deg, rgba(0,0,0,0.85) 0%, rgba(0,0,0,1) 85%);
666
- }
667
-
668
- .asd20-secondary-header--parallax-enabled-background .secondary-parallax__ribbons {
669
- transform: translate3d(0, var(--secondary-parallax-y3, 0px), 0)
670
- rotate(-3deg);
671
- background: radial-gradient(
672
- 180% 160% at 40% 110%,
673
- rgba(255, 255, 255, 0) 0%,
674
- rgba(255, 255, 255, 0) 60%,
675
- rgba(255, 255, 255, 0.9) 64%,
676
- rgba(255, 255, 255, 0) 70%
677
- );
678
- opacity: 0.9;
679
- filter: none;
680
- mask-image: linear-gradient(
681
- 90deg,
682
- rgba(0, 0, 0, 0.3) 0%,
683
- rgba(0, 0, 0, 1) 70%
684
- );
685
- -webkit-mask-image: linear-gradient(90deg, rgba(0,0,0,0.3) 0%, rgba(0,0,0,1) 70%);
686
- }
687
-
688
- .asd20-secondary-header--desktop-background.asd20-secondary-header--hide-desktop-image {
689
- & :deep(.asd20-messaging--fullscreen > .message-image) {
690
- display: none;
374
+ }
375
+ @media (min-width: 1025px) {
376
+ .asd20-secondary-header {
377
+ & :deep(.asd20-messaging__button-group) {
378
+ display: flex;
379
+ flex-wrap: wrap;
691
380
  }
692
381
 
693
- & :deep(.asd20-messaging--fullscreen > .asd20-messaging__content) {
694
- max-width: 60%;
382
+ & :deep(.asd20-button) {
383
+ width: fit-content;
384
+ max-width: 100%;
695
385
  }
696
386
  }
697
387
  }
@@ -210,6 +210,7 @@
210
210
 
211
211
  <asd20-media-section
212
212
  v-if="combinedMessageImages.length > 1 || combinedMessageVideos.length"
213
+ :body-messages="[...primaryMessages, ...secondaryMessages]"
213
214
  :images="combinedMessageImages || []"
214
215
  :videos="combinedMessageVideos || []"
215
216
  >
@@ -211,6 +211,7 @@
211
211
 
212
212
  <asd20-media-section
213
213
  v-if="combinedMessageImages.length > 1 || combinedMessageVideos.length"
214
+ :body-messages="[...primaryMessages, ...secondaryMessages]"
214
215
  :images="combinedMessageImages || []"
215
216
  :videos="combinedMessageVideos || []"
216
217
  >
@@ -230,6 +230,7 @@
230
230
 
231
231
  <asd20-media-section
232
232
  v-if="combinedMessageImages.length > 1 || combinedMessageVideos.length"
233
+ :body-messages="[...primaryMessages, ...secondaryMessages]"
233
234
  :images="combinedMessageImages || []"
234
235
  :videos="combinedMessageVideos || []"
235
236
  >
@@ -95,9 +95,10 @@
95
95
  ></asd20-feeds-section> -->
96
96
 
97
97
  <asd20-media-section
98
- v-if="shouldShowArticleMediaSection"
98
+ v-if="combinedMessageImages.length || combinedMessageVideos.length"
99
+ :body-messages="[...primaryDetailMessages, ...secondaryMessages]"
99
100
  allow-single-image-gallery
100
- :images="articleGalleryImages || []"
101
+ :images="combinedMessageImages || []"
101
102
  :videos="combinedMessageVideos || []"
102
103
  >
103
104
  </asd20-media-section>
@@ -89,6 +89,7 @@
89
89
 
90
90
  <asd20-media-section
91
91
  v-if="combinedMessageImages.length > 1 || combinedMessageVideos.length"
92
+ :body-messages="[...primaryMessages, ...secondaryMessages]"
92
93
  :images="combinedMessageImages || []"
93
94
  :videos="combinedMessageVideos || []"
94
95
  >
@@ -222,6 +222,7 @@
222
222
 
223
223
  <asd20-media-section
224
224
  v-if="combinedMessageImages.length > 1 || combinedMessageVideos.length"
225
+ :body-messages="[...primaryMessages, ...secondaryMessages]"
225
226
  :images="combinedMessageImages || []"
226
227
  :videos="combinedMessageVideos || []"
227
228
  >
@@ -95,6 +95,7 @@
95
95
 
96
96
  <asd20-media-section
97
97
  v-if="combinedMessageImages.length > 1 || combinedMessageVideos.length"
98
+ :body-messages="[...primaryDetailMessages, ...secondaryMessages]"
98
99
  :images="combinedMessageImages || []"
99
100
  :videos="combinedMessageVideos || []"
100
101
  >
@@ -94,6 +94,7 @@
94
94
 
95
95
  <asd20-media-section
96
96
  v-if="combinedMessageImages.length > 1 || combinedMessageVideos.length"
97
+ :body-messages="[...primaryDetailMessages, ...secondaryMessages]"
97
98
  :images="combinedMessageImages || []"
98
99
  :videos="combinedMessageVideos || []"
99
100
  >
@@ -94,6 +94,7 @@
94
94
 
95
95
  <asd20-media-section
96
96
  v-if="combinedMessageImages.length > 1 || combinedMessageVideos.length"
97
+ :body-messages="[...primaryDetailMessages, ...secondaryMessages]"
97
98
  :images="combinedMessageImages || []"
98
99
  :videos="combinedMessageVideos || []"
99
100
  >
@@ -94,6 +94,7 @@
94
94
 
95
95
  <asd20-media-section
96
96
  v-if="combinedMessageImages.length > 1 || combinedMessageVideos.length"
97
+ :body-messages="[...primaryDetailMessages, ...secondaryMessages]"
97
98
  :images="combinedMessageImages || []"
98
99
  :videos="combinedMessageVideos || []"
99
100
  >
@@ -90,6 +90,7 @@
90
90
 
91
91
  <asd20-media-section
92
92
  v-if="combinedMessageImages.length > 1 || combinedMessageVideos.length"
93
+ :body-messages="[...primaryMessages, ...secondaryMessages]"
93
94
  :images="combinedMessageImages || []"
94
95
  :videos="combinedMessageVideos || []"
95
96
  >
@@ -91,6 +91,7 @@
91
91
 
92
92
  <asd20-media-section
93
93
  v-if="combinedMessageImages.length > 1 || combinedMessageVideos.length"
94
+ :body-messages="[...primaryMessagesGroup, ...secondaryMessages]"
94
95
  :images="combinedMessageImages || []"
95
96
  :videos="combinedMessageVideos || []"
96
97
  >
@@ -276,6 +276,7 @@
276
276
 
277
277
  <asd20-media-section
278
278
  v-if="combinedMessageImages.length > 1 || combinedMessageVideos.length"
279
+ :body-messages="[...primaryMessages, ...secondaryMessages]"
279
280
  :images="combinedMessageImages || []"
280
281
  :videos="combinedMessageVideos || []"
281
282
  >
@@ -95,6 +95,7 @@
95
95
  <!-- Photos & Videos -->
96
96
  <asd20-media-section
97
97
  v-if="firstMessage.images.length > 1 || firstMessage.videos.length"
98
+ :body-messages="primaryMessage || []"
98
99
  :images="firstMessage.images || []"
99
100
  :videos="firstMessage.videos || []"
100
101
  >
@@ -96,6 +96,7 @@
96
96
  <!-- Photos & Videos -->
97
97
  <asd20-media-section
98
98
  v-if="firstMessage.images.length > 1 || firstMessage.videos.length"
99
+ :body-messages="primaryMessage || []"
99
100
  :images="firstMessage.images || []"
100
101
  :videos="firstMessage.videos || []"
101
102
  >
@@ -93,6 +93,7 @@
93
93
  <!-- Photos & Videos -->
94
94
  <asd20-media-section
95
95
  v-if="firstMessage.images.length > 1 || firstMessage.videos.length"
96
+ :body-messages="primaryMessage || []"
96
97
  :images="firstMessage.images || []"
97
98
  :videos="firstMessage.videos || []"
98
99
  >
@@ -0,0 +1,96 @@
1
+ import { parseFragment } from 'parse5'
2
+ import { buildRichBodyRenderModel } from '../components/atoms/Asd20RichBodyContent/messageDocumentRenderer'
3
+
4
+ const STORAGE_HOST = 'asd20websitestorage.blob.core.windows.net'
5
+ const RELATIVE_ORIGIN = 'https://ui-next.invalid'
6
+
7
+ function resourceKey(value) {
8
+ if (typeof value !== 'string' || !value.trim()) return ''
9
+ try {
10
+ const url = new URL(value.trim(), RELATIVE_ORIGIN)
11
+ if (!['http:', 'https:'].includes(url.protocol)) return ''
12
+ url.hash = ''
13
+ // The website media proxy and blob origin serve the same stored files.
14
+ if (url.pathname.startsWith('/_media/')) {
15
+ return `storage:${url.pathname.slice('/_media'.length)}${url.search}`
16
+ }
17
+ if (url.hostname === STORAGE_HOST) {
18
+ return `storage:${url.pathname}${url.search}`
19
+ }
20
+ return url.href
21
+ } catch {
22
+ return ''
23
+ }
24
+ }
25
+
26
+ function videoKey(value) {
27
+ const key = resourceKey(value)
28
+ if (!key || key.startsWith('storage:')) return key
29
+ const url = new URL(key)
30
+ const host = url.hostname.replace(/^www\./, '')
31
+ if (['youtube.com', 'youtube-nocookie.com', 'youtu.be'].includes(host)) {
32
+ const id = host === 'youtu.be'
33
+ ? url.pathname.split('/')[1]
34
+ : url.searchParams.get('v') || url.pathname.match(/^\/(?:embed|shorts)\/([^/]+)/)?.[1]
35
+ if (id) return `youtube:${id}`
36
+ }
37
+ if (['vimeo.com', 'player.vimeo.com'].includes(host)) {
38
+ const id = url.pathname.match(/^\/(?:video\/)?(\d+)(?:\/|$)/)?.[1]
39
+ if (id) return `vimeo:${id}`
40
+ }
41
+ if (host === 'live.myvrspot.com' && url.searchParams.get('v')) {
42
+ return `myvrspot:${url.searchParams.get('v')}`
43
+ }
44
+ return key
45
+ }
46
+
47
+ function addSource(sources, value, normalize) {
48
+ const key = normalize(value)
49
+ if (key) sources.add(key)
50
+ }
51
+
52
+ function collectHtmlMedia(html, sources) {
53
+ const visit = node => {
54
+ const attrs = Object.fromEntries((node.attrs || []).map(attr => [attr.name, attr.value]))
55
+ if (node.tagName === 'img') {
56
+ addSource(sources.images, attrs.src, resourceKey)
57
+ }
58
+ if (['iframe', 'video'].includes(node.tagName) ||
59
+ (node.tagName === 'source' && node.parentNode?.tagName === 'video')) {
60
+ addSource(sources.videos, attrs.src, videoKey)
61
+ }
62
+ for (const child of node.childNodes || []) visit(child)
63
+ }
64
+ visit(parseFragment(html || ''))
65
+ }
66
+
67
+ export default function excludeBodyMedia({ images = [], videos = [], bodyMessages = [] } = {}) {
68
+ const sources = { images: new Set(), videos: new Set() }
69
+ for (const message of bodyMessages) {
70
+ if (!message) continue
71
+ // Use the renderer's selected content, including its HTML fallback for
72
+ // invalid/unsupported JSON, rather than inspecting stale saved sidecars.
73
+ const model = buildRichBodyRenderModel({
74
+ bodyContent: message.bodyContent || message.bodyHtml || '',
75
+ bodyJson: message.bodyJson,
76
+ })
77
+ for (const node of model.nodes) {
78
+ if (node.html) collectHtmlMedia(node.html, sources)
79
+ if (node.type === 'accordion') {
80
+ for (const item of node.block.items || []) {
81
+ if (item.contentType !== 'tableau') collectHtmlMedia(item.bodyHtml, sources)
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ return {
88
+ images: images.filter(image => image && ![
89
+ image.url,
90
+ ...(image.files || []).map(file => file?.url),
91
+ ].some(url => sources.images.has(resourceKey(url)))),
92
+ videos: videos.filter(video => video && ![
93
+ video.embedUrl, video.watchUrl, video.url,
94
+ ].some(url => sources.videos.has(videoKey(url)))),
95
+ }
96
+ }
@@ -1,32 +1,6 @@
1
1
  import responsiveBreakpointMixin from './responsiveBreakpointMixin'
2
2
  import { buildBreadcrumbLinks } from '../helpers/breadcrumbLinks'
3
3
 
4
- function getMessageBodyHtml(message) {
5
- return (message && (message.bodyContent || message.bodyHtml)) || ''
6
- }
7
-
8
- function getImageUrls(image) {
9
- const urls = []
10
- const files = (image && image.files) || []
11
- if (image && image.url) urls.push(image.url)
12
- files.forEach(file => {
13
- if (file && file.url) urls.push(file.url)
14
- })
15
- return urls
16
- }
17
-
18
- function normalizeHtmlValue(value) {
19
- return String(value || '').replace(/&amp;/g, '&')
20
- }
21
-
22
- function imageAppearsInBody(image, bodyHtml) {
23
- if (!bodyHtml) return false
24
- const normalizedBodyHtml = normalizeHtmlValue(bodyHtml)
25
- return getImageUrls(image).some(url =>
26
- normalizedBodyHtml.includes(normalizeHtmlValue(url))
27
- )
28
- }
29
-
30
4
  export default {
31
5
  mixins: [responsiveBreakpointMixin],
32
6
 
@@ -270,24 +244,6 @@ export default {
270
244
  }, [])
271
245
  },
272
246
 
273
- articleGalleryImages() {
274
- const messageBodies = (this.primaryDetailMessages || [])
275
- .map(getMessageBodyHtml)
276
- .filter(Boolean)
277
-
278
- return this.combinedMessageImages.filter(image => {
279
- if (!image || image.isCover) return false
280
- return !messageBodies.some(bodyHtml => imageAppearsInBody(image, bodyHtml))
281
- })
282
- },
283
-
284
- shouldShowArticleMediaSection() {
285
- return (
286
- this.articleGalleryImages.length > 0 ||
287
- this.combinedMessageVideos.length > 0
288
- )
289
- },
290
-
291
247
  combinedMessageVideos() {
292
248
  return (this.messages || []).reduce((a, m) => {
293
249
  const videos = m.videos || []