溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

純js如何實(shí)現(xiàn)高度可擴(kuò)展關(guān)鍵詞高亮

發(fā)布時(shí)間:2022-08-31 09:59:59 來(lái)源:億速云 閱讀:145 作者:iii 欄目:開發(fā)技術(shù)

今天小編給大家分享一下純js如何實(shí)現(xiàn)高度可擴(kuò)展關(guān)鍵詞高亮的相關(guān)知識(shí)點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識(shí),所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來(lái)了解一下吧。

    關(guān)鍵詞高亮

    日常需求開發(fā)中常見需要高亮的場(chǎng)景,本文主要記錄字符串渲染時(shí)多個(gè)關(guān)鍵詞同時(shí)高亮的實(shí)現(xiàn)方法,目的是實(shí)現(xiàn)高度可擴(kuò)展的多關(guān)鍵詞高亮方案。

    1. 實(shí)現(xiàn)的主要功能:

    • 關(guān)鍵詞提取和高亮

    • 多個(gè)關(guān)鍵詞同時(shí)高亮

    • 關(guān)鍵詞支持正則匹配

    • 每個(gè)關(guān)鍵字支持獨(dú)立樣式配置,支持高度定制化

      • 不同標(biāo)簽使用不同顏色區(qū)分開

      • 使用不同標(biāo)簽名

      • 使用定制化CSSStyle樣式

      • 自定義渲染函數(shù),渲染成任何樣式

    • 擴(kuò)展性較好,可以根據(jù)解析數(shù)據(jù)自定義渲染,能很好的兼容復(fù)雜的場(chǎng)景

    2. 效果演示

    純js如何實(shí)現(xiàn)高度可擴(kuò)展關(guān)鍵詞高亮

    高級(jí)定制用法

    • 自定義渲染,例如可以將文本變成鏈接

    純js如何實(shí)現(xiàn)高度可擴(kuò)展關(guān)鍵詞高亮

    用法

    1. react中使用

    export default () => {
        const text = `123432123424r2`;
        const keywords = ['123'];
        return (
            <HighlightKeyword content={text} keywords=js高度可擴(kuò)展關(guān)鍵詞高亮,js 關(guān)鍵詞高亮 />
        );
    };

    2. 原生js使用innerHTML

    const div = document.querySelector('#div');
    div.innerHTML = getHighlightKeywordsHtml(templateStr, [keyword]);

    源碼

    核心源碼

    // 關(guān)鍵詞配置
    export interface IKeywordOption {
      keyword: string | RegExp;
      color?: string;
      bgColor?: string;
      style?: Record<string, any>;
      // 高亮標(biāo)簽名
      tagName?: string;
      // 忽略大小寫
      caseSensitive?: boolean;
      // 自定義渲染高亮html
      renderHighlightKeyword?: (content: string) => any;
    }
    export type IKeyword = string | IKeywordOption;
    export interface IMatchIndex {
      index: number;
      subString: string;
    }
    // 關(guān)鍵詞索引
    export interface IKeywordParseIndex {
      keyword: string | RegExp;
      indexList: IMatchIndex[];
      option?: IKeywordOption;
    }
    // 關(guān)鍵詞
    export interface IKeywordParseResult {
      start: number;
      end: number;
      subString?: string;
      option?: IKeywordOption;
    }
    /** ***** 以上是類型,以下是代碼 ********************************************************/
    /**
     * 多關(guān)鍵詞的邊界情況一覽:
     *    1. 關(guān)鍵詞之間存在包含關(guān)系,如: '12345' 和 '234'
     *    2. 關(guān)鍵詞之間存在交叉關(guān)系,如: '1234' 和 '3456'
     */
    // 計(jì)算
    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;
    };
    // 解析關(guān)鍵詞為索引
    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;
    };
    // 解析關(guān)鍵詞為數(shù)據(jù)
    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;
          }
          // 沒(méi)有交集,且在當(dāng)前的前面
          if (index + len <= cur.start) {
            return i;
          }
          // 沒(méi)有交集,且在當(dāng)前的后面的,放在下個(gè)迭代處理
        }
        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);
          }
        });
      });
      // 補(bǔ)上沒(méi)有匹配關(guān)鍵詞的部分
      const list: IKeywordParseResult[] = [];
      splitList.forEach((cur, i) => {
        const { start, end } = cur;
        const next = splitList[i + 1];
        // 第一個(gè)前面補(bǔ)一個(gè)
        if (i === 0 && start > 0) {
          list.push({ start: 0, end: start, subString: content.slice(0, start) });
        }
        list.push({ ...cur, subString: content.slice(start, end) });
        // 當(dāng)前和下一個(gè)中間補(bǔ)一個(gè)
        if (next?.start > end) {
          list.push({
            start: end,
            end: next.start,
            subString: content.slice(end, next.start),
          });
        }
        // 最后一個(gè)后面補(bǔ)一個(gè)
        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渲染

    /** ***** 以上是核心代碼部分,以下渲染部分 ********************************************************/
    // 駝峰轉(zhuǎn)換橫線
    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;
    };
    // 生成關(guān)鍵詞高亮的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>編輯標(biāo)簽</h3>
          <Form.Item field="keyword" label="標(biāo)簽">
            <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="標(biāo)簽名">
            <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="點(diǎn)擊訪問(wèn)鏈接">
              <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>關(guān)鍵詞高亮</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="添加標(biāo)簽">
                <Button
                  type="primary"
                  icon={<IconPlus />}
                  style={{ marginLeft: 'auto' }}
                  onClick={() => {
                    setEditTagIndex(keywords.length);
                  }}>
                  添加標(biāo)簽
                </Button>
              </Tooltip>
            </Popover>
          </div>
          <div style={{ display: 'flex', padding: '15px 0' }}></div>
          {keywords.map((keyword, i) => (
            <Tooltip key={JSON.stringify(keyword)} content="雙擊編輯標(biāo)簽">
              <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="內(nèi)容區(qū)">
            <HighlightContainer>
              <HighlightKeyword content={text} keywords=js高度可擴(kuò)展關(guān)鍵詞高亮,js 關(guān)鍵詞高亮 />
            </HighlightContainer>
          </Card>
        </div>
      );
    };

    以上就是“純js如何實(shí)現(xiàn)高度可擴(kuò)展關(guān)鍵詞高亮”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會(huì)為大家更新不同的知識(shí),如果還想學(xué)習(xí)更多的知識(shí),請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

    向AI問(wèn)一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    js
    AI