From 52b37b12e1e212ad4ee9643aeeb9244699efac07 Mon Sep 17 00:00:00 2001
From: pointhalo <88709023+pointhalo@users.noreply.github.com>
Date: Sat, 30 Nov 2024 19:11:24 +0800
Subject: [PATCH] feat: highlight searchWords support object props (#2600)
* feat: highlight searchWords support object props
* docs: update demo
---
content/show/highlight/index-en-US.md | 26 +++
content/show/highlight/index.md | 33 ++-
.../semi-foundation/highlight/foundation.ts | 211 ++++++++++++++++++
packages/semi-foundation/tree/tree.scss | 2 +
.../semi-foundation/utils/getHighlight.ts | 178 ---------------
packages/semi-ui/_utils/index.tsx | 62 -----
packages/semi-ui/autoComplete/option.tsx | 28 +--
.../highlight/_story/highlight.stories.jsx | 36 ++-
packages/semi-ui/highlight/index.tsx | 52 ++++-
packages/semi-ui/select/option.tsx | 28 +--
packages/semi-ui/tree/treeNode.tsx | 18 +-
11 files changed, 374 insertions(+), 300 deletions(-)
create mode 100644 packages/semi-foundation/highlight/foundation.ts
delete mode 100644 packages/semi-foundation/utils/getHighlight.ts
diff --git a/content/show/highlight/index-en-US.md b/content/show/highlight/index-en-US.md
index 2a3f5be3fd..4a8878d813 100644
--- a/content/show/highlight/index-en-US.md
+++ b/content/show/highlight/index-en-US.md
@@ -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 (
+
+
+
+ );
+};
+```
+
### Specify the highlight tag
diff --git a/content/show/highlight/index.md b/content/show/highlight/index.md
index 6100ac0752..9ee35a65cb 100644
--- a/content/show/highlight/index.md
+++ b/content/show/highlight/index.md
@@ -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 (
+
+
+
+ );
+};
+```
+
### 指定高亮标签
@@ -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 | - |
diff --git a/packages/semi-foundation/highlight/foundation.ts b/packages/semi-foundation/highlight/foundation.ts
new file mode 100644
index 0000000000..a88681d53c
--- /dev/null
+++ b/packages/semi-foundation/highlight/foundation.ts
@@ -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 {}
+
+interface ChunkQuery {
+ autoEscape?: boolean;
+ caseSensitive?: boolean;
+ searchWords: SearchWords;
+ sourceString: string
+}
+export interface Chunk {
+ start: number;
+ end: number;
+ highlight: boolean;
+ className: string;
+ style: Record
+}
+
+export interface ComplexSearchWord {
+ text: string;
+ className?: string;
+ style?: Record
+}
+
+export type SearchWord = string | ComplexSearchWord | undefined;
+export type SearchWords = SearchWord[];
+
+const escapeRegExpFn = (string: string) => string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
+
+export default class HighlightFoundation extends BaseFoundation {
+
+ 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) => {
+ 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;
+ };
+
+}
+
+
+
+
+
diff --git a/packages/semi-foundation/tree/tree.scss b/packages/semi-foundation/tree/tree.scss
index f1f8ac9fbd..ffc53fdce2 100644
--- a/packages/semi-foundation/tree/tree.scss
+++ b/packages/semi-foundation/tree/tree.scss
@@ -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 {
diff --git a/packages/semi-foundation/utils/getHighlight.ts b/packages/semi-foundation/utils/getHighlight.ts
deleted file mode 100644
index f08d5e4bc9..0000000000
--- a/packages/semi-foundation/utils/getHighlight.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-// Modified version based on 'highlight-words-core'
-import { isString } from 'lodash';
-
-const escapeRegExpFn = (string: string) => string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
-interface ChunkQuery {
- autoEscape?: boolean;
- caseSensitive?: boolean;
- searchWords: string[];
- sourceString: string
-}
-interface Chunk {
- start: number;
- end: number;
- highlight: boolean
-}
-/**
- * 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 }[]
- */
-const findChunks = ({
- autoEscape,
- caseSensitive,
- searchWords,
- sourceString
-}: ChunkQuery): Chunk[] => (
- searchWords
- .filter(searchWord => searchWord) // Remove empty words
- .reduce((chunks, searchWord) => {
- if (autoEscape) {
- searchWord = escapeRegExpFn(searchWord);
- }
- const regex = new RegExp(searchWord, caseSensitive ? 'g' : 'gi');
-
- let match;
- while ((match = regex.exec(sourceString))) {
- const start = match.index;
- const end = regex.lastIndex;
- // We do not return zero-length matches
- if (end > start) {
- chunks.push({ highlight: false, start, end });
- }
- // Prevent browsers like Firefox from getting stuck in an infinite loop
- // See http://www.regexguru.com/2008/04/watch-out-for-zero-length-matches/
- 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}[]
- */
-const combineChunks = ({ chunks }: { chunks: Chunk[] }) => {
- chunks = 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: false,
- start: prevChunk.start,
- end: endIndex
- });
- } else {
- processedChunks.push(prevChunk, nextChunk);
- }
- return processedChunks;
- }
- }, []);
-
- return chunks;
-};
-
-
-/**
- * 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}[]
- */
-const fillInChunks = ({ chunksToHighlight, totalLength }: { chunksToHighlight: Chunk[]; totalLength: number }) => {
- const allChunks: Chunk[] = [];
- const append = (start: number, end: number, highlight: boolean) => {
- if (end - start > 0) {
- allChunks.push({
- start,
- end,
- highlight
- });
- }
- };
-
- 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);
- lastIndex = chunk.end;
- });
- append(lastIndex, totalLength, false);
- }
- return allChunks;
-};
-
-
-/**
- * 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 })
- */
-
-const findAll = ({
- autoEscape = true,
- caseSensitive = false,
- searchWords,
- sourceString
-}: ChunkQuery) => {
- if (isString(searchWords)) {
- searchWords = [searchWords];
- }
-
- const chunks = findChunks({
- autoEscape,
- caseSensitive,
- searchWords,
- sourceString
- });
- const chunksToHighlight = combineChunks({ chunks });
- const result = fillInChunks({
- chunksToHighlight,
- totalLength: sourceString ? sourceString.length : 0
- });
- return result;
-};
-
-export { findAll };
\ No newline at end of file
diff --git a/packages/semi-ui/_utils/index.tsx b/packages/semi-ui/_utils/index.tsx
index 8f6d6dd0d5..d40892eae2 100644
--- a/packages/semi-ui/_utils/index.tsx
+++ b/packages/semi-ui/_utils/index.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { cloneDeepWith, set, get } from 'lodash';
import warning from '@douyinfe/semi-foundation/utils/warning';
-import { findAll } from '@douyinfe/semi-foundation/utils/getHighlight';
import { isHTMLElement } from '@douyinfe/semi-foundation/utils/dom';
import semiGlobal from "./semi-global";
/**
@@ -66,48 +65,6 @@ export function cloneDeep(value: any, customizer?: (value: any) => any) {
return undefined;
});
}
-
-/**
- * [getHighLightTextHTML description]
- *
- * @param {string} sourceString [source content text]
- * @param {Array} searchWords [keywords to be highlighted]
- * @param {object} option
- * @param {true} option.highlightTag [The tag wrapped by the highlighted content, mark is used by default]
- * @param {true} option.highlightClassName
- * @param {true} option.highlightStyle
- * @param {boolean} option.caseSensitive
- *
- * @return {Array