@asd20/ui-next 2.17.0 → 2.18.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.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# [2.18.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.17.0...ui-next-v2.18.0) (2026-08-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add tiptap gallery support ([7371f56](https://github.com/academydistrict20/asd20-ui-next/commit/7371f561c6c0a3e6a2275a9537beba72c86e6837))
|
|
9
|
+
|
|
3
10
|
# [2.17.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.16.0...ui-next-v2.17.0) (2026-08-05)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -158,6 +158,37 @@ export default {
|
|
|
158
158
|
margin-bottom: 0;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
:deep(.asd20-message-image-gallery) {
|
|
162
|
+
box-sizing: border-box;
|
|
163
|
+
max-width: 100%;
|
|
164
|
+
margin: 1rem 0;
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
:deep(.asd20-message-image-gallery__images) {
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
gap: 0.75rem;
|
|
173
|
+
width: 100%;
|
|
174
|
+
height: auto !important;
|
|
175
|
+
max-height: min(32rem, 70vh) !important;
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
:deep(.asd20-message-image-gallery__images > img) {
|
|
180
|
+
box-sizing: border-box;
|
|
181
|
+
display: block;
|
|
182
|
+
flex: 0 1 auto;
|
|
183
|
+
width: auto !important;
|
|
184
|
+
min-width: 0;
|
|
185
|
+
max-width: none !important;
|
|
186
|
+
height: auto !important;
|
|
187
|
+
max-height: min(32rem, 70vh) !important;
|
|
188
|
+
margin: 0 !important;
|
|
189
|
+
object-fit: contain;
|
|
190
|
+
}
|
|
191
|
+
|
|
161
192
|
:deep(.asd20-message-video) {
|
|
162
193
|
max-width: 100%;
|
|
163
194
|
margin: 0.75rem 0;
|
|
@@ -27,6 +27,7 @@ const STANDARD_BLOCK_NODE_TYPES = new Set([
|
|
|
27
27
|
'bulletList',
|
|
28
28
|
'orderedList',
|
|
29
29
|
'image',
|
|
30
|
+
'imageGallery',
|
|
30
31
|
'videoEmbed',
|
|
31
32
|
'table',
|
|
32
33
|
])
|
|
@@ -94,6 +95,7 @@ const NODE_ATTRIBUTE_NAMES = Object.freeze({
|
|
|
94
95
|
orderedList: ['start', 'type'],
|
|
95
96
|
listItem: [],
|
|
96
97
|
image: ['src', 'alt', 'title', 'width', 'height'],
|
|
98
|
+
imageGallery: ['images'],
|
|
97
99
|
videoEmbed: ['src', 'title', 'descriptiveText'],
|
|
98
100
|
table: ['caption'],
|
|
99
101
|
tableRow: [],
|
|
@@ -212,6 +214,18 @@ function isSafeResource(value, allowHash = false) {
|
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
216
|
|
|
217
|
+
function isSafeImageResource(value) {
|
|
218
|
+
if (typeof value !== 'string' || !value.trim()) return false
|
|
219
|
+
if (value.trim().startsWith('#')) return false
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const url = new URL(value.trim(), 'https://ui-next.invalid')
|
|
223
|
+
return url.protocol === 'http:' || url.protocol === 'https:'
|
|
224
|
+
} catch (_error) {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
215
229
|
function isCanonicalVideoEmbedSource(value) {
|
|
216
230
|
if (typeof value !== 'string' || !value) return false
|
|
217
231
|
|
|
@@ -328,6 +342,33 @@ function validateAtom(node, path) {
|
|
|
328
342
|
return null
|
|
329
343
|
}
|
|
330
344
|
|
|
345
|
+
function validateImageAttributes(node, path, requiredSource = false) {
|
|
346
|
+
const srcIssue = validateStringAttribute(
|
|
347
|
+
node,
|
|
348
|
+
'src',
|
|
349
|
+
path,
|
|
350
|
+
requiredSource
|
|
351
|
+
)
|
|
352
|
+
if (srcIssue || (node.attrs?.src && !isSafeImageResource(node.attrs.src))) {
|
|
353
|
+
return srcIssue || issue('unsafe-resource', path, 'src')
|
|
354
|
+
}
|
|
355
|
+
for (const name of ['alt', 'title']) {
|
|
356
|
+
const attrIssue = validateStringAttribute(node, name, path)
|
|
357
|
+
if (attrIssue) return attrIssue
|
|
358
|
+
}
|
|
359
|
+
for (const name of ['width', 'height']) {
|
|
360
|
+
const value = node.attrs?.[name]
|
|
361
|
+
if (
|
|
362
|
+
value !== undefined &&
|
|
363
|
+
value !== null &&
|
|
364
|
+
(!Number.isFinite(value) || value <= 0)
|
|
365
|
+
) {
|
|
366
|
+
return issue('malformed-attribute', path, name)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return null
|
|
370
|
+
}
|
|
371
|
+
|
|
331
372
|
function validateNode(node, path) {
|
|
332
373
|
if (!isRecord(node) || typeof node.type !== 'string') {
|
|
333
374
|
return issue('malformed-node', path)
|
|
@@ -430,25 +471,27 @@ function validateNode(node, path) {
|
|
|
430
471
|
: issue('invalid-child-node', `${path}.content[0]`, node.content[0].type)
|
|
431
472
|
}
|
|
432
473
|
if (node.type === 'image') {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
(!Number.isFinite(value) || value <= 0)
|
|
447
|
-
) {
|
|
448
|
-
return issue('malformed-attribute', path, name)
|
|
474
|
+
return validateImageAttributes(node, path) || validateAtom(node, path)
|
|
475
|
+
}
|
|
476
|
+
if (node.type === 'imageGallery') {
|
|
477
|
+
const atomIssue = validateAtom(node, path)
|
|
478
|
+
if (atomIssue) return atomIssue
|
|
479
|
+
const images = node.attrs?.images
|
|
480
|
+
if (!Array.isArray(images) || images.length < 2 || images.length > 3) {
|
|
481
|
+
return issue('malformed-attribute', path, 'images')
|
|
482
|
+
}
|
|
483
|
+
for (let index = 0; index < images.length; index += 1) {
|
|
484
|
+
const imagePath = `${path}.attrs.images[${index}]`
|
|
485
|
+
if (!isRecord(images[index])) {
|
|
486
|
+
return issue('malformed-attribute', imagePath, 'image')
|
|
449
487
|
}
|
|
488
|
+
const imageNode = { type: 'image', attrs: images[index] }
|
|
489
|
+
const attributeNameIssue = validateAttributeNames(imageNode, imagePath)
|
|
490
|
+
if (attributeNameIssue) return attributeNameIssue
|
|
491
|
+
const imageIssue = validateImageAttributes(imageNode, imagePath, true)
|
|
492
|
+
if (imageIssue) return imageIssue
|
|
450
493
|
}
|
|
451
|
-
return
|
|
494
|
+
return null
|
|
452
495
|
}
|
|
453
496
|
if (node.type === 'videoEmbed') {
|
|
454
497
|
const srcIssue = validateStringAttribute(node, 'src', path, true)
|
|
@@ -692,6 +735,17 @@ function renderTable(node) {
|
|
|
692
735
|
)}</tbody></table></div>`
|
|
693
736
|
}
|
|
694
737
|
|
|
738
|
+
function renderImage(attrs) {
|
|
739
|
+
return `<img${attribute('src', attrs.src)}${attribute(
|
|
740
|
+
'alt',
|
|
741
|
+
attrs.alt,
|
|
742
|
+
true
|
|
743
|
+
)}${attribute('title', attrs.title)}${attribute(
|
|
744
|
+
'width',
|
|
745
|
+
attrs.width
|
|
746
|
+
)}${attribute('height', attrs.height)}>`
|
|
747
|
+
}
|
|
748
|
+
|
|
695
749
|
function renderStandardNode(node) {
|
|
696
750
|
if (node.type === 'text') return renderMarkedText(node)
|
|
697
751
|
if (node.type === 'hardBreak') return '<br>'
|
|
@@ -721,14 +775,12 @@ function renderStandardNode(node) {
|
|
|
721
775
|
}
|
|
722
776
|
if (node.type === 'listItem') return `<li>${renderChildren(node)}</li>`
|
|
723
777
|
if (node.type === 'image') {
|
|
724
|
-
return
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
'
|
|
730
|
-
node.attrs.width
|
|
731
|
-
)}${attribute('height', node.attrs.height)}>`
|
|
778
|
+
return renderImage(node.attrs)
|
|
779
|
+
}
|
|
780
|
+
if (node.type === 'imageGallery') {
|
|
781
|
+
return `<figure class="asd20-message-image-gallery" data-cc-node="image-gallery"><div class="asd20-message-image-gallery__images" data-cc-gallery-images="">${node.attrs.images
|
|
782
|
+
.map(renderImage)
|
|
783
|
+
.join('')}</div></figure>`
|
|
732
784
|
}
|
|
733
785
|
if (node.type === 'videoEmbed') {
|
|
734
786
|
return `<div class="asd20-message-video" data-cc-node="video"${attribute(
|