@ndla/ui 34.6.2 → 34.6.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 (153) hide show
  1. package/es/Article/Article.js +11 -6
  2. package/es/Aside/Aside.js +5 -2
  3. package/es/CopyParagraphButton/CopyParagraphButtonV2.js +85 -0
  4. package/es/CopyParagraphButton/index.js +2 -1
  5. package/es/Embed/AudioEmbed.js +254 -0
  6. package/es/Embed/BrightcoveEmbed.js +250 -0
  7. package/es/Embed/ConceptEmbed.js +358 -0
  8. package/es/Embed/ConceptListEmbed.js +71 -0
  9. package/es/Embed/ContentLinkEmbed.js +42 -0
  10. package/es/Embed/ExternalEmbed.js +91 -0
  11. package/es/Embed/FootnoteEmbed.js +32 -0
  12. package/es/Embed/H5pEmbed.js +87 -0
  13. package/es/Embed/IframeEmbed.js +83 -0
  14. package/es/Embed/ImageEmbed.js +322 -0
  15. package/es/Embed/RelatedContentEmbed.js +58 -0
  16. package/es/Embed/UnknownEmbed.js +27 -0
  17. package/es/Embed/conceptComponents.js +282 -0
  18. package/es/Embed/index.js +21 -0
  19. package/es/FactBox/FactBoxV2.js +90 -0
  20. package/es/FactBox/index.js +1 -0
  21. package/es/Figure/Figure.js +8 -5
  22. package/es/Figure/FigureLicenseDialogContent.js +72 -0
  23. package/es/FileList/FileListV2.js +47 -0
  24. package/es/FileList/FileV2.js +34 -0
  25. package/es/FileList/PdfFile.js +25 -0
  26. package/es/FileList/index.js +3 -0
  27. package/es/Notion/Notion.js +5 -5
  28. package/es/Notion/NotionVisualElement.js +2 -2
  29. package/es/RelatedArticleList/RelatedArticleV2.js +101 -0
  30. package/es/RelatedArticleList/index.js +2 -1
  31. package/es/Table/Table.js +95 -8
  32. package/es/all.css +1 -1
  33. package/es/index.js +5 -4
  34. package/es/locale/messages-en.js +8 -1
  35. package/es/locale/messages-nb.js +8 -1
  36. package/es/locale/messages-nn.js +8 -1
  37. package/es/locale/messages-se.js +8 -1
  38. package/es/locale/messages-sma.js +8 -1
  39. package/lib/Article/Article.d.ts +2 -1
  40. package/lib/Article/Article.js +11 -6
  41. package/lib/Aside/Aside.d.ts +2 -1
  42. package/lib/Aside/Aside.js +5 -2
  43. package/lib/CopyParagraphButton/CopyParagraphButtonV2.d.ts +14 -0
  44. package/lib/CopyParagraphButton/CopyParagraphButtonV2.js +84 -0
  45. package/lib/CopyParagraphButton/index.d.ts +2 -1
  46. package/lib/CopyParagraphButton/index.js +7 -0
  47. package/lib/Embed/AudioEmbed.d.ts +20 -0
  48. package/lib/Embed/AudioEmbed.js +252 -0
  49. package/lib/Embed/BrightcoveEmbed.d.ts +16 -0
  50. package/lib/Embed/BrightcoveEmbed.js +250 -0
  51. package/lib/Embed/ConceptEmbed.d.ts +19 -0
  52. package/lib/Embed/ConceptEmbed.js +358 -0
  53. package/lib/Embed/ConceptListEmbed.d.ts +13 -0
  54. package/lib/Embed/ConceptListEmbed.js +70 -0
  55. package/lib/Embed/ContentLinkEmbed.d.ts +14 -0
  56. package/lib/Embed/ContentLinkEmbed.js +50 -0
  57. package/lib/Embed/ExternalEmbed.d.ts +14 -0
  58. package/lib/Embed/ExternalEmbed.js +90 -0
  59. package/lib/Embed/FootnoteEmbed.d.ts +13 -0
  60. package/lib/Embed/FootnoteEmbed.js +39 -0
  61. package/lib/Embed/H5pEmbed.d.ts +14 -0
  62. package/lib/Embed/H5pEmbed.js +86 -0
  63. package/lib/Embed/IframeEmbed.d.ts +14 -0
  64. package/lib/Embed/IframeEmbed.js +91 -0
  65. package/lib/Embed/ImageEmbed.d.ts +37 -0
  66. package/lib/Embed/ImageEmbed.js +326 -0
  67. package/lib/Embed/RelatedContentEmbed.d.ts +16 -0
  68. package/lib/Embed/RelatedContentEmbed.js +64 -0
  69. package/lib/Embed/UnknownEmbed.d.ts +13 -0
  70. package/lib/Embed/UnknownEmbed.js +35 -0
  71. package/lib/Embed/conceptComponents.d.ts +32 -0
  72. package/lib/Embed/conceptComponents.js +280 -0
  73. package/lib/Embed/index.d.ts +20 -0
  74. package/lib/Embed/index.js +97 -0
  75. package/lib/FactBox/FactBoxV2.d.ts +13 -0
  76. package/lib/FactBox/FactBoxV2.js +92 -0
  77. package/lib/FactBox/index.d.ts +1 -0
  78. package/lib/FactBox/index.js +7 -0
  79. package/lib/Figure/Figure.d.ts +5 -2
  80. package/lib/Figure/Figure.js +8 -5
  81. package/lib/Figure/FigureLicenseDialogContent.d.ts +22 -0
  82. package/lib/Figure/FigureLicenseDialogContent.js +71 -0
  83. package/lib/FileList/FileListV2.d.ts +13 -0
  84. package/lib/FileList/FileListV2.js +46 -0
  85. package/lib/FileList/FileV2.d.ts +16 -0
  86. package/lib/FileList/FileV2.js +42 -0
  87. package/lib/FileList/PdfFile.d.ts +13 -0
  88. package/lib/FileList/PdfFile.js +31 -0
  89. package/lib/FileList/index.d.ts +3 -0
  90. package/lib/FileList/index.js +21 -0
  91. package/lib/Notion/Notion.js +5 -5
  92. package/lib/Notion/NotionVisualElement.d.ts +1 -1
  93. package/lib/Notion/NotionVisualElement.js +2 -2
  94. package/lib/RelatedArticleList/RelatedArticleV2.d.ts +25 -0
  95. package/lib/RelatedArticleList/RelatedArticleV2.js +101 -0
  96. package/lib/RelatedArticleList/index.d.ts +2 -1
  97. package/lib/RelatedArticleList/index.js +7 -0
  98. package/lib/Table/Table.js +98 -8
  99. package/lib/all.css +1 -1
  100. package/lib/index.d.ts +5 -4
  101. package/lib/index.js +117 -2
  102. package/lib/locale/messages-en.d.ts +7 -0
  103. package/lib/locale/messages-en.js +8 -1
  104. package/lib/locale/messages-nb.d.ts +7 -0
  105. package/lib/locale/messages-nb.js +8 -1
  106. package/lib/locale/messages-nn.d.ts +7 -0
  107. package/lib/locale/messages-nn.js +8 -1
  108. package/lib/locale/messages-se.d.ts +7 -0
  109. package/lib/locale/messages-se.js +8 -1
  110. package/lib/locale/messages-sma.d.ts +7 -0
  111. package/lib/locale/messages-sma.js +8 -1
  112. package/lib/types.d.ts +1 -1
  113. package/package.json +16 -12
  114. package/src/Article/Article.tsx +8 -3
  115. package/src/Aside/Aside.tsx +9 -1
  116. package/src/Aside/component.aside.scss +3 -0
  117. package/src/CopyParagraphButton/CopyParagraphButtonV2.tsx +84 -0
  118. package/src/CopyParagraphButton/index.tsx +2 -1
  119. package/src/Embed/AudioEmbed.tsx +249 -0
  120. package/src/Embed/BrightcoveEmbed.tsx +203 -0
  121. package/src/Embed/ConceptEmbed.tsx +408 -0
  122. package/src/Embed/ConceptListEmbed.tsx +64 -0
  123. package/src/Embed/ContentLinkEmbed.tsx +41 -0
  124. package/src/Embed/ExternalEmbed.tsx +80 -0
  125. package/src/Embed/FootnoteEmbed.tsx +30 -0
  126. package/src/Embed/H5pEmbed.tsx +74 -0
  127. package/src/Embed/IframeEmbed.tsx +84 -0
  128. package/src/Embed/ImageEmbed.tsx +314 -0
  129. package/src/Embed/RelatedContentEmbed.tsx +62 -0
  130. package/src/Embed/UnknownEmbed.tsx +27 -0
  131. package/src/Embed/conceptComponents.tsx +393 -0
  132. package/src/Embed/index.ts +21 -0
  133. package/src/FactBox/FactBoxV2.tsx +56 -0
  134. package/src/FactBox/index.ts +2 -0
  135. package/src/Figure/Figure.tsx +28 -15
  136. package/src/Figure/FigureLicenseDialogContent.tsx +80 -0
  137. package/src/Figure/component.figure.scss +0 -1
  138. package/src/FileList/FileListV2.tsx +58 -0
  139. package/src/FileList/FileV2.tsx +35 -0
  140. package/src/FileList/PdfFile.tsx +25 -0
  141. package/src/FileList/index.ts +3 -0
  142. package/src/Notion/Notion.tsx +0 -1
  143. package/src/Notion/NotionVisualElement.tsx +1 -1
  144. package/src/RelatedArticleList/RelatedArticleV2.tsx +84 -0
  145. package/src/RelatedArticleList/index.ts +2 -1
  146. package/src/Table/Table.tsx +77 -4
  147. package/src/index.ts +19 -4
  148. package/src/locale/messages-en.ts +7 -0
  149. package/src/locale/messages-nb.ts +7 -0
  150. package/src/locale/messages-nn.ts +7 -0
  151. package/src/locale/messages-se.ts +7 -0
  152. package/src/locale/messages-sma.ts +7 -0
  153. package/src/types.ts +1 -1
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Copyright (c) 2023-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import { useCallback, useRef, useState } from 'react';
10
+ import { useTranslation } from 'react-i18next';
11
+ import styled from '@emotion/styled';
12
+ import { isMobile } from 'react-device-detect';
13
+ import { Root, Trigger, Content, Anchor, Close, Portal } from '@radix-ui/react-popover';
14
+ import { ButtonV2, IconButtonV2 } from '@ndla/button';
15
+ import { Cross } from '@ndla/icons/action';
16
+ import { breakpoints, colors, mq, spacing } from '@ndla/core';
17
+ import { getGroupedContributorDescriptionList, getLicenseByAbbreviation, getLicenseCredits } from '@ndla/licenses';
18
+ import { ModalV2 } from '@ndla/modal';
19
+ import { ConceptMetaData } from '@ndla/types-embed';
20
+ import Tooltip from '@ndla/tooltip';
21
+ import { Notion as UINotion } from '../Notion';
22
+ import { Figure, FigureCaption } from '../Figure';
23
+ import { FigureLicenseDialogContent } from '../Figure/FigureLicenseDialogContent';
24
+ import { NotionImage } from '../Notion/NotionImage';
25
+ import { ConceptNotionV2, ConceptNotionData } from './conceptComponents';
26
+
27
+ const BottomBorder = styled.div`
28
+ margin-top: ${spacing.normal};
29
+ border-bottom: 1px solid ${colors.brand.greyLight};
30
+ `;
31
+
32
+ interface PopoverPosition {
33
+ top?: number;
34
+ }
35
+
36
+ const PopoverWrapper = styled.div<PopoverPosition>`
37
+ div[data-radix-popper-content-wrapper] {
38
+ position: absolute !important;
39
+ left: 50% !important;
40
+ transform: translateX(-50%) !important;
41
+ top: ${({ top }) => top}px !important;
42
+ }
43
+
44
+ ${mq.range({ until: breakpoints.tablet })} {
45
+ div[data-radix-popper-content-wrapper] {
46
+ // Fix for popover positioning on mobile.
47
+ // If we modify all popovers we break license icons.
48
+ // https://github.com/radix-ui/primitives/issues/1839
49
+ position: fixed !important;
50
+ transform: none !important;
51
+ top: 0 !important;
52
+ left: 0 !important;
53
+ width: 100vw;
54
+ z-index: 9999 !important;
55
+ height: 100vh;
56
+ min-width: 100vw !important;
57
+ }
58
+ }
59
+ `;
60
+
61
+ const ImageWrapper = styled.div`
62
+ float: right;
63
+ padding-left: ${spacing.normal};
64
+ position: relative;
65
+
66
+ ${mq.range({ until: breakpoints.tabletWide })} {
67
+ width: 100%;
68
+ padding-left: 0;
69
+ }
70
+ `;
71
+
72
+ interface Props {
73
+ embed: ConceptMetaData;
74
+ fullWidth?: boolean;
75
+ }
76
+
77
+ const StyledButton = styled.button`
78
+ background: none;
79
+ border: none;
80
+ font-family: inherit;
81
+ font-style: inherit;
82
+ line-height: 1em;
83
+ padding: 0 0 4px 0;
84
+ margin-bottom: -4px;
85
+ text-decoration: none;
86
+ color: #000;
87
+ position: relative;
88
+ cursor: pointer;
89
+ &:focus,
90
+ &:hover {
91
+ color: ${colors.brand.primary};
92
+ outline: none;
93
+ }
94
+ `;
95
+
96
+ export const ConceptEmbed = ({ embed, fullWidth }: Props) => {
97
+ if (embed.status === 'error') {
98
+ return <span>{embed.embedData.linkText}</span>;
99
+ }
100
+
101
+ const {
102
+ data: { concept, visualElement },
103
+ } = embed;
104
+
105
+ if (embed.embedData.type === 'block') {
106
+ return (
107
+ <BlockConcept
108
+ fullWidth={fullWidth}
109
+ title={concept.title.title}
110
+ content={concept.content?.content}
111
+ metaImage={concept.metaImage}
112
+ copyright={concept.copyright}
113
+ source={concept.source}
114
+ visualElement={visualElement}
115
+ />
116
+ );
117
+ } else if (embed.embedData.type === 'inline') {
118
+ return (
119
+ <InlineConcept
120
+ title={concept.title.title}
121
+ content={concept.content?.content}
122
+ metaImage={concept.metaImage}
123
+ copyright={concept.copyright}
124
+ source={concept.source}
125
+ visualElement={visualElement}
126
+ linkText={embed.embedData.linkText}
127
+ />
128
+ );
129
+ } else {
130
+ return (
131
+ <ConceptNotionV2
132
+ title={concept.title.title}
133
+ content={concept.content?.content}
134
+ metaImage={concept.metaImage}
135
+ copyright={concept.copyright}
136
+ source={concept.source}
137
+ visualElement={visualElement}
138
+ />
139
+ );
140
+ }
141
+ };
142
+
143
+ interface InlineConceptProps extends ConceptNotionData {
144
+ linkText: string;
145
+ }
146
+
147
+ const BaselineIcon = styled.span`
148
+ display: block;
149
+ border-bottom: 5px double currentColor;
150
+ `;
151
+
152
+ const NotionButton = styled.button`
153
+ background: none;
154
+ border: none;
155
+ font-family: inherit;
156
+ font-style: inherit;
157
+ line-height: 1em;
158
+ padding: 0 0 4px 0;
159
+ margin-bottom: -4px;
160
+ text-decoration: none;
161
+ position: relative;
162
+ text-align: left;
163
+ display: inline;
164
+ color: ${colors.notion.dark};
165
+ cursor: pointer;
166
+ &:focus,
167
+ &:hover {
168
+ background-color: ${colors.notion.dark};
169
+ color: ${colors.white};
170
+ outline: none;
171
+ ${BaselineIcon} {
172
+ border-color: transparent;
173
+ }
174
+ }
175
+
176
+ &:active {
177
+ color: ${colors.notion.dark};
178
+ background-color: ${colors.notion.light};
179
+ ${BaselineIcon} {
180
+ border-color: currentColor;
181
+ }
182
+ }
183
+ `;
184
+
185
+ const StyledAnchor = styled(Anchor)`
186
+ ${mq.range({ until: breakpoints.tablet })} {
187
+ position: fixed;
188
+ top: 0;
189
+ }
190
+ `;
191
+
192
+ const StyledAnchorSpan = styled.span`
193
+ position: absolute;
194
+ left: 50%;
195
+ align-self: center;
196
+ `;
197
+
198
+ const getModalPosition = (anchor: HTMLElement) => {
199
+ const article = document.querySelector('.c-article');
200
+ const articlePos = article?.getBoundingClientRect();
201
+ const anchorPos = anchor.getBoundingClientRect();
202
+ return anchorPos.top - (articlePos?.top || -window.scrollY);
203
+ };
204
+
205
+ const InlineConcept = ({ title, content, copyright, source, visualElement, linkText }: InlineConceptProps) => {
206
+ const { t } = useTranslation();
207
+ const anchorRef = useRef<HTMLDivElement>(null);
208
+ const [modalPos, setModalPos] = useState(-9999);
209
+
210
+ const onOpenChange = useCallback((open: boolean) => {
211
+ if (open) {
212
+ const anchor = anchorRef.current;
213
+ if (anchor) {
214
+ const top = getModalPosition(anchor);
215
+ setModalPos(top);
216
+ }
217
+ } else {
218
+ setModalPos(-9999);
219
+ }
220
+ }, []);
221
+
222
+ return (
223
+ <Root modal={isMobile} onOpenChange={onOpenChange}>
224
+ <StyledAnchor ref={anchorRef} asChild>
225
+ <StyledAnchorSpan />
226
+ </StyledAnchor>
227
+ <Trigger asChild>
228
+ <NotionButton>
229
+ {linkText}
230
+ {<BaselineIcon />}
231
+ </NotionButton>
232
+ </Trigger>
233
+ <Portal
234
+ container={
235
+ typeof document !== 'undefined'
236
+ ? (document.querySelector('.c-article') as HTMLElement | null) || undefined
237
+ : undefined
238
+ }>
239
+ <PopoverWrapper top={modalPos}>
240
+ <Content avoidCollisions={false} side="bottom" asChild>
241
+ <ConceptNotionV2
242
+ title={title}
243
+ content={content}
244
+ copyright={copyright}
245
+ source={source}
246
+ visualElement={visualElement}
247
+ inPopover
248
+ closeButton={
249
+ <Close asChild>
250
+ <IconButtonV2 aria-label={t('close')} variant="ghost">
251
+ <Cross />
252
+ </IconButtonV2>
253
+ </Close>
254
+ }
255
+ />
256
+ </Content>
257
+ </PopoverWrapper>
258
+ </Portal>
259
+ </Root>
260
+ );
261
+ };
262
+
263
+ interface ConceptProps extends ConceptNotionData {
264
+ fullWidth?: boolean;
265
+ }
266
+
267
+ export const BlockConcept = ({
268
+ title,
269
+ content,
270
+ metaImage,
271
+ copyright,
272
+ source,
273
+ visualElement,
274
+ fullWidth,
275
+ }: ConceptProps) => {
276
+ const { t, i18n } = useTranslation();
277
+ const anchorRef = useRef<HTMLDivElement>(null);
278
+ const [modalPos, setModalPos] = useState(-9999);
279
+
280
+ const [isOpen, setIsOpen] = useState(false);
281
+ const licenseCredits = getLicenseCredits(copyright);
282
+ const { creators, rightsholders, processors } = licenseCredits;
283
+ const authors = creators.length || rightsholders.length ? creators.concat(rightsholders) : processors;
284
+ const visualElementType =
285
+ visualElement?.embedData.resource === 'brightcove' ? 'video' : visualElement?.embedData.resource;
286
+
287
+ const groupedAuthors = getGroupedContributorDescriptionList(licenseCredits, i18n.language).map((item) => ({
288
+ name: item.description,
289
+ type: item.label,
290
+ }));
291
+ const license = copyright?.license && getLicenseByAbbreviation(copyright?.license?.license, i18n.language);
292
+
293
+ const onOpenChange = useCallback((open: boolean) => {
294
+ if (open) {
295
+ const anchor = anchorRef.current;
296
+ if (anchor) {
297
+ const top = getModalPosition(anchor);
298
+ setModalPos(top);
299
+ }
300
+ } else {
301
+ setModalPos(-9999);
302
+ }
303
+ }, []);
304
+
305
+ return (
306
+ <Root modal={isMobile} onOpenChange={onOpenChange}>
307
+ <StyledAnchor ref={anchorRef} />
308
+ <Figure resizeIframe type={fullWidth ? 'full' : 'full-column'}>
309
+ <UINotion
310
+ id=""
311
+ title={title}
312
+ text={content}
313
+ visualElement={
314
+ visualElement?.status === 'success' && (
315
+ <>
316
+ <ImageWrapper>
317
+ <Tooltip tooltip={t('searchPage.resultType.showNotion')}>
318
+ <Trigger asChild>
319
+ <StyledButton type="button" aria-label={t('concept.showDescription', { title: title })}>
320
+ {visualElement.resource === 'image' ? (
321
+ <NotionImage
322
+ type={visualElementType}
323
+ id={''}
324
+ src={visualElement.data.imageUrl}
325
+ alt={visualElement.data.alttext.alttext}
326
+ />
327
+ ) : metaImage ? (
328
+ <NotionImage
329
+ type={visualElementType}
330
+ id={''}
331
+ src={metaImage?.url ?? ''}
332
+ alt={metaImage?.alt ?? ''}
333
+ />
334
+ ) : undefined}
335
+ </StyledButton>
336
+ </Trigger>
337
+ </Tooltip>
338
+ </ImageWrapper>
339
+ <Portal
340
+ container={
341
+ typeof document !== 'undefined'
342
+ ? (document.querySelector('.c-article') as HTMLElement | null) || undefined
343
+ : undefined
344
+ }>
345
+ <PopoverWrapper top={modalPos}>
346
+ <Content avoidCollisions={false} asChild side="bottom">
347
+ <ConceptNotionV2
348
+ title={title}
349
+ content={content}
350
+ copyright={copyright}
351
+ source={source}
352
+ visualElement={visualElement}
353
+ inPopover
354
+ closeButton={
355
+ <Close asChild>
356
+ <IconButtonV2 aria-label={t('close')} variant="ghost">
357
+ <Cross />
358
+ </IconButtonV2>
359
+ </Close>
360
+ }
361
+ />
362
+ </Content>
363
+ </PopoverWrapper>
364
+ </Portal>
365
+ </>
366
+ )
367
+ }
368
+ />
369
+ {copyright?.license && license ? (
370
+ <FigureCaption
371
+ figureId=""
372
+ id=""
373
+ authors={authors}
374
+ licenseRights={license.rights}
375
+ locale={i18n.language}
376
+ hideIconsAndAuthors
377
+ modalButton={
378
+ <ButtonV2 variant="outline" size="small" shape="pill" onClick={() => setIsOpen(true)}>
379
+ {t('concept.reuse')}
380
+ </ButtonV2>
381
+ }>
382
+ <ModalV2
383
+ controlled
384
+ isOpen={isOpen}
385
+ onClose={() => setIsOpen(false)}
386
+ labelledBy="license-dialog-rules-heading">
387
+ {(close) => (
388
+ <FigureLicenseDialogContent
389
+ authors={groupedAuthors}
390
+ locale={i18n.language}
391
+ title={title}
392
+ origin={copyright.origin}
393
+ license={license}
394
+ onClose={close}
395
+ type="concept"
396
+ />
397
+ )}
398
+ </ModalV2>
399
+ </FigureCaption>
400
+ ) : (
401
+ <BottomBorder />
402
+ )}
403
+ </Figure>
404
+ </Root>
405
+ );
406
+ };
407
+
408
+ export default ConceptEmbed;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Copyright (c) 2023-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import { useTranslation } from 'react-i18next';
10
+ import styled from '@emotion/styled';
11
+ import { colors } from '@ndla/core';
12
+ import { ConceptListMetaData } from '@ndla/types-embed';
13
+ import { Figure } from '../Figure';
14
+ import { BlockConcept } from './ConceptEmbed';
15
+
16
+ interface Props {
17
+ embed: ConceptListMetaData;
18
+ }
19
+
20
+ const ConceptList = styled.div`
21
+ & > figure:first-of-type {
22
+ margin-top: 32px;
23
+ }
24
+ & li {
25
+ display: block;
26
+ }
27
+ `;
28
+
29
+ const StyledSpan = styled.span`
30
+ color: ${colors.support.red};
31
+ `;
32
+
33
+ const ConceptListEmbed = ({ embed }: Props) => {
34
+ const { t } = useTranslation();
35
+ if (embed.status === 'error') {
36
+ return <StyledSpan>{t('embed.conceptListError')}</StyledSpan>;
37
+ }
38
+ const { embedData, data } = embed;
39
+ return (
40
+ <div>
41
+ <Figure type="full" resizeIframe>
42
+ {embedData.title && <h2>{embedData.title}</h2>}
43
+ <ConceptList>
44
+ <ul>
45
+ {data.concepts.map(({ concept, visualElement }) => (
46
+ <li key={concept.id}>
47
+ <BlockConcept
48
+ title={concept.title.title}
49
+ content={concept.content.content}
50
+ metaImage={concept.metaImage}
51
+ copyright={concept.copyright}
52
+ source={concept.source}
53
+ visualElement={visualElement}
54
+ />
55
+ </li>
56
+ ))}
57
+ </ul>
58
+ </ConceptList>
59
+ </Figure>
60
+ </div>
61
+ );
62
+ };
63
+
64
+ export default ConceptListEmbed;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Copyright (c) 2023-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import { useTranslation } from 'react-i18next';
10
+ import styled from '@emotion/styled';
11
+ import { colors } from '@ndla/core';
12
+ import { ContentLinkMetaData } from '@ndla/types-embed';
13
+ interface Props {
14
+ embed: ContentLinkMetaData;
15
+ isOembed?: boolean;
16
+ }
17
+
18
+ const StyledSpan = styled.span`
19
+ color: ${colors.support.red};
20
+ `;
21
+
22
+ const ContentLinkEmbed = ({ embed, isOembed }: Props) => {
23
+ const { t } = useTranslation();
24
+ if (embed.status === 'error') {
25
+ return <StyledSpan>{`${t('embed.linkError')}: ${embed.embedData.linkText}`}</StyledSpan>;
26
+ }
27
+
28
+ const { embedData, data } = embed;
29
+
30
+ if (embedData.openIn === 'new-context' || isOembed) {
31
+ return (
32
+ <a href={embed.data.path} target="_blank" rel="noopener noreferrer">
33
+ {embedData.linkText}
34
+ </a>
35
+ );
36
+ }
37
+
38
+ return <a href={data.path}>{embedData.linkText}</a>;
39
+ };
40
+
41
+ export default ContentLinkEmbed;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Copyright (c) 2023-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import styled from '@emotion/styled';
10
+ import { OembedMetaData } from '@ndla/types-embed';
11
+ import { useEffect, useRef } from 'react';
12
+ import { useTranslation } from 'react-i18next';
13
+ import { Figure } from '../Figure';
14
+ import { ResourceBox } from '../ResourceBox';
15
+ import { errorSvgSrc } from './ImageEmbed';
16
+
17
+ interface Props {
18
+ embed: OembedMetaData;
19
+ isConcept?: boolean;
20
+ }
21
+
22
+ const StyledFigure = styled.figure`
23
+ iframe {
24
+ height: auto;
25
+ }
26
+ `;
27
+
28
+ const ExternalEmbed = ({ embed, isConcept }: Props) => {
29
+ const { t } = useTranslation();
30
+ const figRef = useRef<HTMLElement>(null);
31
+
32
+ useEffect(() => {
33
+ const iframe = figRef.current?.querySelector(`iframe`);
34
+ if (iframe) {
35
+ const [width, height] = [parseInt(iframe.width), parseInt(iframe.height)];
36
+ iframe.style.aspectRatio = `${width ? width : 16}/${height ? height : 9}`;
37
+ }
38
+ }, []);
39
+ if (embed.status === 'error') {
40
+ return (
41
+ <figure className={isConcept ? '' : 'c-figure'}>
42
+ <img alt={t('external.error')} src={errorSvgSrc} />
43
+ <figcaption>{t('external.error')}</figcaption>
44
+ </figure>
45
+ );
46
+ }
47
+
48
+ const { embedData, data } = embed;
49
+
50
+ if (embedData.type === 'fullscreen') {
51
+ const image = { src: data.iframeImage?.imageUrl ?? '', alt: data.iframeImage?.alttext?.alttext ?? '' };
52
+ return (
53
+ <Figure type="full">
54
+ <ResourceBox
55
+ image={image}
56
+ title={embedData.title ?? ''}
57
+ url={embedData.url}
58
+ caption={embedData.caption ?? ''}
59
+ buttonText={t('license.other.itemImage.ariaLabel')}
60
+ />
61
+ </Figure>
62
+ );
63
+ }
64
+
65
+ const fullColumnClass = isConcept ? 'c-figure--full-column' : '';
66
+ const classes = `c-figure ${fullColumnClass} c-figure--resize`;
67
+
68
+ return (
69
+ <StyledFigure
70
+ ref={figRef}
71
+ className={classes}
72
+ //@ts-ignore
73
+ // eslint-disable-next-line react/no-unknown-property
74
+ resizeiframe="true"
75
+ dangerouslySetInnerHTML={{ __html: data.oembed.html ?? '' }}
76
+ />
77
+ );
78
+ };
79
+
80
+ export default ExternalEmbed;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Copyright (c) 2023-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import { FootnoteMetaData } from '@ndla/types-embed';
10
+ import { useTranslation } from 'react-i18next';
11
+
12
+ interface Props {
13
+ embed: FootnoteMetaData;
14
+ }
15
+ const FootnoteEmbed = ({ embed }: Props) => {
16
+ const { t } = useTranslation();
17
+ if (embed.status === 'error') {
18
+ return <div>{t('error')}</div>;
19
+ }
20
+
21
+ return (
22
+ <span id={`ref${embed.data.entryNum}`} className="c-footnotes__ref">
23
+ <sup>
24
+ <a href={`#note${embed.data.entryNum}`} target="_self">{`[${embed.data.entryNum}]`}</a>
25
+ </sup>
26
+ </span>
27
+ );
28
+ };
29
+
30
+ export default FootnoteEmbed;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Copyright (c) 2023-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import styled from '@emotion/styled';
10
+ import { H5pMetaData } from '@ndla/types-embed';
11
+ import { useEffect, useRef } from 'react';
12
+ import { useTranslation } from 'react-i18next';
13
+ import { errorSvgSrc } from './ImageEmbed';
14
+
15
+ interface Props {
16
+ embed: H5pMetaData;
17
+ isConcept?: boolean;
18
+ }
19
+
20
+ const StyledFigure = styled.figure`
21
+ iframe {
22
+ height: auto;
23
+ }
24
+ `;
25
+
26
+ const H5pEmbed = ({ embed, isConcept }: Props) => {
27
+ const { t } = useTranslation();
28
+
29
+ const iframeRef = useRef<HTMLIFrameElement>(null);
30
+ const figRef = useRef<HTMLElement>(null);
31
+
32
+ useEffect(() => {
33
+ const iframe =
34
+ embed.status === 'success' && embed.data.oembed ? figRef.current?.querySelector('iframe') : iframeRef.current;
35
+ if (iframe) {
36
+ const [width, height] = [parseInt(iframe.width), parseInt(iframe.height)];
37
+ iframe.style.aspectRatio = `${width ? width : 16}/${height ? height : 9}`;
38
+ }
39
+ }, [embed]);
40
+
41
+ if (embed.status === 'error') {
42
+ return (
43
+ <figure className={isConcept ? '' : 'c-figure'}>
44
+ <img alt={t('h5p.error')} src={errorSvgSrc} />
45
+ <figcaption>{t('h5p.error')}</figcaption>
46
+ </figure>
47
+ );
48
+ }
49
+ const fullColumnClass = isConcept ? 'c-figure--full-column' : '';
50
+ const classes = `c-figure ${fullColumnClass} c-figure--resize`;
51
+
52
+ if (embed.data.oembed) {
53
+ return (
54
+ <StyledFigure
55
+ className={classes}
56
+ ref={figRef}
57
+ //@ts-ignore
58
+ // eslint-disable-next-line react/no-unknown-property
59
+ resizeiframe="true"
60
+ dangerouslySetInnerHTML={{ __html: embed.data.oembed.html ?? '' }}
61
+ />
62
+ );
63
+ }
64
+
65
+ return (
66
+ //@ts-ignore
67
+ // eslint-disable-next-line react/no-unknown-property
68
+ <StyledFigure className={classes} resizeiframe="true">
69
+ <iframe title={embed.embedData.url} ref={iframeRef} aria-label={embed.embedData.url} src={embed.embedData.url} />
70
+ </StyledFigure>
71
+ );
72
+ };
73
+
74
+ export default H5pEmbed;