目次とは別の役割から検討しJSによる索引の生成を試作してみましたので、今回も記録として残します。
ある程度動くところまできたと思うのですが、正規表現による取得や置換がうまくできておらず、未完状態であり、動作保証はできかねますのでその点はご了承ください。
実現したいこと
実現したいことは以下の通り。
- 任意のワードを指定し、コンテンツ内の該当ワードの位置まで移動する
- 移動後のワードに装飾を施す
意図しているのは索引でして、PCのページ内検索と同種の役割の何かを、スマホでも使えるような別の形で提供できればと考えたものです。
コード
以下がサンプルコードです。
//このコードは目次用JSより前に記述すること。目次内のテキストにも反応してしまうため
(!function(){
//DOMの構築が完了したら動作開始
document.addEventListener('DOMContentLoaded', myIndex());
function myIndex(){
//索引作成と対象ワードを配列で設定 ※必須
var target = ["索引","ワード","js"];
//索引作成と表示の対象となる範囲を、ラップ要素のIDで指定。 ※必須
var range = "main";
//索引のタイトルを設定。 ※必須
var title = "索引";
//索引の説明文の設定。
//※オプション
var note = "当ページ内に存在する、特定の単語の出現位置まで移動できます。";
//索引を表示する目印となるタグ(指定範囲内に存在する最初のタグの上に索引が表示される)を、取得するh要素から変更したい場合はタグ名を入力。
//※オプション
var tag = "h2";
//IDで指定する場合はこちらを入力。
//IDの指定がない、あるいは、IDで指定した要素がない場合は、タグ指定が優先。
var tagId = "";
//検索対象に指定した文字列を入れる変数
var word = "";
//索引に使うIDを入れる変数
var targetSpanId = "";
//索引全体ののラップ要素を生成
var wrap = document.createElement("div");
//索引のタイトルに使うpタグを生成
var p = document.createElement("p");
//索引の説明文を入れるpタグ生成用の変数
var pNote = "";
//ワード毎ラップ要素を入れる変数
var wrapInbox = "";
//索引に使うolを生成用の変数
var ol = "";
//索引に使うliを入れる変数
var li = "";
//索引に使うaを入れる変数
var a = "";
//索引に使う番号を入れる変数
var aNum = "";
//索引を作るための範囲をIDで指定して取得
range = document.getElementById(range);
var count = 0;
for (var i = 0; i < target.length; i++){
//指定されたワードを取得
word = target[i];
//1.正規表現を使ったreplace()で、大まかにHTMLタグ内と思われる、索引対象ワードを含んだ文字列を取得し、コールバックで処理を行う
range.innerHTML = range.innerHTML.replace(new RegExp('>.*?' + word + '.*?<','g'),
function(){
count++;
//2.指定ワードがHTMLの属性である可能性を除外するために、matchで「="指定ワード"」の状態になっていないか確認。
if(arguments[0].match(new RegExp('=".*?' + word + '.*?"', 'g'))){
//3.指定ワードが「="指定ワード"」のように属性の記述と同じ状態なら、何もせずに戻す
return arguments[0];
} else {
//4.指定のワードが「="指定ワード"」の状態ではないならspanラッピングして戻す
arguments[0] = arguments[0].replace(new RegExp('(' + word + ')','gi'), '<span class="my-index-words' + i + '" id="my-index-words' + i +'-' + count + '">$1</span>');
return arguments[0];
}
});
}
}
//索引を入れる場所の指定位置を確定。
//優先順位は ID指定 > タグ指定。
tag = range.getElementsByTagName(tag);
tagId = document.getElementById(tagId);
if(tagId){
//索引を入れる目印の要素としてIDが指定されている場合。
tag = tagId;
} else {
tag = tag[0];
}
//最初のh2の上に索引のラップ要素を追加
tag.parentNode.insertBefore(wrap , tag);
//ラップ要素にIDを設定
wrap.id = "my-index";
//ラップ要素の中にタイトルのPタグを入れる
wrap.appendChild(p);
//索引のタイトルの文言を設定
p.innerHTML = title;
//索引のタイトルにクラス名を設定
p.classList.add("my-index-title");
if(note){
pNote = document.createElement("p");
pNote.innerHTML = note;
pNote.classList.add("my-index-note");
wrap.appendChild(pNote);
}
for (var i2 = 0; i2 < target.length; i2++){
//索引全体のラップ要素の中に、ワード事のラップ要素を入れる
wrapInbox = document.createElement("div");
wrapInbox.classList.add("my-index-inbox","my-index-inbox-" + i2);
wrap.appendChild(wrapInbox );
//olをラップ要素の中に入れる
ol = document.createElement("ol");
ol.id = 'my-index-words' + i2;
//olをラップ要素の中に入れる。
wrapInbox.appendChild(ol);
//ターゲットワードごとの小見出しをpタグで設定
indexSubheadingP = document.createElement("p");
indexSubheading = target[i2];
indexSubheadingP.innerHTML = indexSubheading;
indexSubheadingP.classList.add("my-index-subheading");
//ターゲットワードごとの小見出しをolの前に入れる
ol.parentNode.insertBefore( indexSubheadingP , ol);
targetSpan = range.getElementsByClassName("my-index-words" + i2);
for (var i3 = 0; i3 < targetSpan.length; i3++){
targetSpanId = targetSpan[i3].id;
//liを生成
li = document.createElement("li");
//aを生成
a = document.createElement("a");
//aタグにページ内リンクを設定
a.setAttribute("href", "#" + targetSpanId);
//aタグのアンカーテキスト設定
aNum = i3 + 1;
a.innerHTML = "[" + aNum + "]";
//liにaを入れる
li.appendChild(a);
//liをolの中に入れる
ol.appendChild(li);
}
}
//処理完了後にイベント削除
document.removeEventListener("DOMContentLoaded", myIndex,false);
}
}());
コード内の「任意のワード1」「任意のワード2」の部分に、索引に使いたいワードを指定します。
コンテンツ内からの索引対象ワードの取得部分などに不安があるため、精度は高くなく、誤動作を起す可能性が高いです。
当記事にこのJSをつかって索引を付けていますが、CSSは上記には含まれていないので、別途CSSによる設定が必要です。
属性内にある索引対象ワードの除外
索引対象ワードにspanを付けるため、altなどの属性内に索引対象ワードがあるとレイアウト崩れを起してしまいます。
検索等で調べた方法がどれも上手くいかず、試行錯誤で以下のコードを作って対応しました。
//1.正規表現を使ったreplace()で、大まかにHTMLタグ内と思われる、索引対象ワードを含んだ文字列を取得し、コールバックで処理を行う
range.innerHTML = range.innerHTML.replace(new RegExp('>.*?' + word + '.*?<','g'),
function(){
count++;
//2.指定ワードがHTMLの属性である可能性を除外するために、matchで「="指定ワード"」の状態になっていないか確認。
if(arguments[0].match(new RegExp('=".*?' + word + '.*?"', 'g'))){
//3.指定ワードが「="指定ワード"」のように属性の記述と同じ状態なら、何もせずに戻す
return arguments[0];
} else {
//4.指定のワードが「="指定ワード"」の状態ではないならspanラッピングして戻す
arguments[0] = arguments[0].replace(new RegExp('(' + word + ')','gi'), '<span class="my-index-words' + i + '" id="my-index-words' + i +'-' + count + '">$1</span>');
return arguments[0];
}
});
コメントにも書いていますが、流れは以下の通りです。
- 正規表現を使ったreplace()で、大まかにHTMLタグで囲まれていると思われる指定ワードを含んだ文字列をすべて取得する
- 指定したワードがHTMLの属性である可能性を除外するために、match()で「=”指定したワード”」の状態になっていないか確認する
- 指定したワードが「=”指定ワード”」のように属性の記述と同じ状態なら、何もせずに戻す
- 指定したワードが「=”指定ワード”」の状態ではないなら、spanでラッピングして戻す
1が最も問題なのですが、ChromeとFirefoxではうまく動いてもIEとEdgeでは意図した動作にはならなかったりと、完成には至っていません。
例えば、当記事上部のアイキャッチ下にあるfigcaption内のワードは取得できていません。
真っ当に使うのであれば、正規表現の部分は精度の高い状態に整えるか、別の手法でタグの挿入を試みる必要があるでしょう。
索引対象ワード設定時の注意点
検索用ワードを設定する際の注意点です。
- 範囲内に1つも存在しないワードを設定した場合の処理は作れていないため、最低1つは存在するワードで設定する必要がある
- 対象範囲を広げるため、アルファベットの大文字小文字は区別している
移動後の位置と装飾
pの内部のspanにpadding-topを大きな値で設定するは不具合の原因にならないかという懸念があったので、padding-topのよる余白の確保は諦めました。
代替案として、移動後にワードの位置が分かりやすくなるように、:targetを利用して、以下のようにCSSを設定しています。
span[id^="my-index-words"]:target{
background-color: #ffc4e9;
}
実現できなかったこと
既に書いた通り索引対象ワードの取得に問題がでていますが、他にもいくつか諦めた事柄があります。
索引対象ワード近辺のテキストを表示
書籍巻末にある索引のイメージで作りましたが、webサイトで再現すると数字のリンクが並んでいるだけなので使いやすいかどうかは微妙な印象です。
解決策の一案として、飛び先の周辺のワードも取得して配置する予定だったのですが、以下の理由により断念しました。
- 周辺の文字列をうまく取得できず(HTMLタグの一部が入り込んでしまうなど)
- 周辺の文字列を取得して表示できた場合、対象項目の数だけ表示用の追加スペースが必要になる
検索との連動
今回の作った機能の一部は、かなり前に試そうとして断念した以下の目的のためのものでもありました。
- 検索経由のアクセスを想定
- 検索ワードを取得し、ページ内の同一ワードを強調表示する
検索ワードが取得できないのと、仮にできたとしても形態素解析などが必要になる点から諦めていました。
その意味では、次善の案としてサイト側で用意したワードを強調表示するボタンを付けるのも多少の効果は見込めるかもしれません。
結び
現在はページの上部にありますが、スマホでは開閉式にするか、ドロワーメニューとして構築する方がよいかもしれません。
補足
索引対象のワードはページごとに変える必要がありますから、例えばWPであればPHPやWPの機能を使えば以下のような形などで構築できるでしょう。
- 投稿画面内に索引対象ワード用のカスタムフィールドを用意
- JSにカスタムフィールドの値を入れて設定する
プラグインとして作成すれば問題なく実現できるかと思います。
試作を試みようとは考えたものの、根幹部分である索引対象ワードの取得に関して再検討の余地が大きく、ここが固められない以上作っても無駄かなと諦めました。
また、PHPが使えるならJSで全てを賄わなくても良い手でしょうし、JSだけで作ろうとしているい今回の趣旨とはずれるから、というのもありますが。
0人がこの記事を評価
役に立ったよという方は上の「記事を評価する」ボタンをクリックしてもらえると嬉しいです。
連投防止のためにCookie使用。SNSへの投稿など他サービスとの連動は一切ありません。