mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 10:37:01 +00:00
feat(client): add delimiters props to variable text (#5620)
* feat(client): add delimiters props to variable text * refactor(plugin-notification-iam): change delimiters for url in message template
This commit is contained in:
parent
7d0aadcf52
commit
90d9231fc5
@ -24,8 +24,6 @@ import { useStyles } from './style';
|
|||||||
|
|
||||||
type RangeIndexes = [number, number, number, number];
|
type RangeIndexes = [number, number, number, number];
|
||||||
|
|
||||||
const VARIABLE_RE = /{{\s*([^{}]+)\s*}}/g;
|
|
||||||
|
|
||||||
function pasteHTML(
|
function pasteHTML(
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
html: string,
|
html: string,
|
||||||
@ -92,11 +90,11 @@ function pasteHTML(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getValue(el) {
|
function getValue(el, delimiters = ['{{', '}}']) {
|
||||||
const values: any[] = [];
|
const values: any[] = [];
|
||||||
for (const node of el.childNodes) {
|
for (const node of el.childNodes) {
|
||||||
if (node.nodeName === 'SPAN' && node['dataset']['variable']) {
|
if (node.nodeName === 'SPAN' && node['dataset']['variable']) {
|
||||||
values.push(`{{${node['dataset']['variable']}}}`);
|
values.push(`${delimiters[0]}${node['dataset']['variable']}${delimiters[1]}`);
|
||||||
} else {
|
} else {
|
||||||
values.push(node.textContent);
|
values.push(node.textContent);
|
||||||
}
|
}
|
||||||
@ -104,8 +102,9 @@ function getValue(el) {
|
|||||||
return values.join('');
|
return values.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHTML(exp: string, keyLabelMap) {
|
function renderHTML(exp: string, keyLabelMap, delimiters: [string, string] = ['{{', '}}']) {
|
||||||
return exp.replace(VARIABLE_RE, (_, i) => {
|
const variableRegExp = new RegExp(`${delimiters[0]}\\s*([^{}]+)\\s*${delimiters[1]}`, 'g');
|
||||||
|
return exp.replace(variableRegExp, (_, i) => {
|
||||||
const key = i.trim();
|
const key = i.trim();
|
||||||
return createVariableTagHTML(key, keyLabelMap) ?? '';
|
return createVariableTagHTML(key, keyLabelMap) ?? '';
|
||||||
});
|
});
|
||||||
@ -210,9 +209,22 @@ function getCurrentRange(element: HTMLElement): RangeIndexes {
|
|||||||
|
|
||||||
const defaultFieldNames = { value: 'value', label: 'label' };
|
const defaultFieldNames = { value: 'value', label: 'label' };
|
||||||
|
|
||||||
|
function useVariablesFromValue(value: string, delimiters: [string, string] = ['{{', '}}']) {
|
||||||
|
const delimitersString = delimiters.join(' ');
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!value?.trim()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const variableRegExp = new RegExp(`${delimiters[0]}\\s*([^{}]+)\\s*${delimiters[1]}`, 'g');
|
||||||
|
const matches = value.match(variableRegExp);
|
||||||
|
return matches?.map((m) => m.replace(variableRegExp, '$1')) ?? [];
|
||||||
|
}, [value, delimitersString]);
|
||||||
|
}
|
||||||
|
|
||||||
export function TextArea(props) {
|
export function TextArea(props) {
|
||||||
const { wrapSSR, hashId, componentCls } = useStyles();
|
const { wrapSSR, hashId, componentCls } = useStyles();
|
||||||
const { value = '', scope, onChange, changeOnSelect, style, fieldNames } = props;
|
const { value = '', scope, onChange, changeOnSelect, style, fieldNames, delimiters = ['{{', '}}'] } = props;
|
||||||
|
const variables = useVariablesFromValue(value, delimiters);
|
||||||
const inputRef = useRef<HTMLDivElement>(null);
|
const inputRef = useRef<HTMLDivElement>(null);
|
||||||
const [options, setOptions] = useState([]);
|
const [options, setOptions] = useState([]);
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
@ -222,25 +234,26 @@ export function TextArea(props) {
|
|||||||
);
|
);
|
||||||
const [ime, setIME] = useState<boolean>(false);
|
const [ime, setIME] = useState<boolean>(false);
|
||||||
const [changed, setChanged] = useState(false);
|
const [changed, setChanged] = useState(false);
|
||||||
const [html, setHtml] = useState(() => renderHTML(value ?? '', keyLabelMap));
|
const [html, setHtml] = useState(() => renderHTML(value ?? '', keyLabelMap, delimiters));
|
||||||
// NOTE: e.g. [startElementIndex, startOffset, endElementIndex, endOffset]
|
// NOTE: e.g. [startElementIndex, startOffset, endElementIndex, endOffset]
|
||||||
const [range, setRange] = useState<[number, number, number, number]>([-1, 0, -1, 0]);
|
const [range, setRange] = useState<[number, number, number, number]>([-1, 0, -1, 0]);
|
||||||
useInputStyle('ant-input');
|
useInputStyle('ant-input');
|
||||||
|
const delimitersString = delimiters.join(' ');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
preloadOptions(scope, value)
|
preloadOptions(scope, variables)
|
||||||
.then((preloaded) => {
|
.then((preloaded) => {
|
||||||
setOptions(preloaded);
|
setOptions(preloaded);
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}, [scope, value]);
|
}, [scope, JSON.stringify(variables)]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHtml(renderHTML(value ?? '', keyLabelMap));
|
setHtml(renderHTML(value ?? '', keyLabelMap, delimiters));
|
||||||
if (!changed) {
|
if (!changed) {
|
||||||
setRange([-1, 0, -1, 0]);
|
setRange([-1, 0, -1, 0]);
|
||||||
}
|
}
|
||||||
}, [value, keyLabelMap]);
|
}, [value, keyLabelMap, delimitersString]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { current } = inputRef;
|
const { current } = inputRef;
|
||||||
@ -310,9 +323,9 @@ export function TextArea(props) {
|
|||||||
|
|
||||||
setChanged(true);
|
setChanged(true);
|
||||||
setRange(getCurrentRange(current));
|
setRange(getCurrentRange(current));
|
||||||
onChange(getValue(current));
|
onChange(getValue(current, delimiters));
|
||||||
},
|
},
|
||||||
[keyLabelMap, onChange, range],
|
[keyLabelMap, onChange, range, delimitersString],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onInput = useCallback(
|
const onInput = useCallback(
|
||||||
@ -322,9 +335,9 @@ export function TextArea(props) {
|
|||||||
}
|
}
|
||||||
setChanged(true);
|
setChanged(true);
|
||||||
setRange(getCurrentRange(currentTarget));
|
setRange(getCurrentRange(currentTarget));
|
||||||
onChange(getValue(currentTarget));
|
onChange(getValue(currentTarget, delimiters));
|
||||||
},
|
},
|
||||||
[ime, onChange],
|
[ime, onChange, delimitersString],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onBlur = useCallback(function ({ currentTarget }) {
|
const onBlur = useCallback(function ({ currentTarget }) {
|
||||||
@ -346,9 +359,9 @@ export function TextArea(props) {
|
|||||||
setIME(false);
|
setIME(false);
|
||||||
setChanged(true);
|
setChanged(true);
|
||||||
setRange(getCurrentRange(currentTarget));
|
setRange(getCurrentRange(currentTarget));
|
||||||
onChange(getValue(currentTarget));
|
onChange(getValue(currentTarget, delimiters));
|
||||||
},
|
},
|
||||||
[onChange],
|
[onChange, delimitersString],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onPaste = useCallback(
|
const onPaste = useCallback(
|
||||||
@ -379,9 +392,9 @@ export function TextArea(props) {
|
|||||||
setChanged(true);
|
setChanged(true);
|
||||||
pasteHTML(ev.currentTarget, sanitizedHTML);
|
pasteHTML(ev.currentTarget, sanitizedHTML);
|
||||||
setRange(getCurrentRange(ev.currentTarget));
|
setRange(getCurrentRange(ev.currentTarget));
|
||||||
onChange(getValue(ev.currentTarget));
|
onChange(getValue(ev.currentTarget, delimiters));
|
||||||
},
|
},
|
||||||
[onChange],
|
[onChange, delimitersString],
|
||||||
);
|
);
|
||||||
|
|
||||||
const disabled = props.disabled || form.disabled;
|
const disabled = props.disabled || form.disabled;
|
||||||
@ -461,19 +474,14 @@ export function TextArea(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function preloadOptions(scope, value: string) {
|
async function preloadOptions(scope, variables: string[]) {
|
||||||
let options = [...(scope ?? [])];
|
let options = [...(scope ?? [])];
|
||||||
|
const paths = variables.map((variable) => variable.split('.'));
|
||||||
options = options.filter((item) => {
|
options = options.filter((item) => {
|
||||||
return !item.deprecated || value?.includes(item.value);
|
return !item.deprecated || paths.find((p) => p[0] === item.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 重置正则的匹配位置
|
for (const keys of paths) {
|
||||||
VARIABLE_RE.lastIndex = 0;
|
|
||||||
|
|
||||||
for (let matcher; (matcher = VARIABLE_RE.exec(value ?? '')); ) {
|
|
||||||
const keys = matcher[1].split('.');
|
|
||||||
|
|
||||||
let prevOption = null;
|
let prevOption = null;
|
||||||
|
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
@ -508,20 +516,20 @@ const textAreaReadPrettyClassName = css`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
TextArea.ReadPretty = function ReadPretty(props): JSX.Element {
|
TextArea.ReadPretty = function ReadPretty(props): JSX.Element {
|
||||||
const { value } = props;
|
const { value, delimiters = ['{{', '}}'] } = props;
|
||||||
const scope = typeof props.scope === 'function' ? props.scope() : props.scope;
|
const scope = typeof props.scope === 'function' ? props.scope() : props.scope;
|
||||||
const { wrapSSR, hashId, componentCls } = useStyles();
|
const { wrapSSR, hashId, componentCls } = useStyles();
|
||||||
const [options, setOptions] = useState([]);
|
const [options, setOptions] = useState([]);
|
||||||
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [options]);
|
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [options]);
|
||||||
const html = useMemo(() => renderHTML(value ?? '', keyLabelMap), [keyLabelMap, value]);
|
const html = useMemo(() => renderHTML(value ?? '', keyLabelMap, delimiters), [delimiters, keyLabelMap, value]);
|
||||||
|
const variables = useVariablesFromValue(value, delimiters);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
preloadOptions(scope, value)
|
preloadOptions(scope, variables)
|
||||||
.then((preloaded) => {
|
.then((preloaded) => {
|
||||||
setOptions(preloaded);
|
setOptions(preloaded);
|
||||||
})
|
})
|
||||||
.catch(error);
|
.catch(error);
|
||||||
}, [scope, value]);
|
}, [scope, variables]);
|
||||||
|
|
||||||
const content = wrapSSR(
|
const content = wrapSSR(
|
||||||
<span
|
<span
|
||||||
|
@ -57,6 +57,7 @@ export const ContentConfigForm = ({ variableOptions }) => {
|
|||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
scope: variableOptions,
|
scope: variableOptions,
|
||||||
useTypedConstant: ['string'],
|
useTypedConstant: ['string'],
|
||||||
|
delimiters: ['{{{', '}}}'],
|
||||||
},
|
},
|
||||||
description: tval(
|
description: tval(
|
||||||
'Support two types of links: internal links and external links. If using an internal link, the link starts with"/", for example, "/admin". If using an external link, the link starts with "http", for example, "https://example.com".',
|
'Support two types of links: internal links and external links. If using an internal link, the link starts with"/", for example, "/admin". If using an external link, the link starts with "http", for example, "https://example.com".',
|
||||||
@ -71,6 +72,7 @@ export const ContentConfigForm = ({ variableOptions }) => {
|
|||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
scope: variableOptions,
|
scope: variableOptions,
|
||||||
useTypedConstant: ['string'],
|
useTypedConstant: ['string'],
|
||||||
|
delimiters: ['{{{', '}}}'],
|
||||||
},
|
},
|
||||||
description: tval(
|
description: tval(
|
||||||
"Support two types of links: internal links and external links. If using an internal link, the link starts with '/', for example, '/m'. If using an external link, the link starts with 'http', for example, 'https://example.com'.",
|
"Support two types of links: internal links and external links. If using an internal link, the link starts with '/', for example, '/m'. If using an external link, the link starts with 'http', for example, 'https://example.com'.",
|
||||||
|
Loading…
Reference in New Issue
Block a user