Помогите сделать поиск по странице "живым"
Поиск по странице, с выделением совпадений, аналогичный Ctrl+F в браузере. Но только он не начнет искать, пока не нажать кнопку. При чем её приходится нажимать для выделения каждого из совпадений. В общем, помогите пожалуйста его "оживить"
superfind = function(){
var surround = document.createElement('span');
surround.style.background='#e9bd13';
return function( str ){
var sel = getSelection(); // получаем выделение
sel.removeAllRanges(); // обнуляем его
if(surround.hasChildNodes()){ //если ужк есть выделение
sel.collapse(surround.nextSibling,0); // ставим в позицию конца предыдущего поиска, если есть (для новых поисков обнуление не предусмотрено)
var temp = document.createDocumentFragment();
while(surround.hasChildNodes()) temp.appendChild(surround.firstChild);
surround.parentNode.replaceChild( temp,surround); //возвращаем как было (тут может быть баг при нахожении одного в другом)
}
while(sel.isCollapsed){ //ищем пока что-нибудь не найдёт в документе (а не в input)
if(!find( str )){ //и пока документ не закончится
return false
}
}
var range = sel.getRangeAt(0); //получаем range
range.surroundContents(surround); //оборачиваем найденное
sel.collapse(surround,1); //схлопываем выделение
return true
}
}();
<input type="button" value="Найти" onclick="superfind(nextSibling.value)"/><input type="text" placeholder="Введите текст..."/>
<p>Вот, надо тыкать кнопку, само не ищет :c</p>
<p>Система вопросов и ответов о программировании, разработанная Джоэлем Спольски и Джеффом Этвудом в 2008 году. Является частью Stack Exchange Network. Как и в других системах подобного рода, Stack Overflow предоставляет возможность оценивать вопросы и ответы, что поднимает или понижает репутацию зарегистрированных пользователей. Проект создан на C# с использованием ASP.NET 4 и ASP.NET MVC.</p>
<p>Система вопросов и ответов о программировании, разработанная Джоэлем Спольски и Джеффом Этвудом в 2008 году. Является частью Stack Exchange Network. Как и в других системах подобного рода, Stack Overflow предоставляет возможность оценивать вопросы и ответы, что поднимает или понижает репутацию зарегистрированных пользователей. Проект создан на C# с использованием ASP.NET 4 и ASP.NET MVC.</p>
<p>Система вопросов и ответов о программировании, разработанная Джоэлем Спольски и Джеффом Этвудом в 2008 году. Является частью Stack Exchange Network. Как и в других системах подобного рода, Stack Overflow предоставляет возможность оценивать вопросы и ответы, что поднимает или понижает репутацию зарегистрированных пользователей. Проект создан на C# с использованием ASP.NET 4 и ASP.NET MVC.</p>
<p>Система вопросов и ответов о программировании, разработанная Джоэлем Спольски и Джеффом Этвудом в 2008 году. Является частью Stack Exchange Network. Как и в других системах подобного рода, Stack Overflow предоставляет возможность оценивать вопросы и ответы, что поднимает или понижает репутацию зарегистрированных пользователей. Проект создан на C# с использованием ASP.NET 4 и ASP.NET MVC.</p>
P.S. Обновил весь код.
Ответы (2 шт):
Если я правильно понял, вам нужно искать сразу после нажатия нужной клавиши?
const greyoutText = function () {
const highlightElements = document.querySelectorAll('.superfind-highlight');
highlightElements.forEach((element) => {
if (element.parentNode) {
element.parentNode.textContent = element.parentNode.textContent;
}
});
};
const highlightText = function (text) {
const searchRegExp = new RegExp(`${text}`, 'i');
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
acceptNode: function (node) {
if (searchRegExp.test(node.data)) {
return NodeFilter.FILTER_ACCEPT;
}
}
}, false);
const textNodes = [];
let currentNode= walker.currentNode;
while (currentNode) {
if (currentNode !== document.body) {
textNodes.push(currentNode);
}
currentNode = walker.nextNode();
}
const searchText = new RegExp(`${text}`, 'ig');
textNodes.forEach((node) => {
node.parentElement.innerHTML = node.parentElement.innerHTML.replace(searchText, `<span class="superfind-highlight">${text}</span>`);
});
};
const superfind = function (text) {
greyoutText();
if (text) {
highlightText(text);
}
}
document.getElementById('search').addEventListener('input', function () {
superfind(this.value);
});
.superfind-highlight {
background-color: #ffff00;
}
<input id="search" type="text" placeholder="Введите текст..."/>
<p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus quis egestas velit. In ornare, magna sit amet malesuada commodo, turpis augue congue neque, a congue urna diam vitae odio. Integer luctus posuere nunc, ut vulputate tortor cursus non. Integer et magna non nisl volutpat sollicitudin. Nunc mollis sapien eget massa auctor condimentum eget vel urna. Etiam ac auctor lectus. Curabitur lacinia metus non posuere egestas.</p>
<p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus quis egestas velit. In ornare, magna sit amet malesuada commodo, turpis augue congue neque, a congue urna diam vitae odio. Integer luctus posuere nunc, ut vulputate tortor cursus non. Integer et magna non nisl volutpat sollicitudin. Nunc mollis sapien eget massa auctor condimentum eget vel urna. Etiam ac auctor lectus. Curabitur lacinia metus non posuere egestas. Proin iaculis justo sed urna eleifend, et dictum justo lobortis. Nullam scelerisque neque non nisl consectetur, et consectetur sem pulvinar. Phasellus ut magna ac lorem rhoncus vehicula. Suspendisse luctus velit lorem, eget pharetra massa mollis in. Fusce vel dapibus massa.</p>
<p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus quis egestas velit. In ornare, magna sit amet malesuada commodo, turpis augue congue neque, a congue urna diam vitae odio. Integer luctus posuere nunc, ut vulputate tortor cursus non. Integer et magna non nisl volutpat sollicitudin. Nunc mollis sapien eget massa auctor condimentum eget vel urna. Etiam ac auctor lectus. Curabitur lacinia metus non posuere egestas. Proin iaculis justo sed urna eleifend, et dictum justo lobortis. Nullam scelerisque neque non nisl consectetur, et consectetur sem pulvinar. Phasellus ut magna ac lorem rhoncus vehicula. Suspendisse luctus velit lorem, eget pharetra massa mollis in. Fusce vel dapibus massa. Nam vel scelerisque ex. Integer ipsum nisl, ultricies ac molestie a, ultrices eget ligula. Aliquam elit dui, varius et sollicitudin a, suscipit vel tellus. Aliquam id tellus sit amet libero bibendum malesuada. Cras ornare odio at lectus dignissim maximus. Vestibulum cursus lectus purus. Phasellus ultrices mauris sit amet odio consequat, ac luctus erat vehicula. Nullam ac tortor sit amet dui laoreet molestie.</p>
Могу предложить следующий несложный алгоритм поиска.
- Перебираем все текстовые узлы-потомки некоторого узла-элемента (внутри которого и осуществляется поиск).
- Если текстовый узел содержит искомый текст, то разбиваем узел на несколько текстовых узлов. Причём искомый текст формирует самостоятельный узел.
- Оборачиваем текстовые узлы с искомым текстом в какой-нибудь тег, например,
<span>с уникальным классом (для идентификации данных узлов).
При изменении поискового запроса первым делом удаляем все теги обёртки с заданным уникальным классом.
В общем, примерно так:
function textSearch() {
function wrapTextInTextNode(textNode, searchText, wrapBlank) {
let textContent = textNode.textContent;
let lowerTextContent = textContent.toLocaleLowerCase();
let resNodes = [];
let beginIndex = 0;
while (true) {
let index = lowerTextContent.indexOf(searchText, beginIndex);
if (index === -1)
break;
let prevText = textContent.slice(beginIndex, index);
if (prevText.length > 0)
resNodes.push(document.createTextNode(prevText));
let elem = wrapBlank.cloneNode(false);
elem.textContent = textContent.slice(index, index + searchText.length);
resNodes.push(elem);
beginIndex = index + searchText.length;
}
if (beginIndex === 0)
return resNodes;
let prevText = textContent.slice(beginIndex);
if (prevText.length > 0)
resNodes.push(document.createTextNode(prevText));
return resNodes;
}
function wrapTextNodes(elementNode, searchText, wrapBlank) {
let childNodes = Array.from(elementNode.childNodes);
for (let child of childNodes)
switch (child.nodeType) {
case Node.TEXT_NODE:
let nodesArr = wrapTextInTextNode(child, searchText, wrapBlank);
if (nodesArr.length === 0)
break;
let prev = child;
for (let node of nodesArr) {
prev.after(node);
prev = node;
}
child.remove();
break;
case Node.ELEMENT_NODE:
wrapTextNodes(child, searchText, wrapBlank);
break;
}
}
function unwrapTextNodes(elementNode, wrapBlank) {
let wrappedNodes = elementNode.querySelectorAll("." + wrapBlank.className);
for (let i = 0; i < wrappedNodes.length; ++i) {
let prevNode = wrappedNodes[i].previousSibling;
let nextNode = wrappedNodes[i].nextSibling;
let textNode = document.createTextNode(wrappedNodes[i].textContent);
wrappedNodes[i].replaceWith(textNode);
if (prevNode && prevNode.nodeType === Node.TEXT_NODE) {
prevNode.textContent += textNode.textContent;
textNode.remove();
textNode = prevNode;
}
if (nextNode && nextNode.nodeType === Node.TEXT_NODE) {
textNode.textContent += nextNode.textContent;
nextNode.remove();
}
}
}
const wrapBlank = document.createElement("span"); //Заготовка для оборачивания результатов поиска
wrapBlank.className = "found-result"; //Класс для идентификации результатов поиска.
const searchBlock = document.getElementById("search-block");
const controlInputText = document.getElementById("input-text");
controlInputText.addEventListener("input", handlerInputText);
handlerInputText();
function handlerInputText() {
unwrapTextNodes(searchBlock, wrapBlank);
let searchText = controlInputText.value.trim().toLocaleLowerCase();
if (searchText.length > 0) {
wrapTextNodes(searchBlock, searchText, wrapBlank);
}
}
}
textSearch();
.found-result {
background-color: lime;
}
<p><input type="text" id="input-text"></p>
<div id="search-block">
<p>Вот, надо тыкать кнопку, само не ищет :c</p>
<p>Система вопросов и ответов о программировании, разработанная Джоэлем Спольски и Джеффом Этвудом в 2008 году. Является частью Stack Exchange Network. Как и в других системах подобного рода, Stack Overflow предоставляет возможность оценивать вопросы и
ответы, что поднимает или понижает репутацию зарегистрированных пользователей. Проект создан на C# с использованием ASP.NET 4 и ASP.NET MVC.</p>
<p>Система вопросов и ответов о программировании, разработанная Джоэлем Спольски и Джеффом Этвудом в 2008 году. Является частью Stack Exchange Network. Как и в других системах подобного рода, Stack Overflow предоставляет возможность оценивать вопросы и
ответы, что поднимает или понижает репутацию зарегистрированных пользователей. Проект создан на C# с использованием ASP.NET 4 и ASP.NET MVC.</p>
<p>Система вопросов и ответов о программировании, разработанная Джоэлем Спольски и Джеффом Этвудом в 2008 году. Является частью Stack Exchange Network. Как и в других системах подобного рода, Stack Overflow предоставляет возможность оценивать вопросы и
ответы, что поднимает или понижает репутацию зарегистрированных пользователей. Проект создан на C# с использованием ASP.NET 4 и ASP.NET MVC.</p>
<p>Система вопросов и ответов о программировании, разработанная Джоэлем Спольски и Джеффом Этвудом в 2008 году. Является частью Stack Exchange Network. Как и в других системах подобного рода, Stack Overflow предоставляет возможность оценивать вопросы и
ответы, что поднимает или понижает репутацию зарегистрированных пользователей. Проект создан на C# с использованием ASP.NET 4 и ASP.NET MVC.</p>
</div>
Из недостатков данного подхода, в первую очередь, можно отметить:
- Если искомый текст принадлежит нескольким текстовым узлам, то он не будет найден. Например, текст
тестне будет найден в такой разметке<p>те<strong>с</strong>т</p>. - Если по какой-то причине есть пара текстовых узлов, являющихся детьми одного и того же родителя и при этом один из узлов следует непосредственно за другим, то в некоторых ситуациях функция
unwrapTextNodesбудет объединять их в единый текстовый узел.
Усложнённый вариант поиска с отдельной подсветкой «текущего местоположения», автоматической прокруткой страницы и продолжением поиска с позиции, по которой был произведён щелчок мыши.
function textSearch() {
function wrapTextInTextNode(textNode, searchText, wrapBlank) {
let textContent = textNode.textContent;
let lowerTextContent = textContent.toLocaleLowerCase();
let resNodes = [];
let beginIndex = 0;
while (true) {
let index = lowerTextContent.indexOf(searchText, beginIndex);
if (index === -1)
break;
let prevText = textContent.slice(beginIndex, index);
if (prevText.length > 0)
resNodes.push(document.createTextNode(prevText));
let elem = wrapBlank.cloneNode(false);
elem.textContent = textContent.slice(index, index + searchText.length);
resNodes.push(elem);
beginIndex = index + searchText.length;
}
if (beginIndex === 0)
return resNodes;
let prevText = textContent.slice(beginIndex);
if (prevText.length > 0)
resNodes.push(document.createTextNode(prevText));
return resNodes;
}
function wrapTextNodes(elementNode, searchText, wrapBlank) {
let childNodes = Array.from(elementNode.childNodes);
for (let child of childNodes)
switch (child.nodeType) {
case Node.TEXT_NODE:
let nodesArr = wrapTextInTextNode(child, searchText, wrapBlank);
if (nodesArr.length === 0)
break;
let prev = child;
for (let node of nodesArr) {
prev.after(node);
prev = node;
}
child.remove();
break;
case Node.ELEMENT_NODE:
wrapTextNodes(child, searchText, wrapBlank);
break;
}
}
function unwrapTextNodes(elementNode, wrapBlank) {
let wrappedNodes = elementNode.querySelectorAll("." + wrapBlank.className);
for (let i = 0; i < wrappedNodes.length; ++i) {
let prevNode = wrappedNodes[i].previousSibling;
let nextNode = wrappedNodes[i].nextSibling;
let textNode = document.createTextNode(wrappedNodes[i].textContent);
wrappedNodes[i].replaceWith(textNode);
if (prevNode && prevNode.nodeType === Node.TEXT_NODE) {
prevNode.textContent += textNode.textContent;
textNode.remove();
textNode = prevNode;
}
if (nextNode && nextNode.nodeType === Node.TEXT_NODE) {
textNode.textContent += nextNode.textContent;
nextNode.remove();
}
}
}
//Функции для управления прокруткой страницы
function scrollToFoundElement(foundElement, topOffset) {
let elementRect = foundElement.getBoundingClientRect();
let windowWidth = document.documentElement.clientWidth;
let windowHeight = document.documentElement.clientHeight;
const borderShift = Math.floor(Math.min(20, windowWidth, windowHeight) * 0.5);
let middleX = windowWidth * 0.5;
let middleY = (windowHeight - topOffset) * 0.5 + topOffset;
if (middleY < 0)
middleY = 0;
let scrollX = 0,
scrollY = 0;
if (elementRect.left < middleX) {
if (elementRect.left < borderShift)
scrollX = elementRect.left - borderShift;
} else {
if (elementRect.right > windowWidth - borderShift)
scrollX = elementRect.right - (windowWidth - borderShift);
}
if (elementRect.top < middleY) {
if (elementRect.top < topOffset + borderShift)
scrollY = elementRect.top - (topOffset + borderShift);
} else {
if (elementRect.bottom > windowHeight - borderShift)
scrollY = elementRect.bottom - (windowHeight - borderShift);
}
window.scrollBy(scrollX, scrollY);
}
function getElementPositionInDocument(element) {
let elementRect = element.getBoundingClientRect();
return {
left: elementRect.left + document.documentElement.scrollLeft,
top: elementRect.top + document.documentElement.scrollTop,
};
}
function elementPositionInDocumentToArrayPosition(elementsArray, elementPositionInDocument) {
let arrayPosition = 0;
let minY = Number.POSITIVE_INFINITY;
let minX = Number.POSITIVE_INFINITY;
let {
left,
top
} = elementPositionInDocument;
let documentScrollLeft = document.documentElement.scrollLeft;
let documentScrollTop = document.documentElement.scrollTop;
for (let i = 0; i < elementsArray.length; ++i) {
let elementRect = elementsArray[i].getBoundingClientRect();
let x = elementRect.left + documentScrollLeft;
let y = elementRect.top + documentScrollTop;
let yb = elementRect.bottom + documentScrollTop;
if (y < minY || y === minY && x < minX)
if (y <= top && yb >= top && left <= x || y > top) {
minY = y;
minX = x;
arrayPosition = i;
}
}
return arrayPosition;
}
//Назначаем обработчики событий на элементы управления страницы
const classNameFoundAll = "found-all"; //Класс для подсвечивания всех результатов поиска
const classNameFoundCurrent = "found-current"; //Класс для подсвечивания текущего результата
const wrapBlank = document.createElement("span"); //Заготовка для оборачивания результатов поиска
wrapBlank.className = "found-result"; //Класс для идентификации результатов поиска.
const searchBlock = document.getElementById("search-block");
const controlBlock = document.getElementById("control-block");
const controlInputText = document.getElementById("input-text");
const controlButtonBegin = document.getElementById("button-begin");
const controlButtonPrevious = document.getElementById("button-previous");
const controlButtonNext = document.getElementById("button-next");
const controlCheckboxAll = document.getElementById("checkbox-all");
const controlBlockHeight = controlBlock.offsetHeight + 5;
searchBlock.style.marginTop = controlBlockHeight + "px";
let foundElements = []; //Коллекция узлов, имеющих класс "found-result"
let foundElementsPos = -1; //Индекс текущего подсвеченного узла в коллекции foundElements
let foundElementPositionInDocument = {
left: 0,
top: 0
}; //Координаты текущего подсвеченного узла, либо клика
let flagClick = false;
searchBlock.addEventListener("click", handlerSearchBlock);
controlInputText.addEventListener("input", handlerInputText);
controlButtonBegin.addEventListener("click", handlerButtonBegin);
controlButtonPrevious.addEventListener("click", () => handlerNextPrevious("previous"));
controlButtonNext.addEventListener("click", () => handlerNextPrevious("next"));
controlCheckboxAll.addEventListener("input", handlerCheckboxAll);
document.addEventListener("keydown", handlerKeydown);
handlerInputText();
function handlerSearchBlock(event) {
flagClick = true;
foundElementPositionInDocument = {
left: event.clientX + document.documentElement.scrollLeft,
top: event.clientY + document.documentElement.scrollTop,
};
}
function handlerInputText() {
unwrapTextNodes(searchBlock, wrapBlank);
foundElements = [];
foundElementsPos = -1;
let searchText = controlInputText.value.trim().toLocaleLowerCase();
if (searchText.length > 0) {
wrapTextNodes(searchBlock, searchText, wrapBlank);
foundElements = searchBlock.querySelectorAll("." + wrapBlank.className);
if (foundElements.length > 0) {
if (controlCheckboxAll.checked)
for (let i = 0; i < foundElements.length; ++i)
foundElements[i].classList.add(classNameFoundAll);
flagClick = false;
foundElementsPos = elementPositionInDocumentToArrayPosition(foundElements, foundElementPositionInDocument);
foundElementPositionInDocument = getElementPositionInDocument(foundElements[foundElementsPos]);
foundElements[foundElementsPos].classList.add(classNameFoundCurrent);
scrollToFoundElement(foundElements[foundElementsPos], controlBlockHeight);
}
}
}
function handlerButtonBegin() {
foundElementPositionInDocument = {
left: 0,
top: 0
};
handlerInputText();
}
function handlerNextPrevious(strNextOrPrevious) {
if (foundElements.length === 0)
return;
foundElements[foundElementsPos].classList.toggle(classNameFoundCurrent);
let inc = 1;
if (flagClick) {
flagClick = false;
inc = 0;
foundElementsPos = elementPositionInDocumentToArrayPosition(foundElements, foundElementPositionInDocument);
}
foundElementsPos += strNextOrPrevious === "next" ? inc : -1;
if (foundElementsPos >= foundElements.length)
foundElementsPos = 0;
else if (foundElementsPos < 0)
foundElementsPos = foundElements.length - 1;
foundElementPositionInDocument = getElementPositionInDocument(foundElements[foundElementsPos]);
foundElements[foundElementsPos].classList.toggle(classNameFoundCurrent);
scrollToFoundElement(foundElements[foundElementsPos], controlBlockHeight);
}
function handlerCheckboxAll() {
const actionName = controlCheckboxAll.checked ? "add" : "remove";
for (let i = 0; i < foundElements.length; ++i)
foundElements[i].classList[actionName](classNameFoundAll);
}
function handlerKeydown(event) {
if (event.code === "KeyF" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
controlInputText.focus();
}
}
}
textSearch();
.found-all {
background-color: yellow;
}
.found-current {
background-color: lime;
}
#control-block {
position: fixed;
top: 0px;
left: 0px;
width: 100%;
z-index: 1;
padding: 5px;
background-color: gainsboro;
}
#search-block {
position: absolute;
margin-top: 105px;
}
<div id="control-block">
<p><label>Текст для поиска: <input type = "text" id = "input-text"></label></p>
<p>
<button type="button" id="button-begin">Найти сначала</button>
<button type="button" id="button-previous">Назад</button>
<button type="button" id="button-next">Вперёд</button>
<label><input type = "checkbox" id = "checkbox-all" checked>Подсветить все</label>
</p>
</div>
<div id="search-block">
<p>Вот, надо тыкать кнопку, само не ищет :c</p>
<p>Система вопросов и ответов о программировании, разработанная Джоэлем Спольски и Джеффом Этвудом в 2008 году. Является частью Stack Exchange Network. Как и в других системах подобного рода, Stack Overflow предоставляет возможность оценивать вопросы и
ответы, что поднимает или понижает репутацию зарегистрированных пользователей. Проект создан на C# с использованием ASP.NET 4 и ASP.NET MVC.</p>
<p>Система вопросов и ответов о программировании, разработанная Джоэлем Спольски и Джеффом Этвудом в 2008 году. Является частью Stack Exchange Network. Как и в других системах подобного рода, Stack Overflow предоставляет возможность оценивать вопросы и
ответы, что поднимает или понижает репутацию зарегистрированных пользователей. Проект создан на C# с использованием ASP.NET 4 и ASP.NET MVC.</p>
<p>Система вопросов и ответов о программировании, разработанная Джоэлем Спольски и Джеффом Этвудом в 2008 году. Является частью Stack Exchange Network. Как и в других системах подобного рода, Stack Overflow предоставляет возможность оценивать вопросы и
ответы, что поднимает или понижает репутацию зарегистрированных пользователей. Проект создан на C# с использованием ASP.NET 4 и ASP.NET MVC.</p>
<p>Система вопросов и ответов о программировании, разработанная Джоэлем Спольски и Джеффом Этвудом в 2008 году. Является частью Stack Exchange Network. Как и в других системах подобного рода, Stack Overflow предоставляет возможность оценивать вопросы и
ответы, что поднимает или понижает репутацию зарегистрированных пользователей. Проект создан на C# с использованием ASP.NET 4 и ASP.NET MVC.</p>
</div>
Если необходимо фокусироваться на строке ввода при нажатии комбинации клавиш Ctrl+F, то добавляем к документу обработчик события keydown, в котором предотвращаем действие браузера по умолчанию и вызываем метод focus() у интересующего элемента.
document.addEventListener("keydown", handlerKeydown);
function handlerKeydown(event) {
if (event.code === "KeyF" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
controlInputText.focus(); //document.getElementById("input-text").focus();
}
}
P.S. Замещать реакцию браузера на некоторые стандартные действия, такие как, например, вызов контекстного меню, или нажатие Ctrl+C, или Ctrl+F — это спорное решение.