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

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

关键词高亮

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

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

      2.>

      体验地址:链接

      高级定制用法

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

        用法

        1.>
        export default () => {
            const text = `123432123424r2`;
            const keywords = ['123'];
            return (
                <HighlightKeyword content={text} keywords=js高度可扩展关键词高亮,js 关键词高亮 />
            );
        };
        

        2.>
        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组件
        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.>
        /** ***** 以上是核心代码部分,以下渲染部分 ********************************************************/
        // 驼峰转换横线
        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高度可扩展关键词高亮的资料请关注易采站长站其它相关文章!