@asd20/ui-next 2.16.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,19 @@
|
|
|
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
|
+
|
|
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)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* add canonical video rendering ([295fbfd](https://github.com/academydistrict20/asd20-ui-next/commit/295fbfd38d3d66c2bc1e0070fb98a15ba6e51d02))
|
|
16
|
+
|
|
3
17
|
# [2.16.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.15.1...ui-next-v2.16.0) (2026-08-04)
|
|
4
18
|
|
|
5
19
|
|
package/package.json
CHANGED
|
@@ -157,5 +157,61 @@ export default {
|
|
|
157
157
|
:deep(.asd20-message-table td > :last-child) {
|
|
158
158
|
margin-bottom: 0;
|
|
159
159
|
}
|
|
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
|
+
|
|
192
|
+
:deep(.asd20-message-video) {
|
|
193
|
+
max-width: 100%;
|
|
194
|
+
margin: 0.75rem 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
:deep(.asd20-message-video__description) {
|
|
198
|
+
margin: 0 0 0.5rem;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
:deep(.asd20-message-video__frame) {
|
|
202
|
+
position: relative;
|
|
203
|
+
width: 100%;
|
|
204
|
+
overflow: hidden;
|
|
205
|
+
aspect-ratio: 16 / 9;
|
|
206
|
+
background: #000;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
:deep(.asd20-message-video__frame iframe) {
|
|
210
|
+
position: absolute;
|
|
211
|
+
inset: 0;
|
|
212
|
+
width: 100%;
|
|
213
|
+
height: 100%;
|
|
214
|
+
border: 0;
|
|
215
|
+
}
|
|
160
216
|
}
|
|
161
217
|
</style>
|
|
@@ -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
|
])
|
|
@@ -56,6 +57,22 @@ const MARK_TYPES = new Set([
|
|
|
56
57
|
'superscript',
|
|
57
58
|
])
|
|
58
59
|
const TABLE_HEADER_SCOPES = new Set(['col', 'row', 'colgroup', 'rowgroup'])
|
|
60
|
+
const VIDEO_TITLE_MAX_LENGTH = 200
|
|
61
|
+
const VIDEO_DESCRIPTION_MAX_LENGTH = 1000
|
|
62
|
+
const VIDEO_PROVIDER_RULES = Object.freeze([
|
|
63
|
+
{
|
|
64
|
+
host: 'www.youtube.com',
|
|
65
|
+
pathPattern: /^\/embed\/([A-Za-z0-9_-]{11})$/,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
host: 'www.youtube-nocookie.com',
|
|
69
|
+
pathPattern: /^\/embed\/([A-Za-z0-9_-]{11})$/,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
host: 'player.vimeo.com',
|
|
73
|
+
pathPattern: /^\/video\/([0-9]+)$/,
|
|
74
|
+
},
|
|
75
|
+
])
|
|
59
76
|
const MARK_RENDER_ORDER = Object.freeze({
|
|
60
77
|
link: 0,
|
|
61
78
|
bold: 1,
|
|
@@ -78,6 +95,7 @@ const NODE_ATTRIBUTE_NAMES = Object.freeze({
|
|
|
78
95
|
orderedList: ['start', 'type'],
|
|
79
96
|
listItem: [],
|
|
80
97
|
image: ['src', 'alt', 'title', 'width', 'height'],
|
|
98
|
+
imageGallery: ['images'],
|
|
81
99
|
videoEmbed: ['src', 'title', 'descriptiveText'],
|
|
82
100
|
table: ['caption'],
|
|
83
101
|
tableRow: [],
|
|
@@ -196,6 +214,57 @@ function isSafeResource(value, allowHash = false) {
|
|
|
196
214
|
}
|
|
197
215
|
}
|
|
198
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
|
+
|
|
229
|
+
function isCanonicalVideoEmbedSource(value) {
|
|
230
|
+
if (typeof value !== 'string' || !value) return false
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const url = new URL(value)
|
|
234
|
+
if (
|
|
235
|
+
url.protocol !== 'https:' ||
|
|
236
|
+
url.username ||
|
|
237
|
+
url.password ||
|
|
238
|
+
url.port ||
|
|
239
|
+
url.search ||
|
|
240
|
+
url.hash
|
|
241
|
+
) {
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const rule = VIDEO_PROVIDER_RULES.find(
|
|
246
|
+
candidate => candidate.host === url.hostname.toLowerCase()
|
|
247
|
+
)
|
|
248
|
+
return Boolean(
|
|
249
|
+
rule &&
|
|
250
|
+
rule.pathPattern.test(url.pathname) &&
|
|
251
|
+
value === `https://${rule.host}${url.pathname}`
|
|
252
|
+
)
|
|
253
|
+
} catch (_error) {
|
|
254
|
+
return false
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function validateRequiredCanonicalText(node, name, path, maximumLength) {
|
|
259
|
+
const attrIssue = validateStringAttribute(node, name, path, true)
|
|
260
|
+
if (attrIssue) return attrIssue
|
|
261
|
+
|
|
262
|
+
const value = node.attrs[name]
|
|
263
|
+
return !value || value !== value.trim() || value.length > maximumLength
|
|
264
|
+
? issue('malformed-attribute', path, name)
|
|
265
|
+
: null
|
|
266
|
+
}
|
|
267
|
+
|
|
199
268
|
function validateMarks(marks, path) {
|
|
200
269
|
if (marks === undefined) return null
|
|
201
270
|
if (!Array.isArray(marks)) return issue('malformed-marks', path)
|
|
@@ -273,6 +342,33 @@ function validateAtom(node, path) {
|
|
|
273
342
|
return null
|
|
274
343
|
}
|
|
275
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
|
+
|
|
276
372
|
function validateNode(node, path) {
|
|
277
373
|
if (!isRecord(node) || typeof node.type !== 'string') {
|
|
278
374
|
return issue('malformed-node', path)
|
|
@@ -375,35 +471,47 @@ function validateNode(node, path) {
|
|
|
375
471
|
: issue('invalid-child-node', `${path}.content[0]`, node.content[0].type)
|
|
376
472
|
}
|
|
377
473
|
if (node.type === 'image') {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
(!Number.isFinite(value) || value <= 0)
|
|
392
|
-
) {
|
|
393
|
-
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')
|
|
394
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
|
|
395
493
|
}
|
|
396
|
-
return
|
|
494
|
+
return null
|
|
397
495
|
}
|
|
398
496
|
if (node.type === 'videoEmbed') {
|
|
399
|
-
const srcIssue = validateStringAttribute(node, 'src', path)
|
|
400
|
-
if (srcIssue || (node.attrs?.src
|
|
497
|
+
const srcIssue = validateStringAttribute(node, 'src', path, true)
|
|
498
|
+
if (srcIssue || !isCanonicalVideoEmbedSource(node.attrs?.src)) {
|
|
401
499
|
return srcIssue || issue('unsafe-resource', path, 'src')
|
|
402
500
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
501
|
+
const titleIssue = validateRequiredCanonicalText(
|
|
502
|
+
node,
|
|
503
|
+
'title',
|
|
504
|
+
path,
|
|
505
|
+
VIDEO_TITLE_MAX_LENGTH
|
|
506
|
+
)
|
|
507
|
+
if (titleIssue) return titleIssue
|
|
508
|
+
const descriptionIssue = validateRequiredCanonicalText(
|
|
509
|
+
node,
|
|
510
|
+
'descriptiveText',
|
|
511
|
+
path,
|
|
512
|
+
VIDEO_DESCRIPTION_MAX_LENGTH
|
|
513
|
+
)
|
|
514
|
+
if (descriptionIssue) return descriptionIssue
|
|
407
515
|
return validateAtom(node, path)
|
|
408
516
|
}
|
|
409
517
|
if (node.type === 'table') {
|
|
@@ -627,6 +735,17 @@ function renderTable(node) {
|
|
|
627
735
|
)}</tbody></table></div>`
|
|
628
736
|
}
|
|
629
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
|
+
|
|
630
749
|
function renderStandardNode(node) {
|
|
631
750
|
if (node.type === 'text') return renderMarkedText(node)
|
|
632
751
|
if (node.type === 'hardBreak') return '<br>'
|
|
@@ -656,26 +775,33 @@ function renderStandardNode(node) {
|
|
|
656
775
|
}
|
|
657
776
|
if (node.type === 'listItem') return `<li>${renderChildren(node)}</li>`
|
|
658
777
|
if (node.type === 'image') {
|
|
659
|
-
return
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
'
|
|
665
|
-
node.attrs.width
|
|
666
|
-
)}${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>`
|
|
667
784
|
}
|
|
668
785
|
if (node.type === 'videoEmbed') {
|
|
669
|
-
return `<div data-cc-node="video"${attribute(
|
|
786
|
+
return `<div class="asd20-message-video" data-cc-node="video"${attribute(
|
|
670
787
|
'data-src',
|
|
671
788
|
node.attrs.src
|
|
672
789
|
)}${attribute('data-title', node.attrs.title)}${attribute(
|
|
673
790
|
'data-descriptive-text',
|
|
674
791
|
node.attrs.descriptiveText
|
|
675
|
-
)}><
|
|
792
|
+
)}><p class="asd20-message-video__description">${escapeHtml(
|
|
793
|
+
node.attrs.descriptiveText
|
|
794
|
+
)}</p><div class="asd20-message-video__frame"><iframe${attribute(
|
|
795
|
+
'src',
|
|
796
|
+
node.attrs.src
|
|
797
|
+
)}${attribute(
|
|
676
798
|
'title',
|
|
677
|
-
node.attrs.title
|
|
678
|
-
)}
|
|
799
|
+
node.attrs.title
|
|
800
|
+
)} loading="lazy" referrerpolicy="strict-origin-when-cross-origin"${attribute(
|
|
801
|
+
'allowfullscreen',
|
|
802
|
+
'',
|
|
803
|
+
true
|
|
804
|
+
)}></iframe></div></div>`
|
|
679
805
|
}
|
|
680
806
|
if (node.type === 'table') return renderTable(node)
|
|
681
807
|
if (node.type === 'tableRow') return `<tr>${renderChildren(node)}</tr>`
|