@docusaurus/plugin-ideal-image 3.7.0-canary-6263 → 3.7.0-canary-6264
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/lib/theme/IdealImage/index.js +1 -2
- package/lib/theme/IdealImageLegacy/LICENSE +20 -0
- package/lib/theme/IdealImageLegacy/README.md +13 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Download.d.ts +3 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Download.js +15 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Loading.d.ts +3 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Loading.js +15 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Offline.d.ts +3 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Offline.js +15 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Warning.d.ts +3 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Warning.js +15 -0
- package/lib/theme/IdealImageLegacy/components/Icon/index.d.ts +8 -0
- package/lib/theme/IdealImageLegacy/components/Icon/index.js +25 -0
- package/lib/theme/IdealImageLegacy/components/IdealImage/index.d.ts +42 -0
- package/lib/theme/IdealImageLegacy/components/IdealImage/index.js +353 -0
- package/lib/theme/IdealImageLegacy/components/IdealImageWithDefaults/index.d.ts +33 -0
- package/lib/theme/IdealImageLegacy/components/IdealImageWithDefaults/index.js +12 -0
- package/lib/theme/IdealImageLegacy/components/Media/index.d.ts +20 -0
- package/lib/theme/IdealImageLegacy/components/Media/index.js +169 -0
- package/lib/theme/IdealImageLegacy/components/MediaWithDefaults/README.md +89 -0
- package/lib/theme/IdealImageLegacy/components/MediaWithDefaults/index.d.ts +33 -0
- package/lib/theme/IdealImageLegacy/components/MediaWithDefaults/index.js +12 -0
- package/lib/theme/IdealImageLegacy/components/composeStyle.d.ts +5 -0
- package/lib/theme/IdealImageLegacy/components/composeStyle.js +37 -0
- package/lib/theme/IdealImageLegacy/components/constants.d.ts +22 -0
- package/lib/theme/IdealImageLegacy/components/constants.js +24 -0
- package/lib/theme/IdealImageLegacy/components/helpers.d.ts +22 -0
- package/lib/theme/IdealImageLegacy/components/helpers.js +142 -0
- package/lib/theme/IdealImageLegacy/components/icons.d.ts +15 -0
- package/lib/theme/IdealImageLegacy/components/icons.js +16 -0
- package/lib/theme/IdealImageLegacy/components/loaders.d.ts +4 -0
- package/lib/theme/IdealImageLegacy/components/loaders.js +112 -0
- package/lib/theme/IdealImageLegacy/components/theme.d.ts +30 -0
- package/lib/theme/IdealImageLegacy/components/theme.js +26 -0
- package/lib/theme/IdealImageLegacy/components/theme.module.css +35 -0
- package/lib/theme/IdealImageLegacy/components/unfetch.d.ts +7 -0
- package/lib/theme/IdealImageLegacy/components/unfetch.js +74 -0
- package/lib/theme/IdealImageLegacy/index.d.ts +2 -0
- package/lib/theme/IdealImageLegacy/index.js +2 -0
- package/package.json +8 -9
- package/src/plugin-ideal-image.d.ts +121 -0
- package/src/theme/IdealImage/index.tsx +2 -3
- package/src/theme/IdealImageLegacy/LICENSE +20 -0
- package/src/theme/IdealImageLegacy/README.md +13 -0
- package/src/theme/IdealImageLegacy/components/Icon/Download.js +15 -0
- package/src/theme/IdealImageLegacy/components/Icon/Loading.js +15 -0
- package/src/theme/IdealImageLegacy/components/Icon/Offline.js +15 -0
- package/src/theme/IdealImageLegacy/components/Icon/Warning.js +15 -0
- package/src/theme/IdealImageLegacy/components/Icon/index.js +25 -0
- package/src/theme/IdealImageLegacy/components/IdealImage/index.js +353 -0
- package/src/theme/IdealImageLegacy/components/IdealImageWithDefaults/index.js +12 -0
- package/src/theme/IdealImageLegacy/components/Media/index.js +169 -0
- package/src/theme/IdealImageLegacy/components/MediaWithDefaults/README.md +89 -0
- package/src/theme/IdealImageLegacy/components/MediaWithDefaults/index.js +12 -0
- package/src/theme/IdealImageLegacy/components/composeStyle.js +37 -0
- package/src/theme/IdealImageLegacy/components/constants.js +24 -0
- package/src/theme/IdealImageLegacy/components/helpers.js +142 -0
- package/src/theme/IdealImageLegacy/components/icons.js +16 -0
- package/src/theme/IdealImageLegacy/components/loaders.js +112 -0
- package/src/theme/IdealImageLegacy/components/theme.js +26 -0
- package/src/theme/IdealImageLegacy/components/theme.module.css +35 -0
- package/src/theme/IdealImageLegacy/components/unfetch.js +74 -0
- package/src/theme/IdealImageLegacy/index.tsx +3 -0
- package/src/deps.d.ts +0 -124
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import React, {Component} from 'react';
|
|
2
|
+
// import PropTypes from 'prop-types'
|
|
3
|
+
import {Waypoint} from 'react-waypoint';
|
|
4
|
+
import Media from '../Media';
|
|
5
|
+
import {icons, loadStates} from '../constants';
|
|
6
|
+
import {xhrLoader, imageLoader, timeout, combineCancel} from '../loaders';
|
|
7
|
+
import {
|
|
8
|
+
guessMaxImageWidth,
|
|
9
|
+
bytesToSize,
|
|
10
|
+
supportsWebp,
|
|
11
|
+
ssr,
|
|
12
|
+
nativeConnection,
|
|
13
|
+
selectSrc,
|
|
14
|
+
fallbackParams,
|
|
15
|
+
} from '../helpers';
|
|
16
|
+
|
|
17
|
+
const {initial, loading, loaded, error} = loadStates;
|
|
18
|
+
|
|
19
|
+
const defaultShouldAutoDownload = ({
|
|
20
|
+
connection,
|
|
21
|
+
size,
|
|
22
|
+
threshold,
|
|
23
|
+
possiblySlowNetwork,
|
|
24
|
+
}) => {
|
|
25
|
+
if (possiblySlowNetwork) return false;
|
|
26
|
+
if (!connection) return true;
|
|
27
|
+
const {downlink, rtt, effectiveType} = connection;
|
|
28
|
+
switch (effectiveType) {
|
|
29
|
+
case 'slow-2g':
|
|
30
|
+
case '2g':
|
|
31
|
+
return false;
|
|
32
|
+
case '3g':
|
|
33
|
+
if (downlink && size && threshold) {
|
|
34
|
+
return (size * 8) / (downlink * 1000) + rtt < threshold;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
case '4g':
|
|
38
|
+
default:
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const defaultGetMessage = (icon, state) => {
|
|
44
|
+
switch (icon) {
|
|
45
|
+
case icons.noicon:
|
|
46
|
+
case icons.loaded:
|
|
47
|
+
return null;
|
|
48
|
+
case icons.loading:
|
|
49
|
+
return 'Loading...';
|
|
50
|
+
case icons.load:
|
|
51
|
+
// we can show `alt` here
|
|
52
|
+
const {pickedSrc} = state;
|
|
53
|
+
const {size} = pickedSrc;
|
|
54
|
+
if (size) {
|
|
55
|
+
return [
|
|
56
|
+
'Click to load (',
|
|
57
|
+
<nobr key="nb">{bytesToSize(size)}</nobr>,
|
|
58
|
+
')',
|
|
59
|
+
];
|
|
60
|
+
} else {
|
|
61
|
+
return 'Click to load';
|
|
62
|
+
}
|
|
63
|
+
case icons.offline:
|
|
64
|
+
return 'Your browser is offline. Image not loaded';
|
|
65
|
+
case icons.error:
|
|
66
|
+
const {loadInfo} = state;
|
|
67
|
+
if (loadInfo === 404) {
|
|
68
|
+
return '404. Image not found';
|
|
69
|
+
} else {
|
|
70
|
+
return 'Error. Click to reload';
|
|
71
|
+
}
|
|
72
|
+
default:
|
|
73
|
+
throw new Error(`Wrong icon: ${icon}`);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const defaultGetIcon = (state) => {
|
|
78
|
+
const {loadState, onLine, overThreshold, userTriggered} = state;
|
|
79
|
+
if (ssr) return icons.noicon;
|
|
80
|
+
switch (loadState) {
|
|
81
|
+
case loaded:
|
|
82
|
+
return icons.loaded;
|
|
83
|
+
case loading:
|
|
84
|
+
return overThreshold ? icons.loading : icons.noicon;
|
|
85
|
+
case initial:
|
|
86
|
+
if (onLine) {
|
|
87
|
+
const {shouldAutoDownload} = state;
|
|
88
|
+
if (shouldAutoDownload === undefined) return icons.noicon;
|
|
89
|
+
return userTriggered || !shouldAutoDownload ? icons.load : icons.noicon;
|
|
90
|
+
} else {
|
|
91
|
+
return icons.offline;
|
|
92
|
+
}
|
|
93
|
+
case error:
|
|
94
|
+
return onLine ? icons.error : icons.offline;
|
|
95
|
+
default:
|
|
96
|
+
throw new Error(`Wrong state: ${loadState}`);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default class IdealImage extends Component {
|
|
101
|
+
constructor(props) {
|
|
102
|
+
super(props);
|
|
103
|
+
// TODO: validate props.srcSet
|
|
104
|
+
this.state = {
|
|
105
|
+
loadState: initial,
|
|
106
|
+
connection: nativeConnection
|
|
107
|
+
? {
|
|
108
|
+
downlink: navigator.connection.downlink, // megabits per second
|
|
109
|
+
rtt: navigator.connection.rtt, // ms
|
|
110
|
+
effectiveType: navigator.connection.effectiveType, // 'slow-2g', '2g', '3g', or '4g'
|
|
111
|
+
}
|
|
112
|
+
: null,
|
|
113
|
+
onLine: true,
|
|
114
|
+
overThreshold: false,
|
|
115
|
+
inViewport: false,
|
|
116
|
+
userTriggered: false,
|
|
117
|
+
possiblySlowNetwork: false,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/*
|
|
122
|
+
static propTypes = {
|
|
123
|
+
/!** how much to wait in ms until concider download to slow *!/
|
|
124
|
+
threshold: PropTypes.number,
|
|
125
|
+
/!** function to generate src based on width and format *!/
|
|
126
|
+
getUrl: PropTypes.func,
|
|
127
|
+
/!** array of sources *!/
|
|
128
|
+
srcSet: PropTypes.arrayOf(
|
|
129
|
+
PropTypes.shape({
|
|
130
|
+
width: PropTypes.number.isRequired,
|
|
131
|
+
src: PropTypes.string,
|
|
132
|
+
size: PropTypes.number,
|
|
133
|
+
format: PropTypes.oneOf(['jpeg', 'jpg', 'webp', 'png', 'gif']),
|
|
134
|
+
}),
|
|
135
|
+
).isRequired,
|
|
136
|
+
/!** function which decides if image should be downloaded *!/
|
|
137
|
+
shouldAutoDownload: PropTypes.func,
|
|
138
|
+
/!** function which decides what message to show *!/
|
|
139
|
+
getMessage: PropTypes.func,
|
|
140
|
+
/!** function which decides what icon to show *!/
|
|
141
|
+
getIcon: PropTypes.func,
|
|
142
|
+
/!** type of loader *!/
|
|
143
|
+
loader: PropTypes.oneOf(['image', 'xhr']),
|
|
144
|
+
/!** Width of the image in px *!/
|
|
145
|
+
width: PropTypes.number.isRequired,
|
|
146
|
+
/!** Height of the image in px *!/
|
|
147
|
+
height: PropTypes.number.isRequired,
|
|
148
|
+
placeholder: PropTypes.oneOfType([
|
|
149
|
+
PropTypes.shape({
|
|
150
|
+
/!** Solid color placeholder *!/
|
|
151
|
+
color: PropTypes.string.isRequired,
|
|
152
|
+
}),
|
|
153
|
+
PropTypes.shape({
|
|
154
|
+
/!**
|
|
155
|
+
* [Low Quality Image Placeholder](https://github.com/zouhir/lqip)
|
|
156
|
+
* [SVG-Based Image Placeholder](https://github.com/technopagan/sqip)
|
|
157
|
+
* base64 encoded image of low quality
|
|
158
|
+
*!/
|
|
159
|
+
lqip: PropTypes.string.isRequired,
|
|
160
|
+
}),
|
|
161
|
+
]).isRequired,
|
|
162
|
+
/!** Map of icons *!/
|
|
163
|
+
icons: PropTypes.object.isRequired,
|
|
164
|
+
/!** theme object - CSS Modules or React styles *!/
|
|
165
|
+
theme: PropTypes.object.isRequired,
|
|
166
|
+
}*/
|
|
167
|
+
|
|
168
|
+
static defaultProps = {
|
|
169
|
+
shouldAutoDownload: defaultShouldAutoDownload,
|
|
170
|
+
getMessage: defaultGetMessage,
|
|
171
|
+
getIcon: defaultGetIcon,
|
|
172
|
+
loader: 'xhr',
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
componentDidMount() {
|
|
176
|
+
if (nativeConnection) {
|
|
177
|
+
this.updateConnection = () => {
|
|
178
|
+
if (!navigator.onLine) return;
|
|
179
|
+
if (this.state.loadState === initial) {
|
|
180
|
+
this.setState({
|
|
181
|
+
connection: {
|
|
182
|
+
effectiveType: navigator.connection.effectiveType,
|
|
183
|
+
downlink: navigator.connection.downlink,
|
|
184
|
+
rtt: navigator.connection.rtt,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
navigator.connection.addEventListener('onchange', this.updateConnection);
|
|
190
|
+
} else if (this.props.threshold) {
|
|
191
|
+
this.possiblySlowNetworkListener = (e) => {
|
|
192
|
+
if (this.state.loadState !== initial) return;
|
|
193
|
+
const {possiblySlowNetwork} = e.detail;
|
|
194
|
+
if (!this.state.possiblySlowNetwork && possiblySlowNetwork) {
|
|
195
|
+
this.setState({possiblySlowNetwork});
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
window.document.addEventListener(
|
|
199
|
+
'possiblySlowNetwork',
|
|
200
|
+
this.possiblySlowNetworkListener,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
this.updateOnlineStatus = () => this.setState({onLine: navigator.onLine});
|
|
204
|
+
this.updateOnlineStatus();
|
|
205
|
+
window.addEventListener('online', this.updateOnlineStatus);
|
|
206
|
+
window.addEventListener('offline', this.updateOnlineStatus);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
componentWillUnmount() {
|
|
210
|
+
this.clear();
|
|
211
|
+
if (nativeConnection) {
|
|
212
|
+
navigator.connection.removeEventListener(
|
|
213
|
+
'onchange',
|
|
214
|
+
this.updateConnection,
|
|
215
|
+
);
|
|
216
|
+
} else if (this.props.threshold) {
|
|
217
|
+
window.document.removeEventListener(
|
|
218
|
+
'possiblySlowNetwork',
|
|
219
|
+
this.possiblySlowNetworkListener,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
window.removeEventListener('online', this.updateOnlineStatus);
|
|
223
|
+
window.removeEventListener('offline', this.updateOnlineStatus);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
onClick = () => {
|
|
227
|
+
const {loadState, onLine, overThreshold} = this.state;
|
|
228
|
+
if (!onLine) return;
|
|
229
|
+
switch (loadState) {
|
|
230
|
+
case loading:
|
|
231
|
+
if (overThreshold) this.cancel(true);
|
|
232
|
+
return;
|
|
233
|
+
case loaded:
|
|
234
|
+
// nothing
|
|
235
|
+
return;
|
|
236
|
+
case initial:
|
|
237
|
+
case error:
|
|
238
|
+
this.load(true);
|
|
239
|
+
return;
|
|
240
|
+
default:
|
|
241
|
+
throw new Error(`Wrong state: ${loadState}`);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
clear() {
|
|
246
|
+
if (this.loader) {
|
|
247
|
+
this.loader.cancel();
|
|
248
|
+
this.loader = undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
cancel(userTriggered) {
|
|
253
|
+
if (loading !== this.state.loadState) return;
|
|
254
|
+
this.clear();
|
|
255
|
+
this.loadStateChange(initial, userTriggered);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
loadStateChange(loadState, userTriggered, loadInfo = null) {
|
|
259
|
+
this.setState({
|
|
260
|
+
loadState,
|
|
261
|
+
overThreshold: false,
|
|
262
|
+
userTriggered: !!userTriggered,
|
|
263
|
+
loadInfo,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
load = (userTriggered) => {
|
|
268
|
+
const {loadState, url} = this.state;
|
|
269
|
+
if (ssr || loaded === loadState || loading === loadState) return;
|
|
270
|
+
this.loadStateChange(loading, userTriggered);
|
|
271
|
+
|
|
272
|
+
const {threshold} = this.props;
|
|
273
|
+
const loader =
|
|
274
|
+
this.props.loader === 'xhr' ? xhrLoader(url) : imageLoader(url);
|
|
275
|
+
loader
|
|
276
|
+
.then(() => {
|
|
277
|
+
this.clear();
|
|
278
|
+
this.loadStateChange(loaded, false);
|
|
279
|
+
})
|
|
280
|
+
.catch((e) => {
|
|
281
|
+
this.clear();
|
|
282
|
+
if (e.status === 404) {
|
|
283
|
+
this.loadStateChange(error, false, 404);
|
|
284
|
+
} else {
|
|
285
|
+
this.loadStateChange(error, false);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (threshold) {
|
|
290
|
+
const timeoutLoader = timeout(threshold);
|
|
291
|
+
timeoutLoader.then(() => {
|
|
292
|
+
if (!this.loader) return;
|
|
293
|
+
window.document.dispatchEvent(
|
|
294
|
+
new CustomEvent('possiblySlowNetwork', {
|
|
295
|
+
detail: {possiblySlowNetwork: true},
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
this.setState({overThreshold: true});
|
|
299
|
+
if (!this.state.userTriggered) this.cancel(true);
|
|
300
|
+
});
|
|
301
|
+
this.loader = combineCancel(loader, timeoutLoader);
|
|
302
|
+
} else {
|
|
303
|
+
this.loader = loader;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
onEnter = () => {
|
|
308
|
+
if (this.state.inViewport) return;
|
|
309
|
+
this.setState({inViewport: true});
|
|
310
|
+
const pickedSrc = selectSrc({
|
|
311
|
+
srcSet: this.props.srcSet,
|
|
312
|
+
maxImageWidth:
|
|
313
|
+
this.props.srcSet.length > 1
|
|
314
|
+
? guessMaxImageWidth(this.state.dimensions) // eslint-disable-line react/no-access-state-in-setstate
|
|
315
|
+
: 0,
|
|
316
|
+
supportsWebp,
|
|
317
|
+
});
|
|
318
|
+
const {getUrl} = this.props;
|
|
319
|
+
const url = getUrl ? getUrl(pickedSrc) : pickedSrc.src;
|
|
320
|
+
const shouldAutoDownload = this.props.shouldAutoDownload({
|
|
321
|
+
...this.state, // eslint-disable-line react/no-access-state-in-setstate
|
|
322
|
+
size: pickedSrc.size,
|
|
323
|
+
});
|
|
324
|
+
this.setState({pickedSrc, shouldAutoDownload, url}, () => {
|
|
325
|
+
if (shouldAutoDownload) this.load(false);
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
onLeave = () => {
|
|
330
|
+
if (this.state.loadState === loading && !this.state.userTriggered) {
|
|
331
|
+
this.setState({inViewport: false});
|
|
332
|
+
this.cancel(false);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
render() {
|
|
337
|
+
const icon = this.props.getIcon(this.state);
|
|
338
|
+
const message = this.props.getMessage(icon, this.state);
|
|
339
|
+
return (
|
|
340
|
+
<Waypoint onEnter={this.onEnter} onLeave={this.onLeave}>
|
|
341
|
+
<Media
|
|
342
|
+
{...this.props}
|
|
343
|
+
{...fallbackParams(this.props)}
|
|
344
|
+
onClick={this.onClick}
|
|
345
|
+
icon={icon}
|
|
346
|
+
src={this.state.url || ''}
|
|
347
|
+
onDimensions={(dimensions) => this.setState({dimensions})}
|
|
348
|
+
message={message}
|
|
349
|
+
/>
|
|
350
|
+
</Waypoint>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import IdealImage from '../IdealImage';
|
|
3
|
+
import icons from '../icons';
|
|
4
|
+
import theme from '../theme';
|
|
5
|
+
|
|
6
|
+
const IdealImageWithDefaults = ({
|
|
7
|
+
icons: iconsProp = icons,
|
|
8
|
+
theme: themeProp = theme,
|
|
9
|
+
...props
|
|
10
|
+
}) => <IdealImage {...props} icons={iconsProp} theme={themeProp} />;
|
|
11
|
+
|
|
12
|
+
export default IdealImageWithDefaults;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import React, {PureComponent} from 'react';
|
|
2
|
+
// import PropTypes from 'prop-types'
|
|
3
|
+
import compose from '../composeStyle';
|
|
4
|
+
import {icons as defaultIcons} from '../constants';
|
|
5
|
+
|
|
6
|
+
const {load, loading, loaded, error, noicon, offline} = defaultIcons;
|
|
7
|
+
|
|
8
|
+
export default class Media extends PureComponent {
|
|
9
|
+
/*static propTypes = {
|
|
10
|
+
/!** URL of the image *!/
|
|
11
|
+
src: PropTypes.string.isRequired,
|
|
12
|
+
/!** Width of the image in px *!/
|
|
13
|
+
width: PropTypes.number.isRequired,
|
|
14
|
+
/!** Height of the image in px *!/
|
|
15
|
+
height: PropTypes.number.isRequired,
|
|
16
|
+
placeholder: PropTypes.oneOfType([
|
|
17
|
+
PropTypes.shape({
|
|
18
|
+
/!** Solid color placeholder *!/
|
|
19
|
+
color: PropTypes.string.isRequired,
|
|
20
|
+
}),
|
|
21
|
+
PropTypes.shape({
|
|
22
|
+
/!**
|
|
23
|
+
* [Low Quality Image Placeholder](https://github.com/zouhir/lqip)
|
|
24
|
+
* [SVG-Based Image Placeholder](https://github.com/technopagan/sqip)
|
|
25
|
+
* base64 encoded image of low quality
|
|
26
|
+
*!/
|
|
27
|
+
lqip: PropTypes.string.isRequired,
|
|
28
|
+
}),
|
|
29
|
+
]).isRequired,
|
|
30
|
+
/!** display icon *!/
|
|
31
|
+
icon: PropTypes.oneOf([load, loading, loaded, error, noicon, offline])
|
|
32
|
+
.isRequired,
|
|
33
|
+
/!** Map of icons *!/
|
|
34
|
+
icons: PropTypes.object.isRequired,
|
|
35
|
+
/!** theme object - CSS Modules or React styles *!/
|
|
36
|
+
theme: PropTypes.object.isRequired,
|
|
37
|
+
/!** Alternative text *!/
|
|
38
|
+
alt: PropTypes.string,
|
|
39
|
+
/!** Color of the icon *!/
|
|
40
|
+
iconColor: PropTypes.string,
|
|
41
|
+
/!** Size of the icon in px *!/
|
|
42
|
+
iconSize: PropTypes.number,
|
|
43
|
+
/!** React's style attribute for root element of the component *!/
|
|
44
|
+
style: PropTypes.object,
|
|
45
|
+
/!** React's className attribute for root element of the component *!/
|
|
46
|
+
className: PropTypes.string,
|
|
47
|
+
/!** On click handler *!/
|
|
48
|
+
onClick: PropTypes.func,
|
|
49
|
+
/!** callback to get dimensions of the placeholder *!/
|
|
50
|
+
onDimensions: PropTypes.func,
|
|
51
|
+
/!** message to show below the icon *!/
|
|
52
|
+
message: PropTypes.node,
|
|
53
|
+
/!** reference for Waypoint *!/
|
|
54
|
+
innerRef: PropTypes.func,
|
|
55
|
+
/!** noscript image src *!/
|
|
56
|
+
nsSrc: PropTypes.string,
|
|
57
|
+
/!** noscript image srcSet *!/
|
|
58
|
+
nsSrcSet: PropTypes.string,
|
|
59
|
+
}*/
|
|
60
|
+
|
|
61
|
+
static defaultProps = {
|
|
62
|
+
iconColor: '#fff',
|
|
63
|
+
iconSize: 64,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
constructor(props) {
|
|
67
|
+
super(props);
|
|
68
|
+
this.state = {isMounted: false};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
componentDidMount() {
|
|
72
|
+
this.setState({isMounted: true});
|
|
73
|
+
|
|
74
|
+
if (this.props.onDimensions && this.dimensionElement)
|
|
75
|
+
/* Firefox returns 0 for both clientWidth and clientHeight.
|
|
76
|
+
To fix this we can check the parentNode's clientWidth and clientHeight as a fallback. */
|
|
77
|
+
this.props.onDimensions({
|
|
78
|
+
width:
|
|
79
|
+
this.dimensionElement.clientWidth ||
|
|
80
|
+
this.dimensionElement.parentNode.clientWidth,
|
|
81
|
+
height:
|
|
82
|
+
this.dimensionElement.clientHeight ||
|
|
83
|
+
this.dimensionElement.parentNode.clientHeight,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
renderIcon(props) {
|
|
88
|
+
const {icon, icons, iconColor: fill, iconSize: size, theme} = props;
|
|
89
|
+
const iconToRender = icons[icon];
|
|
90
|
+
if (!iconToRender) return null;
|
|
91
|
+
const styleOrClass = compose(
|
|
92
|
+
{width: size + 100, height: size, color: fill},
|
|
93
|
+
theme.icon,
|
|
94
|
+
);
|
|
95
|
+
return React.createElement('div', styleOrClass, [
|
|
96
|
+
React.createElement(iconToRender, {fill, size, key: 'icon'}),
|
|
97
|
+
React.createElement('br', {key: 'br'}),
|
|
98
|
+
this.props.message,
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
renderImage(props) {
|
|
103
|
+
return props.icon === loaded ? (
|
|
104
|
+
<img
|
|
105
|
+
{...compose(props.theme.img)}
|
|
106
|
+
src={props.src}
|
|
107
|
+
alt={props.alt}
|
|
108
|
+
width={props.width}
|
|
109
|
+
height={props.height}
|
|
110
|
+
/>
|
|
111
|
+
) : (
|
|
112
|
+
<svg
|
|
113
|
+
{...compose(props.theme.img)}
|
|
114
|
+
width={props.width}
|
|
115
|
+
height={props.height}
|
|
116
|
+
ref={(ref) => (this.dimensionElement = ref)}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
renderNoscript(props) {
|
|
122
|
+
// render noscript in ssr + hydration to avoid hydration mismatch error
|
|
123
|
+
return this.state.isMounted ? null : (
|
|
124
|
+
<noscript>
|
|
125
|
+
<img
|
|
126
|
+
{...compose(props.theme.img, props.theme.noscript)}
|
|
127
|
+
src={props.nsSrc}
|
|
128
|
+
srcSet={props.nsSrcSet}
|
|
129
|
+
alt={props.alt}
|
|
130
|
+
width={props.width}
|
|
131
|
+
height={props.height}
|
|
132
|
+
/>
|
|
133
|
+
</noscript>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
render() {
|
|
138
|
+
const props = this.props;
|
|
139
|
+
const {placeholder, theme} = props;
|
|
140
|
+
let background;
|
|
141
|
+
if (props.icon === loaded) {
|
|
142
|
+
background = {};
|
|
143
|
+
} else if (placeholder.lqip) {
|
|
144
|
+
background = {
|
|
145
|
+
backgroundImage: `url("${placeholder.lqip}")`,
|
|
146
|
+
};
|
|
147
|
+
} else {
|
|
148
|
+
background = {
|
|
149
|
+
backgroundColor: placeholder.color,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return (
|
|
153
|
+
<div
|
|
154
|
+
{...compose(
|
|
155
|
+
theme.placeholder,
|
|
156
|
+
background,
|
|
157
|
+
props.style,
|
|
158
|
+
props.className,
|
|
159
|
+
)}
|
|
160
|
+
onClick={this.props.onClick}
|
|
161
|
+
onKeyPress={this.props.onClick}
|
|
162
|
+
ref={this.props.innerRef}>
|
|
163
|
+
{this.renderImage(props)}
|
|
164
|
+
{this.renderNoscript(props)}
|
|
165
|
+
{this.renderIcon(props)}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
All possible states of the component
|
|
2
|
+
|
|
3
|
+
```js
|
|
4
|
+
const lqip =
|
|
5
|
+
'data:image/jpeg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAA4DASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAUG/8QAIRAAAQQDAAEFAAAAAAAAAAAAAQIDBREABAYhEjEyQVH/xAAUAQEAAAAAAAAAAAAAAAAAAAAE/8QAGBEBAAMBAAAAAAAAAAAAAAAAAQACIRH/2gAMAwEAAhEDEQA/AMJ2DG+7Dw0nz8gsx+uyhlxnWdLakOlfzpIF3aRf1WT5t96P5+N1ug9Tu7ZWS8q1gG6B8H2FDz+YxhjUrEOdZ//Z';
|
|
6
|
+
|
|
7
|
+
const sqip =
|
|
8
|
+
"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4774 3024'%3e%3cfilter id='b'%3e%3cfeGaussianBlur stdDeviation='12' /%3e%3c/filter%3e%3cpath fill='%23515a57' d='M0 0h4774v3021H0z'/%3e%3cg filter='url(%23b)' transform='translate(9.3 9.3) scale(18.64844)' fill-opacity='.5'%3e%3cellipse fill='whitefef' rx='1' ry='1' transform='matrix(74.55002 60.89891 -21.7939 26.67923 151.8 104.4)'/%3e%3cellipse fill='black80c' cx='216' cy='49' rx='59' ry='59'/%3e%3cellipse fill='black60e' cx='22' cy='60' rx='46' ry='89'/%3e%3cellipse fill='%23ffebd5' cx='110' cy='66' rx='42' ry='28'/%3e%3cellipse fill='whiteff9' rx='1' ry='1' transform='rotate(33.3 -113.2 392.6) scale(42.337 17.49703)'/%3e%3cellipse fill='%23031f1e' rx='1' ry='1' transform='matrix(163.4651 -64.93326 6.77862 17.06471 111 16.4)'/%3e%3cpath fill='whitefea' d='M66 74l9 39 16-44z'/%3e%3cellipse fill='%23a28364' rx='1' ry='1' transform='rotate(-32.4 253.2 -179) scale(30.79511 43.65381)'/%3e%3cpath fill='%231a232c' d='M40 139l61-57 33 95z'/%3e%3cpath fill='%230a222b' d='M249.8 153.3l-48.1-48 32.5-32.6 48.1 48z'/%3e%3c/g%3e%3c/svg%3e";
|
|
9
|
+
<table>
|
|
10
|
+
<tbody>
|
|
11
|
+
<tr>
|
|
12
|
+
<th align="left" width="100">
|
|
13
|
+
load
|
|
14
|
+
</th>
|
|
15
|
+
<td>
|
|
16
|
+
<MediaWithDefaults
|
|
17
|
+
width={3500}
|
|
18
|
+
height={2095}
|
|
19
|
+
placeholder={{lqip: lqip}}
|
|
20
|
+
src="andre-spieker-238-unsplash.jpg"
|
|
21
|
+
style={{maxWidth: 200}}
|
|
22
|
+
icon={'load'}
|
|
23
|
+
/>
|
|
24
|
+
</td>
|
|
25
|
+
<th align="left" width="100">
|
|
26
|
+
noicon
|
|
27
|
+
</th>
|
|
28
|
+
<td>
|
|
29
|
+
<MediaWithDefaults
|
|
30
|
+
width={3500}
|
|
31
|
+
height={2095}
|
|
32
|
+
placeholder={{lqip: lqip}}
|
|
33
|
+
src="andre-spieker-238-unsplash.jpg"
|
|
34
|
+
style={{maxWidth: 200}}
|
|
35
|
+
icon={'noicon'}
|
|
36
|
+
/>
|
|
37
|
+
</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<tr>
|
|
40
|
+
<th align="left">loading</th>
|
|
41
|
+
<td>
|
|
42
|
+
<MediaWithDefaults
|
|
43
|
+
width={3500}
|
|
44
|
+
height={2095}
|
|
45
|
+
placeholder={{lqip: lqip}}
|
|
46
|
+
src="andre-spieker-238-unsplash.jpg"
|
|
47
|
+
style={{maxWidth: 200}}
|
|
48
|
+
icon={'loading'}
|
|
49
|
+
/>
|
|
50
|
+
</td>
|
|
51
|
+
<th align="left">offline</th>
|
|
52
|
+
<td>
|
|
53
|
+
<MediaWithDefaults
|
|
54
|
+
width={3500}
|
|
55
|
+
height={2095}
|
|
56
|
+
placeholder={{lqip: lqip}}
|
|
57
|
+
src="andre-spieker-238-unsplash.jpg"
|
|
58
|
+
style={{maxWidth: 200}}
|
|
59
|
+
icon={'offline'}
|
|
60
|
+
/>
|
|
61
|
+
</td>
|
|
62
|
+
</tr>
|
|
63
|
+
<tr>
|
|
64
|
+
<th align="left">loaded</th>
|
|
65
|
+
<td>
|
|
66
|
+
<MediaWithDefaults
|
|
67
|
+
width={3500}
|
|
68
|
+
height={2095}
|
|
69
|
+
placeholder={{lqip: lqip}}
|
|
70
|
+
src="andre-spieker-238-unsplash.jpg"
|
|
71
|
+
style={{maxWidth: 200}}
|
|
72
|
+
icon={'loaded'}
|
|
73
|
+
/>
|
|
74
|
+
</td>
|
|
75
|
+
<th align="left">error</th>
|
|
76
|
+
<td>
|
|
77
|
+
<MediaWithDefaults
|
|
78
|
+
width={3500}
|
|
79
|
+
height={2095}
|
|
80
|
+
placeholder={{lqip: lqip}}
|
|
81
|
+
src="andre-spieker-238-unsplash.jpg"
|
|
82
|
+
style={{maxWidth: 200}}
|
|
83
|
+
icon={'error'}
|
|
84
|
+
/>
|
|
85
|
+
</td>
|
|
86
|
+
</tr>
|
|
87
|
+
</tbody>
|
|
88
|
+
</table>;
|
|
89
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Media from '../Media';
|
|
3
|
+
import icons from '../icons';
|
|
4
|
+
import theme from '../theme';
|
|
5
|
+
|
|
6
|
+
const MediaWithDefaults = ({
|
|
7
|
+
icons: iconsProp = icons,
|
|
8
|
+
theme: themeProp = theme,
|
|
9
|
+
...props
|
|
10
|
+
}) => <Media {...props} icons={iconsProp} theme={themeProp} />;
|
|
11
|
+
|
|
12
|
+
export default MediaWithDefaults;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composes styles and/or classes
|
|
3
|
+
*
|
|
4
|
+
* For classes it will concat them in in one string
|
|
5
|
+
* and return as `className` property.
|
|
6
|
+
* Alternative is https://github.com/JedWatson/classnames
|
|
7
|
+
*
|
|
8
|
+
* For objects it will merge them in one object
|
|
9
|
+
* and return as `style` property.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* Asume you have `theme` object, which can be css-module
|
|
13
|
+
* or object or other css-in-js compatible with css-module
|
|
14
|
+
*
|
|
15
|
+
* <a {...compose(theme.link, theme.active, {color: "#000"})}>link</a>
|
|
16
|
+
*
|
|
17
|
+
* @returns {{className: string, style: object}} - params for React component
|
|
18
|
+
*/
|
|
19
|
+
export default (...stylesOrClasses) => {
|
|
20
|
+
const classes = [];
|
|
21
|
+
let style;
|
|
22
|
+
for (const obj of stylesOrClasses) {
|
|
23
|
+
if (obj instanceof Object) {
|
|
24
|
+
Object.assign(style || (style = {}), obj);
|
|
25
|
+
} else if (obj === undefined || obj === false) {
|
|
26
|
+
// ignore
|
|
27
|
+
} else if (typeof obj === 'string') {
|
|
28
|
+
classes.push(obj);
|
|
29
|
+
} else {
|
|
30
|
+
throw new Error(`Unexpected value ${obj}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
className: classes.length > 1 ? classes.join(' ') : classes[0],
|
|
35
|
+
style,
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const load = 'load';
|
|
2
|
+
const loading = 'loading';
|
|
3
|
+
const loaded = 'loaded';
|
|
4
|
+
const error = 'error';
|
|
5
|
+
const noicon = 'noicon';
|
|
6
|
+
const offline = 'offline';
|
|
7
|
+
|
|
8
|
+
export const icons = {
|
|
9
|
+
load,
|
|
10
|
+
loading,
|
|
11
|
+
loaded,
|
|
12
|
+
error,
|
|
13
|
+
noicon,
|
|
14
|
+
offline,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const initial = 'initial';
|
|
18
|
+
|
|
19
|
+
export const loadStates = {
|
|
20
|
+
initial,
|
|
21
|
+
loading,
|
|
22
|
+
loaded,
|
|
23
|
+
error,
|
|
24
|
+
};
|