Skip to content

Commit

Permalink
feat: highlight searchWords support object props (#2600)
Browse files Browse the repository at this point in the history
* feat: highlight searchWords support object props
* docs: update demo
  • Loading branch information
pointhalo authored Nov 30, 2024
1 parent 4c00534 commit 52b37b1
Show file tree
Hide file tree
Showing 11 changed files with 374 additions and 300 deletions.
26 changes: 26 additions & 0 deletions content/show/highlight/index-en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,32 @@ import { Highlight } from '@douyinfe/semi-ui';
};
```

### Use Different Styles for Different Texts
After v2.71.0, it supports using different highlight styles for different highlighted texts.
The `searchWords` is a string array by default. When an array of objects is passed in, the highlighted text can be specified through `text`, and the `className` and `style` can be specified separately at the same time.

```jsx live=true dir="column"
import React from 'react';
import { Highlight } from '@douyinfe/semi-ui';

() => {
return (
<h2>
<Highlight
component='span'
sourceString='From Semi Design,To Any Design. Quickly define your design system and apply it to design drafts and code'
searchWords={[
{ text: 'Semi', style: { backgroundColor: 'rgba(var(--semi-teal-5), 1)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword1' },
{ text: 'Quickly', style: { backgroundColor: 'var(--semi-color-primary)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword2' },
{ text: 'code', style: { backgroundColor: 'rgba(var(--semi-violet-5), 1)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword3' },
]}
highlightStyle={{ borderRadius: 4 }}
/>
</h2>
);
};
```


### Specify the highlight tag

Expand Down
33 changes: 30 additions & 3 deletions content/show/highlight/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,32 @@ import { Highlight } from '@douyinfe/semi-ui';
};
```

### 不同文本使用差异化样式
v2.71.0 后,支持针对不同的高亮文本使用不同的高亮样式
searchWords 默认为字符串数组。当传入对象数组时,可以通过 text指定高亮文本,同时单独指定 className、style

```jsx live=true dir="column"
import React from 'react';
import { Highlight } from '@douyinfe/semi-ui';

() => {
return (
<h2>
<Highlight
component='span'
sourceString='从 Semi Design 到 Any Design 快速定义你的设计系统,并应用在设计稿和代码中'
searchWords={[
{ text: 'Semi', style: { backgroundColor: 'rgba(var(--semi-teal-5), 1)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword1' },
{ text: '设计系统', style: { backgroundColor: 'var(--semi-color-primary)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword2' },
{ text: '设计稿和代码', style: { backgroundColor: 'rgba(var(--semi-violet-5), 1)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword3' },
]}
highlightStyle={{ borderRadius: 4 }}
/>
</h2>
);
};
```


### 指定高亮标签

Expand All @@ -112,16 +138,17 @@ import { Highlight } from '@douyinfe/semi-ui';
};
```


## API 参考

### Highlight

| 属性 | 说明 | 类型 | 默认值 |
| ------------ | -------------------------------------------------------- | -------------------------------- | ---------- |
| searchWords | 期望高亮显示的文本 | string[] | '' |
| searchWords | 期望高亮显示的文本(对象数组在v2.71后支持) | string[]\|object[] | [] |
| sourceString | 源文本 | string | |
| component | 高亮标签 | string | `mark` |
| highlightClassName | 高亮标签的样式类名 | ReactNode | - |
| highlightStyle | 高亮标签的内联样式 | ReactNode | - |
| highlightClassName | 高亮标签的样式类名 | string | - |
| highlightStyle | 高亮标签的内联样式 | CSSProperties | - |
| caseSensitive | 是否大小写敏感 | false | - |
| autoEscape | 是否自动转义 | true | - |
211 changes: 211 additions & 0 deletions packages/semi-foundation/highlight/foundation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Modified version based on 'highlight-words-core'
import { isString } from 'lodash';
import BaseFoundation, { DefaultAdapter } from '../base/foundation';

interface HighlightAdapter extends Partial<DefaultAdapter> {}

interface ChunkQuery {
autoEscape?: boolean;
caseSensitive?: boolean;
searchWords: SearchWords;
sourceString: string
}
export interface Chunk {
start: number;
end: number;
highlight: boolean;
className: string;
style: Record<string, string>
}

export interface ComplexSearchWord {
text: string;
className?: string;
style?: Record<string, string>
}

export type SearchWord = string | ComplexSearchWord | undefined;
export type SearchWords = SearchWord[];

const escapeRegExpFn = (string: string) => string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');

export default class HighlightFoundation extends BaseFoundation<HighlightAdapter> {

constructor(adapter?: HighlightAdapter) {
super({
...adapter,
});
}

/**
* Creates an array of chunk objects representing both higlightable and non highlightable pieces of text that match each search word.
*
findAll ['z'], 'aaazaaazaaa'
result #=> [
{ start: 0, end: 3, highlight: false }
{ start: 3, end: 4, highlight: true }
{ start: 4, end: 7, highlight: false }
{ start: 7, end: 8, highlight: true }
{ start: 8, end: 11, highlight: false }
]
findAll ['do', 'dollar'], 'aaa do dollar aaa'
#=> chunks: [
{ start: 4, end: 6 },
{ start: 7, end: 9 },
{ start: 7, end: 13 },
]
#=> chunksToHight: [
{ start: 4, end: 6 },
{ start: 7, end: 13 },
]
#=> result: [
{ start: 0, end: 4, highlight: false },
{ start: 4, end: 6, highlight: true },
{ start: 6, end: 7, highlight: false },
{ start: 7, end: 13, highlight: true },
{ start: 13, end: 17, highlight: false },
]
* @return Array of "chunks" (where a Chunk is { start:number, end:number, highlight:boolean })
*/
findAll = ({
autoEscape = true,
caseSensitive = false,
searchWords,
sourceString
}: ChunkQuery) => {
if (isString(searchWords)) {
searchWords = [searchWords];
}

const chunks = this.findChunks({
autoEscape,
caseSensitive,
searchWords,
sourceString
});
const chunksToHighlight = this.combineChunks({ chunks });
const result = this.fillInChunks({
chunksToHighlight,
totalLength: sourceString ? sourceString.length : 0
});
return result;
};

/**
* Examine text for any matches.
* If we find matches, add them to the returned array as a "chunk" object ({start:number, end:number}).
* @return { start:number, end:number }[]
*/
findChunks = ({
autoEscape,
caseSensitive,
searchWords,
sourceString
}: ChunkQuery): Chunk[] => (
searchWords
.map(searchWord => typeof searchWord === 'string' ? { text: searchWord } : searchWord)
.filter(searchWord => searchWord.text) // Remove empty words
.reduce((chunks, searchWord) => {
let searchText = searchWord.text;
if (autoEscape) {
searchText = escapeRegExpFn(searchText);
}
const regex = new RegExp(searchText, caseSensitive ? 'g' : 'gi');

let match;
while ((match = regex.exec(sourceString))) {
const start = match.index;
const end = regex.lastIndex;
if (end > start) {
chunks.push({
highlight: true,
start,
end,
className: searchWord.className,
style: searchWord.style
});
}
if (match.index === regex.lastIndex) {
regex.lastIndex++;
}
}
return chunks;
}, [])
);

/**
* Takes an array of {start:number, end:number} objects and combines chunks that overlap into single chunks.
* @return {start:number, end:number}[]
*/
combineChunks = ({ chunks }: { chunks: Chunk[] }): Chunk[] => {
return chunks
.sort((first, second) => first.start - second.start)
.reduce((processedChunks, nextChunk) => {
// First chunk just goes straight in the array...
if (processedChunks.length === 0) {
return [nextChunk];
} else {
// ... subsequent chunks get checked to see if they overlap...
const prevChunk = processedChunks.pop();
if (nextChunk.start <= prevChunk.end) {
// It may be the case that prevChunk completely surrounds nextChunk, so take the
// largest of the end indeces.
const endIndex = Math.max(prevChunk.end, nextChunk.end);
processedChunks.push({
highlight: true,
start: prevChunk.start,
end: endIndex,
className: prevChunk.className || nextChunk.className,
style: { ...prevChunk.style, ...nextChunk.style }
});
} else {
processedChunks.push(prevChunk, nextChunk);
}
return processedChunks;
}
}, []);
};

/**
* Given a set of chunks to highlight, create an additional set of chunks
* to represent the bits of text between the highlighted text.
* @param chunksToHighlight {start:number, end:number}[]
* @param totalLength number
* @return {start:number, end:number, highlight:boolean}[]
*/
fillInChunks = ({ chunksToHighlight, totalLength }: { chunksToHighlight: Chunk[]; totalLength: number }): Chunk[] => {
const allChunks: Chunk[] = [];
const append = (start: number, end: number, highlight: boolean, className?: string, style?: Record<string, string>) => {
if (end - start > 0) {
allChunks.push({
start,
end,
highlight,
className,
style
});
}
};

if (chunksToHighlight.length === 0) {
append(0, totalLength, false);
} else {
let lastIndex = 0;
chunksToHighlight.forEach(chunk => {
append(lastIndex, chunk.start, false);
append(chunk.start, chunk.end, true, chunk.className, chunk.style);
lastIndex = chunk.end;
});
append(lastIndex, totalLength, false);
}
return allChunks;
};

}





2 changes: 2 additions & 0 deletions packages/semi-foundation/tree/tree.scss
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ $module: #{$prefix}-tree;
&-highlight {
font-weight: $font-tree_option_hightlight-fontWeight;
color: $color-tree_option_hightlight-text;
// set inherit to override highlight component default bgc
background-color: inherit;
}

&-hidden {
Expand Down
Loading

0 comments on commit 52b37b1

Please sign in to comment.