@ndla/ui 34.6.2 → 34.6.4
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/es/Article/Article.js +11 -6
- package/es/Aside/Aside.js +5 -2
- package/es/CopyParagraphButton/CopyParagraphButtonV2.js +85 -0
- package/es/CopyParagraphButton/index.js +2 -1
- package/es/Embed/AudioEmbed.js +254 -0
- package/es/Embed/BrightcoveEmbed.js +250 -0
- package/es/Embed/ConceptEmbed.js +359 -0
- package/es/Embed/ConceptListEmbed.js +71 -0
- package/es/Embed/ContentLinkEmbed.js +42 -0
- package/es/Embed/ExternalEmbed.js +91 -0
- package/es/Embed/FootnoteEmbed.js +32 -0
- package/es/Embed/H5pEmbed.js +87 -0
- package/es/Embed/IframeEmbed.js +83 -0
- package/es/Embed/ImageEmbed.js +322 -0
- package/es/Embed/RelatedContentEmbed.js +58 -0
- package/es/Embed/UnknownEmbed.js +27 -0
- package/es/Embed/conceptComponents.js +282 -0
- package/es/Embed/index.js +21 -0
- package/es/FactBox/FactBoxV2.js +90 -0
- package/es/FactBox/index.js +1 -0
- package/es/Figure/Figure.js +8 -5
- package/es/Figure/FigureLicenseDialogContent.js +72 -0
- package/es/FileList/FileListV2.js +47 -0
- package/es/FileList/FileV2.js +34 -0
- package/es/FileList/PdfFile.js +25 -0
- package/es/FileList/index.js +3 -0
- package/es/Notion/Notion.js +5 -5
- package/es/Notion/NotionVisualElement.js +2 -2
- package/es/RelatedArticleList/RelatedArticleV2.js +101 -0
- package/es/RelatedArticleList/index.js +2 -1
- package/es/Table/Table.js +95 -8
- package/es/all.css +1 -1
- package/es/index.js +5 -4
- package/es/locale/messages-en.js +32 -2
- package/es/locale/messages-nb.js +32 -2
- package/es/locale/messages-nn.js +32 -2
- package/es/locale/messages-se.js +32 -2
- package/es/locale/messages-sma.js +32 -2
- package/lib/Article/Article.d.ts +2 -1
- package/lib/Article/Article.js +11 -6
- package/lib/Aside/Aside.d.ts +2 -1
- package/lib/Aside/Aside.js +5 -2
- package/lib/CopyParagraphButton/CopyParagraphButtonV2.d.ts +14 -0
- package/lib/CopyParagraphButton/CopyParagraphButtonV2.js +84 -0
- package/lib/CopyParagraphButton/index.d.ts +2 -1
- package/lib/CopyParagraphButton/index.js +7 -0
- package/lib/Embed/AudioEmbed.d.ts +20 -0
- package/lib/Embed/AudioEmbed.js +252 -0
- package/lib/Embed/BrightcoveEmbed.d.ts +16 -0
- package/lib/Embed/BrightcoveEmbed.js +250 -0
- package/lib/Embed/ConceptEmbed.d.ts +19 -0
- package/lib/Embed/ConceptEmbed.js +359 -0
- package/lib/Embed/ConceptListEmbed.d.ts +13 -0
- package/lib/Embed/ConceptListEmbed.js +70 -0
- package/lib/Embed/ContentLinkEmbed.d.ts +14 -0
- package/lib/Embed/ContentLinkEmbed.js +50 -0
- package/lib/Embed/ExternalEmbed.d.ts +14 -0
- package/lib/Embed/ExternalEmbed.js +90 -0
- package/lib/Embed/FootnoteEmbed.d.ts +13 -0
- package/lib/Embed/FootnoteEmbed.js +39 -0
- package/lib/Embed/H5pEmbed.d.ts +14 -0
- package/lib/Embed/H5pEmbed.js +86 -0
- package/lib/Embed/IframeEmbed.d.ts +14 -0
- package/lib/Embed/IframeEmbed.js +91 -0
- package/lib/Embed/ImageEmbed.d.ts +37 -0
- package/lib/Embed/ImageEmbed.js +326 -0
- package/lib/Embed/RelatedContentEmbed.d.ts +16 -0
- package/lib/Embed/RelatedContentEmbed.js +64 -0
- package/lib/Embed/UnknownEmbed.d.ts +13 -0
- package/lib/Embed/UnknownEmbed.js +35 -0
- package/lib/Embed/conceptComponents.d.ts +32 -0
- package/lib/Embed/conceptComponents.js +280 -0
- package/lib/Embed/index.d.ts +20 -0
- package/lib/Embed/index.js +97 -0
- package/lib/FactBox/FactBoxV2.d.ts +13 -0
- package/lib/FactBox/FactBoxV2.js +92 -0
- package/lib/FactBox/index.d.ts +1 -0
- package/lib/FactBox/index.js +7 -0
- package/lib/Figure/Figure.d.ts +5 -2
- package/lib/Figure/Figure.js +8 -5
- package/lib/Figure/FigureLicenseDialogContent.d.ts +22 -0
- package/lib/Figure/FigureLicenseDialogContent.js +71 -0
- package/lib/FileList/FileListV2.d.ts +13 -0
- package/lib/FileList/FileListV2.js +46 -0
- package/lib/FileList/FileV2.d.ts +16 -0
- package/lib/FileList/FileV2.js +42 -0
- package/lib/FileList/PdfFile.d.ts +13 -0
- package/lib/FileList/PdfFile.js +31 -0
- package/lib/FileList/index.d.ts +3 -0
- package/lib/FileList/index.js +21 -0
- package/lib/Notion/Notion.js +5 -5
- package/lib/Notion/NotionVisualElement.d.ts +1 -1
- package/lib/Notion/NotionVisualElement.js +2 -2
- package/lib/RelatedArticleList/RelatedArticleV2.d.ts +25 -0
- package/lib/RelatedArticleList/RelatedArticleV2.js +101 -0
- package/lib/RelatedArticleList/index.d.ts +2 -1
- package/lib/RelatedArticleList/index.js +7 -0
- package/lib/Table/Table.js +98 -8
- package/lib/all.css +1 -1
- package/lib/index.d.ts +5 -4
- package/lib/index.js +117 -2
- package/lib/locale/messages-en.d.ts +30 -0
- package/lib/locale/messages-en.js +32 -2
- package/lib/locale/messages-nb.d.ts +30 -0
- package/lib/locale/messages-nb.js +32 -2
- package/lib/locale/messages-nn.d.ts +30 -0
- package/lib/locale/messages-nn.js +32 -2
- package/lib/locale/messages-se.d.ts +30 -0
- package/lib/locale/messages-se.js +32 -2
- package/lib/locale/messages-sma.d.ts +30 -0
- package/lib/locale/messages-sma.js +32 -2
- package/lib/types.d.ts +1 -1
- package/package.json +16 -12
- package/src/Article/Article.tsx +8 -3
- package/src/Aside/Aside.tsx +9 -1
- package/src/Aside/component.aside.scss +3 -0
- package/src/CopyParagraphButton/CopyParagraphButtonV2.tsx +84 -0
- package/src/CopyParagraphButton/index.tsx +2 -1
- package/src/Embed/AudioEmbed.tsx +249 -0
- package/src/Embed/BrightcoveEmbed.tsx +203 -0
- package/src/Embed/ConceptEmbed.tsx +403 -0
- package/src/Embed/ConceptListEmbed.tsx +64 -0
- package/src/Embed/ContentLinkEmbed.tsx +41 -0
- package/src/Embed/ExternalEmbed.tsx +80 -0
- package/src/Embed/FootnoteEmbed.tsx +30 -0
- package/src/Embed/H5pEmbed.tsx +74 -0
- package/src/Embed/IframeEmbed.tsx +84 -0
- package/src/Embed/ImageEmbed.tsx +314 -0
- package/src/Embed/RelatedContentEmbed.tsx +62 -0
- package/src/Embed/UnknownEmbed.tsx +27 -0
- package/src/Embed/conceptComponents.tsx +393 -0
- package/src/Embed/index.ts +21 -0
- package/src/FactBox/FactBoxV2.tsx +56 -0
- package/src/FactBox/index.ts +2 -0
- package/src/Figure/Figure.tsx +28 -15
- package/src/Figure/FigureLicenseDialogContent.tsx +80 -0
- package/src/Figure/component.figure.scss +0 -1
- package/src/FileList/FileListV2.tsx +58 -0
- package/src/FileList/FileV2.tsx +35 -0
- package/src/FileList/PdfFile.tsx +25 -0
- package/src/FileList/index.ts +3 -0
- package/src/Notion/Notion.tsx +0 -1
- package/src/Notion/NotionVisualElement.tsx +1 -1
- package/src/RelatedArticleList/RelatedArticleV2.tsx +84 -0
- package/src/RelatedArticleList/index.ts +2 -1
- package/src/Table/Table.tsx +77 -4
- package/src/index.ts +19 -4
- package/src/locale/messages-en.ts +33 -0
- package/src/locale/messages-nb.ts +33 -0
- package/src/locale/messages-nn.ts +33 -0
- package/src/locale/messages-se.ts +33 -0
- package/src/locale/messages-sma.ts +33 -0
- package/src/types.ts +1 -1
|
@@ -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;
|
|
@@ -0,0 +1,84 @@
|
|
|
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 isNumber from 'lodash/isNumber';
|
|
10
|
+
import { useEffect, useRef } from 'react';
|
|
11
|
+
import { IframeMetaData } from '@ndla/types-embed';
|
|
12
|
+
import { useTranslation } from 'react-i18next';
|
|
13
|
+
import { Figure } from '../Figure';
|
|
14
|
+
import { ResourceBox } from '../ResourceBox';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
embed: IframeMetaData;
|
|
18
|
+
isConcept?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ExternalEmbed = ({ embed, isConcept }: Props) => {
|
|
22
|
+
const { t } = useTranslation();
|
|
23
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
24
|
+
|
|
25
|
+
const { embedData } = embed;
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const iframe = iframeRef.current;
|
|
29
|
+
if (iframe) {
|
|
30
|
+
const [width, height] = [parseInt(iframe.width), parseInt(iframe.height)];
|
|
31
|
+
iframe.style.aspectRatio = `${width ? width : 16}/${height ? height : 9}`;
|
|
32
|
+
}
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
if (embedData.type === 'fullscreen') {
|
|
36
|
+
const iframeImage = embed.status === 'success' ? embed.data.iframeImage : undefined;
|
|
37
|
+
const image = { src: iframeImage?.imageUrl ?? '', alt: iframeImage?.alttext?.alttext ?? '' };
|
|
38
|
+
return (
|
|
39
|
+
<Figure type="full">
|
|
40
|
+
<ResourceBox
|
|
41
|
+
image={image}
|
|
42
|
+
title={embedData.title ?? ''}
|
|
43
|
+
url={embedData.url}
|
|
44
|
+
caption={embedData.caption ?? ''}
|
|
45
|
+
buttonText={t('license.other.itemImage.ariaLabel')}
|
|
46
|
+
/>
|
|
47
|
+
</Figure>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const resize = !embedData.url.includes('trinket.io');
|
|
52
|
+
|
|
53
|
+
const fullColumnClass = isConcept ? 'c-figure--full-column' : '';
|
|
54
|
+
const resizeClass = resize ? 'c-figure--resize' : '';
|
|
55
|
+
const classes = `c-figure ${fullColumnClass} ${resizeClass}`;
|
|
56
|
+
|
|
57
|
+
const { width, height, title, url } = embedData;
|
|
58
|
+
|
|
59
|
+
const strippedWidth = isNumber(width) ? width : width?.replace(/\s*px/, '');
|
|
60
|
+
const strippedHeight = isNumber(height) ? height : height?.replace(/\s*px/, '');
|
|
61
|
+
const urlOrTitle = title || url;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
//@ts-ignore
|
|
65
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
66
|
+
<figure className={classes} resizeiframe={`${resize}`}>
|
|
67
|
+
<iframe
|
|
68
|
+
ref={iframeRef}
|
|
69
|
+
title={urlOrTitle}
|
|
70
|
+
aria-label={urlOrTitle}
|
|
71
|
+
src={url}
|
|
72
|
+
width={strippedWidth}
|
|
73
|
+
height={strippedHeight}
|
|
74
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
75
|
+
allowFullScreen
|
|
76
|
+
scrolling="no"
|
|
77
|
+
frameBorder="0"
|
|
78
|
+
loading="lazy"
|
|
79
|
+
/>
|
|
80
|
+
</figure>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default ExternalEmbed;
|
|
@@ -0,0 +1,314 @@
|
|
|
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 isNumber from 'lodash/isNumber';
|
|
10
|
+
import styled from '@emotion/styled';
|
|
11
|
+
import { figureApa7CopyString, getGroupedContributorDescriptionList, getLicenseByAbbreviation } from '@ndla/licenses';
|
|
12
|
+
import { ImageEmbedData, ImageMetaData } from '@ndla/types-embed';
|
|
13
|
+
import { useTranslation } from 'react-i18next';
|
|
14
|
+
import { ModalV2 } from '@ndla/modal';
|
|
15
|
+
import { SafeLinkButton } from '@ndla/safelink';
|
|
16
|
+
import { MouseEventHandler, useState } from 'react';
|
|
17
|
+
import { ButtonV2, CopyButton } from '@ndla/button';
|
|
18
|
+
import { ExpandTwoArrows } from '@ndla/icons/action';
|
|
19
|
+
import { ArrowCollapse, ChevronDown, ChevronUp } from '@ndla/icons/common';
|
|
20
|
+
import { Figure, FigureCaption, FigureType } from '../Figure';
|
|
21
|
+
import Image, { ImageLink } from '../Image';
|
|
22
|
+
import { FigureLicenseDialogContent } from '../Figure/FigureLicenseDialogContent';
|
|
23
|
+
import { Copyright } from '../types';
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
embed: ImageMetaData;
|
|
27
|
+
articlePath?: string;
|
|
28
|
+
previewAlt?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Author {
|
|
32
|
+
name: string;
|
|
33
|
+
type: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const getLicenseCredits = (copyright?: {
|
|
37
|
+
creators?: Author[];
|
|
38
|
+
rightsholders?: Author[];
|
|
39
|
+
processors?: Author[];
|
|
40
|
+
}) => {
|
|
41
|
+
return {
|
|
42
|
+
creators: copyright?.creators ?? [],
|
|
43
|
+
rightsholders: copyright?.rightsholders ?? [],
|
|
44
|
+
processors: copyright?.processors ?? [],
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const errorSvgSrc = `data:image/svg+xml;charset=UTF-8,%3Csvg fill='%238A8888' height='400' viewBox='0 0 24 12' width='100%25' xmlns='http://www.w3.org/2000/svg' style='background-color: %23EFF0F2'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath transform='scale(0.3) translate(28, 8.5)' 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'/%3E%3C/svg%3E`;
|
|
49
|
+
const isSmall = (size?: string): size is 'xsmall' | 'small' => size === 'xsmall' || size === 'small';
|
|
50
|
+
|
|
51
|
+
const isAlign = (align?: string): align is 'left' | 'right' => align === 'left' || align === 'right';
|
|
52
|
+
|
|
53
|
+
const getFigureType = (size?: string, align?: string): FigureType => {
|
|
54
|
+
if (size && isSmall(size) && align && isAlign(align)) {
|
|
55
|
+
return `${size}-${align}`;
|
|
56
|
+
}
|
|
57
|
+
if (size && isSmall(size) && !align) {
|
|
58
|
+
return size as FigureType;
|
|
59
|
+
}
|
|
60
|
+
if (align && isAlign(align)) {
|
|
61
|
+
return align;
|
|
62
|
+
}
|
|
63
|
+
return 'full';
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const getSizes = (size?: string, align?: string) => {
|
|
67
|
+
if (align && size === 'full') {
|
|
68
|
+
return '(min-width: 1024px) 512px, (min-width: 768px) 350px, 100vw';
|
|
69
|
+
}
|
|
70
|
+
if (align && size === 'small') {
|
|
71
|
+
return '(min-width: 1024px) 350px, (min-width: 768px) 180px, 100vw';
|
|
72
|
+
}
|
|
73
|
+
if (align && size === 'xsmall') {
|
|
74
|
+
return '(min-width: 1024px) 180px, (min-width: 768px) 180px, 100vw';
|
|
75
|
+
}
|
|
76
|
+
return '(min-width: 1024px) 1024px, 100vw';
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const getFocalPoint = (data: ImageEmbedData) => {
|
|
80
|
+
if (isNumber(data.focalX) && isNumber(data.focalY)) {
|
|
81
|
+
return { x: data.focalX, y: data.focalY };
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const getCrop = (data: ImageEmbedData) => {
|
|
87
|
+
if (
|
|
88
|
+
isNumber(data.lowerRightX) &&
|
|
89
|
+
isNumber(data.lowerRightY) &&
|
|
90
|
+
isNumber(data.upperLeftX) &&
|
|
91
|
+
isNumber(data.upperLeftY)
|
|
92
|
+
) {
|
|
93
|
+
return {
|
|
94
|
+
startX: data.lowerRightX,
|
|
95
|
+
startY: data.lowerRightY,
|
|
96
|
+
endX: data.upperLeftX,
|
|
97
|
+
endY: data.upperLeftY,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const StyledSpan = styled.span`
|
|
104
|
+
font-style: italic;
|
|
105
|
+
color: grey;
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
const expandedSizes = '(min-width: 1024px) 1024px, 100vw';
|
|
109
|
+
|
|
110
|
+
const ImageEmbed = ({ embed, articlePath, previewAlt }: Props) => {
|
|
111
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
112
|
+
const [isBylineHidden, setIsBylineHidden] = useState(hideByline(embed.embedData.size));
|
|
113
|
+
const [imageSizes, setImageSizes] = useState<string | undefined>(undefined);
|
|
114
|
+
const { t, i18n } = useTranslation();
|
|
115
|
+
if (embed.status === 'error') {
|
|
116
|
+
const { align, size } = embed.embedData;
|
|
117
|
+
const figureType = getFigureType(size, align);
|
|
118
|
+
return (
|
|
119
|
+
<Figure type={figureType}>
|
|
120
|
+
<div className="c-figure__img">
|
|
121
|
+
<img alt={t('image.error.url')} src={errorSvgSrc} />
|
|
122
|
+
</div>
|
|
123
|
+
<figcaption>{t('image.error.caption')}</figcaption>
|
|
124
|
+
</Figure>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { data, embedData, seq } = embed;
|
|
129
|
+
|
|
130
|
+
const authors = getLicenseCredits(data.copyright);
|
|
131
|
+
|
|
132
|
+
const altText = embedData.alt || '';
|
|
133
|
+
const caption = embedData.caption || '';
|
|
134
|
+
const license = getLicenseByAbbreviation(data.copyright.license.license, i18n.language);
|
|
135
|
+
|
|
136
|
+
const figureType = getFigureType(embedData.size, embedData.align);
|
|
137
|
+
const sizes = getSizes(embedData.size, embedData.align);
|
|
138
|
+
|
|
139
|
+
const focalPoint = getFocalPoint(embedData);
|
|
140
|
+
const crop = getCrop(embedData);
|
|
141
|
+
|
|
142
|
+
const contributors = getGroupedContributorDescriptionList(data.copyright, i18n.language).map((item) => ({
|
|
143
|
+
name: item.description,
|
|
144
|
+
type: item.label,
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
const figureId = `figure-${seq}-${data.id}`;
|
|
148
|
+
|
|
149
|
+
const { creators, rightsholders, processors } = authors;
|
|
150
|
+
const captionAuthors = creators.length || rightsholders.length ? [...creators, ...rightsholders] : processors;
|
|
151
|
+
return (
|
|
152
|
+
<Figure
|
|
153
|
+
id={figureId}
|
|
154
|
+
type={imageSizes ? undefined : figureType}
|
|
155
|
+
className={imageSizes ? 'c-figure--right expanded' : ''}>
|
|
156
|
+
<ImageWrapper src={data.imageUrl} crop={crop} size={embedData.size}>
|
|
157
|
+
<Image
|
|
158
|
+
focalPoint={focalPoint}
|
|
159
|
+
contentType={data.contentType}
|
|
160
|
+
crop={crop}
|
|
161
|
+
sizes={imageSizes ?? sizes}
|
|
162
|
+
alt={altText}
|
|
163
|
+
src={data.imageUrl}
|
|
164
|
+
expandButton={
|
|
165
|
+
<ExpandButton
|
|
166
|
+
size={embedData.size}
|
|
167
|
+
expanded={!!imageSizes}
|
|
168
|
+
bylineHidden={isBylineHidden}
|
|
169
|
+
onExpand={() => setImageSizes((p) => (p ? undefined : expandedSizes))}
|
|
170
|
+
onHideByline={() => setIsBylineHidden((p) => !p)}
|
|
171
|
+
/>
|
|
172
|
+
}
|
|
173
|
+
/>
|
|
174
|
+
</ImageWrapper>
|
|
175
|
+
{previewAlt ? <StyledSpan>{`Alt: ${embedData.alt}`}</StyledSpan> : null}
|
|
176
|
+
<FigureCaption
|
|
177
|
+
hideFigcaption={isSmall(embedData.size) || isBylineHidden}
|
|
178
|
+
figureId={figureId}
|
|
179
|
+
id={figureId}
|
|
180
|
+
caption={caption}
|
|
181
|
+
reuseLabel={t('image.reuse')}
|
|
182
|
+
modalButton={
|
|
183
|
+
<ButtonV2 shape="pill" variant="outline" size="small" onClick={() => setIsOpen(true)}>
|
|
184
|
+
{t('image.reuse')}
|
|
185
|
+
</ButtonV2>
|
|
186
|
+
}
|
|
187
|
+
licenseRights={license.rights}
|
|
188
|
+
authors={captionAuthors}
|
|
189
|
+
locale={i18n.language}>
|
|
190
|
+
<ModalV2 controlled isOpen={isOpen} onClose={() => setIsOpen(false)} labelledBy="license-dialog-rules-heading">
|
|
191
|
+
{(close) => (
|
|
192
|
+
<FigureLicenseDialogContent
|
|
193
|
+
title={data.title.title}
|
|
194
|
+
license={license}
|
|
195
|
+
onClose={close}
|
|
196
|
+
authors={contributors}
|
|
197
|
+
origin={data.copyright.origin}
|
|
198
|
+
locale={i18n.language}
|
|
199
|
+
type="image">
|
|
200
|
+
<ImageLicenseButtons
|
|
201
|
+
articlePath={articlePath}
|
|
202
|
+
title={data.title.title}
|
|
203
|
+
imageUrl={data.imageUrl}
|
|
204
|
+
copyright={data.copyright}
|
|
205
|
+
/>
|
|
206
|
+
</FigureLicenseDialogContent>
|
|
207
|
+
)}
|
|
208
|
+
</ModalV2>
|
|
209
|
+
</FigureCaption>
|
|
210
|
+
</Figure>
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
interface ImageWrapperProps {
|
|
215
|
+
src: string;
|
|
216
|
+
children: React.ReactNode;
|
|
217
|
+
crop?: {
|
|
218
|
+
startX: number;
|
|
219
|
+
startY: number;
|
|
220
|
+
endX: number;
|
|
221
|
+
endY: number;
|
|
222
|
+
};
|
|
223
|
+
size?: string;
|
|
224
|
+
}
|
|
225
|
+
const hideByline = (size?: string): boolean => {
|
|
226
|
+
return !!size && size.endsWith('-hide-byline');
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
interface ImageLicenseButtonsProps {
|
|
230
|
+
imageUrl: string;
|
|
231
|
+
title?: string;
|
|
232
|
+
articlePath?: string;
|
|
233
|
+
copyright?: Partial<Copyright>;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export const ImageLicenseButtons = ({ imageUrl, title, articlePath, copyright }: ImageLicenseButtonsProps) => {
|
|
237
|
+
const { t, i18n } = useTranslation();
|
|
238
|
+
if (!copyright?.license?.license || copyright?.license?.license === 'COPYRIGHTED') return null;
|
|
239
|
+
|
|
240
|
+
const copyString = figureApa7CopyString(
|
|
241
|
+
title,
|
|
242
|
+
undefined,
|
|
243
|
+
imageUrl,
|
|
244
|
+
articlePath,
|
|
245
|
+
copyright,
|
|
246
|
+
copyright?.license?.license,
|
|
247
|
+
'',
|
|
248
|
+
t,
|
|
249
|
+
i18n.language,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<>
|
|
254
|
+
<CopyButton
|
|
255
|
+
variant="outline"
|
|
256
|
+
onClick={() => navigator.clipboard.writeText(copyString)}
|
|
257
|
+
copyNode={t('license.hasCopiedTitle')}
|
|
258
|
+
aria-live="assertive">
|
|
259
|
+
{t('license.copyTitle')}
|
|
260
|
+
</CopyButton>
|
|
261
|
+
<SafeLinkButton to={`${imageUrl}?download=true`} download variant="outline">
|
|
262
|
+
{t('image.download')}
|
|
263
|
+
</SafeLinkButton>
|
|
264
|
+
</>
|
|
265
|
+
);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const ImageWrapper = ({ src, crop, size, children }: ImageWrapperProps) => {
|
|
269
|
+
const { t } = useTranslation();
|
|
270
|
+
if (isSmall(size) || hideByline(size)) {
|
|
271
|
+
return <>{children}</>;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<ImageLink src={src} crop={crop} aria-label={t('license.images.itemImage.ariaLabel')}>
|
|
276
|
+
{children}
|
|
277
|
+
</ImageLink>
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
interface ExpandButtonProps {
|
|
282
|
+
size?: string;
|
|
283
|
+
expanded: boolean;
|
|
284
|
+
bylineHidden: boolean;
|
|
285
|
+
onExpand: MouseEventHandler<HTMLButtonElement>;
|
|
286
|
+
onHideByline: MouseEventHandler<HTMLButtonElement>;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const ExpandButton = ({ size, expanded, bylineHidden, onExpand, onHideByline }: ExpandButtonProps) => {
|
|
290
|
+
const { t } = useTranslation();
|
|
291
|
+
if (isSmall(size)) {
|
|
292
|
+
return (
|
|
293
|
+
<button
|
|
294
|
+
type="button"
|
|
295
|
+
className="c-figure__fullscreen-btn"
|
|
296
|
+
aria-label={t(`license.images.itemImage.zoom${expanded ? 'Out' : ''}ImageButtonLabel`)}
|
|
297
|
+
onClick={onExpand}>
|
|
298
|
+
{expanded ? <ArrowCollapse /> : <ExpandTwoArrows />}
|
|
299
|
+
</button>
|
|
300
|
+
);
|
|
301
|
+
} else if (hideByline(size)) {
|
|
302
|
+
return (
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
className="c-figure__show-byline-btn"
|
|
306
|
+
aria-label={t(`license.images.itemImage.${bylineHidden ? 'expandByline' : 'minimizeByline'}`)}
|
|
307
|
+
onClick={onHideByline}>
|
|
308
|
+
{bylineHidden ? <ChevronDown /> : <ChevronUp />}
|
|
309
|
+
</button>
|
|
310
|
+
);
|
|
311
|
+
} else return null;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export default ImageEmbed;
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { RelatedContentMetaData } from '@ndla/types-embed';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
import { contentTypeMapping } from '../model/ContentType';
|
|
12
|
+
import { RelatedArticleV2 } from '../RelatedArticleList/RelatedArticleV2';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
embed: RelatedContentMetaData;
|
|
16
|
+
isOembed?: boolean;
|
|
17
|
+
subject?: string;
|
|
18
|
+
ndlaFrontendDomain?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const RelatedContentEmbed = ({ embed, isOembed, subject, ndlaFrontendDomain }: Props) => {
|
|
22
|
+
const { t } = useTranslation();
|
|
23
|
+
if (embed.status === 'error') {
|
|
24
|
+
return <></>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { data, embedData } = embed;
|
|
28
|
+
|
|
29
|
+
if (embedData.articleId && data) {
|
|
30
|
+
const typeId = data.resource?.resourceTypes.find((rt) => contentTypeMapping[rt.id])?.id;
|
|
31
|
+
const type = typeId ? contentTypeMapping[typeId] : undefined;
|
|
32
|
+
const path =
|
|
33
|
+
data.resource?.paths.find((p) => p.split('/')[1] === subject?.replace('urn:', '')) ?? data.resource?.path;
|
|
34
|
+
return (
|
|
35
|
+
<RelatedArticleV2
|
|
36
|
+
title={data.article.title.title ?? ''}
|
|
37
|
+
introduction={data.article.metaDescription?.metaDescription ?? ''}
|
|
38
|
+
target={isOembed ? '_blank' : undefined}
|
|
39
|
+
to={`${ndlaFrontendDomain ?? ''}${path ?? ''}`}
|
|
40
|
+
type={type}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
} else if (typeof embedData.url === 'string') {
|
|
44
|
+
return (
|
|
45
|
+
<RelatedArticleV2
|
|
46
|
+
title={embedData.title ?? ''}
|
|
47
|
+
introduction=""
|
|
48
|
+
to={embedData.url}
|
|
49
|
+
target="_blank"
|
|
50
|
+
type={'external-learning-resources'}
|
|
51
|
+
linkInfo={`${t('related.linkInfo')} ${
|
|
52
|
+
// Get domain name only from url
|
|
53
|
+
embedData.url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n]+)/im)?.[1] || embedData.url
|
|
54
|
+
}`}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
} else {
|
|
58
|
+
return <></>;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default RelatedContentEmbed;
|
|
@@ -0,0 +1,27 @@
|
|
|
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 { MetaData } from '@ndla/types-embed';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
embed: MetaData<any, any>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const StyledSpan = styled.span`
|
|
19
|
+
color: ${colors.support.red};
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const UnknownEmbed = ({ embed }: Props) => {
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
return <StyledSpan>{t('embed.unsupported', { type: embed.resource })}</StyledSpan>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default UnknownEmbed;
|