@asd20/ui-next 2.33.2 → 2.34.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.
Files changed (25) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +1 -1
  3. package/src/components/atoms/Asd20RichBodyContent/index.vue +3 -2
  4. package/src/components/molecules/Asd20PublicFormField/index.vue +34 -1
  5. package/src/components/organisms/Asd20MediaSection/index.vue +26 -5
  6. package/src/components/organisms/Asd20PublicFormModal/index.vue +1 -1
  7. package/src/components/templates/Asd20ArticleDigestCompactTemplate/index.vue +1 -0
  8. package/src/components/templates/Asd20ArticleDigestTemplate/index.vue +1 -0
  9. package/src/components/templates/Asd20ArticleListTemplate/index.vue +1 -0
  10. package/src/components/templates/Asd20ArticleTemplate/index.vue +3 -2
  11. package/src/components/templates/Asd20CampusDetailTemplate/index.vue +1 -0
  12. package/src/components/templates/Asd20ClubsTemplate/index.vue +1 -0
  13. package/src/components/templates/Asd20DetailAlternateTemplate/index.vue +1 -0
  14. package/src/components/templates/Asd20DetailImageFullTemplate/index.vue +1 -0
  15. package/src/components/templates/Asd20DetailImageTemplate/index.vue +1 -0
  16. package/src/components/templates/Asd20DetailTemplate/index.vue +1 -0
  17. package/src/components/templates/Asd20FileListPageTemplate/index.vue +1 -0
  18. package/src/components/templates/Asd20GroupFeatureTemplate/index.vue +1 -0
  19. package/src/components/templates/Asd20ProfileTemplate/index.vue +1 -0
  20. package/src/components/templates/Asd20WayfindingAlternateTemplate/index.vue +1 -0
  21. package/src/components/templates/Asd20WayfindingImageTemplate/index.vue +1 -0
  22. package/src/components/templates/Asd20WayfindingTemplate/index.vue +1 -0
  23. package/src/helpers/excludeBodyMedia.js +96 -0
  24. package/src/helpers/publicFormDefinition.js +31 -13
  25. package/src/mixins/pageTemplateMixin.js +0 -44
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ # [2.34.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.33.3...ui-next-v2.34.0) (2026-10-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * support checkbox groups for school transfer records ([9596d21](https://github.com/academydistrict20/asd20-ui-next/commit/9596d21fea018afec4c03de64471549cb77e9487))
9
+
10
+ ## [2.33.3](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.33.2...ui-next-v2.33.3) (2026-10-27)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * 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))
16
+
3
17
  ## [2.33.2](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.33.1...ui-next-v2.33.2) (2026-10-26)
4
18
 
5
19
  ## [2.33.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.33.0...ui-next-v2.33.1) (2026-10-26)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.33.2",
3
+ "version": "2.34.0",
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) {
@@ -149,6 +149,32 @@
149
149
  </label>
150
150
  </fieldset>
151
151
 
152
+ <fieldset
153
+ v-else-if="field.type === 'checkbox-group'"
154
+ :id="inputId"
155
+ class="asd20-public-form-field__choices"
156
+ tabindex="-1"
157
+ :aria-describedby="describedBy"
158
+ :aria-invalid="hasErrors ? 'true' : undefined"
159
+ >
160
+ <legend>{{ displayLabel }}</legend>
161
+ <label
162
+ v-for="option in field.options"
163
+ :key="option.value"
164
+ :for="`${inputId}-${option.value}`"
165
+ >
166
+ <input
167
+ :id="`${inputId}-${option.value}`"
168
+ :name="inputId"
169
+ type="checkbox"
170
+ :value="option.value"
171
+ :checked="Array.isArray(modelValue) && modelValue.includes(option.value)"
172
+ @change="toggleChoice(option.value, $event.target.checked)"
173
+ />
174
+ <span>{{ option.label }}</span>
175
+ </label>
176
+ </fieldset>
177
+
152
178
  <asd20-checkbox
153
179
  v-else-if="field.type === 'checkbox'"
154
180
  :id="inputId"
@@ -276,6 +302,12 @@ export default {
276
302
  },
277
303
  },
278
304
  methods: {
305
+ toggleChoice(value, checked) {
306
+ const selected = Array.isArray(this.modelValue) ? this.modelValue : []
307
+ this.$emit('update:modelValue', checked
308
+ ? [...selected.filter(choice => choice !== value), value]
309
+ : selected.filter(choice => choice !== value))
310
+ },
279
311
  addFiles(event) {
280
312
  const addedFiles = Array.from(event.target.files || [])
281
313
  if (addedFiles.length) {
@@ -407,7 +439,8 @@ export default {
407
439
  align-items: flex-start;
408
440
  }
409
441
 
410
- &__choices input[type='radio'] {
442
+ &__choices input[type='radio'],
443
+ &__choices input[type='checkbox'] {
411
444
  flex: 0 0 auto;
412
445
  margin: 0;
413
446
  }
@@ -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>
@@ -410,7 +410,7 @@ export default {
410
410
  for (const field of this.formDefinition.fields) {
411
411
  if (!isPublicFormFieldVisible(field, this.values)) {
412
412
  this.values[field.name] =
413
- field.type === 'checkbox' ? false : field.type === 'file' ? [] : ''
413
+ field.type === 'checkbox' ? false : ['file', 'checkbox-group'].includes(field.type) ? [] : ''
414
414
  }
415
415
  }
416
416
  if (this.validationAttempted) this.validate(false)
@@ -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
+ }
@@ -7,6 +7,7 @@ export const SUPPORTED_PUBLIC_FORM_FIELD_TYPES = Object.freeze([
7
7
  'select',
8
8
  'radio',
9
9
  'checkbox',
10
+ 'checkbox-group',
10
11
  'file',
11
12
  ])
12
13
 
@@ -48,7 +49,7 @@ function optionalBoolean(value, label) {
48
49
  }
49
50
 
50
51
  function normalizeOptions(field) {
51
- if (!['select', 'radio'].includes(field.type)) return undefined
52
+ if (!['select', 'radio', 'checkbox-group'].includes(field.type)) return undefined
52
53
  if (
53
54
  !Array.isArray(field.options) ||
54
55
  !field.options.length ||
@@ -184,17 +185,19 @@ function normalizeConditions(conditions) {
184
185
  return conditions.map(normalizeCondition)
185
186
  }
186
187
 
188
+ function matchesCondition(condition, values) {
189
+ const value = values[condition.fieldName]
190
+ return Array.isArray(value)
191
+ ? value.some(choice => condition.values.includes(choice))
192
+ : condition.values.includes(String(value || '').trim())
193
+ }
194
+
187
195
  export function isPublicFormFieldVisible(field, values) {
188
- return (
189
- !field.visibleWhen ||
190
- field.visibleWhen.values.includes(String(values[field.visibleWhen.fieldName] || '').trim())
191
- )
196
+ return !field.visibleWhen || matchesCondition(field.visibleWhen, values)
192
197
  }
193
198
 
194
199
  export function isPublicFormFieldRequired(field, values) {
195
- const required = field.required || field.requiredWhen?.some(condition =>
196
- condition.values.includes(String(values[condition.fieldName] || '').trim())
197
- )
200
+ const required = field.required || field.requiredWhen?.some(condition => matchesCondition(condition, values))
198
201
  const uploaded = field.requiredUnlessUploaded &&
199
202
  Array.isArray(values[field.requiredUnlessUploaded]) &&
200
203
  values[field.requiredUnlessUploaded].length > 0
@@ -268,8 +271,8 @@ export function normalizePublicFormDefinition(definition) {
268
271
  )
269
272
  if (
270
273
  !dependency ||
271
- dependency === field ||
272
- !['radio', 'select'].includes(dependency.type) ||
274
+ fields.indexOf(dependency) >= fields.indexOf(field) ||
275
+ !['radio', 'select', 'checkbox-group'].includes(dependency.type) ||
273
276
  condition.values.some(
274
277
  value => !dependency.options.some(option => option.value === value)
275
278
  )
@@ -369,7 +372,7 @@ export function createPublicFormValues(definition) {
369
372
  return Object.fromEntries(
370
373
  definition.fields.map(field => [
371
374
  field.name,
372
- field.type === 'file' ? [] : field.type === 'checkbox' ? false : '',
375
+ ['file', 'checkbox-group'].includes(field.type) ? [] : field.type === 'checkbox' ? false : '',
373
376
  ])
374
377
  )
375
378
  }
@@ -403,9 +406,9 @@ export function validatePublicFormValues(definition, sourceValues = {}) {
403
406
  const values = {}
404
407
 
405
408
  for (const field of definition.fields) {
406
- if (!isPublicFormFieldVisible(field, sourceValues)) {
409
+ if (!isPublicFormFieldVisible(field, values)) {
407
410
  values[field.name] =
408
- field.type === 'file' ? [] : field.type === 'checkbox' ? false : ''
411
+ ['file', 'checkbox-group'].includes(field.type) ? [] : field.type === 'checkbox' ? false : ''
409
412
  continue
410
413
  }
411
414
  const required = isPublicFormFieldRequired(field, sourceValues)
@@ -433,6 +436,21 @@ export function validatePublicFormValues(definition, sourceValues = {}) {
433
436
  'Upload up to 5 PDF documents, 5 MB each and 10 MB total.'
434
437
  )
435
438
  }
439
+ } else if (field.type === 'checkbox-group') {
440
+ const selected = rawValue == null ? [] : rawValue
441
+ const allowed = new Set((field.options || []).map(option => option.value))
442
+ if (
443
+ !Array.isArray(selected) ||
444
+ selected.length > allowed.size ||
445
+ selected.some(choice => typeof choice !== 'string' || !allowed.has(choice)) ||
446
+ new Set(selected).size !== selected.length
447
+ ) {
448
+ values[field.name] = []
449
+ fieldErrors.push(`Select valid choices for ${field.label.toLowerCase()}.`)
450
+ } else {
451
+ values[field.name] = [...selected]
452
+ if (required && !selected.length) fieldErrors.push(`${field.label} is required.`)
453
+ }
436
454
  } else if (field.type === 'checkbox') {
437
455
  const value = rawValue === true
438
456
  values[field.name] = value
@@ -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 || []