纯js实现高度可扩展关键词高亮方案详解
作者:前端君
这篇文章主要为大家介绍了纯js实现高度可扩展关键词高亮方案详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
关键词高亮
日常需求开发中常见需要高亮的场景,本文主要记录字符串渲染时多个关键词同时高亮的实现方法,目的是实现高度可扩展的多关键词高亮方案。
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; } /** ***** 以上是类型,以下是代码 ********************************************************/ /** * 多关键词的边界情况一览: * 1. 关键词之间存在包含关系,如: '12345' 和 '234' * 2. 关键词之间存在交叉关系,如: '1234' 和 '3456' */ // 计算 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演示组件
/* 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>编辑标签</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高度可扩展关键词高亮的资料请关注脚本之家其它相关文章!