纯js实现高度可扩展关键词高亮方案详解

关键词高亮

日常需求开发中常见需要高亮的场景,本文主要记录字符串渲染时多个关键词同时高亮的实现方法,目的是实现高度可扩展的多关键词高亮方案。

1. 实现的主要功能:

  • 关键词提取和高亮
  • 多个关键词同时高亮
  • 关键词支持正则匹配
  • 每个关键字支持独立样式配置,支持高度定制化
    • 不同标签使用不同颜色区分开
    • 使用不同标签名
    • 使用定制化CSSStyle样式
    • 自定义渲染函数,渲染成任何样式
  • 扩展性较好,可以根据解析数据自定义渲染,能很好的兼容复杂的场景

2. 效果演示

体验地址:链接

纯js实现高度可扩展关键词高亮方案详解_第1张图片

高级定制用法

  • 自定义渲染,例如可以将文本变成链接

纯js实现高度可扩展关键词高亮方案详解_第2张图片

用法

1. react中使用

export default () => {
    const text = `123432123424r2`;
    const keywords = ['123'];
    return (
        
    );
};

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;
  // 高亮标签名
  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 (
        
          {subString}
        
      );
    });
  }, [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}`;
};
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(`${keyword}`);
  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) => 
  {children}
; 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 ( {subString} ); }); }, [content, keywords]); return renderList; }; const TabForm = ({ keyword, onChange, onCancel, onSubmit }: any) => { const formRef: any = useRef(); useEffect(() => { formRef.current?.setFieldsValue(keyword); }, [keyword]); return (
{ onChange(values); }}>

编辑标签

onChange({ ...keyword, color, }) } /> } /> onChange({ ...keyword, bgColor: color, }) } /> } /> onChange({ ...keyword, caseSensitive: v, }) } />
); }; export default () => { const [text, setText] = useState(docStr); const [editKeyword, setEditKeyword] = useState({ keyword: '', }); const [editTagIndex, setEditTagIndex] = useState(-1); const [keywords, setKeywords] = useState([ { 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) => ( {str} ), }, ]); return (

关键词高亮

{ setEditKeyword(values); }} onCancel={() => { setEditTagIndex(-1); setEditKeyword({ keyword: '' }); }} onSubmit={() => { setKeywords((_keywords: IKeywordOption[]) => { const newKeywords = [..._keywords]; newKeywords[editTagIndex] = { ...editKeyword }; return newKeywords; }); setEditTagIndex(-1); setEditKeyword({ keyword: '' }); }} /> }>
{keywords.map((keyword, i) => ( { 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()} ))}
); };

以上就是纯js实现高度可扩展关键词高亮方案详解的详细内容,更多关于js高度可扩展关键词高亮的资料请关注脚本之家其它相关文章!

你可能感兴趣的:(纯js实现高度可扩展关键词高亮方案详解)