純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高度可擴展關鍵詞高亮的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 鼠標劃過時整行變色284278過程講解
- Ant Design of Vue select框獲取key和name的問題
- vue parseHTML函數解析器遇到結束標簽
- Vue.$set 失效的坑 問題發現及解決方案
- python實現控制臺輸出顏色