温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

纯js如何实现高度可扩展关键词高亮

发布时间:2022-08-31 09:59:59 来源:亿速云 阅读:149 作者:iii 栏目:开发技术

今天小编给大家分享一下纯js如何实现高度可扩展关键词高亮的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

    关键词高亮

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

    1. 实现的主要功能:

    • 关键词提取和高亮

    • 多个关键词同时高亮

    • 关键词支持正则匹配

    • 每个关键字支持独立样式配置,支持高度定制化

      • 不同标签使用不同颜色区分开

      • 使用不同标签名

      • 使用定制化CSSStyle样式

      • 自定义渲染函数,渲染成任何样式

    • 扩展性较好,可以根据解析数据自定义渲染,能很好的兼容复杂的场景

    2. 效果演示

    纯js如何实现高度可扩展关键词高亮

    高级定制用法

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

    纯js如何实现高度可扩展关键词高亮

    用法

    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 = ``;
      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);
          }}>
          <h3>编辑标签</h3>
          <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' }}>
            <h2>关键词高亮</h2>
            <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如何实现高度可扩展关键词高亮”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注亿速云行业资讯频道。

    向AI问一下细节

    免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

    js
    AI