@player-ui/reference-assets-plugin-react 0.0.1-next.1
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/index.cjs.js +478 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.esm.js +457 -0
- package/package.json +23 -0
- package/src/assets/action/Action.tsx +31 -0
- package/src/assets/action/hooks.tsx +23 -0
- package/src/assets/action/index.ts +2 -0
- package/src/assets/collection/Collection.tsx +19 -0
- package/src/assets/collection/index.tsx +1 -0
- package/src/assets/index.tsx +5 -0
- package/src/assets/info/Info.tsx +71 -0
- package/src/assets/info/index.tsx +1 -0
- package/src/assets/input/Input.tsx +36 -0
- package/src/assets/input/hooks.tsx +277 -0
- package/src/assets/input/index.tsx +2 -0
- package/src/assets/input/types.ts +6 -0
- package/src/assets/text/Text.tsx +29 -0
- package/src/assets/text/hooks.tsx +31 -0
- package/src/assets/text/index.tsx +2 -0
- package/src/index.tsx +2 -0
- package/src/intro.stories.mdx +40 -0
- package/src/plugin.tsx +53 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useBeacon } from '@player-ui/beacon-plugin-react';
|
|
3
|
+
import type { TransformedInput } from '@player-ui/reference-assets-plugin';
|
|
4
|
+
import type { KeyDownHandler } from './types';
|
|
5
|
+
|
|
6
|
+
export interface InputHookConfig {
|
|
7
|
+
/** Format the input as the user keys down */
|
|
8
|
+
liveFormat?: boolean;
|
|
9
|
+
|
|
10
|
+
/** Skip sending beacon events for this input */
|
|
11
|
+
suppressBeacons?: boolean;
|
|
12
|
+
|
|
13
|
+
/** Time (ms) to wait before formatting the user input for normal keys */
|
|
14
|
+
quickFormatDelay?: number;
|
|
15
|
+
|
|
16
|
+
/** Time (ms) to wait before formatting the input after the user types a special _slow_ format key */
|
|
17
|
+
slowFormatDelay?: number;
|
|
18
|
+
|
|
19
|
+
/** Keys to use a slower formatter for. Usually reserved for backspace, arrows, tabs, etc */
|
|
20
|
+
slowFormatKeys?: Array<number | string>;
|
|
21
|
+
|
|
22
|
+
/** Symbol to be used for decimal point */
|
|
23
|
+
decimalSymbol?: string;
|
|
24
|
+
|
|
25
|
+
/** Affix to append to value - does not save to model and is only for display on input */
|
|
26
|
+
prefix?: string;
|
|
27
|
+
|
|
28
|
+
/** Affix to prepend to value - does not save to model and is only for display on input */
|
|
29
|
+
suffix?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const defaultKeyStrings = [
|
|
33
|
+
'Delete',
|
|
34
|
+
'Backspace',
|
|
35
|
+
'Tab',
|
|
36
|
+
'Home',
|
|
37
|
+
'End',
|
|
38
|
+
'ArrowLeft',
|
|
39
|
+
'ArrowRight',
|
|
40
|
+
'ArrowUp',
|
|
41
|
+
'ArrowDown',
|
|
42
|
+
'Escape',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Create a valid config mixing in defaults and user overrides */
|
|
46
|
+
export const getConfig = (
|
|
47
|
+
userConfig: InputHookConfig = {}
|
|
48
|
+
): Required<InputHookConfig> => {
|
|
49
|
+
return {
|
|
50
|
+
liveFormat: true,
|
|
51
|
+
suppressBeacons: false,
|
|
52
|
+
quickFormatDelay: 200,
|
|
53
|
+
slowFormatDelay: 1000,
|
|
54
|
+
slowFormatKeys: defaultKeyStrings,
|
|
55
|
+
decimalSymbol: '.',
|
|
56
|
+
prefix: '',
|
|
57
|
+
suffix: '',
|
|
58
|
+
...userConfig,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** A hook to manage beacon changes for input assets */
|
|
63
|
+
export const useInputBeacon = (props: TransformedInput) => {
|
|
64
|
+
const beaconHandler = useBeacon({ element: 'text_input', asset: props });
|
|
65
|
+
|
|
66
|
+
return (newValue: string) => {
|
|
67
|
+
let action = 'modified';
|
|
68
|
+
|
|
69
|
+
if (newValue === props.value) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (newValue && !props.value) {
|
|
74
|
+
action = 'added';
|
|
75
|
+
} else if (!newValue && props.value) {
|
|
76
|
+
action = 'deleted';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
beaconHandler({ action });
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A hook to manage an input html element as an asset.
|
|
85
|
+
* The hook returns an object containing props that are expected to reside on any html input.
|
|
86
|
+
* It will handle formatting, setting values, beaconing, aria-labels, etc.
|
|
87
|
+
*
|
|
88
|
+
* @param props - The output of the input transform
|
|
89
|
+
* @param config - Local config to manage user interaction overrides
|
|
90
|
+
*/
|
|
91
|
+
export const useInputAsset = (
|
|
92
|
+
props: TransformedInput,
|
|
93
|
+
config?: InputHookConfig
|
|
94
|
+
) => {
|
|
95
|
+
const [localValue, setLocalValue] = React.useState(props.value ?? '');
|
|
96
|
+
const formatTimerRef = React.useRef<NodeJS.Timeout | undefined>(undefined);
|
|
97
|
+
const inputBeacon = useInputBeacon(props);
|
|
98
|
+
|
|
99
|
+
const {
|
|
100
|
+
liveFormat,
|
|
101
|
+
suppressBeacons,
|
|
102
|
+
quickFormatDelay,
|
|
103
|
+
slowFormatDelay,
|
|
104
|
+
slowFormatKeys,
|
|
105
|
+
decimalSymbol,
|
|
106
|
+
prefix,
|
|
107
|
+
suffix,
|
|
108
|
+
} = getConfig(config);
|
|
109
|
+
|
|
110
|
+
/** Reset and pending format timers */
|
|
111
|
+
function clearPending() {
|
|
112
|
+
if (formatTimerRef.current) {
|
|
113
|
+
clearTimeout(formatTimerRef.current);
|
|
114
|
+
formatTimerRef.current = undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Determines whether pressed key should trigger slow format or quick format delay */
|
|
119
|
+
function getFormatDelaySpeed(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
120
|
+
const key = slowFormatKeys.every((k) => typeof k === 'number')
|
|
121
|
+
? e.which
|
|
122
|
+
: e.key;
|
|
123
|
+
|
|
124
|
+
return slowFormatKeys.includes(key) ? slowFormatDelay : quickFormatDelay;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Affix handling logic on focus */
|
|
128
|
+
function handleAffixOnFocus(target: HTMLInputElement) {
|
|
129
|
+
let val = target.value;
|
|
130
|
+
|
|
131
|
+
if (suffix) val = val.substring(0, val.indexOf(suffix));
|
|
132
|
+
|
|
133
|
+
if (prefix && !val.includes(prefix)) {
|
|
134
|
+
val = `${prefix}${val}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return val;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Edge cases handling for prefix */
|
|
141
|
+
function handlePrefixEdgeCases(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
142
|
+
const target = e.target as HTMLInputElement;
|
|
143
|
+
const start = target.selectionStart;
|
|
144
|
+
const end = target.selectionEnd;
|
|
145
|
+
const pl = prefix.length;
|
|
146
|
+
const atStart = start === pl;
|
|
147
|
+
const atEnd = end === pl;
|
|
148
|
+
|
|
149
|
+
if (start && end && start < pl) {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
target.setSelectionRange(pl, end - start + pl);
|
|
152
|
+
} else if (
|
|
153
|
+
(e.key === 'ArrowLeft' && atStart) ||
|
|
154
|
+
(e.key === 'Backspace' && atStart && atEnd) ||
|
|
155
|
+
e.key === 'Home'
|
|
156
|
+
) {
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
target.setSelectionRange(prefix.length, prefix.length);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Helper to add affixes to value where appropriate */
|
|
163
|
+
function formatValueWithAffix(value: string | undefined) {
|
|
164
|
+
if (!value) return '';
|
|
165
|
+
|
|
166
|
+
return `${prefix}${value}${suffix}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Value handling logic on key down */
|
|
170
|
+
const onKeyDownHandler: KeyDownHandler = (currentValue: string) => {
|
|
171
|
+
const symbolPosition = currentValue.indexOf(decimalSymbol);
|
|
172
|
+
const newValue = props.format(currentValue) ?? '';
|
|
173
|
+
const newSymbolPosition = newValue.indexOf(decimalSymbol);
|
|
174
|
+
|
|
175
|
+
if (
|
|
176
|
+
(symbolPosition === -1 || symbolPosition === 0) &&
|
|
177
|
+
newSymbolPosition > 0
|
|
178
|
+
) {
|
|
179
|
+
// formatting added dot, so set cursor before dot
|
|
180
|
+
return {
|
|
181
|
+
newValue: newValue.includes(prefix)
|
|
182
|
+
? `${newValue}`
|
|
183
|
+
: `${prefix}${newValue}`,
|
|
184
|
+
newCursorPosition: newValue.includes(prefix)
|
|
185
|
+
? newSymbolPosition
|
|
186
|
+
: newSymbolPosition + prefix.length,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
newValue: newValue.includes(prefix)
|
|
192
|
+
? `${newValue}`
|
|
193
|
+
: `${prefix}${newValue}`,
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/** On blur, commit the value to the model */
|
|
198
|
+
const onBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
|
|
199
|
+
clearPending();
|
|
200
|
+
|
|
201
|
+
const formatted =
|
|
202
|
+
(prefix
|
|
203
|
+
? e.target.value.replace(prefix, '')
|
|
204
|
+
: props.format(e.target.value)) ?? '';
|
|
205
|
+
|
|
206
|
+
if (formatted) {
|
|
207
|
+
props.set(formatted);
|
|
208
|
+
setLocalValue(formatValueWithAffix(formatted));
|
|
209
|
+
} else {
|
|
210
|
+
props.set('');
|
|
211
|
+
setLocalValue('');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!suppressBeacons) {
|
|
215
|
+
inputBeacon(formatted);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/** Keep track of any user changes */
|
|
220
|
+
const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
221
|
+
setLocalValue(e.target.value);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/** Schedule a format of the current input in the future */
|
|
225
|
+
const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
|
|
226
|
+
clearPending();
|
|
227
|
+
|
|
228
|
+
if (prefix) handlePrefixEdgeCases(e);
|
|
229
|
+
|
|
230
|
+
const target = e.target as HTMLInputElement;
|
|
231
|
+
|
|
232
|
+
if (liveFormat) {
|
|
233
|
+
formatTimerRef.current = setTimeout(() => {
|
|
234
|
+
const cursorPosition = target.selectionStart;
|
|
235
|
+
const currentValue = target.value;
|
|
236
|
+
|
|
237
|
+
/** Skip formatting if we're in the middle of the input */
|
|
238
|
+
if (cursorPosition !== currentValue.length) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const obj = onKeyDownHandler(currentValue);
|
|
243
|
+
|
|
244
|
+
setLocalValue(obj.newValue);
|
|
245
|
+
target.selectionStart = obj.newCursorPosition ?? target.selectionStart;
|
|
246
|
+
target.selectionEnd = obj.newCursorPosition ?? target.selectionEnd;
|
|
247
|
+
}, getFormatDelaySpeed(e));
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/** Format value onFocus if affixes exist */
|
|
252
|
+
const onFocus: React.FocusEventHandler<HTMLInputElement> = (e) => {
|
|
253
|
+
const target = e.target as HTMLInputElement;
|
|
254
|
+
const inputEmpty = target.value === '';
|
|
255
|
+
|
|
256
|
+
if ((!inputEmpty && suffix) || (inputEmpty && prefix)) {
|
|
257
|
+
setLocalValue(handleAffixOnFocus(target));
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Update the stored value if data changes
|
|
262
|
+
const propsValue = props.value;
|
|
263
|
+
React.useEffect(() => {
|
|
264
|
+
setLocalValue(formatValueWithAffix(propsValue));
|
|
265
|
+
}, [propsValue]);
|
|
266
|
+
|
|
267
|
+
/** clear anything pending on unmount of input */
|
|
268
|
+
React.useEffect(() => clearPending, []);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
onBlur,
|
|
272
|
+
onChange,
|
|
273
|
+
onKeyDown,
|
|
274
|
+
onFocus,
|
|
275
|
+
value: localValue,
|
|
276
|
+
};
|
|
277
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Link } from '@chakra-ui/react';
|
|
3
|
+
import type {
|
|
4
|
+
TextAsset,
|
|
5
|
+
LinkModifier,
|
|
6
|
+
} from '@player-ui/reference-assets-plugin';
|
|
7
|
+
import { useText } from './hooks';
|
|
8
|
+
|
|
9
|
+
/** Find any link modifiers on the text */
|
|
10
|
+
export const getLinkModifier = (asset: TextAsset): LinkModifier | undefined => {
|
|
11
|
+
return asset.modifiers?.find(
|
|
12
|
+
(mod) =>
|
|
13
|
+
mod.type === 'link' &&
|
|
14
|
+
(mod.metaData as LinkModifier['metaData'])?.ref !== undefined
|
|
15
|
+
) as LinkModifier;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** A text asset */
|
|
19
|
+
export const Text = (props: TextAsset) => {
|
|
20
|
+
const spanProps = useText(props);
|
|
21
|
+
const linkModifier = getLinkModifier(props);
|
|
22
|
+
const { value } = props;
|
|
23
|
+
|
|
24
|
+
if (linkModifier) {
|
|
25
|
+
return <Link href={linkModifier.metaData.ref}>{value}</Link>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return <span {...spanProps}>{value}</span>;
|
|
29
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import makeClass from 'clsx';
|
|
3
|
+
import { useAssetProps } from '@player-ui/react-utils';
|
|
4
|
+
import type { TextAsset } from '@player-ui/reference-assets-plugin';
|
|
5
|
+
|
|
6
|
+
export interface TextModifierContextType {
|
|
7
|
+
getClassForModifier?<T>(modifier: T): string | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const TextModifierContext = React.createContext<
|
|
11
|
+
TextModifierContextType | undefined
|
|
12
|
+
>(undefined);
|
|
13
|
+
|
|
14
|
+
/** Get the props for a basic text element */
|
|
15
|
+
export const useText = (props: TextAsset): JSX.IntrinsicElements['span'] => {
|
|
16
|
+
let className: string | undefined;
|
|
17
|
+
|
|
18
|
+
const modifierContext = useContext(TextModifierContext);
|
|
19
|
+
|
|
20
|
+
if (props.modifiers && modifierContext?.getClassForModifier) {
|
|
21
|
+
className = makeClass(
|
|
22
|
+
...props.modifiers.map(modifierContext.getClassForModifier)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...useAssetProps(props),
|
|
28
|
+
className,
|
|
29
|
+
children: props.value,
|
|
30
|
+
};
|
|
31
|
+
};
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Reference Assets/Intro" />
|
|
4
|
+
|
|
5
|
+
# Reference Assets
|
|
6
|
+
|
|
7
|
+
The assets in this section are an example implementation of an Asset Library for Player.
|
|
8
|
+
Each of the Asset components follows the same basic pattern:
|
|
9
|
+
|
|
10
|
+
| Export | Description |
|
|
11
|
+
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
12
|
+
| `<Name>Asset` | Typescript type interface representing the authored player content |
|
|
13
|
+
| `Transformed<Name>Asset` | (optional) - A Typescript interface representing the _transformed_ version of the asset interface. This extends the base interface and includes any state/properties added by the associated transform. |
|
|
14
|
+
| `transform` | (optional) - A function (used by the `AssetTransformPlugin`) to adopt Player's current state with the component state. If no `transfrom` is present, the component state is the same as the authored content. |
|
|
15
|
+
| `Component` | React component rendering of the given asset. The props are the _transformed_ type (if supplied) or the _authored_ type. |
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
To import and use these Assets in your App, simply import the `plugin` and attach it to Player:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { useWebPlayer, WebPlayerPlugin } from '@player-ui/react';
|
|
23
|
+
import ReferenceAssetsPlugin from '@player-ui/reference-assets/plugin-react';
|
|
24
|
+
|
|
25
|
+
const plugins: Array<WebPlayerPlugin> = [new ReferenceAssetsPlugin()];
|
|
26
|
+
|
|
27
|
+
export const App = () => {
|
|
28
|
+
// Create Player with our plugins
|
|
29
|
+
const { webPlayer } = useWebPlayer({ plugins });
|
|
30
|
+
|
|
31
|
+
// Start the flow.
|
|
32
|
+
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
webPlayer.start(/** Insert your player content */);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
// Render Player
|
|
38
|
+
return <webPlayer.Component />;
|
|
39
|
+
};
|
|
40
|
+
```
|
package/src/plugin.tsx
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { WebPlayer, WebPlayerPlugin } from '@player-ui/react';
|
|
3
|
+
import type { Player } from '@player-ui/player';
|
|
4
|
+
import { AssetProviderPlugin } from '@player-ui/asset-provider-plugin-react';
|
|
5
|
+
import { ChakraProvider, useTheme } from '@chakra-ui/react';
|
|
6
|
+
import { ReferenceAssetsPlugin as ReferenceAssetsCorePlugin } from '@player-ui/reference-assets-plugin';
|
|
7
|
+
import { Input, Text, Collection, Action, Info } from './assets';
|
|
8
|
+
|
|
9
|
+
const OptionalChakraThemeProvider = (
|
|
10
|
+
props: React.PropsWithChildren<unknown>
|
|
11
|
+
) => {
|
|
12
|
+
const theme = useTheme();
|
|
13
|
+
|
|
14
|
+
if (theme) {
|
|
15
|
+
// eslint-disable-next-line react/jsx-no-useless-fragment
|
|
16
|
+
return <>{props.children}</>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return <ChakraProvider>{props.children}</ChakraProvider>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A plugin to register the base reference assets
|
|
24
|
+
*/
|
|
25
|
+
export class ReferenceAssetsPlugin implements WebPlayerPlugin {
|
|
26
|
+
name = 'reference-assets-web-plugin';
|
|
27
|
+
|
|
28
|
+
applyWeb(webplayer: WebPlayer) {
|
|
29
|
+
webplayer.registerPlugin(
|
|
30
|
+
new AssetProviderPlugin([
|
|
31
|
+
['input', Input],
|
|
32
|
+
['text', Text],
|
|
33
|
+
['action', Action],
|
|
34
|
+
['info', Info],
|
|
35
|
+
['collection', Collection],
|
|
36
|
+
])
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
webplayer.hooks.webComponent.tap(this.name, (Comp) => {
|
|
40
|
+
return () => {
|
|
41
|
+
return (
|
|
42
|
+
<OptionalChakraThemeProvider>
|
|
43
|
+
<Comp />
|
|
44
|
+
</OptionalChakraThemeProvider>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
apply(player: Player) {
|
|
51
|
+
player.registerPlugin(new ReferenceAssetsCorePlugin());
|
|
52
|
+
}
|
|
53
|
+
}
|