@azerion/bunosaurus-plugin-ideal-image 0.0.0-stage → 4.0.0
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/README.md +6 -2
- package/lib/index.d.ts +10 -0
- package/lib/index.js +75 -0
- package/lib/theme/IdealImage/index.d.ts +9 -0
- package/lib/theme/IdealImage/index.js +96 -0
- package/lib/theme/IdealImageLegacy/LICENSE +20 -0
- package/lib/theme/IdealImageLegacy/README.md +13 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Download.d.ts +2 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Download.js +14 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Loading.d.ts +2 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Loading.js +14 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Offline.d.ts +2 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Offline.js +14 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Warning.d.ts +2 -0
- package/lib/theme/IdealImageLegacy/components/Icon/Warning.js +14 -0
- package/lib/theme/IdealImageLegacy/components/Icon/index.d.ts +7 -0
- package/lib/theme/IdealImageLegacy/components/Icon/index.js +24 -0
- package/lib/theme/IdealImageLegacy/components/IdealImage/index.d.ts +46 -0
- package/lib/theme/IdealImageLegacy/components/IdealImage/index.js +352 -0
- package/lib/theme/IdealImageLegacy/components/IdealImage/waypoint.d.ts +10 -0
- package/lib/theme/IdealImageLegacy/components/IdealImage/waypoint.js +188 -0
- package/lib/theme/IdealImageLegacy/components/IdealImageWithDefaults/index.d.ts +32 -0
- package/lib/theme/IdealImageLegacy/components/IdealImageWithDefaults/index.js +11 -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 +32 -0
- package/lib/theme/IdealImageLegacy/components/MediaWithDefaults/index.js +11 -0
- package/lib/theme/IdealImageLegacy/components/composeStyle.d.ts +23 -0
- package/lib/theme/IdealImageLegacy/components/composeStyle.js +37 -0
- package/lib/theme/IdealImageLegacy/components/constants.d.ts +14 -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 +141 -0
- package/lib/theme/IdealImageLegacy/components/icons.d.ts +14 -0
- package/lib/theme/IdealImageLegacy/components/icons.js +16 -0
- package/lib/theme/IdealImageLegacy/components/loaders.d.ts +11 -0
- package/lib/theme/IdealImageLegacy/components/loaders.js +112 -0
- package/lib/theme/IdealImageLegacy/components/theme.d.ts +26 -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 +8 -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 +46 -4
- package/src/index.ts +94 -0
- package/src/plugin-ideal-image.d.ts +199 -0
- package/src/theme/IdealImage/index.tsx +108 -0
- package/src/theme/IdealImageLegacy/LICENSE +20 -0
- package/src/theme/IdealImageLegacy/README.md +13 -0
- package/src/theme/IdealImageLegacy/components/Icon/Download.js +14 -0
- package/src/theme/IdealImageLegacy/components/Icon/Loading.js +14 -0
- package/src/theme/IdealImageLegacy/components/Icon/Offline.js +14 -0
- package/src/theme/IdealImageLegacy/components/Icon/Warning.js +14 -0
- package/src/theme/IdealImageLegacy/components/Icon/index.js +24 -0
- package/src/theme/IdealImageLegacy/components/IdealImage/index.js +352 -0
- package/src/theme/IdealImageLegacy/components/IdealImage/waypoint.tsx +254 -0
- package/src/theme/IdealImageLegacy/components/IdealImageWithDefaults/index.js +11 -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 +11 -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 +141 -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
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import {Component} from 'react';
|
|
2
|
+
import {Waypoint} from './waypoint';
|
|
3
|
+
import Media from '../Media';
|
|
4
|
+
import {icons, loadStates} from '../constants';
|
|
5
|
+
import {xhrLoader, imageLoader, timeout, combineCancel} from '../loaders';
|
|
6
|
+
import {
|
|
7
|
+
guessMaxImageWidth,
|
|
8
|
+
bytesToSize,
|
|
9
|
+
supportsWebp,
|
|
10
|
+
ssr,
|
|
11
|
+
nativeConnection,
|
|
12
|
+
selectSrc,
|
|
13
|
+
fallbackParams,
|
|
14
|
+
} from '../helpers';
|
|
15
|
+
|
|
16
|
+
const {initial, loading, loaded, error} = loadStates;
|
|
17
|
+
|
|
18
|
+
const defaultShouldAutoDownload = ({
|
|
19
|
+
connection,
|
|
20
|
+
size,
|
|
21
|
+
threshold,
|
|
22
|
+
possiblySlowNetwork,
|
|
23
|
+
}) => {
|
|
24
|
+
if (possiblySlowNetwork) return false;
|
|
25
|
+
if (!connection) return true;
|
|
26
|
+
const {downlink, rtt, effectiveType} = connection;
|
|
27
|
+
switch (effectiveType) {
|
|
28
|
+
case 'slow-2g':
|
|
29
|
+
case '2g':
|
|
30
|
+
return false;
|
|
31
|
+
case '3g':
|
|
32
|
+
if (downlink && size && threshold) {
|
|
33
|
+
return (size * 8) / (downlink * 1000) + rtt < threshold;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
case '4g':
|
|
37
|
+
default:
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const defaultGetMessage = (icon, state) => {
|
|
43
|
+
switch (icon) {
|
|
44
|
+
case icons.noicon:
|
|
45
|
+
case icons.loaded:
|
|
46
|
+
return null;
|
|
47
|
+
case icons.loading:
|
|
48
|
+
return 'Loading...';
|
|
49
|
+
case icons.load:
|
|
50
|
+
// we can show `alt` here
|
|
51
|
+
const {pickedSrc} = state;
|
|
52
|
+
const {size} = pickedSrc;
|
|
53
|
+
if (size) {
|
|
54
|
+
return [
|
|
55
|
+
'Click to load (',
|
|
56
|
+
<nobr key="nb">{bytesToSize(size)}</nobr>,
|
|
57
|
+
')',
|
|
58
|
+
];
|
|
59
|
+
} else {
|
|
60
|
+
return 'Click to load';
|
|
61
|
+
}
|
|
62
|
+
case icons.offline:
|
|
63
|
+
return 'Your browser is offline. Image not loaded';
|
|
64
|
+
case icons.error:
|
|
65
|
+
const {loadInfo} = state;
|
|
66
|
+
if (loadInfo === 404) {
|
|
67
|
+
return '404. Image not found';
|
|
68
|
+
} else {
|
|
69
|
+
return 'Error. Click to reload';
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(`Wrong icon: ${icon}`);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const defaultGetIcon = (state) => {
|
|
77
|
+
const {loadState, onLine, overThreshold, userTriggered} = state;
|
|
78
|
+
if (ssr) return icons.noicon;
|
|
79
|
+
switch (loadState) {
|
|
80
|
+
case loaded:
|
|
81
|
+
return icons.loaded;
|
|
82
|
+
case loading:
|
|
83
|
+
return overThreshold ? icons.loading : icons.noicon;
|
|
84
|
+
case initial:
|
|
85
|
+
if (onLine) {
|
|
86
|
+
const {shouldAutoDownload} = state;
|
|
87
|
+
if (shouldAutoDownload === undefined) return icons.noicon;
|
|
88
|
+
return userTriggered || !shouldAutoDownload ? icons.load : icons.noicon;
|
|
89
|
+
} else {
|
|
90
|
+
return icons.offline;
|
|
91
|
+
}
|
|
92
|
+
case error:
|
|
93
|
+
return onLine ? icons.error : icons.offline;
|
|
94
|
+
default:
|
|
95
|
+
throw new Error(`Wrong state: ${loadState}`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default class IdealImage extends Component {
|
|
100
|
+
constructor(props) {
|
|
101
|
+
super(props);
|
|
102
|
+
// TODO: validate props.srcSet
|
|
103
|
+
this.state = {
|
|
104
|
+
loadState: initial,
|
|
105
|
+
connection: nativeConnection
|
|
106
|
+
? {
|
|
107
|
+
downlink: navigator.connection.downlink, // megabits per second
|
|
108
|
+
rtt: navigator.connection.rtt, // ms
|
|
109
|
+
effectiveType: navigator.connection.effectiveType, // 'slow-2g', '2g', '3g', or '4g'
|
|
110
|
+
}
|
|
111
|
+
: null,
|
|
112
|
+
onLine: true,
|
|
113
|
+
overThreshold: false,
|
|
114
|
+
inViewport: false,
|
|
115
|
+
userTriggered: false,
|
|
116
|
+
possiblySlowNetwork: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/*
|
|
121
|
+
static propTypes = {
|
|
122
|
+
/!** how much to wait in ms until consider download to slow *!/
|
|
123
|
+
threshold: PropTypes.number,
|
|
124
|
+
/!** function to generate src based on width and format *!/
|
|
125
|
+
getUrl: PropTypes.func,
|
|
126
|
+
/!** array of sources *!/
|
|
127
|
+
srcSet: PropTypes.arrayOf(
|
|
128
|
+
PropTypes.shape({
|
|
129
|
+
width: PropTypes.number.isRequired,
|
|
130
|
+
src: PropTypes.string,
|
|
131
|
+
size: PropTypes.number,
|
|
132
|
+
format: PropTypes.oneOf(['jpeg', 'jpg', 'webp', 'png', 'gif']),
|
|
133
|
+
}),
|
|
134
|
+
).isRequired,
|
|
135
|
+
/!** function which decides if image should be downloaded *!/
|
|
136
|
+
shouldAutoDownload: PropTypes.func,
|
|
137
|
+
/!** function which decides what message to show *!/
|
|
138
|
+
getMessage: PropTypes.func,
|
|
139
|
+
/!** function which decides what icon to show *!/
|
|
140
|
+
getIcon: PropTypes.func,
|
|
141
|
+
/!** type of loader *!/
|
|
142
|
+
loader: PropTypes.oneOf(['image', 'xhr']),
|
|
143
|
+
/!** Width of the image in px *!/
|
|
144
|
+
width: PropTypes.number.isRequired,
|
|
145
|
+
/!** Height of the image in px *!/
|
|
146
|
+
height: PropTypes.number.isRequired,
|
|
147
|
+
placeholder: PropTypes.oneOfType([
|
|
148
|
+
PropTypes.shape({
|
|
149
|
+
/!** Solid color placeholder *!/
|
|
150
|
+
color: PropTypes.string.isRequired,
|
|
151
|
+
}),
|
|
152
|
+
PropTypes.shape({
|
|
153
|
+
/!**
|
|
154
|
+
* [Low Quality Image Placeholder](https://github.com/zouhir/lqip)
|
|
155
|
+
* [SVG-Based Image Placeholder](https://github.com/technopagan/sqip)
|
|
156
|
+
* base64 encoded image of low quality
|
|
157
|
+
*!/
|
|
158
|
+
lqip: PropTypes.string.isRequired,
|
|
159
|
+
}),
|
|
160
|
+
]).isRequired,
|
|
161
|
+
/!** Map of icons *!/
|
|
162
|
+
icons: PropTypes.object.isRequired,
|
|
163
|
+
/!** theme object - CSS Modules or React styles *!/
|
|
164
|
+
theme: PropTypes.object.isRequired,
|
|
165
|
+
}*/
|
|
166
|
+
|
|
167
|
+
static defaultProps = {
|
|
168
|
+
shouldAutoDownload: defaultShouldAutoDownload,
|
|
169
|
+
getMessage: defaultGetMessage,
|
|
170
|
+
getIcon: defaultGetIcon,
|
|
171
|
+
loader: 'xhr',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
componentDidMount() {
|
|
175
|
+
if (nativeConnection) {
|
|
176
|
+
this.updateConnection = () => {
|
|
177
|
+
if (!navigator.onLine) return;
|
|
178
|
+
if (this.state.loadState === initial) {
|
|
179
|
+
this.setState({
|
|
180
|
+
connection: {
|
|
181
|
+
effectiveType: navigator.connection.effectiveType,
|
|
182
|
+
downlink: navigator.connection.downlink,
|
|
183
|
+
rtt: navigator.connection.rtt,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
navigator.connection.addEventListener('onchange', this.updateConnection);
|
|
189
|
+
} else if (this.props.threshold) {
|
|
190
|
+
this.possiblySlowNetworkListener = (e) => {
|
|
191
|
+
if (this.state.loadState !== initial) return;
|
|
192
|
+
const {possiblySlowNetwork} = e.detail;
|
|
193
|
+
if (!this.state.possiblySlowNetwork && possiblySlowNetwork) {
|
|
194
|
+
this.setState({possiblySlowNetwork});
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
window.document.addEventListener(
|
|
198
|
+
'possiblySlowNetwork',
|
|
199
|
+
this.possiblySlowNetworkListener,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
this.updateOnlineStatus = () => this.setState({onLine: navigator.onLine});
|
|
203
|
+
this.updateOnlineStatus();
|
|
204
|
+
window.addEventListener('online', this.updateOnlineStatus);
|
|
205
|
+
window.addEventListener('offline', this.updateOnlineStatus);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
componentWillUnmount() {
|
|
209
|
+
this.clear();
|
|
210
|
+
if (nativeConnection) {
|
|
211
|
+
navigator.connection.removeEventListener(
|
|
212
|
+
'onchange',
|
|
213
|
+
this.updateConnection,
|
|
214
|
+
);
|
|
215
|
+
} else if (this.props.threshold) {
|
|
216
|
+
window.document.removeEventListener(
|
|
217
|
+
'possiblySlowNetwork',
|
|
218
|
+
this.possiblySlowNetworkListener,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
window.removeEventListener('online', this.updateOnlineStatus);
|
|
222
|
+
window.removeEventListener('offline', this.updateOnlineStatus);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
onClick = () => {
|
|
226
|
+
const {loadState, onLine, overThreshold} = this.state;
|
|
227
|
+
if (!onLine) return;
|
|
228
|
+
switch (loadState) {
|
|
229
|
+
case loading:
|
|
230
|
+
if (overThreshold) this.cancel(true);
|
|
231
|
+
return;
|
|
232
|
+
case loaded:
|
|
233
|
+
// nothing
|
|
234
|
+
return;
|
|
235
|
+
case initial:
|
|
236
|
+
case error:
|
|
237
|
+
this.load(true);
|
|
238
|
+
return;
|
|
239
|
+
default:
|
|
240
|
+
throw new Error(`Wrong state: ${loadState}`);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
clear() {
|
|
245
|
+
if (this.loader) {
|
|
246
|
+
this.loader.cancel();
|
|
247
|
+
this.loader = undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
cancel(userTriggered) {
|
|
252
|
+
if (loading !== this.state.loadState) return;
|
|
253
|
+
this.clear();
|
|
254
|
+
this.loadStateChange(initial, userTriggered);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
loadStateChange(loadState, userTriggered, loadInfo = null) {
|
|
258
|
+
this.setState({
|
|
259
|
+
loadState,
|
|
260
|
+
overThreshold: false,
|
|
261
|
+
userTriggered: !!userTriggered,
|
|
262
|
+
loadInfo,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
load = (userTriggered) => {
|
|
267
|
+
const {loadState, url} = this.state;
|
|
268
|
+
if (ssr || loaded === loadState || loading === loadState) return;
|
|
269
|
+
this.loadStateChange(loading, userTriggered);
|
|
270
|
+
|
|
271
|
+
const {threshold} = this.props;
|
|
272
|
+
const loader =
|
|
273
|
+
this.props.loader === 'xhr' ? xhrLoader(url) : imageLoader(url);
|
|
274
|
+
loader
|
|
275
|
+
.then(() => {
|
|
276
|
+
this.clear();
|
|
277
|
+
this.loadStateChange(loaded, false);
|
|
278
|
+
})
|
|
279
|
+
.catch((e) => {
|
|
280
|
+
this.clear();
|
|
281
|
+
if (e.status === 404) {
|
|
282
|
+
this.loadStateChange(error, false, 404);
|
|
283
|
+
} else {
|
|
284
|
+
this.loadStateChange(error, false);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (threshold) {
|
|
289
|
+
const timeoutLoader = timeout(threshold);
|
|
290
|
+
timeoutLoader.then(() => {
|
|
291
|
+
if (!this.loader) return;
|
|
292
|
+
window.document.dispatchEvent(
|
|
293
|
+
new CustomEvent('possiblySlowNetwork', {
|
|
294
|
+
detail: {possiblySlowNetwork: true},
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
297
|
+
this.setState({overThreshold: true});
|
|
298
|
+
if (!this.state.userTriggered) this.cancel(true);
|
|
299
|
+
});
|
|
300
|
+
this.loader = combineCancel(loader, timeoutLoader);
|
|
301
|
+
} else {
|
|
302
|
+
this.loader = loader;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
onEnter = () => {
|
|
307
|
+
if (this.state.inViewport) return;
|
|
308
|
+
this.setState({inViewport: true});
|
|
309
|
+
const pickedSrc = selectSrc({
|
|
310
|
+
srcSet: this.props.srcSet,
|
|
311
|
+
maxImageWidth:
|
|
312
|
+
this.props.srcSet.length > 1
|
|
313
|
+
? guessMaxImageWidth(this.state.dimensions) // eslint-disable-line react/no-access-state-in-setstate
|
|
314
|
+
: 0,
|
|
315
|
+
supportsWebp,
|
|
316
|
+
});
|
|
317
|
+
const {getUrl} = this.props;
|
|
318
|
+
const url = getUrl ? getUrl(pickedSrc) : pickedSrc.src;
|
|
319
|
+
const shouldAutoDownload = this.props.shouldAutoDownload({
|
|
320
|
+
...this.state, // eslint-disable-line react/no-access-state-in-setstate
|
|
321
|
+
size: pickedSrc.size,
|
|
322
|
+
});
|
|
323
|
+
this.setState({pickedSrc, shouldAutoDownload, url}, () => {
|
|
324
|
+
if (shouldAutoDownload) this.load(false);
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
onLeave = () => {
|
|
329
|
+
if (this.state.loadState === loading && !this.state.userTriggered) {
|
|
330
|
+
this.setState({inViewport: false});
|
|
331
|
+
this.cancel(false);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
render() {
|
|
336
|
+
const icon = this.props.getIcon(this.state);
|
|
337
|
+
const message = this.props.getMessage(icon, this.state);
|
|
338
|
+
return (
|
|
339
|
+
<Waypoint onEnter={this.onEnter} onLeave={this.onLeave}>
|
|
340
|
+
<Media
|
|
341
|
+
{...this.props}
|
|
342
|
+
{...fallbackParams(this.props)}
|
|
343
|
+
onClick={this.onClick}
|
|
344
|
+
icon={icon}
|
|
345
|
+
src={this.state.url || ''}
|
|
346
|
+
onDimensions={(dimensions) => this.setState({dimensions})}
|
|
347
|
+
message={message}
|
|
348
|
+
/>
|
|
349
|
+
</Waypoint>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
type Props = {
|
|
3
|
+
topOffset: number;
|
|
4
|
+
bottomOffset: number;
|
|
5
|
+
onEnter: () => void;
|
|
6
|
+
onLeave: () => void;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
export declare function Waypoint(props: Props): string | number | bigint | boolean | React.JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | React.ReactPortal | null | undefined> | null | undefined;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This is a slimmed down copy of https://github.com/civiccc/react-waypoint
|
|
3
|
+
The MIT License (MIT)
|
|
4
|
+
Copyright (c) 2015 Brigade
|
|
5
|
+
*/
|
|
6
|
+
import React, {createRef} from 'react';
|
|
7
|
+
function addEventListener(element, type, listener, options) {
|
|
8
|
+
element.addEventListener(type, listener, options);
|
|
9
|
+
return () => element.removeEventListener(type, listener, options);
|
|
10
|
+
}
|
|
11
|
+
// Because waypoint may fire before the setState() updates due to batching
|
|
12
|
+
// queueMicrotask is a better option than setTimeout() or React.flushSync()
|
|
13
|
+
// See https://github.com/facebook/docusaurus/issues/11018
|
|
14
|
+
// See https://github.com/civiccc/react-waypoint/blob/0905ac5a073131147c96dd0694bd6f1b6ee8bc97/src/onNextTick.js
|
|
15
|
+
function subscribeMicrotask(callback) {
|
|
16
|
+
let subscribed = true;
|
|
17
|
+
queueMicrotask(() => {
|
|
18
|
+
if (subscribed) callback();
|
|
19
|
+
});
|
|
20
|
+
return () => (subscribed = false);
|
|
21
|
+
}
|
|
22
|
+
export function Waypoint(props) {
|
|
23
|
+
return typeof window !== 'undefined' ? (
|
|
24
|
+
<WaypointClient {...props}>{props.children}</WaypointClient>
|
|
25
|
+
) : (
|
|
26
|
+
props.children
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
// TODO maybe replace this with IntersectionObserver later?
|
|
30
|
+
// IntersectionObserver doesn't support the "fast scroll" thing
|
|
31
|
+
// but it's probably not a big deal
|
|
32
|
+
class WaypointClient extends React.Component {
|
|
33
|
+
static defaultProps = {
|
|
34
|
+
topOffset: 0,
|
|
35
|
+
bottomOffset: 0,
|
|
36
|
+
onEnter() {},
|
|
37
|
+
onLeave() {},
|
|
38
|
+
};
|
|
39
|
+
scrollableAncestor;
|
|
40
|
+
previousPosition = null;
|
|
41
|
+
unsubscribe;
|
|
42
|
+
innerRef = createRef();
|
|
43
|
+
componentDidMount() {
|
|
44
|
+
this.scrollableAncestor = findScrollableAncestor(this.innerRef.current);
|
|
45
|
+
const unsubscribeScroll = addEventListener(
|
|
46
|
+
this.scrollableAncestor,
|
|
47
|
+
'scroll',
|
|
48
|
+
this._handleScroll,
|
|
49
|
+
{passive: true},
|
|
50
|
+
);
|
|
51
|
+
const unsubscribeResize = addEventListener(
|
|
52
|
+
window,
|
|
53
|
+
'resize',
|
|
54
|
+
this._handleScroll,
|
|
55
|
+
{passive: true},
|
|
56
|
+
);
|
|
57
|
+
const unsubscribeInitialScroll = subscribeMicrotask(() => {
|
|
58
|
+
this._handleScroll();
|
|
59
|
+
});
|
|
60
|
+
this.unsubscribe = () => {
|
|
61
|
+
unsubscribeScroll();
|
|
62
|
+
unsubscribeResize();
|
|
63
|
+
unsubscribeInitialScroll();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
componentDidUpdate() {
|
|
67
|
+
this._handleScroll();
|
|
68
|
+
}
|
|
69
|
+
componentWillUnmount() {
|
|
70
|
+
this.unsubscribe?.();
|
|
71
|
+
}
|
|
72
|
+
_handleScroll = () => {
|
|
73
|
+
const node = this.innerRef.current;
|
|
74
|
+
const {topOffset, bottomOffset, onEnter, onLeave} = this.props;
|
|
75
|
+
const bounds = getBounds({
|
|
76
|
+
node: node,
|
|
77
|
+
scrollableAncestor: this.scrollableAncestor,
|
|
78
|
+
topOffset,
|
|
79
|
+
bottomOffset,
|
|
80
|
+
});
|
|
81
|
+
const currentPosition = getCurrentPosition(bounds);
|
|
82
|
+
const previousPosition = this.previousPosition;
|
|
83
|
+
this.previousPosition = currentPosition;
|
|
84
|
+
if (previousPosition === currentPosition) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (currentPosition === 'inside') {
|
|
88
|
+
onEnter();
|
|
89
|
+
} else if (previousPosition === 'inside') {
|
|
90
|
+
onLeave();
|
|
91
|
+
}
|
|
92
|
+
const isRapidScrollDown =
|
|
93
|
+
previousPosition === 'below' && currentPosition === 'above';
|
|
94
|
+
const isRapidScrollUp =
|
|
95
|
+
previousPosition === 'above' && currentPosition === 'below';
|
|
96
|
+
if (isRapidScrollDown || isRapidScrollUp) {
|
|
97
|
+
onEnter();
|
|
98
|
+
onLeave();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
render() {
|
|
102
|
+
// @ts-expect-error: fix this implicit API
|
|
103
|
+
return React.cloneElement(this.props.children, {innerRef: this.innerRef});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Traverses up the DOM to find an ancestor container which has an overflow
|
|
108
|
+
* style that allows for scrolling.
|
|
109
|
+
*
|
|
110
|
+
* @return {Object} the closest ancestor element with an overflow style that
|
|
111
|
+
* allows for scrolling. If none is found, the `window` object is returned
|
|
112
|
+
* as a fallback.
|
|
113
|
+
*/
|
|
114
|
+
function findScrollableAncestor(inputNode) {
|
|
115
|
+
let node = inputNode;
|
|
116
|
+
while (node.parentNode) {
|
|
117
|
+
// @ts-expect-error: it's fine
|
|
118
|
+
node = node.parentNode;
|
|
119
|
+
if (node === document.body) {
|
|
120
|
+
// We've reached all the way to the root node.
|
|
121
|
+
return window;
|
|
122
|
+
}
|
|
123
|
+
const style = window.getComputedStyle(node);
|
|
124
|
+
const overflow =
|
|
125
|
+
style.getPropertyValue('overflow-y') ||
|
|
126
|
+
style.getPropertyValue('overflow');
|
|
127
|
+
if (
|
|
128
|
+
overflow === 'auto' ||
|
|
129
|
+
overflow === 'scroll' ||
|
|
130
|
+
overflow === 'overlay'
|
|
131
|
+
) {
|
|
132
|
+
return node;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// A scrollable ancestor element was not found, which means that we need to
|
|
136
|
+
// do stuff on window.
|
|
137
|
+
return window;
|
|
138
|
+
}
|
|
139
|
+
function getBounds({node, scrollableAncestor, topOffset, bottomOffset}) {
|
|
140
|
+
const {top, bottom} = node.getBoundingClientRect();
|
|
141
|
+
let contextHeight;
|
|
142
|
+
let contextScrollTop;
|
|
143
|
+
if (scrollableAncestor === window) {
|
|
144
|
+
contextHeight = window.innerHeight;
|
|
145
|
+
contextScrollTop = 0;
|
|
146
|
+
} else {
|
|
147
|
+
const ancestorElement = scrollableAncestor;
|
|
148
|
+
contextHeight = ancestorElement.offsetHeight;
|
|
149
|
+
contextScrollTop = ancestorElement.getBoundingClientRect().top;
|
|
150
|
+
}
|
|
151
|
+
const contextBottom = contextScrollTop + contextHeight;
|
|
152
|
+
return {
|
|
153
|
+
top,
|
|
154
|
+
bottom,
|
|
155
|
+
viewportTop: contextScrollTop + topOffset,
|
|
156
|
+
viewportBottom: contextBottom - bottomOffset,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function getCurrentPosition(bounds) {
|
|
160
|
+
if (bounds.viewportBottom - bounds.viewportTop === 0) {
|
|
161
|
+
return 'invisible';
|
|
162
|
+
}
|
|
163
|
+
// top is within the viewport
|
|
164
|
+
if (bounds.viewportTop <= bounds.top && bounds.top <= bounds.viewportBottom) {
|
|
165
|
+
return 'inside';
|
|
166
|
+
}
|
|
167
|
+
// bottom is within the viewport
|
|
168
|
+
if (
|
|
169
|
+
bounds.viewportTop <= bounds.bottom &&
|
|
170
|
+
bounds.bottom <= bounds.viewportBottom
|
|
171
|
+
) {
|
|
172
|
+
return 'inside';
|
|
173
|
+
}
|
|
174
|
+
// top is above the viewport and bottom is below the viewport
|
|
175
|
+
if (
|
|
176
|
+
bounds.top <= bounds.viewportTop &&
|
|
177
|
+
bounds.viewportBottom <= bounds.bottom
|
|
178
|
+
) {
|
|
179
|
+
return 'inside';
|
|
180
|
+
}
|
|
181
|
+
if (bounds.viewportBottom < bounds.top) {
|
|
182
|
+
return 'below';
|
|
183
|
+
}
|
|
184
|
+
if (bounds.top < bounds.viewportTop) {
|
|
185
|
+
return 'above';
|
|
186
|
+
}
|
|
187
|
+
return 'invisible';
|
|
188
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
declare const IdealImageWithDefaults: ({ icons: iconsProp, theme: themeProp, ...props }: {
|
|
2
|
+
[x: string]: any;
|
|
3
|
+
icons?: {
|
|
4
|
+
[x: string]: ((props: any) => import("react").JSX.Element) | null;
|
|
5
|
+
} | undefined;
|
|
6
|
+
theme?: {
|
|
7
|
+
placeholder: {
|
|
8
|
+
backgroundSize: string;
|
|
9
|
+
backgroundRepeat: string;
|
|
10
|
+
position: string;
|
|
11
|
+
};
|
|
12
|
+
img: {
|
|
13
|
+
width: string;
|
|
14
|
+
height: string;
|
|
15
|
+
maxWidth: string;
|
|
16
|
+
marginBottom: string;
|
|
17
|
+
};
|
|
18
|
+
icon: {
|
|
19
|
+
position: string;
|
|
20
|
+
top: string;
|
|
21
|
+
left: string;
|
|
22
|
+
transform: string;
|
|
23
|
+
textAlign: string;
|
|
24
|
+
};
|
|
25
|
+
noscript: {
|
|
26
|
+
position: string;
|
|
27
|
+
top: number;
|
|
28
|
+
left: number;
|
|
29
|
+
};
|
|
30
|
+
} | undefined;
|
|
31
|
+
}) => import("react").JSX.Element;
|
|
32
|
+
export default IdealImageWithDefaults;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import IdealImage from '../IdealImage';
|
|
2
|
+
import icons from '../icons';
|
|
3
|
+
import theme from '../theme';
|
|
4
|
+
|
|
5
|
+
const IdealImageWithDefaults = ({
|
|
6
|
+
icons: iconsProp = icons,
|
|
7
|
+
theme: themeProp = theme,
|
|
8
|
+
...props
|
|
9
|
+
}) => <IdealImage {...props} icons={iconsProp} theme={themeProp} />;
|
|
10
|
+
|
|
11
|
+
export default IdealImageWithDefaults;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React, { PureComponent } from 'react';
|
|
2
|
+
export default class Media extends PureComponent {
|
|
3
|
+
state: {
|
|
4
|
+
isMounted: boolean;
|
|
5
|
+
};
|
|
6
|
+
dimensionElement: SVGSVGElement | null | undefined;
|
|
7
|
+
static defaultProps: {
|
|
8
|
+
iconColor: string;
|
|
9
|
+
iconSize: number;
|
|
10
|
+
};
|
|
11
|
+
constructor(props: any);
|
|
12
|
+
componentDidMount(): void;
|
|
13
|
+
renderIcon(props: any): React.DetailedReactHTMLElement<{
|
|
14
|
+
className: string;
|
|
15
|
+
style: object;
|
|
16
|
+
}, HTMLElement> | null;
|
|
17
|
+
renderImage(props: any): React.JSX.Element;
|
|
18
|
+
renderNoscript(props: any): React.JSX.Element | null;
|
|
19
|
+
render(): React.JSX.Element;
|
|
20
|
+
}
|