@performant-software/semantic-components 0.5.15 → 0.5.16-beta.10

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 (37) hide show
  1. package/build/index.js +1 -1
  2. package/build/index.js.map +1 -1
  3. package/build/main.css +43 -0
  4. package/package.json +3 -3
  5. package/src/components/AudioPlayer.css +3 -0
  6. package/src/components/AudioPlayer.js +54 -0
  7. package/src/components/DownloadButton.js +63 -12
  8. package/src/components/IIIFModal.js +26 -0
  9. package/src/components/LazyAudio.css +23 -0
  10. package/src/components/LazyAudio.js +150 -0
  11. package/src/components/LazyDocument.css +2 -0
  12. package/src/components/LazyDocument.js +29 -20
  13. package/src/components/LazyIIIF.js +44 -0
  14. package/src/components/LazyImage.css +3 -1
  15. package/src/components/LazyImage.js +40 -11
  16. package/src/components/LazyLoader.css +7 -0
  17. package/src/components/LazyLoader.js +28 -0
  18. package/src/components/LazyMedia.js +244 -0
  19. package/src/components/LazyVideo.css +8 -1
  20. package/src/components/LazyVideo.js +40 -5
  21. package/src/components/PhotoViewer.js +38 -25
  22. package/src/components/VideoPlayer.js +23 -2
  23. package/src/i18n/en.json +35 -3
  24. package/src/index.js +5 -0
  25. package/types/components/AudioPlayer.js.flow +54 -0
  26. package/types/components/DownloadButton.js.flow +63 -12
  27. package/types/components/IIIFModal.js.flow +26 -0
  28. package/types/components/LazyAudio.js.flow +150 -0
  29. package/types/components/LazyDocument.js.flow +29 -20
  30. package/types/components/LazyIIIF.js.flow +44 -0
  31. package/types/components/LazyImage.js.flow +40 -11
  32. package/types/components/LazyLoader.js.flow +28 -0
  33. package/types/components/LazyMedia.js.flow +244 -0
  34. package/types/components/LazyVideo.js.flow +40 -5
  35. package/types/components/PhotoViewer.js.flow +38 -25
  36. package/types/components/VideoPlayer.js.flow +23 -2
  37. package/types/index.js.flow +5 -0
@@ -12,23 +12,29 @@ import {
12
12
  Visibility
13
13
  } from 'semantic-ui-react';
14
14
  import i18n from '../i18n/i18n';
15
+ import DownloadButton from './DownloadButton';
16
+ import LazyLoader from './LazyLoader';
15
17
  import PhotoViewer from './PhotoViewer';
16
18
  import './LazyImage.css';
17
19
 
18
20
  type Props = {
19
21
  children?: Node,
20
22
  dimmable: boolean,
23
+ download?: string,
21
24
  duration?: number,
22
25
  image?: any,
26
+ name?: string,
23
27
  preview?: string,
24
28
  size?: string,
25
29
  src?: string
26
30
  };
27
31
 
28
32
  const LazyImage = (props: Props) => {
29
- const [visible, setVisible] = useState(false);
30
- const [modal, setModal] = useState(false);
31
33
  const [dimmer, setDimmer] = useState(false);
34
+ const [error, setError] = useState(false);
35
+ const [loaded, setLoaded] = useState(!(props.src || props.preview));
36
+ const [modal, setModal] = useState(false);
37
+ const [visible, setVisible] = useState(false);
32
38
 
33
39
  if (!visible) {
34
40
  return (
@@ -60,14 +66,28 @@ const LazyImage = (props: Props) => {
60
66
  onMouseEnter={() => setDimmer(true)}
61
67
  onMouseLeave={() => setDimmer(false)}
62
68
  >
63
- { props.src && (
69
+ { !loaded && (
70
+ <LazyLoader
71
+ active
72
+ size={props.size}
73
+ />
74
+ )}
75
+ { !error && (props.preview || props.src) && (
64
76
  <Image
65
77
  {...props.image}
78
+ onError={() => {
79
+ setError(true);
80
+ setLoaded(true);
81
+ }}
82
+ onLoad={() => {
83
+ setError(false);
84
+ setLoaded(true);
85
+ }}
66
86
  size={props.size}
67
87
  src={props.preview || props.src}
68
88
  />
69
89
  )}
70
- { !props.src && (
90
+ { (error || !(props.preview || props.src)) && (
71
91
  <Image
72
92
  {...props.image}
73
93
  className='placeholder-image'
@@ -79,7 +99,7 @@ const LazyImage = (props: Props) => {
79
99
  />
80
100
  </Image>
81
101
  )}
82
- { (props.src || props.children) && props.dimmable && (
102
+ { !error && (props.src || props.children) && props.dimmable && (
83
103
  <Dimmer
84
104
  active={dimmer}
85
105
  >
@@ -94,18 +114,27 @@ const LazyImage = (props: Props) => {
94
114
  primary
95
115
  />
96
116
  )}
117
+ { props.download && (
118
+ <DownloadButton
119
+ color='green'
120
+ filename={props.name}
121
+ url={props.download}
122
+ />
123
+ )}
97
124
  { props.children }
98
125
  </div>
99
126
  </Dimmer>
100
127
  )}
101
128
  </Dimmer.Dimmable>
102
129
  </Transition>
103
- <PhotoViewer
104
- image={props.src || ''}
105
- onClose={() => setModal(false)}
106
- open={modal}
107
- size='large'
108
- />
130
+ { props.src && (
131
+ <PhotoViewer
132
+ image={props.src}
133
+ onClose={() => setModal(false)}
134
+ open={modal}
135
+ size='large'
136
+ />
137
+ )}
109
138
  </>
110
139
  );
111
140
  };
@@ -0,0 +1,7 @@
1
+ .lazy-loader {
2
+ background-color: #f9fafb;
3
+ box-shadow: 0 1px 3px 0 #d4d4d5, 0 0 0 1px #d4d4d5;
4
+ padding-top: 20%;
5
+ padding-bottom: 20%;
6
+ text-align: center;
7
+ }
@@ -0,0 +1,28 @@
1
+ // @flow
2
+
3
+ import React from 'react';
4
+ import { Image, Loader } from 'semantic-ui-react';
5
+ import './LazyLoader.css';
6
+
7
+ type Props = {
8
+ active: boolean,
9
+ size: string
10
+ };
11
+
12
+ const LazyLoader = (props: Props) => (
13
+ <Image
14
+ className='lazy-loader'
15
+ size={props.size}
16
+ >
17
+ <Loader
18
+ active={props.active}
19
+ />
20
+ </Image>
21
+ );
22
+
23
+ LazyLoader.defaultProps = {
24
+ active: false,
25
+ size: 'small'
26
+ };
27
+
28
+ export default LazyLoader;
@@ -0,0 +1,244 @@
1
+ // @flow
2
+
3
+ import React, {
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useState,
8
+ type ComponentType,
9
+ type Node
10
+ } from 'react';
11
+ import { Icon } from 'semantic-ui-react';
12
+ import { Trans } from 'react-i18next';
13
+ import _ from 'underscore';
14
+ import FileInputButton from './FileInputButton';
15
+ import i18n from '../i18n/i18n';
16
+ import LazyAudio from './LazyAudio';
17
+ import LazyDocument from './LazyDocument';
18
+ import LazyImage from './LazyImage';
19
+ import LazyVideo from './LazyVideo';
20
+
21
+ type Props = {
22
+ children?: Node,
23
+ contentType: string,
24
+ dimmable?: boolean,
25
+ downloadUrl?: string,
26
+ name?: string,
27
+ onUpload: (file: File) => void,
28
+ preview?: string,
29
+ size?: string,
30
+ src?: string
31
+ };
32
+
33
+ const ContentTypes = {
34
+ audio: 'audio',
35
+ image: 'image',
36
+ pdf: 'application/pdf',
37
+ video: 'video'
38
+ };
39
+
40
+ const WebContentTypes = [
41
+ 'image/png',
42
+ 'image/jpeg',
43
+ 'image/gif',
44
+ 'image/webp',
45
+ 'image/bmp',
46
+ 'video/m4v',
47
+ 'video/mp4'
48
+ ];
49
+
50
+ const LazyMedia: ComponentType<any> = (props: Props) => {
51
+ const [contentType, setContentType] = useState(props.contentType || '');
52
+ const [name, setName] = useState(props.name);
53
+ const [preview, setPreview] = useState(props.preview);
54
+ const [source, setSource] = useState(props.src);
55
+
56
+ /**
57
+ * Sets the file extension based on the name.
58
+ *
59
+ * @type {*}
60
+ */
61
+ const fileExtension = useMemo(() => {
62
+ let value;
63
+
64
+ if (name) {
65
+ value = name.split('.').pop();
66
+ value = value && value.toUpperCase();
67
+ }
68
+
69
+ return value;
70
+ }, [name]);
71
+
72
+ /**
73
+ * Sets the content type, name, preview, and source attributes. Also calls the onUpload prop.
74
+ *
75
+ * @type {(function(*): void)|*}
76
+ */
77
+ const onUpload = useCallback((files) => {
78
+ const file = _.first(files);
79
+
80
+ setContentType(file.type);
81
+ setName(file.name);
82
+ setPreview(null);
83
+
84
+ if (_.contains(WebContentTypes, file.type)
85
+ || file.type.startsWith(ContentTypes.audio)
86
+ || file.type === ContentTypes.pdf) {
87
+ setSource(URL.createObjectURL(file));
88
+ } else {
89
+ setSource(null);
90
+ }
91
+
92
+ props.onUpload(file);
93
+ }, [props.onUpload]);
94
+
95
+ /**
96
+ * Renders the child buttons.
97
+ *
98
+ * @type {unknown}
99
+ */
100
+ const renderChildren = useCallback(() => (
101
+ <>
102
+ { props.onUpload && (
103
+ <FileInputButton
104
+ color='orange'
105
+ content={i18n.t('Common.buttons.upload')}
106
+ icon='cloud upload'
107
+ onSelection={onUpload}
108
+ />
109
+ )}
110
+ { props.children }
111
+ </>
112
+ ));
113
+
114
+ /**
115
+ * Renders the media component.
116
+ *
117
+ * @type {(function(): *)|*}
118
+ */
119
+ const renderMedia = useCallback(() => {
120
+ if (contentType.startsWith(ContentTypes.image)) {
121
+ return (
122
+ <LazyImage
123
+ dimmable={props.dimmable}
124
+ download={props.downloadUrl}
125
+ preview={preview}
126
+ src={source}
127
+ size={props.size}
128
+ >
129
+ { renderChildren() }
130
+ </LazyImage>
131
+ );
132
+ }
133
+
134
+ if (contentType.startsWith(ContentTypes.video)) {
135
+ return (
136
+ <LazyVideo
137
+ dimmable={props.dimmable}
138
+ download={props.downloadUrl}
139
+ preview={preview}
140
+ src={source}
141
+ size={props.size}
142
+ >
143
+ { renderChildren() }
144
+ </LazyVideo>
145
+ );
146
+ }
147
+
148
+ if (contentType.startsWith(ContentTypes.audio)) {
149
+ return (
150
+ <LazyAudio
151
+ dimmable={props.dimmable}
152
+ download={props.downloadUrl}
153
+ preview={preview}
154
+ src={source}
155
+ size={props.size}
156
+ >
157
+ { renderChildren() }
158
+ </LazyAudio>
159
+ );
160
+ }
161
+
162
+ return (
163
+ <LazyDocument
164
+ dimmable={props.dimmable}
165
+ download={props.downloadUrl}
166
+ pdf={contentType === ContentTypes.pdf}
167
+ preview={preview}
168
+ src={source}
169
+ size={props.size}
170
+ >
171
+ { renderChildren() }
172
+ </LazyDocument>
173
+ );
174
+ }, [contentType, preview, source, props.dimmable, props.downloadUrl, props.size]);
175
+
176
+ /**
177
+ * Renders the upload message.
178
+ *
179
+ * @type {(function(): (null|*))|*}
180
+ */
181
+ const renderMessage = useCallback(() => {
182
+ if (!fileExtension) {
183
+ return null;
184
+ }
185
+
186
+ return (
187
+ <div>
188
+ <Icon
189
+ name='info circle'
190
+ />
191
+ <span>
192
+ <Trans
193
+ components={{ bold: <strong /> }}
194
+ default='Your <bold>{{type}}</bold> has been received'
195
+ i18n={i18n}
196
+ i18nKey='LazyMedia.messages.uploaded'
197
+ values={{
198
+ type: fileExtension
199
+ }}
200
+ />
201
+ </span>
202
+ </div>
203
+ );
204
+ }, [fileExtension]);
205
+
206
+ /**
207
+ * Sets the content type, name, preview, and source.
208
+ */
209
+ useEffect(() => {
210
+ if (props.contentType) {
211
+ setContentType(props.contentType);
212
+ }
213
+
214
+ if (props.name) {
215
+ setName(props.name);
216
+ }
217
+
218
+ if (props.preview) {
219
+ setPreview(props.preview);
220
+ }
221
+
222
+ if (props.src) {
223
+ setSource(props.src);
224
+ } else {
225
+ setSource(null);
226
+ }
227
+ }, [props.contentType, props.name, props.preview, props.src]);
228
+
229
+ return (
230
+ <div
231
+ className='lazy-media'
232
+ >
233
+ { renderMedia() }
234
+ { renderMessage() }
235
+ </div>
236
+ );
237
+ };
238
+
239
+ LazyMedia.defaultProps = {
240
+ dimmable: true,
241
+ size: 'medium'
242
+ };
243
+
244
+ export default LazyMedia;
@@ -1,4 +1,6 @@
1
1
  .lazy-video.ui.segment {
2
+ display: inline-block !important;
3
+ max-width: 100%;
2
4
  padding: 0;
3
5
  }
4
6
 
@@ -18,4 +20,9 @@
18
20
  padding-top: 20%;
19
21
  padding-bottom: 20%;
20
22
  text-align: center;
21
- }
23
+ }
24
+
25
+ .lazy-video.ui.segment > .image > video {
26
+ display: table;
27
+ width: 100%;
28
+ }
@@ -12,6 +12,8 @@ import {
12
12
  Visibility
13
13
  } from 'semantic-ui-react';
14
14
  import i18n from '../i18n/i18n';
15
+ import DownloadButton from './DownloadButton';
16
+ import LazyLoader from './LazyLoader';
15
17
  import VideoPlayer from './VideoPlayer';
16
18
  import './LazyVideo.css';
17
19
 
@@ -19,19 +21,23 @@ type Props = {
19
21
  autoPlay?: boolean,
20
22
  children?: Node,
21
23
  dimmable: boolean,
24
+ download?: string,
22
25
  duration?: number,
23
26
  embedded?: boolean,
24
27
  icon?: string | Element<any>,
25
28
  image?: any,
29
+ name?: string,
26
30
  preview?: ?string,
27
31
  size?: string,
28
32
  src?: string
29
33
  };
30
34
 
31
35
  const LazyVideo = (props: Props) => {
32
- const [visible, setVisible] = useState(false);
33
- const [modal, setModal] = useState(false);
34
36
  const [dimmer, setDimmer] = useState(false);
37
+ const [error, setError] = useState(false);
38
+ const [loaded, setLoaded] = useState(!(props.preview || props.src));
39
+ const [modal, setModal] = useState(false);
40
+ const [visible, setVisible] = useState(false);
35
41
 
36
42
  if (!visible) {
37
43
  return (
@@ -63,24 +69,46 @@ const LazyVideo = (props: Props) => {
63
69
  onMouseEnter={() => setDimmer(true)}
64
70
  onMouseLeave={() => setDimmer(false)}
65
71
  >
66
- { props.preview && (
72
+ { !loaded && (
73
+ <LazyLoader
74
+ active
75
+ size={props.size}
76
+ />
77
+ )}
78
+ { !error && props.preview && (
67
79
  <Image
68
80
  {...props.image}
81
+ onError={() => {
82
+ setError(true);
83
+ setLoaded(true);
84
+ }}
85
+ onLoad={() => {
86
+ setError(false);
87
+ setLoaded(true);
88
+ }}
69
89
  src={props.preview}
70
90
  size={props.size}
71
91
  />
72
92
  )}
73
- { !props.preview && props.src && (
93
+ { !error && !props.preview && props.src && (
74
94
  <Image
75
95
  {...props.image}
76
96
  size={props.size}
77
97
  >
78
98
  <video
99
+ onError={() => {
100
+ setError(true);
101
+ setLoaded(true);
102
+ }}
103
+ onLoadedData={() => {
104
+ setError(false);
105
+ setLoaded(true);
106
+ }}
79
107
  src={props.src}
80
108
  />
81
109
  </Image>
82
110
  )}
83
- { !props.preview && !props.src && (
111
+ { (error || (!props.preview && !props.src)) && (
84
112
  <Image
85
113
  {...props.image}
86
114
  className='placeholder-image'
@@ -107,6 +135,13 @@ const LazyVideo = (props: Props) => {
107
135
  primary
108
136
  />
109
137
  )}
138
+ { props.download && (
139
+ <DownloadButton
140
+ color='green'
141
+ filename={props.name}
142
+ url={props.download}
143
+ />
144
+ )}
110
145
  { props.children }
111
146
  </div>
112
147
  </Dimmer>
@@ -1,9 +1,10 @@
1
1
  // @flow
2
2
 
3
- import React from 'react';
4
- import { Image, Modal } from 'semantic-ui-react';
3
+ import React, { useState } from 'react';
4
+ import { Image, Message, Modal } from 'semantic-ui-react';
5
5
  import ModalContext from '../context/ModalContext';
6
6
  import './PhotoViewer.css';
7
+ import i18n from '../i18n/i18n';
7
8
 
8
9
  type Props = {
9
10
  alt?: string,
@@ -13,29 +14,41 @@ type Props = {
13
14
  size?: string
14
15
  };
15
16
 
16
- const PhotoViewer = (props: Props) => (
17
- <ModalContext.Consumer>
18
- { (mountNode) => (
19
- <Modal
20
- centered={false}
21
- className='photo-viewer'
22
- closeIcon
23
- mountNode={mountNode}
24
- onClose={props.onClose.bind(this)}
25
- open={props.open}
26
- size={props.size}
27
- >
28
- <Modal.Content>
29
- <Image
30
- alt={props.alt}
31
- fluid
32
- src={props.image}
33
- />
34
- </Modal.Content>
35
- </Modal>
36
- )}
37
- </ModalContext.Consumer>
38
- );
17
+ const PhotoViewer = (props: Props) => {
18
+ const [error, setError] = useState(false);
19
+
20
+ return (
21
+ <ModalContext.Consumer>
22
+ {(mountNode) => (
23
+ <Modal
24
+ centered={false}
25
+ className='photo-viewer'
26
+ closeIcon
27
+ mountNode={mountNode}
28
+ onClose={props.onClose.bind(this)}
29
+ open={props.open}
30
+ size={props.size}
31
+ >
32
+ <Modal.Content>
33
+ { error && (
34
+ <Message
35
+ content={i18n.t('PhotoViewer.errors.path.content', { path: props.image })}
36
+ header={i18n.t('PhotoViewer.errors.path.header')}
37
+ icon='exclamation circle'
38
+ />
39
+ )}
40
+ <Image
41
+ alt={props.alt}
42
+ fluid
43
+ onError={() => setError(true)}
44
+ src={props.image}
45
+ />
46
+ </Modal.Content>
47
+ </Modal>
48
+ )}
49
+ </ModalContext.Consumer>
50
+ );
51
+ };
39
52
 
40
53
  PhotoViewer.defaultProps = {
41
54
  size: 'small'
@@ -1,9 +1,20 @@
1
1
  // @flow
2
2
 
3
- import React, { type Element, useEffect, useRef } from 'react';
4
- import { Embed, Modal, Ref } from 'semantic-ui-react';
3
+ import React, {
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ type Element
8
+ } from 'react';
9
+ import {
10
+ Embed,
11
+ Message,
12
+ Modal,
13
+ Ref
14
+ } from 'semantic-ui-react';
5
15
  import ModalContext from '../context/ModalContext';
6
16
  import './VideoPlayer.css';
17
+ import i18n from '../i18n/i18n';
7
18
 
8
19
  type Props = {
9
20
  autoPlay?: boolean,
@@ -18,6 +29,8 @@ type Props = {
18
29
  };
19
30
 
20
31
  const VideoPlayer = (props: Props) => {
32
+ const [error, setError] = useState(false);
33
+
21
34
  const embedRef = useRef();
22
35
 
23
36
  /**
@@ -46,6 +59,13 @@ const VideoPlayer = (props: Props) => {
46
59
  size={props.size}
47
60
  >
48
61
  <Modal.Content>
62
+ { error && (
63
+ <Message
64
+ content={i18n.t('VideoPlayer.errors.path.content', { path: props.video })}
65
+ header={i18n.t('VideoPlayer.errors.path.header')}
66
+ icon='exclamation circle'
67
+ />
68
+ )}
49
69
  { props.embedded && (
50
70
  <Ref
51
71
  innerRef={embedRef}
@@ -63,6 +83,7 @@ const VideoPlayer = (props: Props) => {
63
83
  <video
64
84
  autoPlay={props.autoPlay}
65
85
  controls
86
+ onError={() => setError(true)}
66
87
  src={props.video}
67
88
  />
68
89
  )}