最资讯丨纯js实现高度可扩展关键词高亮方案详解
来源:脚本之家    时间:2022-08-30 20:11:12


【资料图】

目录
关键词高亮1. 实现的主要功能:2. 效果演示高级定制用法用法1. react中使用2. 原生js使用innerHTML源码核心源码渲染方案1. react组件渲染2. innerHTML渲染showcase演示组件

关键词高亮

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

1. 实现的主要功能:

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

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高度可扩展关键词高亮的资料请关注脚本之家其它相关文章!

关键词: 添加标签 主要功能 相关文章

上一篇:

下一篇:

X 关闭

X 关闭