@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
@@ -21,12 +21,20 @@ interface Props {
21
21
  children?: ReactNode;
22
22
  narrowScreen?: boolean;
23
23
  wideScreen?: boolean;
24
+ alwaysShow?: boolean;
24
25
  }
25
26
 
26
- const Aside = ({ children, narrowScreen = false, dangerouslySetInnerHTML, wideScreen = false }: Props) => {
27
+ const Aside = ({
28
+ children,
29
+ narrowScreen = false,
30
+ dangerouslySetInnerHTML,
31
+ wideScreen = false,
32
+ alwaysShow = false,
33
+ }: Props) => {
27
34
  const modifiers = {
28
35
  narrowScreen,
29
36
  wideScreen,
37
+ alwaysShow,
30
38
  };
31
39
  return (
32
40
  <aside {...classes('', modifiers)}>
@@ -28,6 +28,9 @@
28
28
  display: none;
29
29
  }
30
30
  }
31
+ &--alwaysShow {
32
+ display: block !important;
33
+ }
31
34
  }
32
35
 
33
36
  .c-aside__content {
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Copyright (c) 2021-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 { MouseEvent, ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
10
+ import { useTranslation } from 'react-i18next';
11
+ import styled from '@emotion/styled';
12
+ import { colors } from '@ndla/core';
13
+ import Tooltip from '@ndla/tooltip';
14
+ import { Link } from '@ndla/icons/common';
15
+ import { copyTextToClipboard } from '@ndla/util';
16
+
17
+ const ContainerDiv = styled.div`
18
+ position: relative;
19
+ `;
20
+ const IconButton = styled.button`
21
+ position: absolute;
22
+ left: -3em;
23
+ top: 0.1em;
24
+ background: none;
25
+ border: 0;
26
+ z-index: 1;
27
+ transition: 0.2s;
28
+ opacity: 0;
29
+ color: ${colors.brand.grey};
30
+
31
+ & svg {
32
+ width: 30px;
33
+ height: 30px;
34
+ }
35
+
36
+ ${ContainerDiv}:hover &,
37
+ &:focus, &:focus-visible, &:active {
38
+ cursor: pointer;
39
+ opacity: 1;
40
+ }
41
+ `;
42
+
43
+ interface Props {
44
+ // What to render within the h2
45
+ children: ReactNode;
46
+ copyText: string;
47
+ }
48
+ const CopyParagraphButtonV2 = ({ children, copyText }: Props) => {
49
+ const [hasCopied, setHasCopied] = useState(false);
50
+ const { t } = useTranslation();
51
+ const sanitizedTitle = useMemo(() => encodeURIComponent(copyText.replace(/ /g, '-')), [copyText]);
52
+
53
+ useEffect(() => {
54
+ if (hasCopied) {
55
+ setTimeout(() => setHasCopied(false), 3000);
56
+ }
57
+ }, [hasCopied]);
58
+
59
+ const onCopyClick = useCallback(() => {
60
+ setHasCopied(true);
61
+ const { location } = window;
62
+ const newHash = `#${sanitizedTitle}`;
63
+ const port = location.port ? `:${location.port}` : '';
64
+ const urlToCopy = `${location.protocol}//${location.hostname}${port}${location.pathname}${location.search}${newHash}`;
65
+
66
+ copyTextToClipboard(urlToCopy);
67
+ }, [sanitizedTitle]);
68
+
69
+ const tooltip = hasCopied ? t('article.copyPageLinkCopied') : t('article.copyHeaderLink');
70
+ return (
71
+ <ContainerDiv>
72
+ <Tooltip tooltip={tooltip}>
73
+ <IconButton onClick={onCopyClick} aria-label={`${tooltip}: ${copyText}`}>
74
+ <Link />
75
+ </IconButton>
76
+ </Tooltip>
77
+ <h2 id={sanitizedTitle} tabIndex={-1}>
78
+ {children}
79
+ </h2>
80
+ </ContainerDiv>
81
+ );
82
+ };
83
+
84
+ export default CopyParagraphButtonV2;
@@ -7,7 +7,8 @@
7
7
  */
8
8
 
9
9
  import CopyParagraphButton from './CopyParagraphButton';
10
+ import CopyParagraphButtonV2 from './CopyParagraphButtonV2';
10
11
  import initCopyParagraphButtons from './initCopyParagraphButtons';
11
12
 
12
- export { CopyParagraphButton, initCopyParagraphButtons };
13
+ export { CopyParagraphButton, initCopyParagraphButtons, CopyParagraphButtonV2 };
13
14
  export default CopyParagraphButton;
@@ -0,0 +1,249 @@
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 { AudioMetaData } from '@ndla/types-embed';
10
+ import { ICopyright } from '@ndla/types-image-api';
11
+ import {
12
+ figureApa7CopyString,
13
+ getGroupedContributorDescriptionList,
14
+ getLicenseByAbbreviation,
15
+ getLicenseCredits,
16
+ } from '@ndla/licenses';
17
+ import { ModalV2 } from '@ndla/modal';
18
+ import { useState } from 'react';
19
+ import { useTranslation } from 'react-i18next';
20
+ //@ts-ignore
21
+ import { Remarkable } from 'remarkable';
22
+ import { ButtonV2, CopyButton } from '@ndla/button';
23
+ import { SafeLinkButton } from '@ndla/safelink';
24
+ import AudioPlayer from '../AudioPlayer';
25
+ import { Figure, FigureCaption } from '../Figure';
26
+ import { FigureLicenseDialogContent } from '../Figure/FigureLicenseDialogContent';
27
+ import { Author } from './ImageEmbed';
28
+
29
+ interface Props {
30
+ embed: AudioMetaData;
31
+ articlePath?: string;
32
+ }
33
+ export const getFirstNonEmptyLicenseCredits = (authors: {
34
+ creators: Author[];
35
+ rightsholders: Author[];
36
+ processors: Author[];
37
+ }) => Object.values(authors).find((i) => i.length > 0) ?? [];
38
+
39
+ const renderMarkdown = (text: string) => {
40
+ const md = new Remarkable();
41
+ const rendered = md.render(text);
42
+ return <span dangerouslySetInnerHTML={{ __html: rendered }} />;
43
+ };
44
+
45
+ const AudioEmbed = ({ embed, articlePath }: Props) => {
46
+ const { t, i18n } = useTranslation();
47
+ const [isOpen, setIsOpen] = useState(false);
48
+ if (embed.status === 'error') {
49
+ return (
50
+ <Figure>
51
+ <svg
52
+ fill="#8A8888"
53
+ height="50"
54
+ viewBox="0 0 24 12"
55
+ width="100%"
56
+ xmlns="http://www.w3.org/2000/svg"
57
+ style={{ backgroundColor: '#EFF0F2' }}>
58
+ <path d="M0 0h24v24H0V0z" fill="none" />
59
+ <path
60
+ transform="scale(0.3) translate(28, 8.5)"
61
+ d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
62
+ />
63
+ </svg>
64
+ <figcaption>{t('audio.error.caption')}</figcaption>
65
+ </Figure>
66
+ );
67
+ }
68
+
69
+ const { data, embedData, seq } = embed;
70
+
71
+ if (embedData.type === 'minimal') {
72
+ return <AudioPlayer speech src={data.audioFile.url} title={data.title.title} />;
73
+ }
74
+
75
+ const subtitle = data.series ? { title: data.series.title.title, url: `/podkast/${data.series.id}` } : undefined;
76
+
77
+ const textVersion = data.manuscript && renderMarkdown(data.manuscript.manuscript);
78
+ const description = renderMarkdown(data.podcastMeta?.introduction ?? '');
79
+
80
+ const coverPhoto = data.podcastMeta?.coverPhoto;
81
+
82
+ const img = coverPhoto && { url: coverPhoto.url, alt: coverPhoto.altText };
83
+
84
+ const authors = getLicenseCredits(data.copyright);
85
+
86
+ const license = getLicenseByAbbreviation(data.copyright.license.license, i18n.language);
87
+
88
+ const contributors = getGroupedContributorDescriptionList(data.copyright, i18n.language).map((item) => ({
89
+ name: item.description,
90
+ type: item.label,
91
+ }));
92
+
93
+ const figureId = `figure-${seq}-${data.id}`;
94
+
95
+ const copyString =
96
+ data.audioType === 'podcast'
97
+ ? figureApa7CopyString(
98
+ data.title.title,
99
+ undefined,
100
+ data.audioFile.url,
101
+ articlePath,
102
+ data.copyright,
103
+ data.copyright.license.license,
104
+ '',
105
+ (id: string) => t(id),
106
+ i18n.language,
107
+ )
108
+ : undefined;
109
+ const captionAuthors = getFirstNonEmptyLicenseCredits(authors);
110
+ return (
111
+ <Figure id={figureId} type="full">
112
+ <AudioPlayer
113
+ description={description}
114
+ img={img}
115
+ src={data.audioFile.url}
116
+ textVersion={textVersion}
117
+ title={data.title.title}
118
+ subtitle={subtitle}
119
+ />
120
+ <FigureCaption
121
+ id=""
122
+ figureId=""
123
+ modalButton={
124
+ <ButtonV2 variant="outline" shape="pill" size="small" onClick={() => setIsOpen(true)}>
125
+ {t('audio.reuse')}
126
+ </ButtonV2>
127
+ }
128
+ licenseRights={license.rights}
129
+ authors={captionAuthors}
130
+ locale={i18n.language}
131
+ />
132
+ <ModalV2 controlled isOpen={isOpen} onClose={() => setIsOpen(false)} labelledBy="license-dialog-rules-heading">
133
+ {(close) => (
134
+ <FigureLicenseDialogContent
135
+ onClose={close}
136
+ title={data.title.title}
137
+ license={license}
138
+ authors={contributors}
139
+ origin={data.copyright.origin}
140
+ locale={i18n.language}
141
+ type="audio">
142
+ {data.copyright.license.license !== 'COPYRIGHT' && (
143
+ <>
144
+ {copyString && (
145
+ <CopyButton
146
+ variant="outline"
147
+ copyNode={t('license.hasCopiedTitle')}
148
+ onClick={() => navigator.clipboard.writeText(copyString)}>
149
+ {t('license.copyTitle')}
150
+ </CopyButton>
151
+ )}
152
+ <SafeLinkButton download to={data.audioFile.url} variant="outline">
153
+ {t('audio.download')}
154
+ </SafeLinkButton>
155
+ </>
156
+ )}
157
+ </FigureLicenseDialogContent>
158
+ )}
159
+ </ModalV2>
160
+ {data.imageMeta && (
161
+ <ImageLicense
162
+ title={data.imageMeta.title.title}
163
+ imageUrl={data.imageMeta.imageUrl}
164
+ copyright={data.imageMeta.copyright}
165
+ articlePath={articlePath}
166
+ />
167
+ )}
168
+ </Figure>
169
+ );
170
+ };
171
+
172
+ interface ImageLicenseProps {
173
+ title: string;
174
+ imageUrl: string;
175
+ copyright: ICopyright;
176
+ articlePath?: string;
177
+ }
178
+
179
+ const ImageLicense = ({ articlePath, title, imageUrl, copyright }: ImageLicenseProps) => {
180
+ const { t, i18n } = useTranslation();
181
+ const [isOpen, setIsOpen] = useState(false);
182
+ const copyString = figureApa7CopyString(
183
+ title,
184
+ undefined,
185
+ imageUrl,
186
+ articlePath,
187
+ copyright,
188
+ copyright.license.license,
189
+ undefined,
190
+ (id: string) => t(id),
191
+ i18n.language,
192
+ );
193
+ const license = getLicenseByAbbreviation(copyright.license.license, i18n.language);
194
+ const authors = getLicenseCredits(copyright);
195
+
196
+ const contributors = getGroupedContributorDescriptionList(copyright, i18n.language).map((item) => ({
197
+ name: item.description,
198
+ type: item.label,
199
+ }));
200
+
201
+ const captionAuthors = getFirstNonEmptyLicenseCredits(authors);
202
+
203
+ return (
204
+ <>
205
+ <FigureCaption
206
+ figureId=""
207
+ id=""
208
+ licenseRights={license.rights}
209
+ modalButton={
210
+ <ButtonV2 variant="outline" shape="pill" size="small" onClick={() => setIsOpen(true)}>
211
+ {t('image.reuse')}
212
+ </ButtonV2>
213
+ }
214
+ authors={captionAuthors}
215
+ locale={i18n.language}>
216
+ <ModalV2 controlled isOpen={isOpen} onClose={() => setIsOpen(false)}>
217
+ {(close) => (
218
+ <FigureLicenseDialogContent
219
+ onClose={close}
220
+ title={title}
221
+ license={license}
222
+ authors={contributors}
223
+ origin={copyright.origin}
224
+ locale={i18n.language}
225
+ type="image">
226
+ {copyright.license.license !== 'COPYRIGHTED' && (
227
+ <>
228
+ {copyString && (
229
+ <CopyButton
230
+ variant="outline"
231
+ copyNode={t('license.hasCopiedTitle')}
232
+ onClick={() => navigator.clipboard.writeText(copyString)}>
233
+ {t('license.copyTitle')}
234
+ </CopyButton>
235
+ )}
236
+ <SafeLinkButton download to={imageUrl} variant="outline">
237
+ {t('image.download')}
238
+ </SafeLinkButton>
239
+ </>
240
+ )}
241
+ </FigureLicenseDialogContent>
242
+ )}
243
+ </ModalV2>
244
+ </FigureCaption>
245
+ </>
246
+ );
247
+ };
248
+
249
+ export default AudioEmbed;
@@ -0,0 +1,203 @@
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 sortBy from 'lodash/sortBy';
10
+ import isNumber from 'lodash/isNumber';
11
+ import styled from '@emotion/styled';
12
+ import { spacing } from '@ndla/core';
13
+ import { getGroupedContributorDescriptionList, getLicenseByAbbreviation } from '@ndla/licenses';
14
+ import { useEffect, useRef, useState } from 'react';
15
+ import { ModalV2 } from '@ndla/modal';
16
+ import { SafeLinkButton } from '@ndla/safelink';
17
+ import { BrightcoveEmbedData, BrightcoveMetaData, BrightcoveVideoSource } from '@ndla/types-embed';
18
+ import { useTranslation } from 'react-i18next';
19
+ import { ButtonV2, CopyButton } from '@ndla/button';
20
+ import { Figure, FigureCaption } from '../Figure';
21
+ import { FigureLicenseDialogContent } from '../Figure/FigureLicenseDialogContent';
22
+ import { getFirstNonEmptyLicenseCredits } from './AudioEmbed';
23
+
24
+ interface Props {
25
+ embed: BrightcoveMetaData;
26
+ isConcept?: boolean;
27
+ }
28
+
29
+ const LinkedVideoButton = styled(ButtonV2)`
30
+ margin-left: ${spacing.small};
31
+ `;
32
+
33
+ const BrightcoveIframe = styled.iframe`
34
+ height: auto;
35
+ `;
36
+
37
+ export const makeIframeString = (url: string, width: string | number, height: string | number, title: string = '') => {
38
+ const strippedWidth = isNumber(width) ? width : width.replace(/\s*px/, '');
39
+ const strippedHeight = isNumber(height) ? height : height.replace(/\s*px/, '');
40
+ const urlOrTitle = title || url;
41
+ return `<iframe title="${urlOrTitle}" aria-label="${urlOrTitle}" src="${url}" width="${strippedWidth}" height="${strippedHeight}" allowfullscreen scrolling="no" frameborder="0" loading="lazy"></iframe>`;
42
+ };
43
+
44
+ export const isNumeric = (value: any) => !Number.isNaN(value - parseFloat(value));
45
+
46
+ const getIframeProps = (data: BrightcoveEmbedData, sources: BrightcoveVideoSource[]) => {
47
+ const { account, videoid, player = 'default' } = data;
48
+
49
+ const source = sortBy(
50
+ sources.filter((s) => s.width && s.height),
51
+ (s) => s.height,
52
+ )[0];
53
+
54
+ return {
55
+ src: `https://players.brightcove.net/${account}/${player}_default/index.html?videoId=${videoid}`,
56
+ height: source?.height ?? '480',
57
+ width: source?.width ?? '640',
58
+ };
59
+ };
60
+ const BrightcoveEmbed = ({ embed, isConcept }: Props) => {
61
+ const [isOpen, setIsOpen] = useState(false);
62
+ const [showOriginalVideo, setShowOriginalVideo] = useState(true);
63
+ const { t, i18n } = useTranslation();
64
+ const iframeRef = useRef<HTMLIFrameElement>(null);
65
+ const { embedData } = embed;
66
+
67
+ useEffect(() => {
68
+ const iframe = iframeRef.current;
69
+ if (iframe) {
70
+ const [width, height] = [parseInt(iframe.width), parseInt(iframe.height)];
71
+ iframe.style.aspectRatio = `${width}/${height}`;
72
+ }
73
+ }, []);
74
+ if (embed.status === 'error') {
75
+ return (
76
+ <Figure type={isConcept ? 'full-column' : 'full'} resizeIframe>
77
+ <BrightcoveIframe
78
+ ref={iframeRef}
79
+ title={`Video: ${embedData.videoid ?? ''}`}
80
+ aria-label={`Video: ${embedData.videoid ?? ''}`}
81
+ frameBorder="0"
82
+ {...getIframeProps(embedData, [])}
83
+ allowFullScreen
84
+ />
85
+ <figcaption>{t('video.error')}</figcaption>
86
+ </Figure>
87
+ );
88
+ }
89
+ const { data, seq } = embed;
90
+
91
+ const linkedVideoId = isNumeric(data.link?.text) ? data.link?.text : undefined;
92
+
93
+ const license = getLicenseByAbbreviation(data.copyright?.license.license ?? '', i18n.language);
94
+ const contributors = data.copyright
95
+ ? getGroupedContributorDescriptionList(data.copyright, i18n.language).map((item) => ({
96
+ name: item.description,
97
+ type: item.label,
98
+ }))
99
+ : [];
100
+
101
+ const { rightsholders = [], creators = [], processors = [] } = data.copyright ?? {};
102
+
103
+ const download = sortBy(
104
+ data.sources.filter((src) => src.container === 'MP4' && src.src),
105
+ (src) => src.size,
106
+ )?.[0]?.src;
107
+
108
+ const figureId = `figure-${seq}-${data.id}`;
109
+ const originalVideoProps = getIframeProps(embedData, data.sources);
110
+ const alternativeVideoProps = linkedVideoId
111
+ ? getIframeProps({ ...embedData, videoid: linkedVideoId }, data.sources)
112
+ : undefined;
113
+ const captionAuthors = getFirstNonEmptyLicenseCredits({ rightsholders, creators, processors });
114
+
115
+ return (
116
+ <Figure id={figureId} type={isConcept ? 'full-column' : 'full'} resizeIframe>
117
+ <div className="brightcove-video">
118
+ <BrightcoveIframe
119
+ ref={iframeRef}
120
+ className="original"
121
+ title={`Video: ${data.name}`}
122
+ aria-label={`Video: ${data.name}`}
123
+ frameBorder="0"
124
+ {...(alternativeVideoProps && !showOriginalVideo ? alternativeVideoProps : originalVideoProps)}
125
+ allowFullScreen
126
+ />
127
+ </div>
128
+ <FigureCaption
129
+ figureId={figureId}
130
+ id={data.id}
131
+ locale={i18n.language}
132
+ caption={embedData.caption ?? ''}
133
+ modalButton={
134
+ <ButtonV2 variant="outline" shape="pill" size="small" onClick={() => setIsOpen(true)}>
135
+ {t('video.reuse')}
136
+ </ButtonV2>
137
+ }
138
+ linkedVideoButton={
139
+ <LinkedVideoButton
140
+ variant="outline"
141
+ shape="pill"
142
+ size="small"
143
+ onClick={() => setShowOriginalVideo((p) => !p)}>
144
+ {t(`figure.button.${showOriginalVideo ? 'original' : 'alternative'}`)}
145
+ </LinkedVideoButton>
146
+ }
147
+ licenseRights={license.rights}
148
+ authors={captionAuthors}
149
+ hasLinkedVideo={!!linkedVideoId}
150
+ />
151
+ <ModalV2 controlled isOpen={isOpen} onClose={() => setIsOpen(false)} labelledBy="license-dialog-rules-heading">
152
+ {(close) => (
153
+ <FigureLicenseDialogContent
154
+ onClose={close}
155
+ title={data.name}
156
+ locale={i18n.language}
157
+ license={license}
158
+ authors={contributors}
159
+ type="video">
160
+ <VideoLicenseButtons
161
+ download={download}
162
+ licenseCode={data.copyright?.license.license}
163
+ src={originalVideoProps.src}
164
+ width={originalVideoProps.width}
165
+ height={originalVideoProps.height}
166
+ name={data.name}
167
+ />
168
+ </FigureLicenseDialogContent>
169
+ )}
170
+ </ModalV2>
171
+ </Figure>
172
+ );
173
+ };
174
+
175
+ interface VideoLicenseButtonsProps {
176
+ download: string;
177
+ licenseCode?: string;
178
+ src: string;
179
+ width: string | number;
180
+ height: string | number;
181
+ name?: string;
182
+ }
183
+
184
+ const VideoLicenseButtons = ({ download, src, width, height, name, licenseCode }: VideoLicenseButtonsProps) => {
185
+ const { t } = useTranslation();
186
+ return (
187
+ <>
188
+ {licenseCode !== 'COPYRIGHTED' && (
189
+ <SafeLinkButton key="download" to={download} variant="outline" download>
190
+ {t('video.download')}
191
+ </SafeLinkButton>
192
+ )}
193
+ <CopyButton
194
+ variant="outline"
195
+ copyNode={t('license.hasCopiedTitle')}
196
+ onClick={() => navigator.clipboard.writeText(makeIframeString(src, width, height, name))}>
197
+ {t('license.embed')}
198
+ </CopyButton>
199
+ </>
200
+ );
201
+ };
202
+
203
+ export default BrightcoveEmbed;