关键词高亮
日常需求开发中常见需要高亮的场景,本文主要记录字符串渲染时多个关键词同时高亮的实现方法,目的是实现高度可扩展的多关键词高亮方案。
1. 实现的主要功能:
- 关键词提取和高亮
- 多个关键词同时高亮
- 关键词支持正则匹配
- 每个关键字支持独立样式配置,支持高度定制化
- 不同标签使用不同颜色区分开
- 使用不同标签名
- 使用定制化CSSStyle样式
- 自定义渲染函数,渲染成任何样式
- 扩展性较好,可以根据解析数据自定义渲染,能很好的兼容复杂的场景
2. 效果演示
体验地址:链接
高级定制用法
- 自定义渲染,例如可以将文本变成链接
用法
1. react中使用
export default () => {
const text = `123432123424r2`;
const keywords = ['123'];
return (
<HighlightKeyword content={text} keywords=js高度可扩展关键词高亮,js 关键词高亮 />
);
};
2. 原生js使用innerHTML
const div = document.querySelector('#div');
div.innerHTML = getHighlightKeywordsHtml(templateStr, [keyword]);
源码
核心源码
// 关键词配置
export interface IKeywordOption {
keyword: string | RegExp;
color?: string;
bgColor?: string;
style?: Record<string, any>;
// 高亮标签名
tagName?: string;
// 忽略大小写
caseSensitive?: boolean;
// 自定义渲染高亮html
renderHighlightKeyword?: (content: string) => any;
}
export type IKeyword = string | IKeywordOption;
export interface IMatchIndex {
index: number;
subString: string;
}
// 关键词索引
export interface IKeywordParseIndex {
keyword: string | RegExp;
indexList: IMatchIndex[];
option?: IKeywordOption;
}
// 关键词
export interface IKeywordParseResult {
start: number;
end: number;
subString?: string;
option?: IKeywordOption;
}
// 计算
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;
};
// 解析关键词为索引
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;
};
// 解析关键词为数据
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;
}
// 没有交集,且在当前的前面
if (index + len <= cur.start) {
return i;
}
// 没有交集,且在当前的后面的,放在下个迭代处理
}
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);
}
});
});
// 补上没有匹配关键词的部分
const list: IKeywordParseResult[] = [];
splitList.forEach((cur, i) => {
const { start, end } = cur;
const next = splitList[i + 1];
// 第一个前面补一个
if (i === 0 && start > 0) {
list.push({ start: 0, end: start, subString: content.slice(0, start) });
}
list.push({ ...cur, subString: content.slice(start, end) });
// 当前和下一个中间补一个
if (next?.start > end) {
list.push({
start: end,
end: next.start,
subString: content.slice(end, next.start),
});
}
// 最后一个后面补一个
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渲染
// 驼峰转换横线
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;
};
// 生成关键词高亮的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演示组件
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>编辑标签</h2>
<Form.Item field="keyword" label="标签">
<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="标签名">
<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="点击访问链接">
<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>关键词高亮</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="添加标签">
<Button
type="primary"
icon={<IconPlus />}
style={{ marginLeft: 'auto' }}
onClick={() => {
setEditTagIndex(keywords.length);
}}>
添加标签
</Button>
</Tooltip>
</Popover>
</div>
<div style={{ display: 'flex', padding: '15px 0' }}></div>
{keywords.map((keyword, i) => (
<Tooltip key={JSON.stringify(keyword)} content="双击编辑标签">
<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="内容区">
<HighlightContainer>
<HighlightKeyword content={text} keywords=js高度可扩展关键词高亮,js 关键词高亮 />
</HighlightContainer>
</Card>
</div>
);
};
以上就是纯js实现高度可扩展关键词高亮方案详解的详细内容,更多关于js高度可扩展关键词高亮的资料请关注编程网其它相关文章!