@asd20/ui-next 2.17.0 → 2.19.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.19.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.18.0...ui-next-v2.19.0) (2026-08-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add azure blob mp4 and myvrspot video urls for tiptap editor ([b7bc2e9](https://github.com/academydistrict20/asd20-ui-next/commit/b7bc2e94bf8502bb990bfb063c14b6631f981f80))
|
|
9
|
+
|
|
10
|
+
# [2.18.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.17.0...ui-next-v2.18.0) (2026-08-06)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* add tiptap gallery support ([7371f56](https://github.com/academydistrict20/asd20-ui-next/commit/7371f561c6c0a3e6a2275a9537beba72c86e6837))
|
|
16
|
+
|
|
3
17
|
# [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
18
|
|
|
5
19
|
|
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
|
])
|
|
@@ -58,6 +59,10 @@ const MARK_TYPES = new Set([
|
|
|
58
59
|
const TABLE_HEADER_SCOPES = new Set(['col', 'row', 'colgroup', 'rowgroup'])
|
|
59
60
|
const VIDEO_TITLE_MAX_LENGTH = 200
|
|
60
61
|
const VIDEO_DESCRIPTION_MAX_LENGTH = 1000
|
|
62
|
+
const MYVRSPOT_VIDEO_ID_PATTERN = /^[A-Za-z0-9_-]{16,256}$/
|
|
63
|
+
const MYVRSPOT_HOST = 'live.myvrspot.com'
|
|
64
|
+
const ASD20_VIDEO_HOST = 'asd20websitestorage.blob.core.windows.net'
|
|
65
|
+
const ASD20_VIDEO_PATH_PREFIX = '/asd20-images/videos/'
|
|
61
66
|
const VIDEO_PROVIDER_RULES = Object.freeze([
|
|
62
67
|
{
|
|
63
68
|
host: 'www.youtube.com',
|
|
@@ -94,6 +99,7 @@ const NODE_ATTRIBUTE_NAMES = Object.freeze({
|
|
|
94
99
|
orderedList: ['start', 'type'],
|
|
95
100
|
listItem: [],
|
|
96
101
|
image: ['src', 'alt', 'title', 'width', 'height'],
|
|
102
|
+
imageGallery: ['images'],
|
|
97
103
|
videoEmbed: ['src', 'title', 'descriptiveText'],
|
|
98
104
|
table: ['caption'],
|
|
99
105
|
tableRow: [],
|
|
@@ -212,6 +218,18 @@ function isSafeResource(value, allowHash = false) {
|
|
|
212
218
|
}
|
|
213
219
|
}
|
|
214
220
|
|
|
221
|
+
function isSafeImageResource(value) {
|
|
222
|
+
if (typeof value !== 'string' || !value.trim()) return false
|
|
223
|
+
if (value.trim().startsWith('#')) return false
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const url = new URL(value.trim(), 'https://ui-next.invalid')
|
|
227
|
+
return url.protocol === 'http:' || url.protocol === 'https:'
|
|
228
|
+
} catch (_error) {
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
215
233
|
function isCanonicalVideoEmbedSource(value) {
|
|
216
234
|
if (typeof value !== 'string' || !value) return false
|
|
217
235
|
|
|
@@ -222,14 +240,36 @@ function isCanonicalVideoEmbedSource(value) {
|
|
|
222
240
|
url.username ||
|
|
223
241
|
url.password ||
|
|
224
242
|
url.port ||
|
|
225
|
-
url.search ||
|
|
226
243
|
url.hash
|
|
227
244
|
) {
|
|
228
245
|
return false
|
|
229
246
|
}
|
|
230
247
|
|
|
248
|
+
const host = url.hostname.toLowerCase()
|
|
249
|
+
if (host === MYVRSPOT_HOST) {
|
|
250
|
+
const videoId = url.searchParams.get('v') || ''
|
|
251
|
+
return (
|
|
252
|
+
url.pathname === '/iframe' &&
|
|
253
|
+
MYVRSPOT_VIDEO_ID_PATTERN.test(videoId) &&
|
|
254
|
+
url.searchParams.get('share') === '1' &&
|
|
255
|
+
[...url.searchParams].length === 2 &&
|
|
256
|
+
value === `https://${MYVRSPOT_HOST}/iframe?v=${videoId}&share=1`
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (host === ASD20_VIDEO_HOST) {
|
|
261
|
+
return (
|
|
262
|
+
!url.search &&
|
|
263
|
+
url.pathname.startsWith(ASD20_VIDEO_PATH_PREFIX) &&
|
|
264
|
+
url.pathname.toLowerCase().endsWith('.mp4') &&
|
|
265
|
+
value === `https://${ASD20_VIDEO_HOST}${url.pathname}`
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (url.search) return false
|
|
270
|
+
|
|
231
271
|
const rule = VIDEO_PROVIDER_RULES.find(
|
|
232
|
-
candidate => candidate.host ===
|
|
272
|
+
candidate => candidate.host === host
|
|
233
273
|
)
|
|
234
274
|
return Boolean(
|
|
235
275
|
rule &&
|
|
@@ -328,6 +368,33 @@ function validateAtom(node, path) {
|
|
|
328
368
|
return null
|
|
329
369
|
}
|
|
330
370
|
|
|
371
|
+
function validateImageAttributes(node, path, requiredSource = false) {
|
|
372
|
+
const srcIssue = validateStringAttribute(
|
|
373
|
+
node,
|
|
374
|
+
'src',
|
|
375
|
+
path,
|
|
376
|
+
requiredSource
|
|
377
|
+
)
|
|
378
|
+
if (srcIssue || (node.attrs?.src && !isSafeImageResource(node.attrs.src))) {
|
|
379
|
+
return srcIssue || issue('unsafe-resource', path, 'src')
|
|
380
|
+
}
|
|
381
|
+
for (const name of ['alt', 'title']) {
|
|
382
|
+
const attrIssue = validateStringAttribute(node, name, path)
|
|
383
|
+
if (attrIssue) return attrIssue
|
|
384
|
+
}
|
|
385
|
+
for (const name of ['width', 'height']) {
|
|
386
|
+
const value = node.attrs?.[name]
|
|
387
|
+
if (
|
|
388
|
+
value !== undefined &&
|
|
389
|
+
value !== null &&
|
|
390
|
+
(!Number.isFinite(value) || value <= 0)
|
|
391
|
+
) {
|
|
392
|
+
return issue('malformed-attribute', path, name)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return null
|
|
396
|
+
}
|
|
397
|
+
|
|
331
398
|
function validateNode(node, path) {
|
|
332
399
|
if (!isRecord(node) || typeof node.type !== 'string') {
|
|
333
400
|
return issue('malformed-node', path)
|
|
@@ -430,25 +497,27 @@ function validateNode(node, path) {
|
|
|
430
497
|
: issue('invalid-child-node', `${path}.content[0]`, node.content[0].type)
|
|
431
498
|
}
|
|
432
499
|
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)
|
|
500
|
+
return validateImageAttributes(node, path) || validateAtom(node, path)
|
|
501
|
+
}
|
|
502
|
+
if (node.type === 'imageGallery') {
|
|
503
|
+
const atomIssue = validateAtom(node, path)
|
|
504
|
+
if (atomIssue) return atomIssue
|
|
505
|
+
const images = node.attrs?.images
|
|
506
|
+
if (!Array.isArray(images) || images.length < 2 || images.length > 3) {
|
|
507
|
+
return issue('malformed-attribute', path, 'images')
|
|
508
|
+
}
|
|
509
|
+
for (let index = 0; index < images.length; index += 1) {
|
|
510
|
+
const imagePath = `${path}.attrs.images[${index}]`
|
|
511
|
+
if (!isRecord(images[index])) {
|
|
512
|
+
return issue('malformed-attribute', imagePath, 'image')
|
|
449
513
|
}
|
|
514
|
+
const imageNode = { type: 'image', attrs: images[index] }
|
|
515
|
+
const attributeNameIssue = validateAttributeNames(imageNode, imagePath)
|
|
516
|
+
if (attributeNameIssue) return attributeNameIssue
|
|
517
|
+
const imageIssue = validateImageAttributes(imageNode, imagePath, true)
|
|
518
|
+
if (imageIssue) return imageIssue
|
|
450
519
|
}
|
|
451
|
-
return
|
|
520
|
+
return null
|
|
452
521
|
}
|
|
453
522
|
if (node.type === 'videoEmbed') {
|
|
454
523
|
const srcIssue = validateStringAttribute(node, 'src', path, true)
|
|
@@ -692,6 +761,17 @@ function renderTable(node) {
|
|
|
692
761
|
)}</tbody></table></div>`
|
|
693
762
|
}
|
|
694
763
|
|
|
764
|
+
function renderImage(attrs) {
|
|
765
|
+
return `<img${attribute('src', attrs.src)}${attribute(
|
|
766
|
+
'alt',
|
|
767
|
+
attrs.alt,
|
|
768
|
+
true
|
|
769
|
+
)}${attribute('title', attrs.title)}${attribute(
|
|
770
|
+
'width',
|
|
771
|
+
attrs.width
|
|
772
|
+
)}${attribute('height', attrs.height)}>`
|
|
773
|
+
}
|
|
774
|
+
|
|
695
775
|
function renderStandardNode(node) {
|
|
696
776
|
if (node.type === 'text') return renderMarkedText(node)
|
|
697
777
|
if (node.type === 'hardBreak') return '<br>'
|
|
@@ -721,14 +801,12 @@ function renderStandardNode(node) {
|
|
|
721
801
|
}
|
|
722
802
|
if (node.type === 'listItem') return `<li>${renderChildren(node)}</li>`
|
|
723
803
|
if (node.type === 'image') {
|
|
724
|
-
return
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
'
|
|
730
|
-
node.attrs.width
|
|
731
|
-
)}${attribute('height', node.attrs.height)}>`
|
|
804
|
+
return renderImage(node.attrs)
|
|
805
|
+
}
|
|
806
|
+
if (node.type === 'imageGallery') {
|
|
807
|
+
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
|
|
808
|
+
.map(renderImage)
|
|
809
|
+
.join('')}</div></figure>`
|
|
732
810
|
}
|
|
733
811
|
if (node.type === 'videoEmbed') {
|
|
734
812
|
return `<div class="asd20-message-video" data-cc-node="video"${attribute(
|