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

目录
  • 关键词高亮
    • 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 (
        <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高度可扩展关键词高亮的资料请关注我们其它相关文章!

(0)

相关推荐

  • js 关键词高亮(根据ID/tag高亮关键字)案例介绍

    复制代码 代码如下: <!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv=&qu

  • Vue.JS实现垂直方向展开、收缩不定高度模块的JS组件

    需求分析: 如图,有很多高度不固定的模块(图中只显示两个,本人项目有十三个),点击模块标题展开相应的模块,再次点击此模块匿藏,如何实现此需求并实现复用? 点击红框前: 点击后: 难点分析: 模块高度不固定.比如,本人一开始想到的方法如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title

  • angularjs 页面自适应高度的方法

    需求 在angularjs构建的业务系统中,通过ui-view路由实现页面跳转,初始化进入系统后,右侧内容区域需要自适应浏览器高度. 实现方案 在ui-view所在的Div添加directive,directive中通过element.css初始化计算div的高度,动态更新div高度 directive监听($$watch)angular的$digest,实时获取body高度,动态赋值model或element.css改变 方案1:添加directive和element.css自适应高度 1.创

  • javascript实现页面内关键词高亮显示代码

    复制代码 代码如下: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv=&qu

  • JS实现关键词高亮显示正则匹配

    html 和ajax 部分就不写了,只需将需要匹配的文字传进去就可以了 比如匹配后台传回的字符串data.content中的关键词:直接调用: data.content = highLightKeywords(data.content,keywords)即可 以下两个函数分辨是匹配1:匹配关键词words中每一个字符,2:匹配整个关键词words //高亮关键字 text =>内容 words:关键词 tag 被包裹的标签 //匹配每一个关键字字符 function highLightKeywo

  • firefox下javascript实现高亮关键词的方法

    复制代码 代码如下: IE下有:   var range = document.createRange();   FireFox下有:   var range = document.body.createTextRange(); IE下有findText及pasteHTML,但是fireFox下就没有!怎么办?查了好多资料,都没有能说出个所以然的,皇天不负有心人,终于让我给搞出来了! 注:我这里不是用正则替换,因为正则替换有它的不足之处! 不知道先前有没有高人研究过这种方法. Untitled

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

    目录 关键词高亮 1. 实现的主要功能: 2. 效果演示 高级定制用法 用法 1. react中使用 2. 原生js使用innerHTML 源码 核心源码 渲染方案 1. react组件渲染 2. innerHTML渲染 showcase演示组件 关键词高亮 日常需求开发中常见需要高亮的场景,本文主要记录字符串渲染时多个关键词同时高亮的实现方法,目的是实现高度可扩展的多关键词高亮方案. 1. 实现的主要功能: 关键词提取和高亮 多个关键词同时高亮 关键词支持正则匹配 每个关键字支持独立样式配置,

  • js中getBoundingClientRect的作用及兼容方案详解

    1.getBoundingClientRect的作用 getBoundingClientRect用于获取某个html元素相对于视窗的位置集合. 执行 object.getBoundingClientRect();会得到元素的top.right.bottom.left.width.height属性,这些属性以一个对象的方式返回. getBoundingClientRect() 这个方法返回一个矩形对象,包含四个属性:left.top.right和bottom.分别表示元素各边与页面上边和左边的距离

  • 基于js中style.width与offsetWidth的区别(详解)

    作为一个初学者,经常会遇到在获取某一元素的宽度(高度.top值...)时,到底是用 style.width还是offsetWidth的疑惑. 1. 当样式写在行内的时候,如 <div id="box" style="width:100px">时,用 style.width或者offsetWidth都可以获取元素的宽度. 但是,当样式写在样式表中时,如 #box{ width: 100px; }, 此时只能用offsetWidth来获取元素的宽度,而sty

  • JS中实现浅拷贝和深拷贝的代码详解

    (一)JS中基本类型和引用类型 JavaScript的变量中包含两种类型的值:基本类型值 和 引用类型值,在内存中的表现形式在于:前者是存储在栈中的一些简单的数据段,后者则是保存在堆内存中的一个对象. 基本类型值 在JavaScript中基本数据类型有 String , Number , Undefined , Null , Boolean ,在ES6中,又定义了一种新的基本数据类型 Symbol ,所以一共有6种. 基本类型是按值访问的,从一个变量复制基本类型的值到另一个变量后,这两个变量的值

  • 原生js实现贪食蛇小游戏的思路详解

    先不多说先上图 下面是代码部分(这里你可以根据需要改变蛇头和身体还有食物的图片,然后默认的样式是使用纯颜色的如果没有更改我的背景图片的话------改这些图开始是想搞笑一下朋友哈哈哈,请不要在意哈),还有操作键是使用 ↑ ↓ ← → ) <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>贪食蛇</title>

  • js中hasOwnProperty的属性及实例用法详解

    1.js不会保护hasOwnProperty被非法占用,如果一个对象碰巧存在这个属性, 就需要使用外部的hasOwnProperty 函数来获取正确的结果. 2.当检查对象上某个属性是否存在时,hasOwnProperty 是唯一可用的方法. 实例 var foo = { hasOwnProperty: function() { return false; }, bar: 'Here be dragons' }; foo.hasOwnProperty('bar'); // 总是返回 false

  • 使用纯前端JavaScript实现Excel导入导出方法过程详解

    公司最近要为某国企做一个**统计和管理系统, 具体要求包含 Excel导入导出根据导入的数据进行展示报表图表展示(包括柱状图,折线图,饼图),而且还要求要有动画效果,扁平化风格Excel导出,并要提供客户端来管理Excel 文件... 要求真多! 现在总算是完成了,于是将我的经验分析出来. 在整个项目架构中,首先就要解决Excel导入的问题. 由于公司没有自己的框架做Excel IO,就只有通过其他渠道了. 嗯,我在github上找到了一个开源库xlsx,通过npm方式来安装. npm inst

  • JS技巧多状态页面中的mock方案详解

    目录 引言 技术选型 业务逻辑改造 Eruda 插件 Mock 数据整理 引言 我们有时候会遇到一个业务页面存在很多个状态,甚至子状态,比如订单详情就是其中的典型,涉及从订单创建到订单结束,以及售后等流程.维护起来每个状态对应一份数据,虽然我们 QA 提供了数据构造平台,但构造一份对应状态的数据还是需要花费不少时间,而且串行流程一旦出错的话只能重新来一遍. 后期维护阶段也不容易构造对应状态的数据,导致排查页面问题比较耗时. 另外一个问题就是从头熟悉业务的话成本比较高,如果有一个直观的页面能够看到

  • 使用纯JavaScript封装一个消息提示条功能示例详解

    目录 介绍 思路&布局 操作逻辑 完整代码 介绍 一个类似Element UI.Ant-Design UI等 UI 框架的消息提示功能,方便在任何网页环境中直接调用函数使用:区别在不依赖 js 及 css 引用,而是使用纯 js 进行封装实现,代码更精简,同时保持和 UI 框架一样的视觉效果(可自行修改成自己喜欢的样式) 代码仓库 在线预览效果(点击[登录].[点击复制]按钮时触发提示效果) 思路&布局 先来写单个提示条,并实现想要的过渡效果,最后再用逻辑操作输出节点即可:这里不需要父节点

  • 前端JS图片懒加载原理方案详解

    目录 背景 原理 方案 方案一:img的loading属性设为“lazy” 使用方法 优点 兼容性 缺点 方案二:通过offsetTop来计算是否在可视区域内 优化 优点 缺点 方案三:通过getBoundingClientRect来计算是否在可视区域内 方案四:使用IntersectionObserver来判断是否在可视区域内 兼容性 优点 缺点 问题 布局抖动 响应式图片 SEO不友好 插件 背景 懒加载经常出现在前端面试中,是前端性能优化的常用技巧.懒加载也叫延迟加载,把非关键资源先不加载

随机推荐