在保持 html 结构的同时搜索并突出显示页面上的文本

Search and highlight text on page while keeping html structure

类似的问题可能已经有人问过了,但请仔细阅读细节。

我正在使用一种自制的自动完成功能,现在我想在结果集中突出显示搜索词。 到目前为止,这有效,但仅适用于纯文本。问题是:我需要保留 html 结构,如果结果 div 中有一个。请查看我的示例:目前我正在丢失包含 class 粗体的跨度。我怎样才能保留它们?

感谢您的任何建议!

$('#box').keyup(function () {
  const valThis = this.value;
  const length  = this.value.length;

  $('.objType').each(function () {
    const text  = $(this).text();
    const textL = text.toLowerCase();
    const position = textL.indexOf(valThis.toLowerCase());

    if (position !== -1) {
      const matches = text.substring(position, (valThis.length + position));
      const regex = new RegExp(matches, 'ig');
      const highlighted = text.replace(regex, `<mark>${matches}</mark>`);

      $(this).html(highlighted).show();
    } else {
     $(this).text(text);
      $(this).hide();
    }
  });

});
input[type="text"] { 
    width: 50%;
    margin:10px;
    padding: 5px;
    float:left;
    clear:left;
}
div{
  float:left;
  clear:left;
  margin:10px 10px;
}
.bold {
  font-weight: 700;
}
table td {
  border: solid 1px #ccc;
  padding: 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.0/jquery.min.js"></script>
<input placeholder="Filter results" id="box" type="text" />

<div class="objType" id="item1">
  <span class="bold">Accepted</span> Event Relation
  <table>
  <tr>
    <td>Lorem</td>
    <td>ipsum</td>
  </tr>
  </table>
</div>
<div class="objType" id="item2">
  Case <span class="bold">Status</span> Value
  <table>
  <tr>
    <td>Lorem</td>
    <td>ipsum</td>
  </tr>
  </table>
</div>
<div class="objType" id="item3">
  External <span class="bold">Data Source</span>
  <table>
  <tr>
    <td>Lorem</td>
    <td>ipsum</td>
  </tr>
  </table>
</div>
<div class="objType" id="item4">
  Navigation <span class="bold">Link</span> Set
  <table>
  <tr>
    <td>Lorem</td>
    <td>ipsum</td>
  </tr>
  </table>
</div>

PS:另外一个 JSFiddle 可能会有帮助 => https://jsfiddle.net/SchweizerSchoggi/6x3ak5d0/7/

我已尝试研究您的方法并提出了以下解决方案。它在大多数情况下工作正常。

/*
This function will get all the indices of searched term in the html in array format
e.g. if html is <span>Accept</span> and user types a
Input: getAllIndicesOf(a, "<span>Accept</span>", false)
Output: [3,6,16]
*/
function getAllIndicesOf(searchStr, str, caseSensitive) {
    var searchStrLen = searchStr.length;
    if (searchStrLen == 0) {
        return [];
    }
    var startIndex = 0, index, indices = [];
    if (!caseSensitive) {
        str = str.toLowerCase();
        searchStr = searchStr.toLowerCase();
    }
    while ((index = str.indexOf(searchStr, startIndex)) > -1) {
        indices.push(index);
        startIndex = index + searchStrLen;
    }
    return indices;
}

/*
What ever values I am getting from getAllIndicesOf, here I try to find if the searched value is not present inside html tag.
e.g. if html is <span>Accept</span> and user types a
getAllIndicesOf will output [3,6,16]
Input: findValidMatches([3,6,16], "a")
Output: [6]
Logic: The valid matching text will lie between > and <. If it lies between < and >, it is a html tag. 
*/
function findValidMatches(pseudoPosition, str) {
    const pos = []
    for (let i = 0; i<pseudoPosition.length; ++i) {
     const splitText = str.substr(pseudoPosition[i])
      const indexOfLT = splitText.indexOf("<")
      const indexOfGT = splitText.indexOf(">")
      if (indexOfLT > -1 && indexOfGT > -1 && indexOfLT < indexOfGT) {
        pos.push(pseudoPosition[i])
      }
      else if (indexOfLT === -1 && indexOfGT > -1 && indexOfGT < 0) {
        pos.push(pseudoPosition[i])
      }
      else if (indexOfGT === -1 && indexOfLT > -1 && indexOfLT > 0) {
        pos.push(pseudoPosition[i])
      }
      else if (indexOfLT === -1 && indexOfGT === -1) {
        pos.push(pseudoPosition[i])
      }
    }
    return pos
}

/*
This will replace the matched valid string with <mark>text</mark> to highlight
if html is <span>Accept</span> and user types a
getAllIndicesOf will output [3,6,16] -> findValidMatches will output [6] -> input to replaceText

replaceText("<span>Accept</span>", [6], "a") will output <span><mark>A</mark></span>
*/
function replaceText(text, correctPositions, valueToReplace) {
  let copyText = text
  for (let i = 0; i<correctPositions.length; ++i) {
    const upValue = correctPositions[i] + 13*i
    const firstPart = copyText.slice(0, upValue)
    const lastPart = copyText.slice(upValue + valueToReplace.length, copyText.length)
    const valueWithCase = copyText.substr(upValue, valueToReplace.length)
    copyText = firstPart + "<mark>" + valueWithCase +"</mark>" + lastPart
  }
  return copyText
}

$('#box').keyup(function () {
  const valThis = this.value;

  $('.objType').each(function () {
    const text  = $(this).html().replace(/<mark>/gi, "").replace(/<\/mark>/gi, "");
    const position = getAllIndicesOf(valThis, text) //Get all indices of valThis in the html
    const correctPositions = findValidMatches(position, text) //Filter only those indices which indicate that they are text and not html
    const updatedText = replaceText(text, correctPositions, valThis) //Get the updated text with mark tags
    if (correctPositions.length > 0) {
      $(this).html(updatedText)
      $(this).show();
    } else {
      if (valThis.length > 0) $(this).hide();
      else {
        $(this).html(text)
        $(this).show();
      }
    }
  });

});
input[type="text"] { 
    width: 50%;
    margin:10px;
    padding: 5px;
    float:left;
    clear:left;
}
div{
  float:left;
  clear:left;
  margin:10px 10px;
}
.bold {
  font-weight: 700;
}
table td {
  border: solid 1px #ccc;
  padding: 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<input placeholder="Filter results" id="box" type="text" />

<div class="objType" id="item1">
  <span class="bold">Accepted</span> Event Relation
  <table>
  <tr>
    <td>Lorem</td>
    <td>ipsum</td>
  </tr>
  </table>
</div>
<div class="objType" id="item2">
  Case <span class="bold">Status</span> Value
  <table>
  <tr>
    <td>Lorem</td>
    <td>ipsum</td>
  </tr>
  </table>
</div>
<div class="objType" id="item3">
  External <span class="bold">Data Source</span>
  <table>
  <tr>
    <td>Lorem</td>
    <td>ipsum</td>
  </tr>
  </table>
</div>
<div class="objType" id="item4">
  Navigation <span class="bold">Link</span> Set
  <table>
  <tr>
    <td>Lorem</td>
    <td>ipsum</td>
  </tr>
  </table>
</div>

我采用的方法是直接搜索html部分,然后更新。

接近

  1. 将整个html作为字符串(const text = $(this).html().replace(/<mark>/gi, "").replace(/<\/mark>/gi, "");)
  2. 查找所有出现的搜索词(使用getAllIndicesOf
  3. 只选择那些不在 html 标签内的事件(使用 findValidMatches
  4. 通过在适当的位置插入 <mark></mark> 标签(使用 replaceText)重建 html
  5. 将字符串重新插入为 html

可能会有很多相关的问题(例如,如果事件处理程序在那里,如果您尝试搜索带有 html 标签的文本,搜索将失败,例如尝试搜索 AcceptedEvent)。我将尝试对此进行更新。

希望对您有所帮助。

使用 mark.js 插件

$('#box').on('input', function() {
  const valThis = this.value;

  const options = {
    filter: function(node, term, totalCounter, counter) {
      $(node).parents('.objType').show()
      return true
    }
  };

  $('.objType').unmark({
    done: function() {
      $('.objType')
        .toggle(valThis.length === 0)
        .mark(valThis, options);
    }
  })
});
input[type="text"] {
  width: 50%;
  margin: 10px;
  padding: 5px;
  float: left;
  clear: left;
}

div {
  float: left;
  clear: left;
  margin: 10px 10px;
}

.bold {
  font-weight: 700;
}

table td {
  border: solid 1px #ccc;
  padding: 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/jquery.mark.min.js" integrity="sha256-4HLtjeVgH0eIB3aZ9mLYF6E8oU5chNdjU6p6rrXpl9U=" crossorigin="anonymous"></script>

<input placeholder="Filter results" id="box" type="text" />

<div class="objType" id="item1">
  <span class="bold">Accepted</span> Event Relation
  <table>
    <tr>
      <td>Lorem</td>
      <td>ipsum</td>
    </tr>
  </table>
</div>
<div class="objType" id="item2">
  Case <span class="bold">Status</span> Value
  <table>
    <tr>
      <td>Lorem</td>
      <td>ipsum</td>
    </tr>
  </table>
</div>
<div class="objType" id="item3">
  External <span class="bold">Data Source</span>
  <table>
    <tr>
      <td>Lorem</td>
      <td>ipsum</td>
    </tr>
  </table>
</div>
<div class="objType" id="item4">
  Navigation <span class="bold">Link</span> Set
  <table>
    <tr>
      <td>Lorem</td>
      <td>ipsum</td>
    </tr>
  </table>
</div>

按照@Sunil 解决方案导致问题的原因是 <span> 标签,您需要将其删除,所以我编辑了这部分代码:

$('.objType').each(function () {

    const text  = $(this).html().replace(/<mark>/gi, "").replace(/<\/mark>/gi, "");
    const text2  = text.replace(/<span class=\"bold\">/gi, "").replace(/<\/span>/gi, "");

    const position = getAllIndicesOf(valThis, text2) //Get all indices of valThis in the html
    const correctPositions = findValidMatches(position, text2) //Filter only those indices which indicate that they are text and not html
    const updatedText = replaceText(text2, correctPositions, valThis) //Get the updated text with mark tags
    if (correctPositions.length > 0) {
      $(this).html(updatedText)

      $(this).show();
    } else {
      if (valThis.length > 0) $(this).hide();
      else {
        $(this).html(text2)
        $(this).show();
      }
    }
  });

为了简单起见,我在 vue 文件中创建了功能(因为,这些功能易于实现且变量易于插入):

<template>
  <div class="container">
    <div class="position-relative">
      <input
        v-model="inputValue"
        type="search"
        autocomplete="off"
        class="form-control"
        @input="onInput" />
    </div>
    <pre v-html="results" />
  </div>
</template>

<script>
export default {
  name: 'Typeahead',
  data() {
    return {
      inputValue: '',
      items: [
        { value: '' },
        { value: '' },
        { value: '' },
        // and so on (Value is the field to be searched).
      ],
      results: [],
    };
  },

  created() {
    this.results = this.items; // initially assign all the items as results
  },

  methods: {
    onInput() { // input event (use event target value for vanilla js.)
      const value = this.inputValue;
      if (!value) {
        this.results = this.items;
        return;
      }
      const escapedQuery = this.escapeRegExp(value); // escape any special characters
      const queryReg = new RegExp(escapedQuery, 'gi'); // create a regular expression out of it
      this.results = this.matchItem(this.items, queryReg) // call match function
        .map(item => ({
          ...item,
          value: this.highlight(item.value, queryReg), // highlight the matched text range
        }));
    },

    escapeHtml(text) {
      return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
    },

    escapeRegExp(text) {
      return text.replace(/[.*+?^${}()|[\]\]/g, '\$&');
    },

    matchItem(items, queryReg) {
      return items
        .filter(i => i.value.match(queryReg) !== null)
        .sort((a, b) => {
          const aIndex = a.value.indexOf(a.value.match(queryReg)[0]);
          const bIndex = b.value.indexOf(b.value.match(queryReg)[0]);
          if (aIndex < bIndex) return -1;
          if (aIndex > bIndex) return 1;
          return 0;
        });
    },

    highlight(text, queryReg) {
      const escaped = this.escapeHtml(text);
      return escaped.replace(queryReg, '<b>$&</b>');
    },
  },
};
</script>

Check this fiddle I've created

基本上,它所做的是从输入文本中转义任何特殊符号并从中创建一个正则表达式,然后过滤出与该正则表达式匹配的记录。然后根据匹配的强度对元素进行排序。现在,匹配的部分(记录中的值)使用 strongb html 标记突出显示(我在这里使用粗体标记),可以轻松嵌入到 html 会按预期产生突出显示的输出。

我使用了预标记来显示结果。您可以根据需要创建 table 结构。

这些方法是原版的 javascript,其中没有太多 vue(this 引用除外)。

希望对您有所帮助:)

这是仅使用 native javascript 的可能基础。这有点像 CTRL+F

这似乎保留了 <td> 个元素。

清除函数用 wbr 元素替换 mark 元素:

On UTF-8 encoded pages, <wbr> behaves like the U+200B ZERO-WIDTH SPACE code point. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/wbr

function mark(it){
  clearIt()
  if (it.length > 2){
    let c = new RegExp(it, "ig") 
    main.innerHTML = main.innerHTML.replace(c,"<mark>"+it+"</mark>")
  }  
}

function clearIt(){
  let b = new RegExp("mark>", "ig") 
  main.innerHTML = main.innerHTML.replace(b,"wbr>")
}

mark(search.value)
input[type="text"] { 
    width: 50%;
    margin:10px;
    padding: 5px;
    float:left;
    clear:left;
}
div{
  float:left;
  clear:left;
  margin:10px 10px;
}
.bold {
  font-weight: 700;
}
table td {
  border: solid 1px #ccc;
  padding: 3px;
}
<input onfocusout="clearIt()" oninput="mark(this.value)"  value="Lorem" id="search" placeholder="Lorem">
<button onclick="mark(search.value)">SEARCH</button>
<button onclick="clearIt()">CLEAR</button>

<div id="main">
<div class="objType" id="item1">
  <span class="bold">Accepted</span> Event Relation
  <table>
  <tr>
    <td>Lorem</td>
    <td>ipsum</td>
  </tr>
  </table>
</div>
<div class="objType" id="item2">
  Case <span class="bold">Status</span> Value
  <table>
  <tr>
    <td>Lorem</td>
    <td>ipsum</td>
  </tr>
  </table>
</div>
<div class="objType" id="item3">
  External <span class="bold">Data Source</span>
  <table>
  <tr>
    <td>Lorem</td>
    <td>ipsum</td>
  </tr>
  </table>
</div>
<div class="objType" id="item4">
  Navigation <span class="bold">Link</span> Set
  <table>
  <tr>
    <td>Lorem</td>
    <td>ipsum</td>
  </tr>
  </table>
</div>

</div>

顺便说一下,restored/cleared 标记不是原始标记,要完全恢复它,您可能需要在标记之前复制整个 html。