@pushwoosh/dumb-components 1.1.218 → 1.1.220
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/EmojiPicker/EmojiPicker.js +20 -10
- package/EmojiPicker/types.d.ts +1 -0
- package/RichMessageEditor/EditorEmojiPicker.helpers.d.ts +2 -0
- package/RichMessageEditor/EditorEmojiPicker.helpers.js +5 -0
- package/RichMessageEditor/EditorEmojiPicker.js +8 -2
- package/RichMessageEditor/EditorEmojiPicker.styled.d.ts +4 -1
- package/RichMessageEditor/EditorEmojiPicker.styled.js +5 -2
- package/RichMessageEditor/slate/DynamicContentElement.js +3 -1
- package/RichMessageEditor/slate/helpers.d.ts +1 -1
- package/RichMessageEditor/slate/helpers.js +3 -3
- package/hooks/index.d.ts +1 -0
- package/hooks/index.js +2 -1
- package/hooks/useThemeName.d.ts +6 -0
- package/hooks/useThemeName.js +30 -0
- package/package.json +1 -1
- package/shared/dynamicContent/__tests__/token.test.d.ts +1 -0
- package/shared/dynamicContent/__tests__/token.test.js +32 -0
- package/shared/dynamicContent/index.d.ts +1 -1
- package/shared/dynamicContent/index.js +1 -1
- package/shared/dynamicContent/token.d.ts +2 -0
- package/shared/dynamicContent/token.js +10 -0
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
import { Picker } from 'emoji-mart';
|
|
2
2
|
import { createElement, useEffect, useRef } from 'react';
|
|
3
|
+
import { useThemeName } from '../hooks';
|
|
3
4
|
async function data() {
|
|
4
5
|
const response = await fetch('https://cdn.jsdelivr.net/npm/@emoji-mart/data');
|
|
5
6
|
return response.json();
|
|
6
7
|
}
|
|
7
|
-
export const EmojiPicker =
|
|
8
|
+
export const EmojiPicker = ({
|
|
9
|
+
theme,
|
|
10
|
+
...props
|
|
11
|
+
}) => {
|
|
8
12
|
const ref = useRef(null);
|
|
9
13
|
const instance = useRef(null);
|
|
14
|
+
const themeOfBuiltPicker = useRef(null);
|
|
15
|
+
const observedTheme = useThemeName(ref, !theme);
|
|
16
|
+
const pickerTheme = theme ?? observedTheme;
|
|
10
17
|
useEffect(() => {
|
|
11
|
-
|
|
18
|
+
const isThemeAlreadyBuilt = instance.current && themeOfBuiltPicker.current === pickerTheme;
|
|
19
|
+
if (isThemeAlreadyBuilt) {
|
|
12
20
|
instance.current.update({
|
|
13
21
|
data,
|
|
14
22
|
...props
|
|
15
23
|
});
|
|
16
|
-
|
|
17
|
-
const PickerElement = window?.customElements.get('em-emoji-picker');
|
|
18
|
-
instance.current = new (PickerElement || Picker)({
|
|
19
|
-
data,
|
|
20
|
-
...props,
|
|
21
|
-
ref
|
|
22
|
-
});
|
|
24
|
+
return;
|
|
23
25
|
}
|
|
24
|
-
|
|
26
|
+
const PickerElement = window?.customElements.get('em-emoji-picker');
|
|
27
|
+
instance.current = new (PickerElement || Picker)({
|
|
28
|
+
data,
|
|
29
|
+
...props,
|
|
30
|
+
theme: pickerTheme,
|
|
31
|
+
ref
|
|
32
|
+
});
|
|
33
|
+
themeOfBuiltPicker.current = pickerTheme;
|
|
34
|
+
}, [props, pickerTheme]);
|
|
25
35
|
return createElement('div', {
|
|
26
36
|
ref
|
|
27
37
|
});
|
package/EmojiPicker/types.d.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from 'react';
|
|
2
3
|
import { EditorEmojiPickerStyled } from './EditorEmojiPicker.styled';
|
|
3
4
|
import { EmojiPicker } from '../EmojiPicker';
|
|
5
|
+
import { useThemeName } from '../hooks';
|
|
4
6
|
export const EditorEmojiPicker = ({
|
|
5
7
|
onEmojiSelect
|
|
6
8
|
}) => {
|
|
9
|
+
const ref = useRef(null);
|
|
10
|
+
const theme = useThemeName(ref);
|
|
7
11
|
return _jsx(EditorEmojiPickerStyled, {
|
|
12
|
+
ref: ref,
|
|
13
|
+
"$theme": theme,
|
|
8
14
|
children: _jsx(EmojiPicker, {
|
|
15
|
+
theme: theme,
|
|
9
16
|
onEmojiSelect: emoji => {
|
|
10
17
|
onEmojiSelect(emoji.native);
|
|
11
|
-
}
|
|
12
|
-
theme: "light"
|
|
18
|
+
}
|
|
13
19
|
})
|
|
14
20
|
});
|
|
15
21
|
};
|
|
@@ -1 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ThemeName } from '@pushwoosh/kit-constants';
|
|
2
|
+
export declare const EditorEmojiPickerStyled: import("styled-components").StyledComponent<"div", any, {
|
|
3
|
+
$theme: ThemeName;
|
|
4
|
+
}, never>;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import styled from 'styled-components';
|
|
2
|
-
import { Color, Shadow, ShapeRadius } from '@pushwoosh/kit-constants';
|
|
2
|
+
import { Color, getThemeColors, Shadow, ShapeRadius } from '@pushwoosh/kit-constants';
|
|
3
|
+
import { rgbChannels } from './EditorEmojiPicker.helpers';
|
|
3
4
|
export const EditorEmojiPickerStyled = styled.div.withConfig({
|
|
4
5
|
displayName: "EditorEmojiPickerStyled",
|
|
5
6
|
componentId: "sc-sdjrip-0"
|
|
6
|
-
})(["min-width:352px;em-emoji-picker{border:1px solid ", ";--border-radius:", ";--shadow:", ";background-color:", ";--rgb-background:", ";}"], Color.FORM, ShapeRadius.CONTROL, Shadow.REGULAR, Color.FROZEN,
|
|
7
|
+
})(["min-width:352px;em-emoji-picker{border:1px solid ", ";--border-radius:", ";--shadow:", ";background-color:", ";--rgb-background:", ";}"], Color.FORM, ShapeRadius.CONTROL, Shadow.REGULAR, Color.FROZEN, ({
|
|
8
|
+
$theme
|
|
9
|
+
}) => rgbChannels(getThemeColors($theme).FROZEN));
|
|
@@ -3,6 +3,7 @@ import { Transforms } from 'slate';
|
|
|
3
3
|
import { useSelected, useSlateStatic } from 'slate-react';
|
|
4
4
|
import { findPath } from './helpers';
|
|
5
5
|
import { Chip } from '../../Chip';
|
|
6
|
+
import { isResolvableDc } from '../../shared/dynamicContent';
|
|
6
7
|
const DynamicContentElementComponent = ({
|
|
7
8
|
attributes,
|
|
8
9
|
element,
|
|
@@ -14,11 +15,12 @@ const DynamicContentElementComponent = ({
|
|
|
14
15
|
const {
|
|
15
16
|
tagName
|
|
16
17
|
} = element.dc;
|
|
18
|
+
const isResolvable = isResolvableDc(element.dc);
|
|
17
19
|
return _jsxs(Chip, {
|
|
18
20
|
...attributes,
|
|
19
21
|
as: "span",
|
|
20
22
|
contentEditable: false,
|
|
21
|
-
"$variant":
|
|
23
|
+
"$variant": isResolvable ? 'primary' : 'danger',
|
|
22
24
|
"$selected": selected,
|
|
23
25
|
"$onClose": event => {
|
|
24
26
|
event.preventDefault();
|
|
@@ -41,7 +41,7 @@ export declare function string2content(value: string): SlateEditorContent;
|
|
|
41
41
|
*/
|
|
42
42
|
export declare function content2string(state: SlateEditorContent): string;
|
|
43
43
|
/**
|
|
44
|
-
* Validates
|
|
44
|
+
* Validates that every dynamic content element is one the sender can substitute
|
|
45
45
|
* @param value - The Slate editor content to validate
|
|
46
46
|
* @returns True if all dynamic content elements are valid, false otherwise
|
|
47
47
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseDc, splitDynamicContent, stringifyDc } from '../../shared/dynamicContent';
|
|
1
|
+
import { isResolvableDc, parseDc, splitDynamicContent, stringifyDc } from '../../shared/dynamicContent';
|
|
2
2
|
/**
|
|
3
3
|
* Checks if a Slate node is a custom element
|
|
4
4
|
* @param x - The node to check
|
|
@@ -127,14 +127,14 @@ export function content2string(state) {
|
|
|
127
127
|
}).join('\n');
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
|
-
* Validates
|
|
130
|
+
* Validates that every dynamic content element is one the sender can substitute
|
|
131
131
|
* @param value - The Slate editor content to validate
|
|
132
132
|
* @returns True if all dynamic content elements are valid, false otherwise
|
|
133
133
|
*/
|
|
134
134
|
export function validateContent(value) {
|
|
135
135
|
for (const el of value) {
|
|
136
136
|
if (isElement(el)) {
|
|
137
|
-
if (el.type === 'dc' && !el.dc
|
|
137
|
+
if (el.type === 'dc' && !isResolvableDc(el.dc)) {
|
|
138
138
|
return false;
|
|
139
139
|
}
|
|
140
140
|
if (!validateContent(el.children)) {
|
package/hooks/index.d.ts
CHANGED
package/hooks/index.js
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { type ThemeName } from '@pushwoosh/kit-constants';
|
|
3
|
+
/** Current theme as a value, for widgets that cannot read `var(--pw-color-*)` — canvas, shadow DOM.
|
|
4
|
+
* @param ref - element inside the theme scope, so a light island wins over the document theme
|
|
5
|
+
* @param isEnabled - pass `false` when the caller already knows the theme and needs no observer */
|
|
6
|
+
export declare function useThemeName(ref?: RefObject<Element>, isEnabled?: boolean): ThemeName;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useLayoutEffect, useState } from 'react';
|
|
2
|
+
import { DEFAULT_THEME, THEME_ATTRIBUTE } from '@pushwoosh/kit-constants';
|
|
3
|
+
function readThemeName(element) {
|
|
4
|
+
if (typeof document === 'undefined') {
|
|
5
|
+
return DEFAULT_THEME;
|
|
6
|
+
}
|
|
7
|
+
const scope = element?.closest(`[${THEME_ATTRIBUTE}]`) ?? document.documentElement;
|
|
8
|
+
return scope.getAttribute(THEME_ATTRIBUTE) === 'dark' ? 'dark' : DEFAULT_THEME;
|
|
9
|
+
}
|
|
10
|
+
/** Current theme as a value, for widgets that cannot read `var(--pw-color-*)` — canvas, shadow DOM.
|
|
11
|
+
* @param ref - element inside the theme scope, so a light island wins over the document theme
|
|
12
|
+
* @param isEnabled - pass `false` when the caller already knows the theme and needs no observer */
|
|
13
|
+
export function useThemeName(ref, isEnabled = true) {
|
|
14
|
+
const [themeName, setThemeName] = useState(() => readThemeName(ref?.current ?? null));
|
|
15
|
+
useLayoutEffect(() => {
|
|
16
|
+
if (!isEnabled) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const syncThemeName = () => setThemeName(readThemeName(ref?.current ?? null));
|
|
20
|
+
syncThemeName();
|
|
21
|
+
const observer = new MutationObserver(syncThemeName);
|
|
22
|
+
observer.observe(document.documentElement, {
|
|
23
|
+
attributes: true,
|
|
24
|
+
attributeFilter: [THEME_ATTRIBUTE],
|
|
25
|
+
subtree: true
|
|
26
|
+
});
|
|
27
|
+
return () => observer.disconnect();
|
|
28
|
+
}, [ref, isEnabled]);
|
|
29
|
+
return themeName;
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { validateContentSting } from '../../../RichMessageEditor/slate/helpers';
|
|
2
|
+
import { isResolvableDc, parseDc } from '../token';
|
|
3
|
+
// The lists below mirror channels/publisher
|
|
4
|
+
// internal/services/dynamiccontentreplacer/constants.go — keep them in sync.
|
|
5
|
+
const caseInsensitiveModifiers = ['capitalizefirst', 'capitalizeallfirst', 'uppercase', 'lowercase', 'regular', 'base64', 'cent', 'dollar', 'comma', 'euro', 'jpy', 'lira'];
|
|
6
|
+
const dateModifiers = ['M-d-y', 'm-d-y', 'M d y', 'M d Y', 'l', 'M d', 'H:i', 'm-d-y H:i'];
|
|
7
|
+
describe('isResolvableDc', () => {
|
|
8
|
+
test.each(caseInsensitiveModifiers)('accepts %s', modifier => {
|
|
9
|
+
expect(isResolvableDc(parseDc(`{Name|${modifier}}`))).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
test.each(caseInsensitiveModifiers)('accepts %s whatever the case', modifier => {
|
|
12
|
+
expect(isResolvableDc(parseDc(`{Name|${modifier.toUpperCase()}}`))).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
test.each(dateModifiers)('accepts the date modifier %s', modifier => {
|
|
15
|
+
expect(isResolvableDc(parseDc(`{Name|${modifier}}`))).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
test('rejects a date modifier in the wrong case, as the sender does', () => {
|
|
18
|
+
expect(isResolvableDc(parseDc('{Name|m d y}'))).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
test.each([['{Name|}', 'no modifier at all'], ['{Name|nope}', 'a modifier outside the list'], ['{|regular}', 'no tag name']])('rejects %s (%s)', token => {
|
|
21
|
+
expect(isResolvableDc(parseDc(token))).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
test('keeps a default value out of the verdict', () => {
|
|
24
|
+
expect(isResolvableDc(parseDc('{WalletPassLink|regular|no pass}'))).toBe(true);
|
|
25
|
+
expect(isResolvableDc(parseDc('{WalletPassLink||no pass}'))).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('validateContentSting', () => {
|
|
29
|
+
test.each([['Tap here!\n{WalletSerialNo|regular}', true], ['Tap here!\n{WalletSerialNo|}', false], ['{WalletSerialNo|regular}{WalletPassLink|}', false], ['Hi {{ user.name | upcase }}, plain liquid is not a token', true], ['no tokens at all', true], ['', true]])('%s -> %s', (content, expected) => {
|
|
30
|
+
expect(validateContentSting(content)).toBe(expected);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { DynamicContentChip, type DynamicContentChipProps } from './DynamicContentChip';
|
|
2
2
|
export { DynamicContentText, type DynamicContentTextProps } from './DynamicContentText';
|
|
3
|
-
export { parseDc, splitDynamicContent, stringifyDc } from './token';
|
|
3
|
+
export { isResolvableDc, parseDc, splitDynamicContent, stringifyDc } from './token';
|
|
4
4
|
export { Paragraph } from './styles';
|
|
5
5
|
export type { DynamicContent, DynamicContentPart, DynamicContentTone } from './types';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { DynamicContentChip } from './DynamicContentChip';
|
|
2
2
|
export { DynamicContentText } from './DynamicContentText';
|
|
3
|
-
export { parseDc, splitDynamicContent, stringifyDc } from './token';
|
|
3
|
+
export { isResolvableDc, parseDc, splitDynamicContent, stringifyDc } from './token';
|
|
4
4
|
export { Paragraph } from './styles';
|
|
@@ -18,3 +18,5 @@ export declare function splitDynamicContent(text: string): DynamicContentPart[];
|
|
|
18
18
|
export declare function parseDc(value: string): DynamicContent;
|
|
19
19
|
/** Serializes a token back to `{tagName|modifier|defaultValue}`. */
|
|
20
20
|
export declare function stringifyDc(dc: DynamicContent): string;
|
|
21
|
+
/** Whether the sender substitutes this token, or ships it verbatim (dates are case-sensitive, other modifiers are not). */
|
|
22
|
+
export declare function isResolvableDc(dc: DynamicContent): boolean;
|
|
@@ -42,4 +42,14 @@ export function stringifyDc(dc) {
|
|
|
42
42
|
defaultValue
|
|
43
43
|
} = dc;
|
|
44
44
|
return `{${tagName}|${modifier}${defaultValue ? `|${defaultValue}` : ''}}`;
|
|
45
|
+
}
|
|
46
|
+
const CASE_INSENSITIVE_MODIFIERS = ['capitalizefirst', 'capitalizeallfirst', 'uppercase', 'lowercase', 'regular', 'base64', 'cent', 'dollar', 'comma', 'euro', 'jpy', 'lira'];
|
|
47
|
+
const DATE_MODIFIERS = ['M-d-y', 'm-d-y', 'M d y', 'M d Y', 'l', 'M d', 'H:i', 'm-d-y H:i'];
|
|
48
|
+
/** Whether the sender substitutes this token, or ships it verbatim (dates are case-sensitive, other modifiers are not). */
|
|
49
|
+
export function isResolvableDc(dc) {
|
|
50
|
+
const {
|
|
51
|
+
tagName,
|
|
52
|
+
modifier
|
|
53
|
+
} = dc;
|
|
54
|
+
return Boolean(tagName) && (CASE_INSENSITIVE_MODIFIERS.includes(modifier.toLowerCase()) || DATE_MODIFIERS.includes(modifier));
|
|
45
55
|
}
|