@ndla/ui 34.6.1 → 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 +11 -1
  35. package/es/locale/messages-nb.js +11 -1
  36. package/es/locale/messages-nn.js +11 -1
  37. package/es/locale/messages-se.js +11 -1
  38. package/es/locale/messages-sma.js +11 -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 +10 -0
  103. package/lib/locale/messages-en.js +11 -1
  104. package/lib/locale/messages-nb.d.ts +10 -0
  105. package/lib/locale/messages-nb.js +11 -1
  106. package/lib/locale/messages-nn.d.ts +10 -0
  107. package/lib/locale/messages-nn.js +11 -1
  108. package/lib/locale/messages-se.d.ts +10 -0
  109. package/lib/locale/messages-se.js +11 -1
  110. package/lib/locale/messages-sma.d.ts +10 -0
  111. package/lib/locale/messages-sma.js +11 -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 +10 -0
  149. package/src/locale/messages-nb.ts +10 -0
  150. package/src/locale/messages-nn.ts +10 -0
  151. package/src/locale/messages-se.ts +10 -1
  152. package/src/locale/messages-sma.ts +10 -0
  153. package/src/types.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ndla/ui",
3
- "version": "34.6.1",
3
+ "version": "34.6.3",
4
4
  "description": "UI component library for NDLA.",
5
5
  "license": "GPL-3.0",
6
6
  "main": "lib/index.js",
@@ -31,28 +31,30 @@
31
31
  "types"
32
32
  ],
33
33
  "dependencies": {
34
- "@ndla/article-scripts": "^3.0.14",
35
- "@ndla/button": "^9.1.3",
34
+ "@ndla/article-scripts": "^3.0.15",
35
+ "@ndla/button": "^9.1.4",
36
36
  "@ndla/carousel": "^3.0.3",
37
37
  "@ndla/core": "^3.1.2",
38
- "@ndla/forms": "^4.2.6",
39
- "@ndla/hooks": "^2.0.1",
38
+ "@ndla/forms": "^4.2.7",
39
+ "@ndla/hooks": "^2.0.2",
40
40
  "@ndla/icons": "^2.2.3",
41
41
  "@ndla/licenses": "^7.0.1",
42
42
  "@ndla/modal": "^2.2.7",
43
- "@ndla/notion": "^4.2.3",
44
- "@ndla/safelink": "^4.0.8",
43
+ "@ndla/notion": "^4.2.4",
44
+ "@ndla/safelink": "^4.0.9",
45
45
  "@ndla/switch": "^1.0.7",
46
- "@ndla/tabs": "^2.1.6",
47
- "@ndla/tooltip": "^4.0.9",
46
+ "@ndla/tabs": "^2.1.7",
47
+ "@ndla/tooltip": "^4.0.10",
48
48
  "@ndla/types-learningpath-api": "^0.0.17",
49
- "@ndla/util": "^3.1.9",
49
+ "@ndla/util": "^3.1.10",
50
+ "@radix-ui/react-accordion": "1.1.0",
50
51
  "@radix-ui/react-dropdown-menu": "2.0.2",
52
+ "@radix-ui/react-popover": "^1.0.3",
51
53
  "@reach/menu-button": "^0.16.2",
52
54
  "@reach/slider": "^0.16.0",
53
55
  "focus-trap-react": "^8.9.2",
54
56
  "framer-motion": "^6.5.1",
55
- "html-react-parser": "^0.14.1",
57
+ "html-react-parser": "^3.0.8",
56
58
  "i18next-browser-languagedetector": "^6.1.1",
57
59
  "invariant": "^2.2.3",
58
60
  "react-bem-helper": "1.4.1",
@@ -73,6 +75,8 @@
73
75
  },
74
76
  "devDependencies": {
75
77
  "@babel/plugin-proposal-optional-chaining": "^7.11.0",
78
+ "@ndla/types-embed": "^1.0.0",
79
+ "@ndla/types-image-api": "0.0.10",
76
80
  "@types/reach__dialog": "^0.1.0",
77
81
  "css-loader": "^6.7.3",
78
82
  "mini-css-extract-plugin": "^2.7.2",
@@ -83,5 +87,5 @@
83
87
  "publishConfig": {
84
88
  "access": "public"
85
89
  },
86
- "gitHead": "f4df50f25d48eeea4c064864366cf0ccb2d4f778"
90
+ "gitHead": "3be4405a57a209633f9e8534b5f208ab50bae6f0"
87
91
  }
@@ -123,6 +123,7 @@ type Props = {
123
123
  modifier?: string;
124
124
  children?: ReactNode;
125
125
  messages: Messages;
126
+ contentTransformed?: boolean;
126
127
  locale: Locale;
127
128
  messageBoxLinks?: [];
128
129
  copyText?: string;
@@ -137,7 +138,10 @@ type Props = {
137
138
  accessMessage?: string;
138
139
  };
139
140
 
140
- const getArticleContent = (content: any, locale: Locale) => {
141
+ const getArticleContent = (content: any, locale: Locale, contentTransformed?: boolean) => {
142
+ if (contentTransformed) {
143
+ return content;
144
+ }
141
145
  switch (typeof content) {
142
146
  case 'string':
143
147
  return <ArticleContent content={content} locale={locale} />;
@@ -166,6 +170,7 @@ export const Article = ({
166
170
  accessMessage,
167
171
  heartButton,
168
172
  copyText,
173
+ contentTransformed,
169
174
  }: Props) => {
170
175
  const articleRef = useRef<HTMLDivElement>(null);
171
176
  const wrapperRef = useRef<HTMLDivElement>(null);
@@ -232,7 +237,7 @@ export const Article = ({
232
237
  buttonOffsetRight={articlePositionRight}
233
238
  />
234
239
  )}
235
- {getArticleContent(content, locale)}
240
+ {getArticleContent(content, locale, contentTransformed)}
236
241
  </LayoutItem>
237
242
 
238
243
  <LayoutItem layout="center">
@@ -242,7 +247,7 @@ export const Article = ({
242
247
  authors={authors}
243
248
  suppliers={rightsholders}
244
249
  published={published}
245
- license={licenseObj.license}
250
+ license={licenseObj?.license ?? ''}
246
251
  licenseBox={licenseBox}
247
252
  printUrl={printUrl}
248
253
  />
@@ -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;