Поиск по содержимому страницы без CSS?
Есть два поиска, аналоги Ctrl+F в браузере.
Первый
var lastResFind=""; // последний удачный результат
function TrimStr(s) {
s = s.replace( /^\s+/g, '');
return s.replace( /\s+$/g, '');
}
function FindOnPage(inputId) {//ищет текст на странице, в параметр передается ID поля для ввода
var obj = window.document.getElementById(inputId);
var textToFind;
if (obj) {
textToFind = TrimStr(obj.value);//обрезаем пробелы
} else {
alert("Введенная фраза не найдена");
return;
}
if (textToFind == "") {
alert("Вы ничего не ввели");
return;
}
if(document.body.innerHTML.indexOf(textToFind)=="-1")
alert("Ничего не найдено, проверьте правильность ввода!");
document.body.innerHTML = document.body.innerHTML.replace(eval("/name="+lastResFind+"/gi")," ");//стираем предыдущие якори для скрола
document.body.innerHTML = document.body.innerHTML.replace(eval("/"+textToFind+"/gi"),"<a name="+textToFind+" style='background:red'>"+textToFind+"</a>"); //Заменяем найденный текст ссылками с якорем;
lastResFind=textToFind; // сохраняем фразу для поиска, чтобы в дальнейшем по ней стереть все ссылки
window.location = '#'+textToFind;//перемещаем скрол к последнему найденному совпадению
}
<input type="text" id="text-to-find" value="">
<input type="button" onclick="javascript: FindOnPage('text-to-find'); return false;" value="Искать"/>
<br/><i>Слова для теста Слова для теста Слова для теста Слова для теста</i>
<hr/>
Второй (Живой)
const minCharsToSearch = 3;
const $ = (...q) => q.reduce((r,s)=>!r?null:'object'==typeof s?s:r.querySelector(s),document),
$$ = (...q) => q.reduce((r,s,i,q)=>!r?null:'object'==typeof s?s:r["querySelector"+(i==q.length-1?'All':'')](s),document),
on = (o,e,c) => o.addEventListener(e,c,false),
stop = e => ['preventDefault','stopPropagation'].map(f=>e[f]()),
addCls = (o,c) => (o.classList.add(c),o),
remCls = (o,c) => (o.classList.remove(c),o),
newEl = t => document.createElement(t),
append = (p,o) => (p.appendChild(o),o),
pageTop = () => document.documentElement.scrollTop + document.body.scrollTop,
// @todo document.documentElement.clientHeight или document.body.clientHeight
pageBottom = () => pageTop() + document.documentElement.clientHeight;
const form = $('form'),
input = $(form,'input'),
search = $(form,'.search'),
clear = $(form,'.clear'),
output = $(form,'.output'),
next = $(form,'.next'),
prev = $(form,'.prev'),
content = $('.content');
on(form, 'submit', onSubmit);
on(form, 'input', onInput);
on(window, 'resize', onResize);
on(clear,'click', doClear);
on(output,'click', curPosLast);
on(next, 'click', goNext);
on(prev, 'click', goPrev);
function onSubmit(e){
stop(e);
}
let timeout, searchResult = [], searchResultActive = null;
function onInput(e){
stop(e);
if(input.value){
remCls(search,'visible');
addCls(clear,'visible');
} else {
output.innerHTML = ``;
addCls(search,'visible');
remCls(clear,'visible');
}
remCls(next,'enabled');
remCls(prev,'enabled');
if(timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(()=>{
timeout = null;
const searchValue = input.value;
if(searchValue.length>=minCharsToSearch){
doSearch(searchValue);
} else {
output.innerHTML = `от 3х букв`;
}
},200);
}
function doSearch(searchValue){
searchResultActive = null;
searchResult = findAll(searchValue);
if(searchResult.length){
let top=pageTop(),found = 0;
for(const [idx,res] of searchResult.entries()){
if(res.rects.every(rect => rect.top > top)){
found = idx;
break;
}
}
setActive(found);
} else {
output.innerHTML = `совпадений нет`;
}
}
function doClear(){
searchResultActive = null;
searchResult = [];
input.value = '';
addCls(search,'visible');
remCls(clear ,'visible');
remCls(next ,'enabled');
remCls(prev ,'enabled');
$$('.highlight').forEach(hl=>hl.remove());
output.innerHTML = ``;
}
function goNext(){
setActive(searchResultActive+1);
}
function goPrev(){
setActive(searchResultActive-1);
}
function onResize(){
setTimeout(()=>
searchResult.map(res=>(
res.rects = getRects(res.range)
.map((rect,i)=>(setRectSize(res.hls[i],rect),rect))
))
,1);
}
function findAll(text){
$$('.highlight').forEach(hl=>hl.remove());
const { textContent } = content;
const rx = new RegExp(text.replace(/([-.*+?^${}()|[\]\/\\])/g,"\\$1")
.replace(/\s+/g, '\\s+'),'ig');
const res=[];
let match;
while(match = rx.exec(textContent)){
res.push({ startAbs : match.index,
startNode: null,
startOfs : null,
endAbs : match.index + match[0].length,
endNode : null,
endOfs : null,
range : null,
rects : null,
hls : null });
}
const walk = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, null, false);
let idx = 0, len = res.length, curStart = 0, node;
while(idx < len && (node = walk.nextNode())){
const curEnd = curStart + node.textContent.length;
let found = false;
do{
found = false;
const curRes = res[idx], { startAbs, endAbs } = curRes;
if(!curRes.startNode && curStart <= startAbs && startAbs < curEnd ){
curRes.startNode = node;
curRes.startOfs = startAbs - curStart;
}
if(curRes.startNode && curStart < endAbs && endAbs < curEnd ){
const range = document.createRange();
curRes.range = range;
curRes.endNode = node;
curRes.endOfs = endAbs - curStart;
range.setStart(curRes.startNode,curRes.startOfs);
range.setEnd(curRes.endNode,curRes.endOfs);
curRes.hls = (curRes.rects = getRects(range))
.map(rect=>append(document.body,
setRectSize(addCls(newEl('div'),'highlight'),rect)));
found = true;
idx++;
}
} while(found && idx < len);
curStart = curEnd;
}
return res;
}
function setActive(resNum){
$$('.highlight.active').forEach(hl=>remCls(hl,'active'));
const res = searchResult[resNum], top = pageTop(), bottom = pageBottom();
if(!res.rects.every(rect=>rect.top>top && rect.top+rect.height<bottom)){
const minRectTop = Math.min(...res.rects.map(rect=>rect.top));
const maxRectBottom = Math.max(...res.rects.map(rect=>rect.top+rect.height));
const centerRect = (maxRectBottom + minRectTop) / 2;
const centerScr = (bottom - top) / 2;
// @todo document.documentElement или document.body
document.documentElement.scrollTop = centerRect - centerScr;
}
searchResultActive = resNum;
res.hls.forEach(hl=>addCls(hl,'active'));
const len = searchResult.length;
output.innerHTML = `${resNum+1} из ${len}`;
if(len > 1){
(resNum < len - 1 ? addCls : remCls)(next,'enabled');
(resNum > 0 ? addCls : remCls)(prev, 'enabled');
}
}
function curPosLast(){
const len = input.value.length;
input.focus();
input.setSelectionRange(len,len);
}
function getRects(range){
return [...range.getClientRects()]
.map((rect)=>['left','width','top','height']
.reduce((r,k)=>(r[k]=rect[k],r),{}))
.map(rect=>(rect.top += pageTop(),rect));
}
function setRectSize(el, rect){
Object.assign(el.style,
Object.fromEntries(
Object.entries(rect).map(([key,val])=>([key,val+'px']))));
return el;
}
* { box-sizing: border-box;}
html,body{ margin: 0; scroll-behavior: smooth; }
form {
position: fixed;
top: 0;
width: 100vw;
height: 3rem;
padding: 5px;
z-index: 5;
background: #ccc;
}
.input {
border: 1px solid #888;
width: calc( 100vw - 10px - 6rem);
height: calc( 3rem - 10px);
border-radius: 3px;
background: #fff;
overflow: hidden;
}
.input:hover {
border: 1px solid #ad1d1d;
}
.input:focus,.input:focus-within,.input:active {
border: 1px solid #c58e37;
box-shadow: 0 0 2px #c58e37;
}
.input * {
margin: 0;
padding:0;
height: calc( 3rem - 12px);
display: inline-block;
white-space: nowrap;
vertical-align: middle;
}
.input input {
border: none;
width: calc( 100vw - 16rem - 12px - 6rem);
height: calc( 3rem - 12px);
outline: none;
}
.input .search,
.input .clear,
.input .next,
.input .prev,
.input .output {
user-select: none;
}
.search {
display:none;
width: 3rem;
height: calc(3rem - 12px);
text-align: center;
padding: .2rem 0 0 0;
}
.clear {
width: calc(3rem - 20px);
height: calc(3rem - 24px);
text-align: center;
display: none;
line-height: 1.4rem;
margin: 6px 10px 8px 10px;
cursor: default;
border: 0px solid transparent;
background: #8888;
color: #fff;
border-radius: 3px;
}
.clear:hover {
color:#fff;
background:#f008;
}
.visible {
display: inline-block;
}
.output {
width: 7rem;
text-align: right;
height: 2rem;
display: inline-block;
height: calc(3rem - 12px);
padding: .5rem 1rem 0 0;
cursor: text;
}
.input button{
width: 3rem;
height: calc(3rem - 11px);
pointer-events: none;
background: #aaa;
color: #888;
border-radius: 0;
border: 1px solid transparent;
border-left: 1px solid #888;
outline: none;
margin: -1px 0 1px 0;
}
.input button.enabled {
pointer-events: all;
background: #eee;
color: #000;
}
.input button.enabled:focus{
box-shadow: inset 0 0 2px 1px #f84;
}
.input button.enabled:active{
border-top: 1px solid #888;
border-left: 2px solid #888;
padding-top: 2px;
padding-left: 1px;
}
.content{
clear:both;
margin-top: 3.5rem;
z-index: 3;
position: relative;
padding: 1rem;
}
.highlight{
background: #ff04;
box-shadow: 0 0 2px #ff0;
position:absolute;
z-index:1;
}
.highlight.active {
background: #f844;
box-shadow: 0 0 2px #f00;
}
<form><div class="input"><label for="search" class="search visible">?</label><span class="clear">✖</span><input id="search" type="text"/><span class="output"></span><button class="prev">⫷</button><button class="next">⫸</button></div></form>
<div class="content">
<p>Слова для теста Слова для теста Слова для теста Слова для теста</div>
Как видно, у первого нет css, и он отлично работает.
Но вот если убрать из второго css, то результат уже не выделяется.
const minCharsToSearch = 3;
const $ = (...q) => q.reduce((r,s)=>!r?null:'object'==typeof s?s:r.querySelector(s),document),
$$ = (...q) => q.reduce((r,s,i,q)=>!r?null:'object'==typeof s?s:r["querySelector"+(i==q.length-1?'All':'')](s),document),
on = (o,e,c) => o.addEventListener(e,c,false),
stop = e => ['preventDefault','stopPropagation'].map(f=>e[f]()),
addCls = (o,c) => (o.classList.add(c),o),
remCls = (o,c) => (o.classList.remove(c),o),
newEl = t => document.createElement(t),
append = (p,o) => (p.appendChild(o),o),
pageTop = () => document.documentElement.scrollTop + document.body.scrollTop,
// @todo document.documentElement.clientHeight или document.body.clientHeight
pageBottom = () => pageTop() + document.documentElement.clientHeight;
const form = $('form'),
input = $(form,'input'),
search = $(form,'.search'),
clear = $(form,'.clear'),
output = $(form,'.output'),
next = $(form,'.next'),
prev = $(form,'.prev'),
content = $('.content');
on(form, 'submit', onSubmit);
on(form, 'input', onInput);
on(window, 'resize', onResize);
on(clear,'click', doClear);
on(output,'click', curPosLast);
on(next, 'click', goNext);
on(prev, 'click', goPrev);
function onSubmit(e){
stop(e);
}
let timeout, searchResult = [], searchResultActive = null;
function onInput(e){
stop(e);
if(input.value){
remCls(search,'visible');
addCls(clear,'visible');
} else {
output.innerHTML = ``;
addCls(search,'visible');
remCls(clear,'visible');
}
remCls(next,'enabled');
remCls(prev,'enabled');
if(timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(()=>{
timeout = null;
const searchValue = input.value;
if(searchValue.length>=minCharsToSearch){
doSearch(searchValue);
} else {
output.innerHTML = `от 3х букв`;
}
},200);
}
function doSearch(searchValue){
searchResultActive = null;
searchResult = findAll(searchValue);
if(searchResult.length){
let top=pageTop(),found = 0;
for(const [idx,res] of searchResult.entries()){
if(res.rects.every(rect => rect.top > top)){
found = idx;
break;
}
}
setActive(found);
} else {
output.innerHTML = `совпадений нет`;
}
}
function doClear(){
searchResultActive = null;
searchResult = [];
input.value = '';
addCls(search,'visible');
remCls(clear ,'visible');
remCls(next ,'enabled');
remCls(prev ,'enabled');
$$('.highlight').forEach(hl=>hl.remove());
output.innerHTML = ``;
}
function goNext(){
setActive(searchResultActive+1);
}
function goPrev(){
setActive(searchResultActive-1);
}
function onResize(){
setTimeout(()=>
searchResult.map(res=>(
res.rects = getRects(res.range)
.map((rect,i)=>(setRectSize(res.hls[i],rect),rect))
))
,1);
}
function findAll(text){
$$('.highlight').forEach(hl=>hl.remove());
const { textContent } = content;
const rx = new RegExp(text.replace(/([-.*+?^${}()|[\]\/\\])/g,"\\$1")
.replace(/\s+/g, '\\s+'),'ig');
const res=[];
let match;
while(match = rx.exec(textContent)){
res.push({ startAbs : match.index,
startNode: null,
startOfs : null,
endAbs : match.index + match[0].length,
endNode : null,
endOfs : null,
range : null,
rects : null,
hls : null });
}
const walk = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, null, false);
let idx = 0, len = res.length, curStart = 0, node;
while(idx < len && (node = walk.nextNode())){
const curEnd = curStart + node.textContent.length;
let found = false;
do{
found = false;
const curRes = res[idx], { startAbs, endAbs } = curRes;
if(!curRes.startNode && curStart <= startAbs && startAbs < curEnd ){
curRes.startNode = node;
curRes.startOfs = startAbs - curStart;
}
if(curRes.startNode && curStart < endAbs && endAbs < curEnd ){
const range = document.createRange();
curRes.range = range;
curRes.endNode = node;
curRes.endOfs = endAbs - curStart;
range.setStart(curRes.startNode,curRes.startOfs);
range.setEnd(curRes.endNode,curRes.endOfs);
curRes.hls = (curRes.rects = getRects(range))
.map(rect=>append(document.body,
setRectSize(addCls(newEl('div'),'highlight'),rect)));
found = true;
idx++;
}
} while(found && idx < len);
curStart = curEnd;
}
return res;
}
function setActive(resNum){
$$('.highlight.active').forEach(hl=>remCls(hl,'active'));
const res = searchResult[resNum], top = pageTop(), bottom = pageBottom();
if(!res.rects.every(rect=>rect.top>top && rect.top+rect.height<bottom)){
const minRectTop = Math.min(...res.rects.map(rect=>rect.top));
const maxRectBottom = Math.max(...res.rects.map(rect=>rect.top+rect.height));
const centerRect = (maxRectBottom + minRectTop) / 2;
const centerScr = (bottom - top) / 2;
// @todo document.documentElement или document.body
document.documentElement.scrollTop = centerRect - centerScr;
}
searchResultActive = resNum;
res.hls.forEach(hl=>addCls(hl,'active'));
const len = searchResult.length;
output.innerHTML = `${resNum+1} из ${len}`;
if(len > 1){
(resNum < len - 1 ? addCls : remCls)(next,'enabled');
(resNum > 0 ? addCls : remCls)(prev, 'enabled');
}
}
function curPosLast(){
const len = input.value.length;
input.focus();
input.setSelectionRange(len,len);
}
function getRects(range){
return [...range.getClientRects()]
.map((rect)=>['left','width','top','height']
.reduce((r,k)=>(r[k]=rect[k],r),{}))
.map(rect=>(rect.top += pageTop(),rect));
}
function setRectSize(el, rect){
Object.assign(el.style,
Object.fromEntries(
Object.entries(rect).map(([key,val])=>([key,val+'px']))));
return el;
}
<form><div class="input"><label for="search" class="search visible">?</label><span class="clear">✖</span><input id="search" type="text"/><span class="output"></span><button class="prev">⫷</button><button class="next">⫸</button></div></form>
<div class="content">
<p>Слова для теста Слова для теста Слова для теста Слова для теста</div>
Можно ли как нибудь сделать так, что бы второй тоже выделял результаты без css?