純js實(shí)現(xiàn)高度可擴(kuò)展關(guān)鍵詞高亮方案詳解
關(guān)鍵詞高亮
日常需求開(kāi)發(fā)中常見(jiàn)需要高亮的場(chǎng)景,本文主要記錄字符串渲染時(shí)多個(gè)關(guān)鍵詞同時(shí)高亮的實(shí)現(xiàn)方法,目的是實(shí)現(xiàn)高度可擴(kuò)展的多關(guān)鍵詞高亮方案。
1. 實(shí)現(xiàn)的主要功能:
- 關(guān)鍵詞提取和高亮
- 多個(gè)關(guān)鍵詞同時(shí)高亮
- 關(guān)鍵詞支持正則匹配
- 每個(gè)關(guān)鍵字支持獨(dú)立樣式配置,支持高度定制化
- 不同標(biāo)簽使用不同顏色區(qū)分開(kāi)
- 使用不同標(biāo)簽名
- 使用定制化CSSStyle樣式
- 自定義渲染函數(shù),渲染成任何樣式
- 擴(kuò)展性較好,可以根據(jù)解析數(shù)據(jù)自定義渲染,能很好的兼容復(fù)雜的場(chǎng)景
2. 效果演示
體驗(yàn)地址:鏈接

高級(jí)定制用法
- 自定義渲染,例如可以將文本變成鏈接

用法
1. react中使用
export default () => {
const text = `123432123424r2`;
const keywords = ['123'];
return (
<HighlightKeyword content={text} keywords=js高度可擴(kuò)展關(guān)鍵詞高亮,js 關(guān)鍵詞高亮 />
);
};
2. 原生js使用innerHTML
const div = document.querySelector('#div');
div.innerHTML = getHighlightKeywordsHtml(templateStr, [keyword]);
源碼
核心源碼
// 關(guān)鍵詞配置
export interface IKeywordOption {
keyword: string | RegExp;
color?: string;
bgColor?: string;
style?: Record<string, any>;
// 高亮標(biāo)簽名
tagName?: string;
// 忽略大小寫
caseSensitive?: boolean;
// 自定義渲染高亮html
renderHighlightKeyword?: (content: string) => any;
}
export type IKeyword = string | IKeywordOption;
export interface IMatchIndex {
index: number;
subString: string;
}
// 關(guān)鍵詞索引
export interface IKeywordParseIndex {
keyword: string | RegExp;
indexList: IMatchIndex[];
option?: IKeywordOption;
}
// 關(guān)鍵詞
export interface IKeywordParseResult {
start: number;
end: number;
subString?: string;
option?: IKeywordOption;
}
/** ***** 以上是類型,以下是代碼 ********************************************************/
/**
* 多關(guān)鍵詞的邊界情況一覽:
* 1. 關(guān)鍵詞之間存在包含關(guān)系,如: '12345' 和 '234'
* 2. 關(guān)鍵詞之間存在交叉關(guān)系,如: '1234' 和 '3456'
*/
// 計(jì)算
const getKeywordIndexList = (
content: string,
keyword: string | RegExp,
flags = 'ig',
) => {
const reg = new RegExp(keyword, flags);
const res = (content as any).matchAll(reg);
const arr = [...res];
const allIndexArr: IMatchIndex[] = arr.map(e => ({
index: e.index,
subString: e['0'],
}));
return allIndexArr;
};
// 解析關(guān)鍵詞為索引
const parseHighlightIndex = (content: string, keywords: IKeyword[]) => {
const result: IKeywordParseIndex[] = [];
keywords.forEach((keywordOption: IKeyword) => {
let option: IKeywordOption = { keyword: '' };
if (typeof keywordOption === 'string') {
option = { keyword: keywordOption };
} else {
option = keywordOption;
}
const { keyword, caseSensitive = true } = option;
const indexList = getKeywordIndexList(
content,
keyword,
caseSensitive ? 'g' : 'gi',
);
const res = {
keyword,
indexList,
option,
};
result.push(res);
});
return result;
};
// 解析關(guān)鍵詞為數(shù)據(jù)
export const parseHighlightString = (content: string, keywords: IKeyword[]) => {
const result = parseHighlightIndex(content, keywords);
const splitList: IKeywordParseResult[] = [];
const findSplitIndex = (index: number, len: number) => {
for (let i = 0; i < splitList.length; i++) {
const cur = splitList[i];
// 有交集
if (
(index > cur.start && index < cur.end) ||
(index + len > cur.start && index + len < cur.end) ||
(cur.start > index && cur.start < index + len) ||
(cur.end > index && cur.end < index + len) ||
(index === cur.start && index + len === cur.end)
) {
return -1;
}
// 沒(méi)有交集,且在當(dāng)前的前面
if (index + len <= cur.start) {
return i;
}
// 沒(méi)有交集,且在當(dāng)前的后面的,放在下個(gè)迭代處理
}
return splitList.length;
};
result.forEach(({ indexList, option }: IKeywordParseIndex) => {
indexList.forEach(e => {
const { index, subString } = e;
const item = {
start: index,
end: index + subString.length,
option,
};
const splitIndex = findSplitIndex(index, subString.length);
if (splitIndex !== -1) {
splitList.splice(splitIndex, 0, item);
}
});
});
// 補(bǔ)上沒(méi)有匹配關(guān)鍵詞的部分
const list: IKeywordParseResult[] = [];
splitList.forEach((cur, i) => {
const { start, end } = cur;
const next = splitList[i + 1];
// 第一個(gè)前面補(bǔ)一個(gè)
if (i === 0 && start > 0) {
list.push({ start: 0, end: start, subString: content.slice(0, start) });
}
list.push({ ...cur, subString: content.slice(start, end) });
// 當(dāng)前和下一個(gè)中間補(bǔ)一個(gè)
if (next?.start > end) {
list.push({
start: end,
end: next.start,
subString: content.slice(end, next.start),
});
}
// 最后一個(gè)后面補(bǔ)一個(gè)
if (i === splitList.length - 1 && end < content.length - 1) {
list.push({
start: end,
end: content.length - 1,
subString: content.slice(end, content.length - 1),
});
}
});
console.log('list:', keywords, list);
return list;
};
渲染方案
1. react組件渲染
// react組件
const HighlightKeyword = ({
content,
keywords,
}: {
content: string;
keywords: IKeywordOption[];
}): any => {
const renderList = useMemo(() => {
if (keywords.length === 0) {
return <>{content}</>;
}
const splitList = parseHighlightString(content, keywords);
if (splitList.length === 0) {
return <>{content}</>;
}
return splitList.map((item: IKeywordParseResult, i: number) => {
const { subString, option = {} } = item;
const {
color,
bgColor,
style = {},
tagName = 'mark',
renderHighlightKeyword,
} = option as IKeywordOption;
if (typeof renderHighlightKeyword === 'function') {
return renderHighlightKeyword(subString as string);
}
if (!item.option) {
return <>{subString}</>;
}
const TagName: any = tagName;
return (
<TagName
key={`${subString}_${i}`}
style={{
...style,
backgroundColor: bgColor || style.backgroundColor,
color: color || style.color,
}}>
{subString}
</TagName>
);
});
}, [content, keywords]);
return renderList;
};
2. innerHTML渲染
/** ***** 以上是核心代碼部分,以下渲染部分 ********************************************************/
// 駝峰轉(zhuǎn)換橫線
function humpToLine(name: string) {
return name.replace(/([A-Z])/g, '-$1').toLowerCase();
}
const renderNodeTag = (subStr: string, option: IKeywordOption) => {
const s = subStr;
if (!option) {
return s;
}
const {
tagName = 'mark',
bgColor,
color,
style = {},
renderHighlightKeyword,
} = option;
if (typeof renderHighlightKeyword === 'function') {
return renderHighlightKeyword(subStr);
}
style.backgroundColor = bgColor;
style.color = color;
const styleContent = Object.keys(style)
.map(k => `${humpToLine(k)}:${style[k]}`)
.join(';');
const styleStr = `style="${styleContent}"`;
return `<${tagName} ${styleStr}>${s}</${tagName}>`;
};
const renderHighlightHtml = (content: string, list: any[]) => {
let str = '';
list.forEach(item => {
const { start, end, option } = item;
const s = content.slice(start, end);
const subStr = renderNodeTag(s, option);
str += subStr;
item.subString = subStr;
});
return str;
};
// 生成關(guān)鍵詞高亮的html字符串
export const getHighlightKeywordsHtml = (
content: string,
keywords: IKeyword[],
) => {
// const keyword = keywords[0] as string;
// return content.split(keyword).join(`<mark>${keyword}</mark>`);
const splitList = parseHighlightString(content, keywords);
const html = renderHighlightHtml(content, splitList);
return html;
};
showcase演示組件
/* eslint-disable @typescript-eslint/no-shadow */
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
Card,
Tag,
Button,
Tooltip,
Popover,
Form,
Input,
Switch,
} from '@arco-design/web-react';
import { IconPlus } from '@arco-design/web-react/icon';
import ColorBlock from './color-block';
import {
parseHighlightString,
IKeywordOption,
IKeywordParseResult,
} from './core';
import './index.less';
import { docStr, shortStr } from './data';
const HighlightContainer = ({ children, ...rest }: any) => <pre {...rest} className="highlight-container">
{children}
</pre>;
const HighlightKeyword = ({
content,
keywords,
}: {
content: string;
keywords: IKeywordOption[];
}): any => {
const renderList = useMemo(() => {
if (keywords.length === 0) {
return <>{content}</>;
}
const splitList = parseHighlightString(content, keywords);
if (splitList.length === 0) {
return <>{content}</>;
}
return splitList.map((item: IKeywordParseResult, i: number) => {
const { subString, option = {} } = item;
const {
color,
bgColor,
style = {},
tagName = 'mark',
renderHighlightKeyword,
} = option as IKeywordOption;
if (typeof renderHighlightKeyword === 'function') {
return renderHighlightKeyword(subString as string);
}
if (!item.option) {
return <>{subString}</>;
}
const TagName: any = tagName;
return (
<TagName
key={`${subString}_${i}`}
style={{
...style,
backgroundColor: bgColor || style.backgroundColor,
color: color || style.color,
}}>
{subString}
</TagName>
);
});
}, [content, keywords]);
return renderList;
};
const TabForm = ({ keyword, onChange, onCancel, onSubmit }: any) => {
const formRef: any = useRef();
useEffect(() => {
formRef.current?.setFieldsValue(keyword);
}, [keyword]);
return (
<Form
ref={formRef}
style={{ width: 300 }}
onChange={(_, values) => {
onChange(values);
}}>
<h2>編輯標(biāo)簽</h2>
<Form.Item field="keyword" label="標(biāo)簽">
<Input />
</Form.Item>
<Form.Item field="color" label="顏色">
<Input
prefix={
<ColorBlock
color={keyword.color}
onChange={(color: string) =>
onChange({
...keyword,
color,
})
}
/>
}
/>
</Form.Item>
<Form.Item field="bgColor" label="背景色">
<Input
prefix={
<ColorBlock
color={keyword.bgColor}
onChange={(color: string) =>
onChange({
...keyword,
bgColor: color,
})
}
/>
}
/>
</Form.Item>
<Form.Item field="tagName" label="標(biāo)簽名">
<Input />
</Form.Item>
<Form.Item label="大小寫敏感">
<Switch
checked={keyword.caseSensitive}
onChange={(v: boolean) =>
onChange({
...keyword,
caseSensitive: v,
})
}
/>
</Form.Item>
<Form.Item>
<Button onClick={onCancel} style={{ margin: '0 10px 0 100px' }}>
取消
</Button>
<Button onClick={onSubmit} type="primary">
確定
</Button>
</Form.Item>
</Form>
);
};
export default () => {
const [text, setText] = useState(docStr);
const [editKeyword, setEditKeyword] = useState<IKeywordOption>({
keyword: '',
});
const [editTagIndex, setEditTagIndex] = useState(-1);
const [keywords, setKeywords] = useState<IKeywordOption[]>([
{ keyword: 'antd', bgColor: 'yellow', color: '#000' },
{
keyword: '文件',
bgColor: '#8600FF',
color: '#fff',
style: { padding: '0 4px' },
},
{ keyword: '文件' },
// eslint-disable-next-line no-octal-escape
// { keyword: '\\d+' },
{
keyword: 'react',
caseSensitive: false,
renderHighlightKeyword: (str: string) => (
<Tooltip content="點(diǎn)擊訪問(wèn)鏈接">
<a
href={'https://zh-hans.reactjs.org'}
target="_blank"
style={{
textDecoration: 'underline',
fontStyle: 'italic',
color: 'blue',
}}>
{str}
</a>
</Tooltip>
),
},
]);
return (
<div style={{ width: 800, margin: '0 auto' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<h1>關(guān)鍵詞高亮</h1>
<Popover
popupVisible={editTagIndex !== -1}
position="left"
content={
<TabForm
keyword={editKeyword}
onChange={(values: any) => {
setEditKeyword(values);
}}
onCancel={() => {
setEditTagIndex(-1);
setEditKeyword({ keyword: '' });
}}
onSubmit={() => {
setKeywords((_keywords: IKeywordOption[]) => {
const newKeywords = [..._keywords];
newKeywords[editTagIndex] = { ...editKeyword };
return newKeywords;
});
setEditTagIndex(-1);
setEditKeyword({ keyword: '' });
}}
/>
}>
<Tooltip content="添加標(biāo)簽">
<Button
type="primary"
icon={<IconPlus />}
style={{ marginLeft: 'auto' }}
onClick={() => {
setEditTagIndex(keywords.length);
}}>
添加標(biāo)簽
</Button>
</Tooltip>
</Popover>
</div>
<div style={{ display: 'flex', padding: '15px 0' }}></div>
{keywords.map((keyword, i) => (
<Tooltip key={JSON.stringify(keyword)} content="雙擊編輯標(biāo)簽">
<Tag
closable={true}
style={{
margin: '0 16px 16px 0 ',
backgroundColor: keyword.bgColor,
color: keyword.color,
}}
onClose={() => {
setKeywords((_keywords: IKeywordOption[]) => {
const newKeywords = [..._keywords];
newKeywords.splice(i, 1);
return newKeywords;
});
}}
onDoubleClick={() => {
setEditTagIndex(i);
setEditKeyword({ ...keywords[i] });
}}>
{typeof keyword.keyword === 'string'
? keyword.keyword
: keyword.keyword.toString()}
</Tag>
</Tooltip>
))}
<Card title="內(nèi)容區(qū)">
<HighlightContainer>
<HighlightKeyword content={text} keywords=js高度可擴(kuò)展關(guān)鍵詞高亮,js 關(guān)鍵詞高亮 />
</HighlightContainer>
</Card>
</div>
);
};以上就是純js實(shí)現(xiàn)高度可擴(kuò)展關(guān)鍵詞高亮方案詳解的詳細(xì)內(nèi)容,更多關(guān)于js高度可擴(kuò)展關(guān)鍵詞高亮的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序中頁(yè)面FOR循環(huán)和嵌套循環(huán)
這篇文章主要介紹了微信小程序中頁(yè)面FOR循環(huán)和嵌套循環(huán)的相關(guān)資料,需要的朋友可以參考下2017-06-06
JavaScript?定時(shí)器關(guān)鍵點(diǎn)及使用場(chǎng)景解析
這篇文章主要為大家介紹了JavaScript?定時(shí)器關(guān)鍵點(diǎn)及使用場(chǎng)景解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
微信小程序 出現(xiàn)錯(cuò)誤:{"baseresponse":{"errcode":-80002,"errmsg":""}}解決
這篇文章主要介紹了微信小程序 出現(xiàn)錯(cuò)誤:{"baseresponse":{"errcode":-80002,"errmsg":""}}解決辦法的相關(guān)資料,需要的朋友可以參考下2017-02-02
微信小程序 實(shí)戰(zhàn)程序簡(jiǎn)易新聞的制作
這篇文章主要介紹了微信小程序 實(shí)戰(zhàn)程序簡(jiǎn)易新聞的制作的相關(guān)資料,需要的朋友可以參考下2017-01-01

