dce-reactkit 3.0.42 → 3.0.43
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/dist/cjs/index.js +124 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/components/CopiableBox.d.ts +16 -0
- package/dist/cjs/types/index.d.ts +2 -1
- package/dist/esm/index.js +126 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/components/CopiableBox.d.ts +16 -0
- package/dist/esm/types/index.d.ts +2 -1
- package/dist/index.d.ts +41 -25
- package/package.json +1 -1
- package/src/components/CopiableBox.tsx +267 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copiable text box
|
|
3
|
+
* @author Gabe Abrams
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Import React
|
|
7
|
+
import React, { useReducer } from 'react';
|
|
8
|
+
|
|
9
|
+
// Import other reactkit functions
|
|
10
|
+
import { alert, waitMs } from '..';
|
|
11
|
+
|
|
12
|
+
// Import FontAwesome
|
|
13
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
14
|
+
import { faClipboard } from '@fortawesome/free-solid-svg-icons';
|
|
15
|
+
|
|
16
|
+
/*------------------------------------------------------------------------*/
|
|
17
|
+
/* Types */
|
|
18
|
+
/*------------------------------------------------------------------------*/
|
|
19
|
+
|
|
20
|
+
// Props definition
|
|
21
|
+
type Props = {
|
|
22
|
+
// Unique name of the item to copy (no spaces, compatible with css classes)
|
|
23
|
+
name: string,
|
|
24
|
+
// The text to copy
|
|
25
|
+
text: string,
|
|
26
|
+
// Human-readable label of the copy field
|
|
27
|
+
label: string,
|
|
28
|
+
// FontAwesome icon to place before the label
|
|
29
|
+
labelIcon?: any,
|
|
30
|
+
// If defined, the label will have a minimum width
|
|
31
|
+
minLabelWidthRem: number,
|
|
32
|
+
// If true, the box will be a textarea to support larger, multiline text
|
|
33
|
+
multiline?: boolean,
|
|
34
|
+
// Number of lines to show in multiline view (only relevant if multiline)
|
|
35
|
+
numVisibleLines?: number,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/*------------------------------------------------------------------------*/
|
|
39
|
+
/* State */
|
|
40
|
+
/*------------------------------------------------------------------------*/
|
|
41
|
+
|
|
42
|
+
/* -------- State Definition -------- */
|
|
43
|
+
|
|
44
|
+
type State = {
|
|
45
|
+
// True if text was recently copied
|
|
46
|
+
recentlyCopied: boolean,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/* ------------- Actions ------------ */
|
|
50
|
+
|
|
51
|
+
// Types of actions
|
|
52
|
+
enum ActionType {
|
|
53
|
+
// Indicate that the text was recently copied
|
|
54
|
+
IndicateRecentlyCopied = 'indicate-recently-copied',
|
|
55
|
+
// Clear the status
|
|
56
|
+
ClearRecentlyCopiedStatus = 'clear-recently-copied-status',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Action definitions
|
|
60
|
+
type Action = {
|
|
61
|
+
// Action type
|
|
62
|
+
type: (
|
|
63
|
+
| ActionType.IndicateRecentlyCopied
|
|
64
|
+
| ActionType.ClearRecentlyCopiedStatus
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reducer that executes actions
|
|
70
|
+
* @author Gabe Abrams
|
|
71
|
+
* @param state current state
|
|
72
|
+
* @param action action to execute
|
|
73
|
+
*/
|
|
74
|
+
const reducer = (state: State, action: Action): State => {
|
|
75
|
+
switch (action.type) {
|
|
76
|
+
case ActionType.IndicateRecentlyCopied: {
|
|
77
|
+
return {
|
|
78
|
+
recentlyCopied: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
case ActionType.ClearRecentlyCopiedStatus: {
|
|
82
|
+
return {
|
|
83
|
+
recentlyCopied: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
default: {
|
|
87
|
+
return state;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/*------------------------------------------------------------------------*/
|
|
93
|
+
/* Component */
|
|
94
|
+
/*------------------------------------------------------------------------*/
|
|
95
|
+
|
|
96
|
+
const CopiableBox: React.FC<Props> = (props) => {
|
|
97
|
+
/*------------------------------------------------------------------------*/
|
|
98
|
+
/* Setup */
|
|
99
|
+
/*------------------------------------------------------------------------*/
|
|
100
|
+
|
|
101
|
+
/* -------------- Props ------------- */
|
|
102
|
+
|
|
103
|
+
// Destructure all props
|
|
104
|
+
const {
|
|
105
|
+
name,
|
|
106
|
+
text,
|
|
107
|
+
label,
|
|
108
|
+
labelIcon,
|
|
109
|
+
minLabelWidthRem,
|
|
110
|
+
multiline,
|
|
111
|
+
numVisibleLines = 10,
|
|
112
|
+
} = props;
|
|
113
|
+
|
|
114
|
+
/* -------------- State ------------- */
|
|
115
|
+
|
|
116
|
+
// Initial state
|
|
117
|
+
const initialState: State = {
|
|
118
|
+
recentlyCopied: false,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Initialize state
|
|
122
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
123
|
+
|
|
124
|
+
// Destructure common state
|
|
125
|
+
const {
|
|
126
|
+
recentlyCopied,
|
|
127
|
+
} = state;
|
|
128
|
+
|
|
129
|
+
/*------------------------------------------------------------------------*/
|
|
130
|
+
/* Component Functions */
|
|
131
|
+
/*------------------------------------------------------------------------*/
|
|
132
|
+
|
|
133
|
+
// Determine the id for the copiable text field
|
|
134
|
+
const copiableFieldClassName = `CopiableBox-text-box-${name}`;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Perform a copy
|
|
138
|
+
* @author Gabe Abrams
|
|
139
|
+
*/
|
|
140
|
+
const performCopy = async () => {
|
|
141
|
+
// Write to clipboard
|
|
142
|
+
let copyFailed = false;
|
|
143
|
+
try {
|
|
144
|
+
await navigator.clipboard.writeText(text);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
copyFailed = true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Try copy again if it failed
|
|
150
|
+
if (copyFailed) {
|
|
151
|
+
try {
|
|
152
|
+
const input = (
|
|
153
|
+
document.getElementsByClassName(copiableFieldClassName)[0]
|
|
154
|
+
) as HTMLInputElement;
|
|
155
|
+
input.focus();
|
|
156
|
+
input.select();
|
|
157
|
+
document.execCommand('copy');
|
|
158
|
+
input.blur();
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return alert(
|
|
161
|
+
'Unable to copy',
|
|
162
|
+
'Oops! We couldn\'t copy that to the clipboard. Please copy the text manually.',
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Show copied notice
|
|
168
|
+
dispatch({
|
|
169
|
+
type: ActionType.IndicateRecentlyCopied,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Wait a moment
|
|
173
|
+
await waitMs(4000);
|
|
174
|
+
|
|
175
|
+
// Hide copied notice
|
|
176
|
+
dispatch({
|
|
177
|
+
type: ActionType.ClearRecentlyCopiedStatus,
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/*------------------------------------------------------------------------*/
|
|
182
|
+
/* Render */
|
|
183
|
+
/*------------------------------------------------------------------------*/
|
|
184
|
+
|
|
185
|
+
/*----------------------------------------*/
|
|
186
|
+
/* Main UI */
|
|
187
|
+
/*----------------------------------------*/
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div className="input-group mb-2">
|
|
191
|
+
{/* Label */}
|
|
192
|
+
<span
|
|
193
|
+
className="input-group-text"
|
|
194
|
+
style={{
|
|
195
|
+
minWidth: (
|
|
196
|
+
minLabelWidthRem
|
|
197
|
+
? `${minLabelWidthRem}rem`
|
|
198
|
+
: undefined
|
|
199
|
+
),
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{labelIcon && (
|
|
203
|
+
<FontAwesomeIcon
|
|
204
|
+
icon={labelIcon}
|
|
205
|
+
className="me-1"
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
208
|
+
{label}
|
|
209
|
+
</span>
|
|
210
|
+
|
|
211
|
+
{/* Text */}
|
|
212
|
+
{
|
|
213
|
+
multiline
|
|
214
|
+
? (
|
|
215
|
+
<textarea
|
|
216
|
+
className={`${copiableFieldClassName} CopiableBox-text-multiline form-control bg-white text-dark`}
|
|
217
|
+
value={text}
|
|
218
|
+
aria-label={`${label} text`}
|
|
219
|
+
rows={numVisibleLines}
|
|
220
|
+
readOnly
|
|
221
|
+
/>
|
|
222
|
+
)
|
|
223
|
+
: (
|
|
224
|
+
<input
|
|
225
|
+
type="text"
|
|
226
|
+
className={`${copiableFieldClassName} CopiableBox-text-single-line form-control bg-white text-dark`}
|
|
227
|
+
value={text}
|
|
228
|
+
aria-label={`${label} text`}
|
|
229
|
+
readOnly
|
|
230
|
+
/>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
<button
|
|
235
|
+
className="btn btn-secondary"
|
|
236
|
+
type="button"
|
|
237
|
+
aria-label={`copy ${label} to the clipboard`}
|
|
238
|
+
disabled={recentlyCopied}
|
|
239
|
+
style={{
|
|
240
|
+
minWidth: '5.2rem',
|
|
241
|
+
}}
|
|
242
|
+
onClick={performCopy}
|
|
243
|
+
>
|
|
244
|
+
{
|
|
245
|
+
recentlyCopied
|
|
246
|
+
? 'Copied!'
|
|
247
|
+
: (
|
|
248
|
+
<span>
|
|
249
|
+
<FontAwesomeIcon
|
|
250
|
+
icon={faClipboard}
|
|
251
|
+
className="me-1"
|
|
252
|
+
/>
|
|
253
|
+
Copy
|
|
254
|
+
</span>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/*------------------------------------------------------------------------*/
|
|
263
|
+
/* Wrap Up */
|
|
264
|
+
/*------------------------------------------------------------------------*/
|
|
265
|
+
|
|
266
|
+
// Export component
|
|
267
|
+
export default CopiableBox;
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import Drawer from './components/Drawer';
|
|
|
12
12
|
import PopSuccessMark from './components/PopSuccessMark';
|
|
13
13
|
import PopFailureMark from './components/PopFailureMark';
|
|
14
14
|
import PopPendingMark from './components/PopPendingMark';
|
|
15
|
+
import CopiableBox from './components/CopiableBox';
|
|
15
16
|
|
|
16
17
|
// Import errors
|
|
17
18
|
import ErrorWithCode from './errors/ErrorWithCode';
|
|
@@ -67,6 +68,7 @@ export {
|
|
|
67
68
|
PopSuccessMark,
|
|
68
69
|
PopFailureMark,
|
|
69
70
|
PopPendingMark,
|
|
71
|
+
CopiableBox,
|
|
70
72
|
// Global functions
|
|
71
73
|
alert,
|
|
72
74
|
confirm,
|