<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
日常需求開發中常見需要高亮的場景,本文主要記錄字串渲染時多個關鍵詞同時高亮的實現方法,目的是實現高度可延伸的多關鍵詞高亮方案。
體驗地址:連結
export default () => { const text = `123432123424r2`; const keywords = ['123']; return ( <HighlightKeyword content={text} keywords=js高度可延伸關鍵詞高亮,js 關鍵詞高亮 /> ); };
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; };
// 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; };
/** ***** 以上是核心程式碼部分,以下渲染部分 ********************************************************/ // 駝峰轉換橫線 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; };
/* 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高度可延伸關鍵詞高亮的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45