@aarhus-university/au-lib-react-components 12.6.2 → 12.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.
- package/.eslintrc.js +35 -35
- package/.storybook/main.js +34 -34
- package/.storybook/preview.js +17 -17
- package/README.md +19 -19
- package/__tests__/jest/AUButtonComponent.test.tsx +165 -165
- package/__tests__/jest/AUDynamicContentComponent.test.tsx +386 -386
- package/__tests__/jest/AUErrorComponent.test.tsx +142 -142
- package/__tests__/jest/AUModalComponent.test.tsx +186 -186
- package/__tests__/jest/AUNotificationComponent.test.tsx +115 -115
- package/__tests__/jest/AUSpinnerComponent.test.tsx +57 -57
- package/__tests__/jest/AUToolbarComponent.test.tsx +46 -46
- package/__tests__/jest/context.test.ts +25 -25
- package/__tests__/jest/helpers.test.ts +15 -15
- package/__tests__/jest/setupTests.ts +2 -2
- package/babel.config.js +8 -8
- package/build/umd/all.css +2 -2
- package/build/umd/all.js +1 -1
- package/build/umd/alphabox.js +1 -1
- package/build/umd/databox.js +1 -1
- package/build/umd/diagramme.js +1 -1
- package/build/umd/flowbox.js +1 -1
- package/build/umd/flowbox.js.map +1 -1
- package/build/umd/universe.js +1 -1
- package/build-storybook.log +386 -386
- package/esbuild.mjs +22 -22
- package/package.json +107 -107
- package/src/components/AUAlertComponent.tsx +128 -128
- package/src/components/AUAutoSuggestComponent.js +148 -148
- package/src/components/AUButtonComponent.tsx +99 -99
- package/src/components/AUCalendarComponent.tsx +497 -497
- package/src/components/AUCharacterCountComponent.tsx +56 -56
- package/src/components/AUComboBoxComponent.tsx +195 -195
- package/src/components/AUContentToggleComponent.tsx +50 -50
- package/src/components/AUDatepickerComponent.tsx +124 -124
- package/src/components/AUDialogModalComponent.tsx +124 -124
- package/src/components/AUDynamicContentComponent.tsx +137 -137
- package/src/components/AUEditorComponent.tsx +126 -126
- package/src/components/AUErrorComponent.tsx +73 -73
- package/src/components/AUMobilePrefixComponent.tsx +20 -20
- package/src/components/AUModalComponent.tsx +72 -72
- package/src/components/AUNotificationComponent.tsx +44 -44
- package/src/components/AUReceiptComponent.tsx +34 -34
- package/src/components/AUSpinnerComponent.tsx +40 -40
- package/src/components/AUStepComponent.tsx +75 -75
- package/src/components/AUSubNavComponent.tsx +57 -57
- package/src/components/AUSubmitButtonContainerComponent.tsx +38 -38
- package/src/components/AUTabbedContentComponent.tsx +154 -154
- package/src/components/AUTableComponent.tsx +29 -29
- package/src/components/AUToastComponent.tsx +104 -104
- package/src/components/AUToolbarComponent.tsx +108 -108
- package/src/components/AUTruncatorComponent.tsx +141 -141
- package/src/components/wrapping/AUEmbedComponent.js +47 -47
- package/src/layout-2016/components/alphabox/AlphaBoxComponent.js +142 -142
- package/src/layout-2016/components/alphabox/AlphaBoxContentComponent.js +136 -136
- package/src/layout-2016/components/common/AUCollapsibleComponent.js +152 -152
- package/src/layout-2016/components/common/AUSpinnerComponent.js +103 -103
- package/src/layout-2016/components/databox/DataBoxAlphabetComponent.js +144 -144
- package/src/layout-2016/components/databox/DataBoxAssociationComponent.js +122 -122
- package/src/layout-2016/components/databox/DataBoxButtonComponent.js +157 -157
- package/src/layout-2016/components/databox/DataBoxComponent.js +297 -297
- package/src/layout-2016/components/databox/DataBoxGroupingComponent.js +64 -64
- package/src/layout-2016/components/databox/DataBoxSearchResultComponent.js +36 -36
- package/src/layout-2016/components/databox/DataBoxStackedAssociationComponent.js +54 -54
- package/src/layout-2016/components/databox/DataBoxSuggestionComponent.js +39 -39
- package/src/layout-2016/components/diagramme/AUDiagrammeComponent.js +309 -309
- package/src/layout-2016/components/flowbox/FlowBoxComponent.js +126 -126
- package/src/layout-2016/components/flowbox/FlowBoxPhoneComponent.js +104 -104
- package/src/layout-2016/lib/all.js +3 -3
- package/src/layout-2016/lib/au-alphabox.js +99 -99
- package/src/layout-2016/lib/au-databox.js +399 -399
- package/src/layout-2016/lib/au-diagramme.js +85 -85
- package/src/layout-2016/lib/au-flowbox.js +116 -119
- package/src/lib/context.tsx +59 -59
- package/src/lib/dates.ts +52 -52
- package/src/lib/helpers.ts +208 -208
- package/src/lib/hooks.ts +157 -157
- package/src/lib/i18n.ts +600 -600
- package/src/lib/portals.tsx +150 -150
- package/src/lib/tinymce.ts +84 -84
- package/src/lib/wrapping.ts +21 -21
- package/src/styles/_settings.scss +10 -10
- package/src/styles/alphabox.scss +222 -222
- package/src/styles/app.scss +7 -7
- package/src/styles/autosuggest.scss +57 -57
- package/src/styles/databox.scss +563 -563
- package/src/styles/diagramme.scss +119 -119
- package/src/styles/flowbox.scss +72 -72
- package/src/styles/maps.scss +395 -395
- package/stories/AUAlertComponent.stories.tsx +133 -133
- package/stories/AUAutoSuggestComponent.stories.tsx +95 -95
- package/stories/AUButtonComponent.stories.tsx +139 -139
- package/stories/AUCharacterCountComponent.stories.tsx +121 -121
- package/stories/AUComboBoxComponent.stories.tsx +101 -101
- package/stories/AUContentToggleComponent.stories.tsx +87 -87
- package/stories/AUDialogModalComponent.stories.tsx +75 -75
- package/stories/AUDynamicContentComponent.stories.tsx +119 -119
- package/stories/AUEditorComponent.stories.tsx +66 -66
- package/stories/AUErrorComponent.stories.tsx +132 -132
- package/stories/AUModalComponent.stories.tsx +160 -160
- package/stories/AUNotificationComponent.stories.tsx +151 -151
- package/stories/AUSpinnerComponent.stories.tsx +44 -44
- package/stories/AUStepComponent.stories.tsx +91 -91
- package/stories/AUToolbarComponent.stories.tsx +389 -389
- package/stories/AUTruncatorComponent.stories.tsx +123 -123
- package/stories/lib/helpers.tsx +146 -146
- package/tsconfig.json +46 -46
- package/webpack.config.js +88 -88
- package/.claude/settings.local.json +0 -12
- package/.vscode/settings.json +0 -22
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
import React, { FC } from 'react';
|
|
2
|
-
|
|
3
|
-
export interface IDynamicContentLink {
|
|
4
|
-
title: string;
|
|
5
|
-
description: string;
|
|
6
|
-
url: string;
|
|
7
|
-
linkType: 'system' | 'information';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface IDynamicContentVideo {
|
|
11
|
-
type: 'video';
|
|
12
|
-
url: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface IDynamicContentImage {
|
|
16
|
-
type: 'image';
|
|
17
|
-
url: string;
|
|
18
|
-
altText: string;
|
|
19
|
-
caption?: string;
|
|
20
|
-
linkUrl?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface IDynamicContentInfoBox {
|
|
24
|
-
type: 'infoBox';
|
|
25
|
-
htmlContent: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type IDynamicContentFeaturedContent =
|
|
29
|
-
| IDynamicContentVideo
|
|
30
|
-
| IDynamicContentImage
|
|
31
|
-
| IDynamicContentInfoBox;
|
|
32
|
-
|
|
33
|
-
export interface IDynamicContentProps {
|
|
34
|
-
name: string;
|
|
35
|
-
featuredContentDescription: string;
|
|
36
|
-
links: IDynamicContentLink[];
|
|
37
|
-
featuredContent: IDynamicContentFeaturedContent;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const FeaturedVideo: FC<{
|
|
41
|
-
content: IDynamicContentVideo;
|
|
42
|
-
title: string;
|
|
43
|
-
}> = ({ content, title }) => (
|
|
44
|
-
<iframe
|
|
45
|
-
src={content.url}
|
|
46
|
-
title={`Video player: ${title}`}
|
|
47
|
-
loading="lazy"
|
|
48
|
-
allow="clipboard-write; encrypted-media; picture-in-picture; web-share"
|
|
49
|
-
referrerPolicy="strict-origin-when-cross-origin"
|
|
50
|
-
allowFullScreen
|
|
51
|
-
/>
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const FeaturedImage: FC<{ content: IDynamicContentImage }> = ({ content }) => {
|
|
55
|
-
const imageElement = (
|
|
56
|
-
<figure>
|
|
57
|
-
<img
|
|
58
|
-
src={content.url}
|
|
59
|
-
alt={content.altText}
|
|
60
|
-
loading="lazy"
|
|
61
|
-
/>
|
|
62
|
-
{content.caption && (
|
|
63
|
-
<figcaption className="text--small">{content.caption}</figcaption>
|
|
64
|
-
)}
|
|
65
|
-
</figure>
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
return content.linkUrl ? <a href={content.linkUrl}>{imageElement}</a> : imageElement;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// HTML content is expected to be sanitized by the backend
|
|
72
|
-
const FeaturedInfoBox: FC<{ content: IDynamicContentInfoBox }> = ({ content }) => (
|
|
73
|
-
<div
|
|
74
|
-
className="dynamic-content__infobox"
|
|
75
|
-
dangerouslySetInnerHTML={{ __html: content.htmlContent }}
|
|
76
|
-
/>
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const AUDynamicContentComponent: FC<IDynamicContentProps> = ({
|
|
80
|
-
name,
|
|
81
|
-
featuredContentDescription,
|
|
82
|
-
links,
|
|
83
|
-
featuredContent,
|
|
84
|
-
}) => {
|
|
85
|
-
// Determine which featured component to render
|
|
86
|
-
let featuredElement: React.ReactNode;
|
|
87
|
-
if (featuredContent) {
|
|
88
|
-
switch (featuredContent.type) {
|
|
89
|
-
case 'video':
|
|
90
|
-
featuredElement = <FeaturedVideo content={featuredContent} title={name} />;
|
|
91
|
-
break;
|
|
92
|
-
case 'image':
|
|
93
|
-
featuredElement = <FeaturedImage content={featuredContent} />;
|
|
94
|
-
break;
|
|
95
|
-
case 'infoBox':
|
|
96
|
-
featuredElement = <FeaturedInfoBox content={featuredContent} />;
|
|
97
|
-
break;
|
|
98
|
-
default:
|
|
99
|
-
featuredElement = null;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<section className="dynamic-content" aria-labelledby="dynamic-content-heading">
|
|
105
|
-
{name && <h3 id="dynamic-content-heading grid__item">{name}</h3>}
|
|
106
|
-
<div className="dynamic-content__featured">
|
|
107
|
-
{featuredElement}
|
|
108
|
-
{/* HTML content is expected to be sanitized by the backend */}
|
|
109
|
-
{featuredContentDescription && (
|
|
110
|
-
<div className="content-description" dangerouslySetInnerHTML={{ __html: featuredContentDescription }} />
|
|
111
|
-
)}
|
|
112
|
-
</div>
|
|
113
|
-
|
|
114
|
-
<div className="dynamic-content__links">
|
|
115
|
-
{links?.length > 0 && links.map((link, index) => {
|
|
116
|
-
const isSystemLink = link.linkType === 'system';
|
|
117
|
-
|
|
118
|
-
// Using index in key is safe here since links array is not mutated after initial render
|
|
119
|
-
return (
|
|
120
|
-
<a
|
|
121
|
-
href={link.url}
|
|
122
|
-
// eslint-disable-next-line react/no-array-index-key
|
|
123
|
-
key={link.url + index}
|
|
124
|
-
className={`grid__item shortcut${isSystemLink ? ' shortcut--highlighted' : ''}`}
|
|
125
|
-
>
|
|
126
|
-
<h3>{link.title}</h3>
|
|
127
|
-
<p>{link.description}</p>
|
|
128
|
-
</a>
|
|
129
|
-
);
|
|
130
|
-
})}
|
|
131
|
-
</div>
|
|
132
|
-
</section>
|
|
133
|
-
);
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
AUDynamicContentComponent.displayName = 'AUDynamicContentComponent';
|
|
137
|
-
export default AUDynamicContentComponent;
|
|
1
|
+
import React, { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface IDynamicContentLink {
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
url: string;
|
|
7
|
+
linkType: 'system' | 'information';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface IDynamicContentVideo {
|
|
11
|
+
type: 'video';
|
|
12
|
+
url: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface IDynamicContentImage {
|
|
16
|
+
type: 'image';
|
|
17
|
+
url: string;
|
|
18
|
+
altText: string;
|
|
19
|
+
caption?: string;
|
|
20
|
+
linkUrl?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IDynamicContentInfoBox {
|
|
24
|
+
type: 'infoBox';
|
|
25
|
+
htmlContent: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type IDynamicContentFeaturedContent =
|
|
29
|
+
| IDynamicContentVideo
|
|
30
|
+
| IDynamicContentImage
|
|
31
|
+
| IDynamicContentInfoBox;
|
|
32
|
+
|
|
33
|
+
export interface IDynamicContentProps {
|
|
34
|
+
name: string;
|
|
35
|
+
featuredContentDescription: string;
|
|
36
|
+
links: IDynamicContentLink[];
|
|
37
|
+
featuredContent: IDynamicContentFeaturedContent;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const FeaturedVideo: FC<{
|
|
41
|
+
content: IDynamicContentVideo;
|
|
42
|
+
title: string;
|
|
43
|
+
}> = ({ content, title }) => (
|
|
44
|
+
<iframe
|
|
45
|
+
src={content.url}
|
|
46
|
+
title={`Video player: ${title}`}
|
|
47
|
+
loading="lazy"
|
|
48
|
+
allow="clipboard-write; encrypted-media; picture-in-picture; web-share"
|
|
49
|
+
referrerPolicy="strict-origin-when-cross-origin"
|
|
50
|
+
allowFullScreen
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const FeaturedImage: FC<{ content: IDynamicContentImage }> = ({ content }) => {
|
|
55
|
+
const imageElement = (
|
|
56
|
+
<figure>
|
|
57
|
+
<img
|
|
58
|
+
src={content.url}
|
|
59
|
+
alt={content.altText}
|
|
60
|
+
loading="lazy"
|
|
61
|
+
/>
|
|
62
|
+
{content.caption && (
|
|
63
|
+
<figcaption className="text--small">{content.caption}</figcaption>
|
|
64
|
+
)}
|
|
65
|
+
</figure>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return content.linkUrl ? <a href={content.linkUrl}>{imageElement}</a> : imageElement;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// HTML content is expected to be sanitized by the backend
|
|
72
|
+
const FeaturedInfoBox: FC<{ content: IDynamicContentInfoBox }> = ({ content }) => (
|
|
73
|
+
<div
|
|
74
|
+
className="dynamic-content__infobox"
|
|
75
|
+
dangerouslySetInnerHTML={{ __html: content.htmlContent }}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const AUDynamicContentComponent: FC<IDynamicContentProps> = ({
|
|
80
|
+
name,
|
|
81
|
+
featuredContentDescription,
|
|
82
|
+
links,
|
|
83
|
+
featuredContent,
|
|
84
|
+
}) => {
|
|
85
|
+
// Determine which featured component to render
|
|
86
|
+
let featuredElement: React.ReactNode;
|
|
87
|
+
if (featuredContent) {
|
|
88
|
+
switch (featuredContent.type) {
|
|
89
|
+
case 'video':
|
|
90
|
+
featuredElement = <FeaturedVideo content={featuredContent} title={name} />;
|
|
91
|
+
break;
|
|
92
|
+
case 'image':
|
|
93
|
+
featuredElement = <FeaturedImage content={featuredContent} />;
|
|
94
|
+
break;
|
|
95
|
+
case 'infoBox':
|
|
96
|
+
featuredElement = <FeaturedInfoBox content={featuredContent} />;
|
|
97
|
+
break;
|
|
98
|
+
default:
|
|
99
|
+
featuredElement = null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<section className="dynamic-content" aria-labelledby="dynamic-content-heading">
|
|
105
|
+
{name && <h3 id="dynamic-content-heading grid__item">{name}</h3>}
|
|
106
|
+
<div className="dynamic-content__featured">
|
|
107
|
+
{featuredElement}
|
|
108
|
+
{/* HTML content is expected to be sanitized by the backend */}
|
|
109
|
+
{featuredContentDescription && (
|
|
110
|
+
<div className="content-description" dangerouslySetInnerHTML={{ __html: featuredContentDescription }} />
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="dynamic-content__links">
|
|
115
|
+
{links?.length > 0 && links.map((link, index) => {
|
|
116
|
+
const isSystemLink = link.linkType === 'system';
|
|
117
|
+
|
|
118
|
+
// Using index in key is safe here since links array is not mutated after initial render
|
|
119
|
+
return (
|
|
120
|
+
<a
|
|
121
|
+
href={link.url}
|
|
122
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
123
|
+
key={link.url + index}
|
|
124
|
+
className={`grid__item shortcut${isSystemLink ? ' shortcut--highlighted' : ''}`}
|
|
125
|
+
>
|
|
126
|
+
<h3>{link.title}</h3>
|
|
127
|
+
<p>{link.description}</p>
|
|
128
|
+
</a>
|
|
129
|
+
);
|
|
130
|
+
})}
|
|
131
|
+
</div>
|
|
132
|
+
</section>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
AUDynamicContentComponent.displayName = 'AUDynamicContentComponent';
|
|
137
|
+
export default AUDynamicContentComponent;
|
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
/* eslint-disable camelcase */
|
|
2
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
-
import React, { FC, useState, useRef, useMemo } from 'react';
|
|
4
|
-
import { Editor } from '@tinymce/tinymce-react';
|
|
5
|
-
import AUTinyMCEBuilder from '../lib/tinymce';
|
|
6
|
-
import { replace } from '../lib/helpers';
|
|
7
|
-
|
|
8
|
-
type Translation = IGenericTranslation['generics']['forms'];
|
|
9
|
-
type CounterType = 'recommended' | 'allowed';
|
|
10
|
-
type Props = {
|
|
11
|
-
lang: string;
|
|
12
|
-
id: string;
|
|
13
|
-
initialValue: string;
|
|
14
|
-
textareaName: string;
|
|
15
|
-
showCounter?: boolean;
|
|
16
|
-
counterType?: CounterType;
|
|
17
|
-
maxChars?: number;
|
|
18
|
-
translations: Translation;
|
|
19
|
-
onEditorChange: (value: string) => void;
|
|
20
|
-
disabled?: boolean;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const onKeyUp = (
|
|
24
|
-
editor: any,
|
|
25
|
-
parent: HTMLDivElement,
|
|
26
|
-
maxChars: number,
|
|
27
|
-
counterType: string,
|
|
28
|
-
setOverflow: React.Dispatch<React.SetStateAction<number>>,
|
|
29
|
-
): void => {
|
|
30
|
-
const container = editor.getContainer();
|
|
31
|
-
const numChars = editor.plugins.wordcount.body.getCharacterCount();
|
|
32
|
-
let isActive = false;
|
|
33
|
-
if (numChars >= maxChars) {
|
|
34
|
-
isActive = true;
|
|
35
|
-
parent.setAttribute(`data-${counterType}-length`, maxChars.toString());
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (isActive) {
|
|
39
|
-
container.classList.add('info-active');
|
|
40
|
-
} else {
|
|
41
|
-
container.classList.remove('info-active');
|
|
42
|
-
parent.removeAttribute(`data-${counterType}-length`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const overflow = numChars - maxChars;
|
|
46
|
-
if (overflow > 0) {
|
|
47
|
-
parent.classList.add('form__field__character-count--overflow');
|
|
48
|
-
} else {
|
|
49
|
-
parent.classList.remove('form__field__character-count--overflow');
|
|
50
|
-
}
|
|
51
|
-
setOverflow(overflow);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const AUEditorComponent: FC<Props> = ({
|
|
55
|
-
lang,
|
|
56
|
-
id,
|
|
57
|
-
initialValue,
|
|
58
|
-
textareaName,
|
|
59
|
-
showCounter = false,
|
|
60
|
-
counterType = 'recommended',
|
|
61
|
-
maxChars = 0,
|
|
62
|
-
translations,
|
|
63
|
-
onEditorChange,
|
|
64
|
-
disabled = false,
|
|
65
|
-
}) => {
|
|
66
|
-
const [overflow, setOverflow] = useState(0);
|
|
67
|
-
const parentRef = useRef<HTMLDivElement>(null);
|
|
68
|
-
// Capture initialValue on first render only.
|
|
69
|
-
// Passing a changing initialValue to TinyMCE's <Editor>
|
|
70
|
-
// causes it to call setContent() on every update,
|
|
71
|
-
// which resets the cursor to position 0.
|
|
72
|
-
const stableInitialValue = useRef(initialValue);
|
|
73
|
-
const tiny = useMemo(() => new AUTinyMCEBuilder(
|
|
74
|
-
lang,
|
|
75
|
-
showCounter as boolean,
|
|
76
|
-
(editor: any) => onKeyUp(
|
|
77
|
-
editor,
|
|
78
|
-
parentRef.current as HTMLDivElement,
|
|
79
|
-
maxChars as number,
|
|
80
|
-
counterType as string,
|
|
81
|
-
setOverflow,
|
|
82
|
-
),
|
|
83
|
-
), [lang, showCounter, maxChars, counterType]);
|
|
84
|
-
const tinyConfig = useMemo(() => tiny.tinyGeneralConfig(), [tiny]);
|
|
85
|
-
|
|
86
|
-
const { character_count } = translations;
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<div
|
|
90
|
-
ref={parentRef}
|
|
91
|
-
className="form__field__character-count"
|
|
92
|
-
>
|
|
93
|
-
<Editor
|
|
94
|
-
tinymceScriptSrc={tiny.tinyScriptSrc}
|
|
95
|
-
initialValue={stableInitialValue.current}
|
|
96
|
-
id={id}
|
|
97
|
-
// value={tinyDanish}
|
|
98
|
-
disabled={disabled}
|
|
99
|
-
textareaName={textareaName}
|
|
100
|
-
init={tinyConfig}
|
|
101
|
-
onEditorChange={onEditorChange}
|
|
102
|
-
/>
|
|
103
|
-
{
|
|
104
|
-
(typeof showCounter !== 'undefined'
|
|
105
|
-
&& typeof counterType !== 'undefined'
|
|
106
|
-
&& typeof maxChars !== 'undefined') && (
|
|
107
|
-
<div
|
|
108
|
-
className="form__field__character-count__info"
|
|
109
|
-
aria-live="polite"
|
|
110
|
-
>
|
|
111
|
-
<span className="form__field__character-count__info__overflow-message">
|
|
112
|
-
{`${replace(character_count.overflow.message[counterType], ['maxchars'], [maxChars.toString()])}`}
|
|
113
|
-
</span>
|
|
114
|
-
{' '}
|
|
115
|
-
<span className="form__field__character-count__info__overflow-count">
|
|
116
|
-
{`${replace(character_count.overflow.count[counterType][overflow === 1 ? 'one' : 'other'], ['overflow'], [overflow.toString()])}`}
|
|
117
|
-
</span>
|
|
118
|
-
</div>
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
</div>
|
|
122
|
-
);
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
AUEditorComponent.displayName = 'AUEditorComponent';
|
|
126
|
-
export default AUEditorComponent;
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import React, { FC, useState, useRef, useMemo } from 'react';
|
|
4
|
+
import { Editor } from '@tinymce/tinymce-react';
|
|
5
|
+
import AUTinyMCEBuilder from '../lib/tinymce';
|
|
6
|
+
import { replace } from '../lib/helpers';
|
|
7
|
+
|
|
8
|
+
type Translation = IGenericTranslation['generics']['forms'];
|
|
9
|
+
type CounterType = 'recommended' | 'allowed';
|
|
10
|
+
type Props = {
|
|
11
|
+
lang: string;
|
|
12
|
+
id: string;
|
|
13
|
+
initialValue: string;
|
|
14
|
+
textareaName: string;
|
|
15
|
+
showCounter?: boolean;
|
|
16
|
+
counterType?: CounterType;
|
|
17
|
+
maxChars?: number;
|
|
18
|
+
translations: Translation;
|
|
19
|
+
onEditorChange: (value: string) => void;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const onKeyUp = (
|
|
24
|
+
editor: any,
|
|
25
|
+
parent: HTMLDivElement,
|
|
26
|
+
maxChars: number,
|
|
27
|
+
counterType: string,
|
|
28
|
+
setOverflow: React.Dispatch<React.SetStateAction<number>>,
|
|
29
|
+
): void => {
|
|
30
|
+
const container = editor.getContainer();
|
|
31
|
+
const numChars = editor.plugins.wordcount.body.getCharacterCount();
|
|
32
|
+
let isActive = false;
|
|
33
|
+
if (numChars >= maxChars) {
|
|
34
|
+
isActive = true;
|
|
35
|
+
parent.setAttribute(`data-${counterType}-length`, maxChars.toString());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isActive) {
|
|
39
|
+
container.classList.add('info-active');
|
|
40
|
+
} else {
|
|
41
|
+
container.classList.remove('info-active');
|
|
42
|
+
parent.removeAttribute(`data-${counterType}-length`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const overflow = numChars - maxChars;
|
|
46
|
+
if (overflow > 0) {
|
|
47
|
+
parent.classList.add('form__field__character-count--overflow');
|
|
48
|
+
} else {
|
|
49
|
+
parent.classList.remove('form__field__character-count--overflow');
|
|
50
|
+
}
|
|
51
|
+
setOverflow(overflow);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const AUEditorComponent: FC<Props> = ({
|
|
55
|
+
lang,
|
|
56
|
+
id,
|
|
57
|
+
initialValue,
|
|
58
|
+
textareaName,
|
|
59
|
+
showCounter = false,
|
|
60
|
+
counterType = 'recommended',
|
|
61
|
+
maxChars = 0,
|
|
62
|
+
translations,
|
|
63
|
+
onEditorChange,
|
|
64
|
+
disabled = false,
|
|
65
|
+
}) => {
|
|
66
|
+
const [overflow, setOverflow] = useState(0);
|
|
67
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
68
|
+
// Capture initialValue on first render only.
|
|
69
|
+
// Passing a changing initialValue to TinyMCE's <Editor>
|
|
70
|
+
// causes it to call setContent() on every update,
|
|
71
|
+
// which resets the cursor to position 0.
|
|
72
|
+
const stableInitialValue = useRef(initialValue);
|
|
73
|
+
const tiny = useMemo(() => new AUTinyMCEBuilder(
|
|
74
|
+
lang,
|
|
75
|
+
showCounter as boolean,
|
|
76
|
+
(editor: any) => onKeyUp(
|
|
77
|
+
editor,
|
|
78
|
+
parentRef.current as HTMLDivElement,
|
|
79
|
+
maxChars as number,
|
|
80
|
+
counterType as string,
|
|
81
|
+
setOverflow,
|
|
82
|
+
),
|
|
83
|
+
), [lang, showCounter, maxChars, counterType]);
|
|
84
|
+
const tinyConfig = useMemo(() => tiny.tinyGeneralConfig(), [tiny]);
|
|
85
|
+
|
|
86
|
+
const { character_count } = translations;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
ref={parentRef}
|
|
91
|
+
className="form__field__character-count"
|
|
92
|
+
>
|
|
93
|
+
<Editor
|
|
94
|
+
tinymceScriptSrc={tiny.tinyScriptSrc}
|
|
95
|
+
initialValue={stableInitialValue.current}
|
|
96
|
+
id={id}
|
|
97
|
+
// value={tinyDanish}
|
|
98
|
+
disabled={disabled}
|
|
99
|
+
textareaName={textareaName}
|
|
100
|
+
init={tinyConfig}
|
|
101
|
+
onEditorChange={onEditorChange}
|
|
102
|
+
/>
|
|
103
|
+
{
|
|
104
|
+
(typeof showCounter !== 'undefined'
|
|
105
|
+
&& typeof counterType !== 'undefined'
|
|
106
|
+
&& typeof maxChars !== 'undefined') && (
|
|
107
|
+
<div
|
|
108
|
+
className="form__field__character-count__info"
|
|
109
|
+
aria-live="polite"
|
|
110
|
+
>
|
|
111
|
+
<span className="form__field__character-count__info__overflow-message">
|
|
112
|
+
{`${replace(character_count.overflow.message[counterType], ['maxchars'], [maxChars.toString()])}`}
|
|
113
|
+
</span>
|
|
114
|
+
{' '}
|
|
115
|
+
<span className="form__field__character-count__info__overflow-count">
|
|
116
|
+
{`${replace(character_count.overflow.count[counterType][overflow === 1 ? 'one' : 'other'], ['overflow'], [overflow.toString()])}`}
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
AUEditorComponent.displayName = 'AUEditorComponent';
|
|
126
|
+
export default AUEditorComponent;
|
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
import { SerializedError } from '@reduxjs/toolkit';
|
|
2
|
-
import { FetchBaseQueryError } from '@reduxjs/toolkit/dist/query';
|
|
3
|
-
import React, { FC } from 'react';
|
|
4
|
-
import AUNotificationComponent from './AUNotificationComponent';
|
|
5
|
-
|
|
6
|
-
type Props = {
|
|
7
|
-
error?: AU.IError;
|
|
8
|
-
withStatus?: boolean;
|
|
9
|
-
fetchError?: FetchBaseQueryError,
|
|
10
|
-
serializedError?: SerializedError,
|
|
11
|
-
type?: 'warning' | 'attention' | 'confirm' | 'information';
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const AUErrorComponent: FC<Props> = ({
|
|
15
|
-
error: auError = undefined,
|
|
16
|
-
fetchError = undefined,
|
|
17
|
-
serializedError = undefined,
|
|
18
|
-
withStatus = false,
|
|
19
|
-
type = 'warning',
|
|
20
|
-
}) => {
|
|
21
|
-
if (auError) {
|
|
22
|
-
const { status, header, message } = auError;
|
|
23
|
-
return (
|
|
24
|
-
<AUNotificationComponent
|
|
25
|
-
type={type}
|
|
26
|
-
header={`${withStatus ? `${status}: ` : ''}${header}`}
|
|
27
|
-
content={[
|
|
28
|
-
<p dangerouslySetInnerHTML={{ __html: message }} />,
|
|
29
|
-
]}
|
|
30
|
-
/>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
if (fetchError && 'error' in fetchError) {
|
|
34
|
-
const { status, error } = fetchError;
|
|
35
|
-
return (
|
|
36
|
-
<AUNotificationComponent
|
|
37
|
-
type={type}
|
|
38
|
-
header={withStatus ? status : undefined}
|
|
39
|
-
content={[
|
|
40
|
-
<p dangerouslySetInnerHTML={{ __html: error }} />,
|
|
41
|
-
]}
|
|
42
|
-
/>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
if (fetchError && 'data' in fetchError) {
|
|
46
|
-
const { status, data } = fetchError;
|
|
47
|
-
return (
|
|
48
|
-
<AUNotificationComponent
|
|
49
|
-
type={type}
|
|
50
|
-
header={withStatus ? `${status}` : undefined}
|
|
51
|
-
content={[
|
|
52
|
-
<p dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }} />,
|
|
53
|
-
]}
|
|
54
|
-
/>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
if (serializedError && 'message' in serializedError) {
|
|
58
|
-
const { code, message } = serializedError;
|
|
59
|
-
return (
|
|
60
|
-
<AUNotificationComponent
|
|
61
|
-
type={type}
|
|
62
|
-
header={withStatus ? code : undefined}
|
|
63
|
-
content={[
|
|
64
|
-
<p dangerouslySetInnerHTML={{ __html: message || '' }} />,
|
|
65
|
-
]}
|
|
66
|
-
/>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
return null;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
AUErrorComponent.displayName = 'AUErrorComponent';
|
|
73
|
-
export default AUErrorComponent;
|
|
1
|
+
import { SerializedError } from '@reduxjs/toolkit';
|
|
2
|
+
import { FetchBaseQueryError } from '@reduxjs/toolkit/dist/query';
|
|
3
|
+
import React, { FC } from 'react';
|
|
4
|
+
import AUNotificationComponent from './AUNotificationComponent';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
error?: AU.IError;
|
|
8
|
+
withStatus?: boolean;
|
|
9
|
+
fetchError?: FetchBaseQueryError,
|
|
10
|
+
serializedError?: SerializedError,
|
|
11
|
+
type?: 'warning' | 'attention' | 'confirm' | 'information';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const AUErrorComponent: FC<Props> = ({
|
|
15
|
+
error: auError = undefined,
|
|
16
|
+
fetchError = undefined,
|
|
17
|
+
serializedError = undefined,
|
|
18
|
+
withStatus = false,
|
|
19
|
+
type = 'warning',
|
|
20
|
+
}) => {
|
|
21
|
+
if (auError) {
|
|
22
|
+
const { status, header, message } = auError;
|
|
23
|
+
return (
|
|
24
|
+
<AUNotificationComponent
|
|
25
|
+
type={type}
|
|
26
|
+
header={`${withStatus ? `${status}: ` : ''}${header}`}
|
|
27
|
+
content={[
|
|
28
|
+
<p dangerouslySetInnerHTML={{ __html: message }} />,
|
|
29
|
+
]}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (fetchError && 'error' in fetchError) {
|
|
34
|
+
const { status, error } = fetchError;
|
|
35
|
+
return (
|
|
36
|
+
<AUNotificationComponent
|
|
37
|
+
type={type}
|
|
38
|
+
header={withStatus ? status : undefined}
|
|
39
|
+
content={[
|
|
40
|
+
<p dangerouslySetInnerHTML={{ __html: error }} />,
|
|
41
|
+
]}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (fetchError && 'data' in fetchError) {
|
|
46
|
+
const { status, data } = fetchError;
|
|
47
|
+
return (
|
|
48
|
+
<AUNotificationComponent
|
|
49
|
+
type={type}
|
|
50
|
+
header={withStatus ? `${status}` : undefined}
|
|
51
|
+
content={[
|
|
52
|
+
<p dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }} />,
|
|
53
|
+
]}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (serializedError && 'message' in serializedError) {
|
|
58
|
+
const { code, message } = serializedError;
|
|
59
|
+
return (
|
|
60
|
+
<AUNotificationComponent
|
|
61
|
+
type={type}
|
|
62
|
+
header={withStatus ? code : undefined}
|
|
63
|
+
content={[
|
|
64
|
+
<p dangerouslySetInnerHTML={{ __html: message || '' }} />,
|
|
65
|
+
]}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
AUErrorComponent.displayName = 'AUErrorComponent';
|
|
73
|
+
export default AUErrorComponent;
|