@arcblock/ux 0.78.25 → 1.6.59
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/LICENSE +1 -1
- package/README.md +0 -56
- package/lib/ActionButton/index.js +6 -4
- package/lib/ActivityIndicator/index.js +75 -23
- package/lib/Alert/index.js +15 -11
- package/lib/Async/index.js +1 -1
- package/lib/Badge/index.js +17 -15
- package/lib/Blocklet/index.js +261 -0
- package/lib/Button/wrap.js +96 -43
- package/lib/ButtonGroup/index.js +3 -16
- package/lib/Center/index.js +30 -4
- package/lib/ClickToCopy/index.js +10 -8
- package/lib/CodeBlock/index.js +40 -13
- package/lib/Colors/index.js +15 -0
- package/lib/Colors/themes/default.js +85 -0
- package/lib/ContactForm/index.js +9 -10
- package/lib/CookieConsent/index.js +98 -0
- package/lib/CountDown/index.js +18 -14
- package/lib/Dialog/confirm.js +84 -0
- package/lib/Dialog/dialog.js +137 -0
- package/lib/Dialog/index.js +23 -0
- package/lib/Earth/index.js +33 -33
- package/lib/Empty/index.js +61 -0
- package/lib/Footer/index.js +16 -18
- package/lib/Icon/image.js +10 -13
- package/lib/Icon/index.js +10 -8
- package/lib/Img/index.js +212 -0
- package/lib/InfoRow/index.js +7 -6
- package/lib/Layout/dashboard/header.js +60 -42
- package/lib/Layout/dashboard/index.js +72 -60
- package/lib/Layout/dashboard/sidebar.js +41 -25
- package/lib/Layout/index.js +113 -51
- package/lib/Locale/browser-lang.js +0 -2
- package/lib/Locale/context.js +85 -61
- package/lib/Locale/selector.js +33 -20
- package/lib/Logo/index.js +15 -13
- package/lib/Metric/index.js +5 -6
- package/lib/NFTDisplay/README.md +59 -0
- package/lib/NFTDisplay/aspect-ratio-container.js +52 -0
- package/lib/NFTDisplay/broken.js +25 -0
- package/lib/NFTDisplay/index.js +317 -0
- package/lib/NFTDisplay/loading.js +23 -0
- package/lib/NFTDisplay/svg-embedder/img.js +68 -0
- package/lib/NFTDisplay/svg-embedder/inline-svg.js +54 -0
- package/lib/PageScroller/index.js +10 -11
- package/lib/PageScroller/usePrevValue.js +2 -2
- package/lib/PricingTable/PricingPlan.js +12 -15
- package/lib/PricingTable/index.js +5 -5
- package/lib/Result/common.js +176 -0
- package/lib/Result/index.js +61 -0
- package/lib/Result/result.js +69 -0
- package/lib/Result/translations.js +61 -0
- package/lib/Screenshot/index.js +14 -13
- package/lib/Spinner/index.js +37 -0
- package/lib/SplitButton/index.js +126 -0
- package/lib/Switch/index.js +107 -0
- package/lib/Tabs/index.js +24 -47
- package/lib/Tag/index.js +15 -13
- package/lib/Terminal/Player.js +43 -45
- package/lib/Terminal/index.js +3 -1
- package/lib/Terminal/util.js +2 -3
- package/lib/TextCollapse/index.js +21 -14
- package/lib/Theme/index.js +79 -63
- package/lib/Theme/responsiveFontSizes.js +8 -8
- package/lib/Toast/index.js +12 -11
- package/lib/Util/index.js +197 -26
- package/lib/Video/index.js +8 -11
- package/lib/Wallet/Action.js +15 -13
- package/lib/Wallet/Download.js +60 -58
- package/lib/Wallet/Open.js +2 -2
- package/lib/WechatPrompt/index.js +10 -10
- package/lib/index.js +6 -6
- package/lib/withTheme/index.js +5 -17
- package/lib/withTracker/error_boundary.js +3 -3
- package/lib/withTracker/index.js +6 -7
- package/package.json +22 -17
- package/src/ActionButton/index.js +65 -0
- package/src/ActivityIndicator/index.js +141 -0
- package/src/Alert/index.js +104 -0
- package/src/Async/index.js +39 -0
- package/src/Badge/index.js +71 -0
- package/src/Blocklet/index.js +424 -0
- package/src/Button/index.js +4 -0
- package/src/Button/wrap.js +101 -0
- package/src/ButtonGroup/index.js +6 -0
- package/src/Center/index.js +40 -0
- package/src/ClickToCopy/index.js +90 -0
- package/src/CodeBlock/index.js +160 -0
- package/src/Colors/index.js +1 -0
- package/src/Colors/themes/default.js +54 -0
- package/src/ContactForm/index.js +240 -0
- package/src/CookieConsent/index.js +90 -0
- package/src/CountDown/index.js +151 -0
- package/src/Dialog/confirm.js +76 -0
- package/src/Dialog/dialog.js +162 -0
- package/src/Dialog/index.js +2 -0
- package/src/DriftBot/index.js +81 -0
- package/src/Earth/countries.json +8057 -0
- package/src/Earth/index.js +511 -0
- package/src/Earth/util.js +69 -0
- package/src/Empty/index.js +41 -0
- package/src/Footer/index.js +110 -0
- package/src/Icon/image.js +55 -0
- package/src/Icon/index.js +69 -0
- package/src/Img/index.js +172 -0
- package/src/InfoRow/index.js +83 -0
- package/src/Layout/dashboard/header.js +157 -0
- package/src/Layout/dashboard/index.js +150 -0
- package/src/Layout/dashboard/sidebar.js +122 -0
- package/src/Layout/index.js +318 -0
- package/src/Locale/browser-lang.js +63 -0
- package/src/Locale/context.js +94 -0
- package/src/Locale/images/globe-dark.png +0 -0
- package/src/Locale/images/globe-light.png +0 -0
- package/src/Locale/selector.js +135 -0
- package/src/Logo/images/logo-dark-text.svg +3 -0
- package/src/Logo/images/logo-dark-top.svg +6 -0
- package/src/Logo/images/logo-light-text.svg +3 -0
- package/src/Logo/images/logo-light-top.svg +6 -0
- package/src/Logo/index.js +47 -0
- package/src/Metric/index.js +115 -0
- package/src/NFTDisplay/README.md +59 -0
- package/src/NFTDisplay/aspect-ratio-container.js +34 -0
- package/src/NFTDisplay/broken.js +18 -0
- package/src/NFTDisplay/index.js +257 -0
- package/src/NFTDisplay/loading.js +17 -0
- package/src/NFTDisplay/svg-embedder/img.js +36 -0
- package/src/NFTDisplay/svg-embedder/inline-svg.js +37 -0
- package/src/PageScroller/index.js +342 -0
- package/src/PageScroller/usePrevValue.js +12 -0
- package/src/PricingTable/PricingPlan.js +112 -0
- package/src/PricingTable/index.js +43 -0
- package/src/Result/common.js +116 -0
- package/src/Result/index.js +31 -0
- package/src/Result/result.js +57 -0
- package/src/Result/translations.js +56 -0
- package/src/Screenshot/devices.css +1366 -0
- package/src/Screenshot/index.js +181 -0
- package/src/Spinner/index.js +19 -0
- package/src/SplitButton/index.js +112 -0
- package/src/Switch/index.js +78 -0
- package/src/Tabs/index.js +46 -0
- package/src/Tag/index.js +73 -0
- package/src/Terminal/Player.js +364 -0
- package/src/Terminal/index.js +150 -0
- package/src/Terminal/player.css +378 -0
- package/src/Terminal/util.js +167 -0
- package/src/Terminal/xterm.css +171 -0
- package/src/TextCollapse/index.js +92 -0
- package/src/Theme/index.js +184 -0
- package/src/Theme/responsiveFontSizes.js +94 -0
- package/src/Toast/index.js +118 -0
- package/src/Util/index.js +281 -0
- package/src/Video/index.js +72 -0
- package/src/Wallet/Action.js +105 -0
- package/src/Wallet/Download.js +130 -0
- package/src/Wallet/Open.js +50 -0
- package/src/Wallet/images/abtwallet.png +0 -0
- package/src/Wallet/images/android_download.svg +23 -0
- package/src/Wallet/images/app-store.svg +20 -0
- package/src/Wallet/images/google-play.svg +70 -0
- package/src/WechatPrompt/images/android.png +0 -0
- package/src/WechatPrompt/images/ios.png +0 -0
- package/src/WechatPrompt/index.js +81 -0
- package/src/index.js +63 -0
- package/src/withTheme/index.js +72 -0
- package/src/withTracker/README.md +34 -0
- package/src/withTracker/error_boundary.js +34 -0
- package/src/withTracker/index.js +70 -0
- package/lib/GraphQLPlayground/graphiql.css +0 -1850
- package/lib/GraphQLPlayground/index.js +0 -302
- package/lib/GraphQLPlayground/util.js +0 -55
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import clsx from 'clsx';
|
|
5
|
+
import get from 'lodash/get';
|
|
6
|
+
import pako from 'pako';
|
|
7
|
+
import base64 from 'base64-url';
|
|
8
|
+
import isSvg from 'is-svg';
|
|
9
|
+
import AspectRatioContainer from './aspect-ratio-container';
|
|
10
|
+
import ImgSvgEmbedder from './svg-embedder/img';
|
|
11
|
+
import InlineSvgEmbedder from './svg-embedder/inline-svg';
|
|
12
|
+
import DefaultLoading from './loading';
|
|
13
|
+
import DefaultBrokenImage from './broken';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 从 assetState 中获取 nft data, 兼容新旧两种类型的数据结构, 建议将该方法的返回值传入 NFTDisplay 组件的 data prop
|
|
17
|
+
* - 旧: assetState.data.value (.credentialSubject.display)
|
|
18
|
+
* - 新: assetState.display
|
|
19
|
+
*/
|
|
20
|
+
export function getNFTData(assetState) {
|
|
21
|
+
return assetState?.display || assetState?.data?.value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fromBase64(v) {
|
|
25
|
+
if (typeof v !== 'string') {
|
|
26
|
+
throw new Error('fromBase64 requires input to be a string');
|
|
27
|
+
}
|
|
28
|
+
return Buffer.from(base64.unescape(v), 'base64');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// const isVC = type => {
|
|
32
|
+
// return String(type).includes('VerifiableCredential');
|
|
33
|
+
// };
|
|
34
|
+
|
|
35
|
+
// 仅针对非 url type 的情况
|
|
36
|
+
const getSvgEmbedder = preferredSvgEmbedder => {
|
|
37
|
+
const embedders = {
|
|
38
|
+
img: ImgSvgEmbedder,
|
|
39
|
+
svg: InlineSvgEmbedder,
|
|
40
|
+
};
|
|
41
|
+
return embedders[preferredSvgEmbedder];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* TODO:
|
|
46
|
+
* 考虑把 asset data 解析部分和 nft display 分离, android 端有相关使用场景 - 只传入 svg 或 url, 也可以传入 asset data,
|
|
47
|
+
* 目前如果想直接传入 svg 或 url, 需要构造一个 asset data 的数据才能使用 NFTDisplay 组件
|
|
48
|
+
*/
|
|
49
|
+
function NFTDisplay({
|
|
50
|
+
data,
|
|
51
|
+
address,
|
|
52
|
+
inset,
|
|
53
|
+
aspect,
|
|
54
|
+
component,
|
|
55
|
+
className,
|
|
56
|
+
renderError,
|
|
57
|
+
renderLoading,
|
|
58
|
+
preferredSvgEmbedder,
|
|
59
|
+
checkSvg,
|
|
60
|
+
minimumLoadingTime,
|
|
61
|
+
onCompleted,
|
|
62
|
+
...rest
|
|
63
|
+
}) {
|
|
64
|
+
const wrapRoot = children => (
|
|
65
|
+
<Root as={component} {...rest} className={clsx(className, { 'nft-display--inset': inset })}>
|
|
66
|
+
{children}
|
|
67
|
+
</Root>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const parsed = React.useRef(data);
|
|
72
|
+
// 如果是 raw data 先解析
|
|
73
|
+
if (typeof parsed.current === 'string') {
|
|
74
|
+
parsed.current = JSON.parse(data);
|
|
75
|
+
// console.log('[debug] parse data')
|
|
76
|
+
}
|
|
77
|
+
const { vcId } = parsed.current;
|
|
78
|
+
// 需要兼容新旧两种类型的数据结构, nft data 有 credentialSubject 属性, 说明是旧 nft data, 否则是新 nft data
|
|
79
|
+
const display = parsed.current.credentialSubject
|
|
80
|
+
? get(parsed.current, 'credentialSubject.display')
|
|
81
|
+
: parsed.current;
|
|
82
|
+
const { content, type } = display;
|
|
83
|
+
const isUrlType = type === 'url';
|
|
84
|
+
|
|
85
|
+
// 首次加载, 对于 url type 的情况, loading 为 true
|
|
86
|
+
const [state, setState] = useState({ loading: isUrlType, error: false });
|
|
87
|
+
const [minimumLoadingReady, setMinimumLoadingReady] = useState(minimumLoadingTime <= 0);
|
|
88
|
+
// console.log('[debug] render', {type, minimumLoadingTime}, JSON.stringify(state))
|
|
89
|
+
|
|
90
|
+
React.useEffect(() => {
|
|
91
|
+
let timer;
|
|
92
|
+
if (minimumLoadingTime > 0) {
|
|
93
|
+
timer = setTimeout(() => setMinimumLoadingReady(true), minimumLoadingTime);
|
|
94
|
+
}
|
|
95
|
+
return () => clearTimeout(timer);
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
// onCompleted
|
|
99
|
+
React.useEffect(() => {
|
|
100
|
+
if ((!state.loading && minimumLoadingReady) || state.error) {
|
|
101
|
+
onCompleted();
|
|
102
|
+
}
|
|
103
|
+
}, [state, minimumLoadingReady]);
|
|
104
|
+
|
|
105
|
+
if (state.error) {
|
|
106
|
+
throw new Error('Failed to render NFT Display.');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const renderNFT = () => {
|
|
110
|
+
if (content) {
|
|
111
|
+
switch (type) {
|
|
112
|
+
case 'url': {
|
|
113
|
+
const urlObj = new URL(content);
|
|
114
|
+
if (!urlObj.searchParams.has('assetId')) {
|
|
115
|
+
urlObj.searchParams.append('assetId', address);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!urlObj.searchParams.has('vcId')) {
|
|
119
|
+
urlObj.searchParams.append('vcId', vcId);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const url = urlObj.href;
|
|
123
|
+
return (
|
|
124
|
+
<img
|
|
125
|
+
src={url}
|
|
126
|
+
onError={() => setState({ ...state, error: true })}
|
|
127
|
+
onLoad={() => setState({ ...state, loading: false })}
|
|
128
|
+
alt="NFT Display"
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
case 'uri': {
|
|
133
|
+
return (
|
|
134
|
+
<img
|
|
135
|
+
src={content}
|
|
136
|
+
onError={() => setState({ ...state, error: true })}
|
|
137
|
+
onLoad={() => setState({ ...state, loading: false })}
|
|
138
|
+
alt="NFT Display"
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
case 'svg_gzipped': {
|
|
143
|
+
const buffer = pako.ungzip(fromBase64(content), {});
|
|
144
|
+
const svg = Buffer.from(buffer).toString('utf8');
|
|
145
|
+
if (checkSvg && !isSvg(svg)) {
|
|
146
|
+
throw new Error(`Invalid SVG: ${svg}`);
|
|
147
|
+
}
|
|
148
|
+
const Embedder = getSvgEmbedder(preferredSvgEmbedder);
|
|
149
|
+
return <Embedder svg={svg} />;
|
|
150
|
+
}
|
|
151
|
+
case 'svg': {
|
|
152
|
+
if (checkSvg && !isSvg(content)) {
|
|
153
|
+
throw new Error(`Invalid SVG: ${content}`);
|
|
154
|
+
}
|
|
155
|
+
const Embedder = getSvgEmbedder(preferredSvgEmbedder);
|
|
156
|
+
return <Embedder svg={content} />;
|
|
157
|
+
}
|
|
158
|
+
// TODO: 准备测试数据
|
|
159
|
+
case 'html': {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
default:
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`unsupported display protocol: ${display.type}`);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return wrapRoot(
|
|
169
|
+
<>
|
|
170
|
+
{(state.loading || !minimumLoadingReady) &&
|
|
171
|
+
(renderLoading ? renderLoading() : <DefaultLoading />)}
|
|
172
|
+
{renderNFT()}
|
|
173
|
+
</>
|
|
174
|
+
);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
console.error(e);
|
|
177
|
+
return wrapRoot(renderError ? renderError() : <DefaultBrokenImage />);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
NFTDisplay.propTypes = {
|
|
182
|
+
// asset data 可以是 raw data 和 parsed data
|
|
183
|
+
data: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
|
|
184
|
+
address: PropTypes.string.isRequired,
|
|
185
|
+
component: PropTypes.string,
|
|
186
|
+
inset: PropTypes.bool,
|
|
187
|
+
aspect: PropTypes.number,
|
|
188
|
+
className: PropTypes.string,
|
|
189
|
+
renderError: PropTypes.func,
|
|
190
|
+
renderLoading: PropTypes.func,
|
|
191
|
+
// 对于非 url type 的情况, 支持优先选用的 svg 嵌入方式, 默认是 img
|
|
192
|
+
preferredSvgEmbedder: PropTypes.oneOf(['img', 'svg']),
|
|
193
|
+
// 针对非 url type 的情况, 检测 svg 有效性, 默认禁用
|
|
194
|
+
checkSvg: PropTypes.bool,
|
|
195
|
+
// loading 最小显示时间 (避免闪烁)
|
|
196
|
+
minimumLoadingTime: PropTypes.number,
|
|
197
|
+
// 完成回调, 无论加载成功|失败
|
|
198
|
+
onCompleted: PropTypes.func,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
NFTDisplay.defaultProps = {
|
|
202
|
+
component: 'span',
|
|
203
|
+
inset: false,
|
|
204
|
+
aspect: 0,
|
|
205
|
+
className: '',
|
|
206
|
+
renderError: null,
|
|
207
|
+
renderLoading: null,
|
|
208
|
+
preferredSvgEmbedder: 'img',
|
|
209
|
+
checkSvg: false,
|
|
210
|
+
minimumLoadingTime: 0,
|
|
211
|
+
onCompleted: () => {},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const Root = styled.div`
|
|
215
|
+
display: flex;
|
|
216
|
+
justify-content: center;
|
|
217
|
+
align-items: center;
|
|
218
|
+
position: relative;
|
|
219
|
+
/* 默认尺寸 */
|
|
220
|
+
width: 150px;
|
|
221
|
+
height: 150px;
|
|
222
|
+
overflow: hidden;
|
|
223
|
+
|
|
224
|
+
&,
|
|
225
|
+
img {
|
|
226
|
+
max-width: 100%;
|
|
227
|
+
max-height: 100%;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
img {
|
|
231
|
+
width: 100%;
|
|
232
|
+
height: 100%;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
&.nft-display--inset {
|
|
236
|
+
width: 100%;
|
|
237
|
+
height: 100%;
|
|
238
|
+
}
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
function withAspectRatio(Component) {
|
|
242
|
+
// eslint-disable-next-line func-names, react/prop-types
|
|
243
|
+
return function ({ aspect, inset, ...rest }) {
|
|
244
|
+
// inset 比 aspect ratio 优先级高, 如果同时设置了 inset 和 aspect, 则后者不生效
|
|
245
|
+
const applyAspectRatio = aspect > 0 && !inset;
|
|
246
|
+
if (applyAspectRatio) {
|
|
247
|
+
return (
|
|
248
|
+
<AspectRatioContainer aspect={aspect}>
|
|
249
|
+
<Component inset {...rest} />
|
|
250
|
+
</AspectRatioContainer>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return <Component inset={inset} {...rest} />;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export default withAspectRatio(NFTDisplay);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
export default function Loading() {
|
|
5
|
+
return <Root className="nft-display__loading">loading...</Root>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const Root = styled.span`
|
|
9
|
+
display: flex;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
align-items: center;
|
|
12
|
+
position: absolute;
|
|
13
|
+
width: 100%;
|
|
14
|
+
height: 100%;
|
|
15
|
+
color: #ccc;
|
|
16
|
+
background-color: #eee;
|
|
17
|
+
`;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import InlineSvgEmbedder from './inline-svg';
|
|
4
|
+
|
|
5
|
+
const svgToImgUrl = svg => {
|
|
6
|
+
// fix: #225, https://stackoverflow.com/a/52135328)
|
|
7
|
+
const blob = new Blob([svg], { type: 'image/svg+xml' });
|
|
8
|
+
return URL.createObjectURL(blob);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 基于 <img> 嵌入 svg
|
|
13
|
+
*/
|
|
14
|
+
function ImgEmbedder({ svg, alt, fallback, ...rest }) {
|
|
15
|
+
// 包含 foreignObject 的 svg, fallback 到 shadow dom
|
|
16
|
+
if (fallback && svg.indexOf('</foreignObject>') > -1) {
|
|
17
|
+
return <InlineSvgEmbedder svg={svg} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const url = svgToImgUrl(svg);
|
|
21
|
+
return <img src={url} onLoad={() => URL.revokeObjectURL(url)} alt={alt} {...rest} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
ImgEmbedder.propTypes = {
|
|
25
|
+
svg: PropTypes.string.isRequired,
|
|
26
|
+
alt: PropTypes.string,
|
|
27
|
+
// 对于包含 foreignObject 的 svg, fallback 到 inline svg + shadow DOM
|
|
28
|
+
fallback: PropTypes.bool,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
ImgEmbedder.defaultProps = {
|
|
32
|
+
alt: '',
|
|
33
|
+
fallback: true,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default ImgEmbedder;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import root from 'react-shadow/styled-components';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* inline svg 的方式嵌入 svg, 使用 shadow DOM 避免样式污染
|
|
8
|
+
*/
|
|
9
|
+
function InlineSvg({ svg, ...rest }) {
|
|
10
|
+
return (
|
|
11
|
+
<Root {...rest}>
|
|
12
|
+
<Inner dangerouslySetInnerHTML={{ __html: svg }} />
|
|
13
|
+
</Root>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
InlineSvg.propTypes = {
|
|
18
|
+
svg: PropTypes.string.isRequired,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const Root = styled(root.span)`
|
|
22
|
+
display: block;
|
|
23
|
+
width: 100%;
|
|
24
|
+
height: 100%;
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const Inner = styled.div`
|
|
28
|
+
&,
|
|
29
|
+
& > svg {
|
|
30
|
+
height: 100%;
|
|
31
|
+
width: 100%;
|
|
32
|
+
min-width: 100%;
|
|
33
|
+
max-width: 100%;
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
export default InlineSvg;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import isEqual from 'lodash/isEqual';
|
|
5
|
+
import isNil from 'lodash/isNil';
|
|
6
|
+
import isNull from 'lodash/isNull';
|
|
7
|
+
|
|
8
|
+
import usePrevious from './usePrevValue';
|
|
9
|
+
|
|
10
|
+
const Events = {
|
|
11
|
+
TOUCHMOVE: 'touchmove',
|
|
12
|
+
KEYDOWN: 'keydown',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DEFAULT_ANIMATION_TIMER = 500;
|
|
16
|
+
const DEFAULT_ANIMATION = 'ease-in-out';
|
|
17
|
+
const DEFAULT_CONTAINER_HEIGHT = '100vh';
|
|
18
|
+
const DEFAULT_CONTAINER_WIDTH = '100vw';
|
|
19
|
+
const DEFAULT_COMPONENT_INDEX = 0;
|
|
20
|
+
const DEFAULT_COMPONENTS_TO_RENDER_LENGTH = 0;
|
|
21
|
+
|
|
22
|
+
const ANIMATION_TIMER_BUFFER = 100;
|
|
23
|
+
const KEY_UP = 38;
|
|
24
|
+
const KEY_DOWN = 40;
|
|
25
|
+
const DISABLED_CLASS_NAME = 'rps-scroll--disabled';
|
|
26
|
+
|
|
27
|
+
let previousTouchMove = null;
|
|
28
|
+
let isScrolling = false;
|
|
29
|
+
let isMounted = false;
|
|
30
|
+
let isBodyScrollEnabled = true;
|
|
31
|
+
let isTransitionAfterComponentsToRenderChanged = false;
|
|
32
|
+
const containers = [];
|
|
33
|
+
|
|
34
|
+
const PageScroller = ({
|
|
35
|
+
animationTimer,
|
|
36
|
+
blockScrollDown,
|
|
37
|
+
blockScrollUp,
|
|
38
|
+
children,
|
|
39
|
+
height,
|
|
40
|
+
width,
|
|
41
|
+
customPageNumber,
|
|
42
|
+
onScrollUnavailable,
|
|
43
|
+
onChange,
|
|
44
|
+
renderAllPagesOnFirstRender,
|
|
45
|
+
transitionTimingFunction,
|
|
46
|
+
}) => {
|
|
47
|
+
const [componentIndex, setComponentIndex] = useState(DEFAULT_COMPONENT_INDEX);
|
|
48
|
+
const [componentsToRenderLength, setComponentsToRenderLength] = useState(
|
|
49
|
+
DEFAULT_COMPONENTS_TO_RENDER_LENGTH
|
|
50
|
+
);
|
|
51
|
+
const prevComponentIndex = usePrevious(componentIndex);
|
|
52
|
+
const pageContainer = useRef(null);
|
|
53
|
+
|
|
54
|
+
const addNextComponent = useCallback(
|
|
55
|
+
componentsToRenderOnMountLength => {
|
|
56
|
+
let tempComponentsToRenderLength = 0;
|
|
57
|
+
|
|
58
|
+
if (!isNil(componentsToRenderOnMountLength)) {
|
|
59
|
+
tempComponentsToRenderLength = componentsToRenderOnMountLength;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
tempComponentsToRenderLength = Math.max(
|
|
63
|
+
tempComponentsToRenderLength,
|
|
64
|
+
componentsToRenderLength
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (tempComponentsToRenderLength <= componentIndex + 1) {
|
|
68
|
+
if (!isNil(children[componentIndex + 1])) {
|
|
69
|
+
tempComponentsToRenderLength++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setComponentsToRenderLength(tempComponentsToRenderLength);
|
|
74
|
+
},
|
|
75
|
+
[children, componentIndex, componentsToRenderLength]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const checkRenderOnMount = useCallback(() => {
|
|
79
|
+
if (renderAllPagesOnFirstRender) {
|
|
80
|
+
setComponentsToRenderLength(React.Children.count(children));
|
|
81
|
+
} else if (!isNil(children[DEFAULT_COMPONENT_INDEX + 1])) {
|
|
82
|
+
addNextComponent(DEFAULT_COMPONENTS_TO_RENDER_LENGTH + 1);
|
|
83
|
+
}
|
|
84
|
+
}, [addNextComponent, children, renderAllPagesOnFirstRender]);
|
|
85
|
+
|
|
86
|
+
const disableScroll = useCallback(() => {
|
|
87
|
+
if (isBodyScrollEnabled) {
|
|
88
|
+
isBodyScrollEnabled = false;
|
|
89
|
+
window.scrollTo({
|
|
90
|
+
left: 0,
|
|
91
|
+
top: 0,
|
|
92
|
+
behavior: 'smooth',
|
|
93
|
+
});
|
|
94
|
+
document.body.classList.add(DISABLED_CLASS_NAME);
|
|
95
|
+
document.documentElement.classList.add(DISABLED_CLASS_NAME);
|
|
96
|
+
}
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
const enableDocumentScroll = useCallback(() => {
|
|
100
|
+
if (!isBodyScrollEnabled) {
|
|
101
|
+
isBodyScrollEnabled = true;
|
|
102
|
+
document.body.classList.remove(DISABLED_CLASS_NAME);
|
|
103
|
+
document.documentElement.classList.remove(DISABLED_CLASS_NAME);
|
|
104
|
+
}
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
const setRenderComponents = useCallback(() => {
|
|
108
|
+
const newComponentsToRender = [];
|
|
109
|
+
|
|
110
|
+
let i = 0;
|
|
111
|
+
|
|
112
|
+
while (i < componentsToRenderLength && !isNil(children[i])) {
|
|
113
|
+
containers[i] = true;
|
|
114
|
+
newComponentsToRender.push(
|
|
115
|
+
<div key={i} style={{ height: '100%', width: '100%' }}>
|
|
116
|
+
{React.cloneElement(children[i], { ...children[i].props })}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return newComponentsToRender;
|
|
123
|
+
}, [children, componentsToRenderLength]);
|
|
124
|
+
|
|
125
|
+
const scrollWindowDown = useCallback(() => {
|
|
126
|
+
if (!isScrolling && !blockScrollDown) {
|
|
127
|
+
if (!isNil(containers[componentIndex + 1])) {
|
|
128
|
+
disableScroll();
|
|
129
|
+
isScrolling = true;
|
|
130
|
+
pageContainer.current.style.transform = `translate3d(0, ${(componentIndex + 1) * -100}%, 0)`; // prettier-ignore
|
|
131
|
+
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
if (isMounted) {
|
|
134
|
+
setComponentIndex(prevState => prevState + 1);
|
|
135
|
+
}
|
|
136
|
+
}, animationTimer + ANIMATION_TIMER_BUFFER);
|
|
137
|
+
} else {
|
|
138
|
+
enableDocumentScroll();
|
|
139
|
+
if (onScrollUnavailable) {
|
|
140
|
+
onScrollUnavailable();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}, [
|
|
145
|
+
animationTimer,
|
|
146
|
+
blockScrollDown,
|
|
147
|
+
componentIndex,
|
|
148
|
+
disableScroll,
|
|
149
|
+
enableDocumentScroll,
|
|
150
|
+
onScrollUnavailable,
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
const scrollWindowUp = useCallback(() => {
|
|
154
|
+
if (!isScrolling && !blockScrollUp) {
|
|
155
|
+
if (!isNil(containers[componentIndex - 1])) {
|
|
156
|
+
disableScroll();
|
|
157
|
+
isScrolling = true;
|
|
158
|
+
pageContainer.current.style.transform = `translate3d(0, ${(componentIndex - 1) * -100}%, 0)`; // prettier-ignore
|
|
159
|
+
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
if (isMounted) {
|
|
162
|
+
setComponentIndex(prevState => prevState - 1);
|
|
163
|
+
}
|
|
164
|
+
}, animationTimer + ANIMATION_TIMER_BUFFER);
|
|
165
|
+
} else {
|
|
166
|
+
enableDocumentScroll();
|
|
167
|
+
if (onScrollUnavailable) {
|
|
168
|
+
onScrollUnavailable();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}, [
|
|
173
|
+
animationTimer,
|
|
174
|
+
blockScrollUp,
|
|
175
|
+
componentIndex,
|
|
176
|
+
disableScroll,
|
|
177
|
+
enableDocumentScroll,
|
|
178
|
+
onScrollUnavailable,
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
const touchMove = useCallback(
|
|
182
|
+
event => {
|
|
183
|
+
if (!isNull(previousTouchMove)) {
|
|
184
|
+
if (event.touches[0].clientY > previousTouchMove) {
|
|
185
|
+
scrollWindowUp();
|
|
186
|
+
} else {
|
|
187
|
+
scrollWindowDown();
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
previousTouchMove = event.touches[0].clientY;
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
[scrollWindowDown, scrollWindowUp]
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const wheelScroll = useCallback(
|
|
197
|
+
event => {
|
|
198
|
+
if (event.deltaY < 0) {
|
|
199
|
+
scrollWindowUp();
|
|
200
|
+
} else {
|
|
201
|
+
scrollWindowDown();
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
[scrollWindowDown, scrollWindowUp]
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const keyPress = useCallback(
|
|
208
|
+
event => {
|
|
209
|
+
if (isEqual(event.keyCode, KEY_UP)) {
|
|
210
|
+
scrollWindowUp();
|
|
211
|
+
}
|
|
212
|
+
if (isEqual(event.keyCode, KEY_DOWN)) {
|
|
213
|
+
scrollWindowDown();
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
[scrollWindowDown, scrollWindowUp]
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
pageContainer.current.addEventListener(Events.TOUCHMOVE, touchMove);
|
|
221
|
+
pageContainer.current.addEventListener(Events.KEYDOWN, keyPress);
|
|
222
|
+
return () => {
|
|
223
|
+
pageContainer.current.removeEventListener(Events.TOUCHMOVE, touchMove);
|
|
224
|
+
pageContainer.current.removeEventListener(Events.KEYDOWN, keyPress);
|
|
225
|
+
};
|
|
226
|
+
}, [touchMove, keyPress]);
|
|
227
|
+
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
isMounted = true;
|
|
230
|
+
|
|
231
|
+
checkRenderOnMount();
|
|
232
|
+
return () => {
|
|
233
|
+
isMounted = false;
|
|
234
|
+
};
|
|
235
|
+
}, []);
|
|
236
|
+
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
isScrolling = false;
|
|
239
|
+
previousTouchMove = null;
|
|
240
|
+
if (componentIndex > prevComponentIndex) {
|
|
241
|
+
addNextComponent();
|
|
242
|
+
}
|
|
243
|
+
}, [addNextComponent, componentIndex, prevComponentIndex]);
|
|
244
|
+
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
if (onChange) {
|
|
247
|
+
onChange(componentIndex);
|
|
248
|
+
}
|
|
249
|
+
}, [onChange, componentIndex]);
|
|
250
|
+
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
if (!isNil(customPageNumber) && !isEqual(customPageNumber, componentIndex)) {
|
|
253
|
+
let newComponentsToRenderLength = componentsToRenderLength;
|
|
254
|
+
|
|
255
|
+
if (!isEqual(componentIndex, customPageNumber)) {
|
|
256
|
+
if (!isNil(containers[customPageNumber]) && !isScrolling) {
|
|
257
|
+
isScrolling = true;
|
|
258
|
+
pageContainer.current.style.transform = `translate3d(0, ${customPageNumber * -100}%, 0)`;
|
|
259
|
+
|
|
260
|
+
if (isNil(containers[customPageNumber]) && !isNil(children[customPageNumber])) {
|
|
261
|
+
newComponentsToRenderLength++;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
setComponentIndex(customPageNumber);
|
|
266
|
+
setComponentsToRenderLength(newComponentsToRenderLength);
|
|
267
|
+
}, animationTimer + ANIMATION_TIMER_BUFFER);
|
|
268
|
+
} else if (!isScrolling && !isNil(children[customPageNumber])) {
|
|
269
|
+
for (let i = componentsToRenderLength; i <= customPageNumber; i++) {
|
|
270
|
+
newComponentsToRenderLength++;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!isNil(children[customPageNumber])) {
|
|
274
|
+
newComponentsToRenderLength++;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
isScrolling = true;
|
|
278
|
+
isTransitionAfterComponentsToRenderChanged = true;
|
|
279
|
+
setComponentsToRenderLength(newComponentsToRenderLength);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}, [customPageNumber]);
|
|
284
|
+
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
if (isTransitionAfterComponentsToRenderChanged) {
|
|
287
|
+
isTransitionAfterComponentsToRenderChanged = false;
|
|
288
|
+
|
|
289
|
+
pageContainer.current.style.transform = `translate3d(0, ${customPageNumber * -100}%, 0)`;
|
|
290
|
+
|
|
291
|
+
setTimeout(() => {
|
|
292
|
+
setComponentIndex(customPageNumber);
|
|
293
|
+
}, animationTimer + ANIMATION_TIMER_BUFFER);
|
|
294
|
+
}
|
|
295
|
+
}, [animationTimer, componentsToRenderLength, customPageNumber]);
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div style={{ height, width, overflow: 'hidden' }}>
|
|
299
|
+
<div
|
|
300
|
+
ref={pageContainer}
|
|
301
|
+
onWheel={wheelScroll}
|
|
302
|
+
style={{
|
|
303
|
+
height: '100%',
|
|
304
|
+
width: '100%',
|
|
305
|
+
transition: `transform ${animationTimer}ms ${transitionTimingFunction}`,
|
|
306
|
+
outline: 'none',
|
|
307
|
+
}}
|
|
308
|
+
tabIndex={0}>
|
|
309
|
+
{setRenderComponents()}
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
PageScroller.propTypes = {
|
|
316
|
+
animationTimer: PropTypes.number,
|
|
317
|
+
blockScrollDown: PropTypes.bool,
|
|
318
|
+
blockScrollUp: PropTypes.bool,
|
|
319
|
+
children: PropTypes.any.isRequired,
|
|
320
|
+
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
321
|
+
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
322
|
+
customPageNumber: PropTypes.number,
|
|
323
|
+
onScrollUnavailable: PropTypes.func,
|
|
324
|
+
onChange: PropTypes.func,
|
|
325
|
+
renderAllPagesOnFirstRender: PropTypes.bool,
|
|
326
|
+
transitionTimingFunction: PropTypes.string,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
PageScroller.defaultProps = {
|
|
330
|
+
animationTimer: DEFAULT_ANIMATION_TIMER,
|
|
331
|
+
transitionTimingFunction: DEFAULT_ANIMATION,
|
|
332
|
+
height: DEFAULT_CONTAINER_HEIGHT,
|
|
333
|
+
width: DEFAULT_CONTAINER_WIDTH,
|
|
334
|
+
onChange: undefined,
|
|
335
|
+
onScrollUnavailable: undefined,
|
|
336
|
+
blockScrollUp: false,
|
|
337
|
+
blockScrollDown: false,
|
|
338
|
+
renderAllPagesOnFirstRender: false,
|
|
339
|
+
customPageNumber: undefined,
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export default PageScroller;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// eslint-disable-next-line no-unused-vars
|
|
2
|
+
import React, { useEffect, useRef } from 'react';
|
|
3
|
+
|
|
4
|
+
export default function usePrevious(value) {
|
|
5
|
+
const ref = useRef({});
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
ref.current = value;
|
|
9
|
+
}, [value]);
|
|
10
|
+
|
|
11
|
+
return ref.current;
|
|
12
|
+
}
|