Skip to content

Commit 5273d7c

Browse files
committed
support customize trigger validate
1 parent d675943 commit 5273d7c

File tree

4 files changed

+84
-13
lines changed

4 files changed

+84
-13
lines changed

examples/split.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/* eslint no-console: 0 */
2+
3+
import React from 'react';
4+
import Mentions from '../src';
5+
import '../assets/index.less';
6+
7+
const { Option } = Mentions;
8+
9+
function validateSearch(text) {
10+
return text.length <= 5;
11+
}
12+
13+
class Demo extends React.Component {
14+
state = {};
15+
16+
render() {
17+
return (
18+
<div>
19+
<h1>Customize Split Logic</h1>
20+
<p>Only validate string length less than 5</p>
21+
<Mentions
22+
style={{ width: '100%', fontSize: 50 }}
23+
split=""
24+
validateSearch={validateSearch}
25+
autoFocus
26+
>
27+
<Option value="light">Light</Option>
28+
<Option value="bamboo">Bamboo</Option>
29+
<Option value="cat">Cat</Option>
30+
</Mentions>
31+
</div>
32+
);
33+
}
34+
}
35+
36+
export default Demo;

src/Mentions.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import {
1212
getLastMeasureIndex,
1313
replaceWithMeasure,
1414
setInputSelection,
15+
validateSearch as defaultValidateSearch,
1516
} from './util';
1617

17-
interface MentionsProps {
18+
export interface MentionsProps {
1819
defaultValue?: string;
1920
value?: string;
2021
onChange?: (text: string) => void;
@@ -24,6 +25,8 @@ interface MentionsProps {
2425
className?: string;
2526
style?: React.CSSProperties;
2627
autoFocus?: boolean;
28+
split?: string;
29+
validateSearch?: typeof defaultValidateSearch;
2730
}
2831
interface MentionsState {
2932
value: string;
@@ -39,6 +42,8 @@ class Mentions extends React.Component<MentionsProps, MentionsState> {
3942
public static defaultProps = {
4043
prefixCls: 'rc-mentions',
4144
prefix: '@',
45+
split: ' ',
46+
validateSearch: defaultValidateSearch,
4247
};
4348

4449
public static getDerivedStateFromProps(props: MentionsProps, prevState: MentionsState) {
@@ -135,21 +140,27 @@ class Mentions extends React.Component<MentionsProps, MentionsState> {
135140
public onKeyUp: React.KeyboardEventHandler<HTMLTextAreaElement> = event => {
136141
const { key, which } = event;
137142
const { measureText: prevMeasureText, measuring } = this.state;
138-
const { prefix = '', onSearch } = this.props;
139-
const selectionStartText = getBeforeSelectionText(event.target as HTMLTextAreaElement);
143+
const { prefix = '', split = ' ', onSearch, validateSearch } = this.props;
144+
const target = event.target as HTMLTextAreaElement;
145+
const selectionStartText = getBeforeSelectionText(target);
140146
const { location: measureIndex, prefix: measurePrefix } = getLastMeasureIndex(
141147
selectionStartText,
142148
prefix,
143149
);
144150

145151
// Skip if match the white key list
146-
if ([KeyCode.ESC, KeyCode.UP, KeyCode.DOWN].indexOf(which) !== -1) {
152+
if ([KeyCode.ESC, KeyCode.UP, KeyCode.DOWN, KeyCode.ENTER].indexOf(which) !== -1) {
153+
return;
154+
}
155+
156+
// Skip if is the last one
157+
if (KeyCode.RIGHT === which && target.selectionStart === target.value.length) {
147158
return;
148159
}
149160

150161
if (measureIndex !== -1) {
151162
const measureText = selectionStartText.slice(measureIndex + measurePrefix.length);
152-
const validateMeasure = measureText.indexOf(' ') === -1;
163+
const validateMeasure: boolean = validateSearch!(measureText, this.props);
153164
const matchOption: boolean = !!this.getOptions(measureText).length;
154165

155166
if (key === measurePrefix || measuring || (measureText !== prevMeasureText && matchOption)) {
@@ -175,13 +186,15 @@ class Mentions extends React.Component<MentionsProps, MentionsState> {
175186

176187
public selectOption = (option: OptionProps) => {
177188
const { value, measureLocation, measurePrefix } = this.state;
189+
const { split } = this.props;
178190

179191
const { value: mentionValue = '' } = option;
180192
const { text, selectionLocation } = replaceWithMeasure(value, {
181193
measureLocation,
182194
targetText: mentionValue,
183195
prefix: measurePrefix,
184196
selectionStart: this.textarea!.selectionStart,
197+
split: split!,
185198
});
186199
this.triggerChange(text);
187200
this.stopMeasure(() => {

src/util.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { MentionsProps } from './Mentions';
2+
13
/**
24
* Cut input selection into 2 part and return text before selection start
35
*/
@@ -35,15 +37,16 @@ interface MeasureConfig {
3537
prefix: string;
3638
targetText: string;
3739
selectionStart: number;
40+
split: string;
3841
}
3942

4043
function lower(char: string | undefined): string {
4144
return (char || '').toLowerCase();
4245
}
4346

44-
function reduceText(text: string, targetText: string) {
47+
function reduceText(text: string, targetText: string, split: string) {
4548
const firstChar = text[0];
46-
if (!firstChar || firstChar === ' ') {
49+
if (!firstChar || firstChar === split) {
4750
return text;
4851
}
4952

@@ -69,24 +72,31 @@ function reduceText(text: string, targetText: string) {
6972
* => little @light test
7073
*/
7174
export function replaceWithMeasure(text: string, measureConfig: MeasureConfig) {
72-
const { measureLocation, prefix, targetText, selectionStart } = measureConfig;
75+
const { measureLocation, prefix, targetText, selectionStart, split } = measureConfig;
7376

7477
// Before text will append one space if have other text
75-
let beforeMeasureText = text.slice(0, measureLocation).replace(/ $/, '');
78+
let beforeMeasureText = text.slice(0, measureLocation);
79+
if (beforeMeasureText[beforeMeasureText.length - split.length] === split) {
80+
beforeMeasureText = beforeMeasureText.slice(0, beforeMeasureText.length - split.length);
81+
}
7682
if (beforeMeasureText) {
77-
beforeMeasureText = `${beforeMeasureText} `;
83+
beforeMeasureText = `${beforeMeasureText}${split}`;
7884
}
7985

8086
// Cut duplicate string with current targetText
81-
const restText = reduceText(
87+
let restText = reduceText(
8288
text.slice(selectionStart),
8389
targetText.slice(selectionStart - measureLocation - prefix.length),
90+
split,
8491
);
92+
if (restText.slice(0, split.length) === split) {
93+
restText = restText.slice(split.length);
94+
}
8595

86-
const connectedStartText = `${beforeMeasureText}${prefix}${targetText} `;
96+
const connectedStartText = `${beforeMeasureText}${prefix}${targetText}${split}`;
8797

8898
return {
89-
text: `${connectedStartText}${restText.replace(/^ /, '')}`,
99+
text: `${connectedStartText}${restText}`,
90100
selectionLocation: connectedStartText.length,
91101
};
92102
}
@@ -101,3 +111,8 @@ export function setInputSelection(input: HTMLTextAreaElement, location: number)
101111
input.blur();
102112
input.focus();
103113
}
114+
115+
export function validateSearch(text: string, props: MentionsProps) {
116+
const { split } = props;
117+
return !split || text.indexOf(split) === -1;
118+
}

storybook/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import { withInfo } from '@storybook/addon-info';
99
import BasicSource from 'rc-source-loader!../examples/basic';
1010
import DynamicSource from 'rc-source-loader!../examples/dynamic';
1111
import MultiplePrefixSource from 'rc-source-loader!../examples/multiple-prefix';
12+
import SplitSource from 'rc-source-loader!../examples/split';
1213
import Basic from '../examples/basic';
1314
import Dynamic from '../examples/dynamic';
1415
import MultiplePrefix from '../examples/multiple-prefix';
16+
import Split from '../examples/split';
1517
import READMECode from '../README.md';
1618

1719
storiesOf('rc-mentions', module)
@@ -51,4 +53,9 @@ storiesOf('rc-mentions', module)
5153
source: {
5254
code: MultiplePrefixSource,
5355
},
56+
})
57+
.add('split', () => <Split />, {
58+
source: {
59+
code: SplitSource,
60+
},
5461
});

0 commit comments

Comments
 (0)