ハイライト表示のスクロールで嵌る
「.scrollIntoView()」は非常に有難いメソッドです。 編集画面の様なスクロールするブロック要素で、特定の要素を表示範囲までスクロールさせる指定が可能です。
「S-R in Editor」では、検索ヒットしてハイライト表示した文字列を順次表示する機能は、これを活用しています。 この様な操作は、他のメソッドを組み合わせて可能ですが、「.scrollIntoView()」は対象要素にこれを指定するだけでOKで、とてもコードがシンプルに出来ます。
フラグの変更の過程で、欲が出てこのハイライト表示の仕方を改めました。 改善点は、ヒット文字列を先頭から下方へ順に表示するのを、検索開始場所に最も近いヒット文字列から表示する様にしたのです。 この結果、検索を開始した位置から上下方向に、ヒット文字列を見る形になります。
検索を始めた場所はユーザーに必要な部分である可能性が強く、ハイライト表示でいきなり文書の先頭にとばすのは、好ましくないと考えて改めたものです。 調べると Chromeの標準検索もその様に設計されていました。
しかし、この変更で嵌りました。
最も近いヒット要素を探すコード
複数の対象要素がある場合に、現在のスクロール位置に一番近い対象要素を、表示枠(実際には編集枠)までスクロールして表示するコードを考えました。 JavaScriptで使えるコードを探すと「.getBoundingClientRect()」という長たらしいメソッドがありました。 調べたい対象要素を「Element」とした時、このメソッドに「.top」を付けた以下のコードで、表示枠の上縁からの「ピクセル値」が得られます。
この値の「正負」は、「対象要素」「表示枠」の相対位置で以下の様になります。
このメソッドで複数の対象要素を順に調べ、初めて「0」を越える値になる要素を見つければ良いわけで、以下のコードを作りました。
「forループ」の中で、対象要素ごとに「mark[k].getBoundingClientRect().top;」の値を調べて、初めてこの値が 0を超えたらループが止まります。 変数 k は、ループが止まった時の値を保持したままになるので、対象要素の k番目が表示枠(上縁)に一番近い事になります。 この後は「.scrollIntoView()」メソッドで、その要素をスクロールして呼び寄せるという塩梅です。
ところが、このコードには欠点がありました。 対象要素が画面より下方にある場合は動作しますが、対象要素のすべてが画面より上方にある場合は動作しないのです。 スクロールバックできないのです。
ハイライトの対象要素が沢山ある検索文字でテストしていると上手く動作するのに、対象要素が少ない検索文字では検索数表示が狂い始め、この問題に気付いたのです。 スクリプトエラーが生じていました。
なんとか欠点を修正
対象要素の全てが表示枠の上方にある場合は、「forループ」は k が対象の全要素数の「mark.length」の値で止まります。「forループ」は「0」から「mark.length-1」の範囲の k を配列のインデックスとして使う事が一般的です。 これはどこでも見かける「for(k=0; k<mark.length; k++)」のループ条件の通りですが、今回の様なループが「break」できずに終了した場合は、ループ条件より1つ過ぎた「mark.length」の値になります。 これが周囲で暗黙に取決めた k の範囲を逸脱して、他のスクリプトエラーを誘った様です。
結局、この場合を別コードで対応し、なんとか使える様にしていますが、これはまだ修正が必要かもしれません。
フラグを変更して、幾つかの部分の構成を入れ替えました。 操作のインターフェイスが、細かなところで変更しています。 未だ調整するかも知れませんが、少しずつ使い易くなっています。
「S-R in Editor」 ver. 1.1
以下の「S-R in Editor」は、「検索語入力」~「TEXTのヒット文字列の巡回チェック」~「HTML一括置換」「TEXT一括置換」と「各置換チェック」「UNDO」が可能な、制作過程のツールです。
Chrome版 / Firefox版の「Tampermonkey」の新規作成編集枠に、以下のスクリプトコードをコピー&ペーストする事で、このテストコードを試すことが出来ます。
〔コピー方法〕 軽量シンプルなツール「PreBox Button 」を使うと
コード枠内を「Ctrl+左Click」➔「Copy code 」を「左Click」
の操作で、掲載コードのコピーが可能になります。
〔 S-R in Editor 〕 ver. 1.1
// ==UserScript== // @name S-R in Editor // @namespace http://tampermonkey.net/ // @version 1.1 // @description 通常編集枠で実行できる 検索 / 置換 ツール // @author Ameba Blog User // @match https://blog.ameba.jp/ucs/entry/srventry* // @grant none // ==/UserScript== window.addEventListener('load', function(){ let p_flag; // Precess let t_flag; // TEXT処理 let count_t; let count_h; let buffer; let native_line; let editor_iframe; let iframe_doc; let iframe_html; let iframe_body; let js_cover; let search_box; let search_word; let search_word_es; let replace_box; let replace_word; let result_box; let s_1; let s_3; let s_4; let ua=0; // Chromeの場合のフラグ let agent=window.navigator.userAgent.toLowerCase(); if(agent.indexOf('firefox') > -1){ ua=1; } // Firefoxの場合のフラグ let cke_1_contents=document.querySelector('#cke_1_contents'); // 監視 target let monitor=new MutationObserver(catch_key); monitor.observe(cke_1_contents, {childList: true}); // ショートカット待受け開始 catch_key(); function catch_key(){ search_box=document.querySelector('#search_box'); editor_iframe=document.querySelector('.cke_wysiwyg_frame'); if(editor_iframe){ //「通常表示」の場合 delete_m(); //「HTML表示」を開いて戻った場合にmタグを削除 add_m_style(); // mタグ用 styleをiframeに再設定 search_box=document.querySelector('#search_box'); if(search_box){ search_box.disabled=false; } document.addEventListener("keydown", check_key); // documentは先に指定 iframe_doc=editor_iframe.contentWindow.document; iframe_doc.addEventListener("keydown", check_key); // iframeは後に指定 function check_key(event){ if(event.ctrlKey==true){ if(event.keyCode==123){ event.preventDefault(); if(p_flag!=3){ // 置換チェック時でなければ「Ctrl+F12」でON/OFF search_replace(); }}} if(event.keyCode==9 || event.keyCode==16 || event.keyCode==17 || event.keyCode==18 ||event.keyCode==19 || event.keyCode==27){ if(p_flag==3){ // 置換チェック時に「Tab/Shift/Ctrl/Alt/Pause/Esc」を無効化 event.preventDefault(); out_h_flag_2();}}}} else{ //「HTML表示」の場合 if_html(); } function out_h_flag_2(){ js_cover.style.display='none'; cke_1_contents.style.zIndex='0'; if(iframe_body){ iframe_body.contentEditable='true'; } // 編集可能にする iframe_body.innerHTML=buffer; // 置換処理をUNDO ⏹ search_box.disabled=false; replace_box.disabled=false; s_3.style.display='none'; s_4.style.display='none'; replace_box.focus(); if(t_flag==1){ replace_word='<m>'+ search_box.value +'</m>'; iframe_body.innerHTML= iframe_body.innerHTML.replace(new RegExp(search_word_es, 'g'), replace_word); } // 🔳 RegExp p_flag=2; } // 2=置換入力 function if_html(){ if(search_box){ search_box.disabled=true; } result_box.textContent=' '; s_1.style.display='none'; replace_box.style.display='none'; replace_box.value=''; s_3.style.display='none'; s_4.style.display='none'; p_flag=0; } } // catch_key function search_replace(){ let i_body=document.querySelector('body.l-body'); editor_iframe=document.querySelector('.cke_wysiwyg_frame'); if(editor_iframe){ //「通常表示」の場合 iframe_doc=editor_iframe.contentWindow.document; iframe_html=iframe_doc.querySelector('html'); iframe_body=iframe_doc.querySelector('.cke_editable'); } let css= '#s_container {position: fixed; top: 12px; left: calc(50% - 490px); '+ 'background: #fff; border: 1px solid #aaa; border-radius: 4px; '+ 'padding: 6px 15px; min-width: 948px; z-index: 10;}'+ '#search_box {width: 215px;} #replace_box {width: 215px; display: none;}'+ '::placeholder {font-size: 15px; color: #bbb;}'+ '#s_container input:disabled {color: #000; background: #eef1f3;}'+ '#s_container input {font-size: 16px; padding: 2px 6px 0; -moz-appearance: none;}'+ '#result {display: inline-block; min-width: 12px; padding: 4px 6px 2px; '+ 'margin-left: 5px; border: 1px solid #aaa; font-size: 16px;}'+ '.s_sw {display: inline-block; vertical-align: -9px; font-size: 15px; '+ 'padding: 5px 8px 2px; border: 1px solid #aaa; overflow: hidden;}'+ '.s_1 {margin: 0 15px; min-width: 4em; display: none;}'+ '.s_3, .s_4 {margin-left: 5px; color:#fff; background: #1e88e5; '+ 'cursor: pointer; display: none;}'+ '.js_cover {position: fixed; top: 0; width: 100%; height: 100%; '+ 'background: rgba(0, 0, 0, .6); z-index: 10; display: none;}'; let style_tag=document.createElement("style"); // css設定styleタグ style_tag.type="text/css"; style_tag.setAttribute("class", "ep"); style_tag.appendChild(iframe_doc.createTextNode(css)); if(i_body.querySelector('.ep')){ i_body.querySelector('.ep').remove(); } i_body.appendChild(style_tag); js_cover=document.createElement("div"); // クリック操作のブロックカバー js_cover.setAttribute("class", "js_cover"); if(i_body.querySelector('.js_cover')){ i_body.querySelector('.js_cover').remove(); } document.querySelector('#js-container').appendChild(js_cover); let s_container=document.querySelector('#s_container'); if(!s_container){ //#s_containerが無い場合 生成して開始 let insert_node_d=document.createElement('div'); insert_node_d.setAttribute('id', 's_container'); i_body.appendChild(insert_node_d); insert_node_d.innerHTML= '<input id="search_box" placeholder="検索文字" autocomplete="off">'+ '<span id="result"> </span>'+ '<span class="s_sw s_1"> </span>'+ '<input id="replace_box" placeholder="置換文字" autocomplete="off">'+ '<span><span class="s_sw s_3">OK</span>'+ '<span class="s_sw s_4">UNDO</span></span>'; search_box=document.querySelector('#search_box'); result_box=document.querySelector('#result'); replace_box=document.querySelector('#replace_box'); s_1=document.querySelector('.s_1'); s_3=document.querySelector('.s_3'); s_4=document.querySelector('.s_4'); p_flag=0; // 0=検索文字 未確定 search_box.focus(); search_box.onkeydown=function(event){ // 🔽 検索ツール操作の開始点 if(event.keyCode==13){ event.preventDefault(); if(p_flag==0){ event.stopImmediatePropagation(); // 巡回で動作するのを抑止 search_word=search_box.value; get_search(); rbox_disp(); }} if(event.keyCode==9){ //「Tab」で入力枠外に出るのを無効化 event.preventDefault(); }} search_box.onchange=function(){ //「Enter」を押さず移動した場合は検索語を再表示 if(search_box.value!==search_word){ search_box.style.outline='2px solid #2196f3'; search_box.style.outlineOffset='-3px'; setTimeout(()=>{ search_box.style.outline='none'; search_box.value=search_word; }, 500); }} function get_search(){ search_word_es=escapeRegExp(search_word); // 🔳 RegExp editor_iframe=document.querySelector('.cke_wysiwyg_frame'); // ここで取得 if(editor_iframe){ //「通常表示」が実行条件 iframe_doc=editor_iframe.contentWindow.document; iframe_body=iframe_doc.querySelector('.cke_editable'); native_line=iframe_html.scrollTop; // 通常表示のスクロール位置を記録 buffer=iframe_body.innerHTML; // ハイライト表示のためソースコードを保存 🟦 let childs=searchNodes(iframe_body); count_t=0; // テキストノードのヒット数 let result_t for(let k=0; k<childs.length; k++){ if(childs[k].nodeType==3){ result_t=childs[k].textContent.match(new RegExp(search_word_es, 'g')); // 🔳 RegExp if(result_t){ count_t+=result_t.length; }}} count_h=0; // HTMLソース全体のヒット数 let result_h=iframe_body.innerHTML.match(new RegExp(search_word_es, 'g')); // 🔳 RegExp if(result_h){ count_h=result_h.length; }}} function searchNodes(root){ // 全子孫ノードリストを作成 var list=[]; var search=function (node){ while (node !=null){ list.push(node); search(node.firstChild); node=node.nextSibling; }} search(root.firstChild); return list; } function rbox_disp(){ search_box.disabled=false; replace_box.disabled=false; replace_box.value=''; t_flag=0; if(count_t!=0 && count_h-count_t==0){ s_1.textContent='TEXT処理'; s_1.style.display='inline-block'; s_1.style.color='#000'; replace_box.style.display='none'; s_3.style.display='none'; s_4.style.display='none'; p_flag=1; // 1=検索文字確定 処理開始 t_flag=1; // TEXT処理 highlight(); html_replace(); } if(count_t!=0 && count_h-count_t!=0){ result_box.textContent='T:'+count_t+' H:'+(count_h-count_t); s_1.textContent='処理不能'; s_1.style.display='inline-block'; s_1.style.color='red'; replace_box.style.display='none'; s_3.style.display='none'; s_4.style.display='none'; p_flag=0; } // 0=検索文字未確定 検索前 if(count_t==0 && count_h-count_t!=0){ result_box.textContent='H:'+(count_h-count_t); s_1.textContent='HTML処理'; s_1.style.display='inline-block'; s_1.style.color='#000'; replace_box.style.display='none'; s_3.style.display='none'; s_4.style.display='none'; p_flag=1; // 1=検索文字確定 処理開始 html_replace(); } if(count_t==0 && count_h-count_t==0){ result_box.textContent='T:'+count_t+' H:'+(count_h-count_t); s_1.textContent=' - - - '; s_1.style.display='inline-block'; s_1.style.color='#000'; replace_box.style.display='none'; s_3.style.display='none'; s_4.style.display='none'; s_4.style.display='none'; p_flag=0; }} // 0=検索文字未確定 検索前 function highlight(){ if(t_flag==1){ replace_word='<m>'+ search_box.value +'</m>'; iframe_body.innerHTML= iframe_body.innerHTML.replace(new RegExp(search_word_es, 'g'), replace_word); // 🔳 RegExp let mark=iframe_body.querySelectorAll('m'); let k; for(k=0; k<mark.length; k++){ // 現在のスクロール位置側近のmark[k]を取得 let offsetY=mark[k].getBoundingClientRect().top; if(offsetY>0){ break; }} if(k==mark.length){ // 現在のスクロール位置より手前にしかmark[k]がない場合 iframe_html.scrollTop=0; k=mark.length -1; view(k); } result_box.textContent='T:'+count_t+'│⇕'; while(k<mark.length && p_flag==1){ // p_flag=1=巡回 k=next(k); } function next(k){ search_box.addEventListener("keydown", check_key); function check_key(event){ if(p_flag==1){ if(event.keyCode==37 || event.keyCode==38){ //「←」「↑」 event.preventDefault(); if(k>1){ k=k-1; } result_box.textContent='T:'+count_t+'│'+k; view(k-1); } if(event.keyCode==39 || event.keyCode==40){ //「→」「↓」 event.preventDefault(); if(k<mark.length){ k+=1; } result_box.textContent='T:'+count_t+'│'+k; view(k-1); } if(event.keyCode==13){ event.preventDefault(); if(search_box.value==search_word){ p_flag=2; // 巡回ループを抜けて 置換入力へ replace_box.style.display='inline-block'; replace_box.focus(); } else{ iframe_html.scrollTop=native_line; // 検索開始位置に戻る iframe_body.innerHTML=buffer; // highlight を抜ける時はリセット ⏹ search_word=search_box.value; // 検索文字変更 p_flag=0; // 巡回ループを抜けて 検索文字 未確定へ t_flag=0; }} if(event.keyCode==9){ //「Tab」で置換入力へ event.preventDefault(); p_flag=2; // 巡回ループを抜ける replace_box.style.display='inline-block'; replace_box.focus(); } if(event.keyCode==27 ){ //「Esc」 event.preventDefault(); result_box.textContent='T:'+count_t+'│-'; iframe_html.scrollTop=native_line; // 検索開始位置に戻る iframe_body.innerHTML=buffer; // highlight を抜ける時はリセット ⏹ p_flag=0; }}} search_box.onclick=function(){ if(p_flag==1 || p_flag==2){ result_box.textContent='T:'+count_t+'│-'; s_1.style.display='none'; replace_box.style.display='none'; replace_box.value=''; iframe_html.scrollTop=native_line; // 検索開始位置に戻る iframe_body.innerHTML=buffer; // highlight を抜ける時はリセット ⏹ p_flag=0; }}} // 0=検索文字未確定 検索前 function view(k){ mark[k].scrollIntoView(); i_body.scrollIntoView(); } iframe_doc.addEventListener("click", stop_out); // 編集画面のクリックで巡回終了 function stop_out(){ if(p_flag==1 || p_flag==2){ // 1=検索文字入力枠 2=置換文字入力枠 result_box.textContent='T:'+count_t+'│-'; s_1.style.display='none'; replace_box.style.display='none'; replace_box.value=''; native_line=iframe_html.scrollTop; // クリックした場所をスクロール位置に指定 iframe_body.innerHTML=buffer; // highlight を抜ける時はリセット ⏹ p_flag=0; }} }} // highlight() function html_replace(){ // HTML置換処理 replace_box.focus(); html_roop(); function html_roop(){ replace_box.onkeydown=function(event){ // 🔽 置換操作の開始点 if(event.keyCode==13){ event.preventDefault(); if(t_flag==0){ buffer=iframe_body.innerHTML; } // HTML置換の処理直前のソースコード保存 🟦 replace_word=replace_box.value; iframe_body.innerHTML= iframe_body.innerHTML.replace(new RegExp(search_word_es, 'g'), replace_word); // 🔳 RegExp js_cover.style.display='block'; cke_1_contents.style.zIndex='11'; iframe_body.contentEditable='false'; // 編集不可にする search_box.disabled=true; replace_box.disabled=true; s_3.style.display='inline-block'; s_4.style.display='inline-block'; p_flag=3; } // 3=置換時 if(event.keyCode==9 || event.keyCode==27){ //「Tab」「Esc」で処理前に戻る event.preventDefault(); result_box.textContent=' '; s_1.style.display='none'; replace_box.style.display='none'; replace_box.value=''; if(t_flag==1){ iframe_html.scrollTop=native_line; // 検索開始位置に戻る iframe_body.innerHTML=buffer; } // 置換処理をUNDO ⏹ search_box.focus(); p_flag=0; }} // 0=検索文字 未確定 s_3.onclick=function(){ //「OK」ボタンで一括置換確定 delete_m(); // mタグを削除 js_cover.style.display='none'; cke_1_contents.style.zIndex='0'; iframe_body.contentEditable='true'; // 編集可能にする search_box.disabled=false; replace_box.disabled=false; result_box.textContent=' '; // 検索結果は変更される s_1.style.display='none'; replace_box.style.display='none'; replace_box.value=''; s_3.style.display='none'; s_4.style.display='none'; search_box.focus(); p_flag=0; } // 0=検索文字 未確定 s_4.onclick=function(){ //「UNDO」ボタン js_cover.style.display='none'; cke_1_contents.style.zIndex='0'; iframe_body.contentEditable='true'; // 編集可能にする search_box.disabled=false; replace_box.disabled=false; s_3.style.display='none'; s_4.style.display='none'; iframe_body.innerHTML=buffer; // 置換処理をUNDO ⏹ replace_box.focus(); if(t_flag==1){ highlight(); } p_flag=1; } // 1=検索文字確定 処理開始 } // html_roop() } // html_replace() } // #s_container が無い場合「Ctrl+F12」で開始 else{ // #s_container がある場合は「Ctrl+F12」で終了 「mタグを削除」 delete_m(); document.querySelector('#s_container').remove(); } } // search_replace() function add_m_style(){ editor_iframe=document.querySelector('.cke_wysiwyg_frame'); if(editor_iframe){ //「通常表示」の場合 iframe_doc=editor_iframe.contentWindow.document; iframe_html=iframe_doc.querySelector('html'); let css_iframe='.cke_editable m {background: #ffcc00;}'; // mタグの背景色指定 let style_tag_iframe=iframe_doc.createElement("style"); style_tag_iframe.type="text/css"; style_tag_iframe.setAttribute("class", "ep"); style_tag_iframe.appendChild(document.createTextNode(css_iframe)); if(iframe_html.querySelector('.ep')){ iframe_html.querySelector('.ep').remove(); } iframe_html.appendChild(style_tag_iframe); }} function delete_m(){ editor_iframe=document.querySelector('.cke_wysiwyg_frame'); if(editor_iframe){ //「通常表示」の場合 iframe_doc=editor_iframe.contentWindow.document; iframe_body=iframe_doc.querySelector('.cke_editable'); if(iframe_body){ let mark=iframe_body.querySelectorAll('m'); if(mark.length!=0){ iframe_body.innerHTML= iframe_body.innerHTML.replace(new RegExp('<m>', 'g'), '' ); }}}} // 🔳 RegExp function escapeRegExp(string){ let reRegExp=/[\\^$.*+?()[\]{}|]/g; let reHasRegExp=new RegExp(reRegExp.source); return (string && reHasRegExp.test(string)) ? string.replace(reRegExp, '\\$&') : string; } });
「S-R in Editor ⭐」最新版について
旧いバージョンの JavaScriptツールは、アメーバのページ構成の変更で動作しない場合があり、導入する場合は最新バージョンをお勧めします。
●「S-R in Editor ⭐」の最新バージョンへのリンクは、以下のページのリンクリストから探せます。