前の関連記事:Blogger:ページ番号付ページナビ(8)Paginavi_Bloggerモジュールの導入
HTML要素のコピーに四苦八苦
Blogger:ページ番号付ページナビ(7)複数表示とモバイルサイトへの対応でページナビを複数表示にしました。
Blogger:ページ番号付ページナビ(8)Paginavi_Bloggerモジュールの導入のpagenavi.jsではHTMLを文字列で作成してelement.innerHTMLで<div class='blog-pager' id='blog-pager'/>と<div class='blog-pager' id='blog-pager2'/>を置換しました。
これは問題なく動いたのですが、これをHTMLの文字列ではなくHTML 要素をdocument.createElementで作成してHTMLを作成していったのですが、それを複数個所に表示させるのに四苦八苦しました。
まず普通に文字列のHTMLのようにdiv要素にNode.appendChildで追加してみたら、2番目のdivしか表示できませんでした。
表示したものは動作しました。
HTML要素は同時に二つの要素の子要素になれないとわかったので、HTML要素を複製する方法を考えました。
Node.cloneNode
これでうまくいくかと思ったら、ページナビは複数表示されるのですがクリックしても関数が起動しません。
デバッグしてみるとonclick属性がnullになっていました。
ノードを複製すると、そのノードのすべての属性とその値がコピーされます。つまり、HTML属性のイベントを含みます。addEventListener() を使用したものや、要素のプロパティに代入されたもの (例: node.onclick = fn;) は複製されません。onclick属性はコピーされないとちゃんと書いてありました、、、
Node.cloneNode - Web API インターフェイス | MDN
Object.create()
これでHTML要素を複製してみましたが、appendChildするときにNodeではないと言われて追加できませんでした。
それで仕方ないので、cloneNodeで複製してからonclick属性を付け直してみました。
すると動いたと思いきや、最後に追加したonclick属性の関数の引数のみが入っていました。
なのでどのボタンをクリックしても右端に表示されているページに飛んでしまいます。
代入する関数をObject.create()してみましたがそれは実行されませんでした。
結局HTML要素をふくせいのために一から作り直すという面倒なことをするしかないと思ったら解決法がわかりました。
イベントバブリングを利用してひとつのハンドラですべてのボタンのクリックイベントを受ける
How to Maintain Correct Javascript Event After Using cloneNode(true) - Stack Overflow
これを読んでイベントバブリングを初めて知りました。
イベントハンドラ | JavaScript プログラミング解説
イベント | JavaScript プログラミング解説
addEventListenerの第3引数の意味 | JavaScript プログラミング解説
これらを読んでだいたい理解できました。
三章第三回 イベントバブリング — JavaScript初級者から中級者になろう — uhyohyo.net
これの例を動かしてみて実際のやり方がわかりました。
PageNavi2_Blogger/PageNavi2_Blogger.js at bb556b2c6f08f1fde9e85f5ae59142ef94109920 · p--q/PageNavi2_Blogger
で、これは思った通り動きました。
ラベルインデックスページで動かすにはBlogger:ページ番号付ページナビ(1)何ページ目を見ているのか一目でわかる方法の「ラベルインデックスページの1ページあたりの投稿数を設定する」の設定が必要です。
function writeHtml(pageStart, pageEnd, lastPageNo) { // htmlの書き込み。
var divNode = createElem('div');
vars.buttunElems.forEach(function(b){divNode.appendChild(b);}); // ボタンノードを新しいdivノードの子ノードに追加する。
var dupNode;
vars.elements.forEach(function(elem){
dupNode = divNode.cloneNode(true); // ボタンノードを子ノードとするdivノードを複製する。デフォルトのプロパティしかコピーされない。イベントもコピーされない。
dupNode.onclick = onclickEvent; // 複製したノードにイベントのプロパティを追加する。
elem.appendChild(dupNode); // 既存のノードに追加して表示させる。
})
137行目でNode.cloneNodeでDIV要素を複製して新しいノードを作っていますが、onclick属性はコピーされないために次の行で複製したノードに追加しています。このほか自分で追加したプロパティも複製されなかったのでボタン名はnameプロパティに入れました。
モバイルサイトの表示を工夫する
いまのままでは、ボタンに表示するページ番号が2桁や3桁になってくると画面からページナビボタンがはみ出てしまいます。
Blogger:ページ番号付ページナビ(7)複数表示とモバイルサイトへの対応
HTMLのときは出力させる画面の幅を固定したものに想定して表示を変えましたが、今回はflexを使うことにしました。
CSS3 Flexbox の各プロパティの使い方をヴィジュアルで詳しく解説 | コリス
JavaScriptを使用したdisplayプロパティの実装メモ - Qiita
A Complete Guide to Flexbox | CSS-Tricks
これらを読んで理解しました。
// PageNavi2_Bloggerモジュール
var PageNavi2_Blogger = PageNavi2_Blogger || function() {
var pg = { // グローバルスコープに出すオブジェクト。グローバルスコープから呼び出すときはPageNavi2_Bloggerになる。
defaults : { // 既定値。
"perPage" : 10, //1ページあたりの投稿数。
"numPages" : 5, // ページナビに表示する通常ページボタンの数。スタートページからエンドページまで。
},
callback : { // フィードを受け取るコールバック関数。
getURL : function(root){ // フィードからタイムスタンプを得て表示させるURLを作成してそこに移動する。
var post = root.feed.entry[0]; // フィードから先頭の投稿を取得。
var m = /(\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d)\.\d\d\d(.\d\d:\d\d)/i.exec(post.published.$t);
var timestamp = encodeURIComponent(m[1] + m[2]); // 先頭の投稿からタイプスタンプを取得。
var addr_label = "/search/label/" + vars.postLabel + "?updated-max=" + timestamp + "&max-results=" + vars.perPage + "#PageNo=" + vars.pageNo;
var addr_page = "/search?updated-max=" + timestamp + "&max-results=" + vars.perPage + "#PageNo=" + vars.pageNo;
location.href =(vars.postLabel)?addr_label:addr_page; // ラベルインデックスページとインデックスページでURLが異なることへの対応。
},
getTotalPostCount : function(root){
var total_posts = parseInt(root.feed.openSearch$totalResults.$t, 10); // 取得したフィードから総投稿総数を得る。
createNodes(total_posts); // 総投稿数をもとにページナビのボタンを作成する。
}
},
all: function(array_elementIDs) { // ここから開始する。引数にページナビを置換する要素のidを配列に入れる。
array_elementIDs.forEach(function(e){
var elem = document.getElementById(e); // elementIDの要素の取得。
if (elem) {vars.elements.push(elem);} // elementIDの要素を配列に取得。
});
if (vars.elements.length > 0) {createPageNavi();} // ページナビの作成。
}
}; // end of pg
var vars = { // PageNavi2_Bloggerモジュール内の"グローバル"変数。
perPage : pg.defaults.perPage, // デフォルト値の取得。
numPages : pg.defaults.numPages, // デフォルト値の取得。
jumpPages : pg.defaults.numPages, // ジャンプボタンでページ番号が総入れ替えになる設定値。
postLabel : null, // ラベル名。
pageNo : null, // ページ番号。
currentPageNo : null, // 現在のページ番号。
elements : [], // ページナビを挿入するhtmlの要素の配列。
};
function redirect(pageNo) { // ページ番号のボタンをクリックされた時に呼び出される関数。
vars.pageNo = pageNo; // 表示するページ番号
if (pageNo==1) { // 1ページ目を取得するときはページ番号からURLを算出する必要がない。
location.href = (!vars.postLabel)?"/":"/search/label/" + vars.postLabel + "?max-results=" + vars.perPage; // ラベルページインデックスの場合分け。
} else {
var startPost = (vars.pageNo - 1) * vars.perPage; // 新たに表示する先頭ページの先頭になる投稿番号を取得。
var url;
if (vars.postLabel) { // ラベルページインデックスの場合分け。
url = "/feeds/posts/summary/-/" + vars.postLabel + "?start-index=" + startPost + "&max-results=1&alt=json-in-script&callback=PageNavi2_Blogger.callback.getURL";
} else {
url = "/feeds/posts/summary?start-index=" + startPost + "&max-results=1&alt=json-in-script&callback=PageNavi2_Blogger.callback.getURL";
}
writeScript(url); //スクリプト注入。
}
}
function createElem(tag){ // tagの要素を作成して返す。
return document.createElement(tag);
}
function createNodes(total_posts) { // 総投稿数からページナビのボタンを作成。
var buttunElems = []; // ボタン要素を入れる配列。
var numPages = vars.numPages; // ページナビに表示するページ数。
var prevText = '\u00ab'; // 左向きスキップのための矢印«。
var nextText = '\u00bb'; // 右向きスキップのための矢印»。
var diff = Math.floor(numPages / 2); // スタートページ - 現在のページ = diff。
var pageStart = vars.currentPageNo - diff; // スタートページの算出。
if (pageStart < 1) {pageStart = 1;} // スタートページが1より小さい時はスタートページを1にする。
var lastPageNo = Math.ceil(total_posts / vars.perPage); // 総投稿数から総ページ数を算出。
var pageEnd = pageStart + numPages - 1; // エンドページの算出。
if (pageEnd > lastPageNo) {pageEnd = lastPageNo;} // エンドページが総ページ数より大きい時はエンドページを総ページ数にする。
if (pageStart > 1) {buttunElems.push(createButton(1,1));} // スタートページが2以上のときはスタートページより左に1ページ目のボタンを作成する。
if (pageStart == 3) {buttunElems.push(createButton(2, 2));} // スタートページが3のときはジャンプボタンの代わりに2ページ目のボタンを作成する。
if (pageStart > 3) { // スタートページが4以上のときはジャンプボタンを作成する。
var prevNumber = pageStart - vars.jumpPages + diff; // ジャンプボタンでジャンプしたときに表示するページ番号。
(prevNumber < 2)?buttunElems.push(createButton(1,prevText)):buttunElems.push(createButton(prevNumber, prevText)); // ページ番号が1のときだけボタンの作り方が異なるための場合分け。
}
for (var j = pageStart; j <= pageEnd; j++) {buttunElems.push((j == vars.currentPageNo)?createCurrentNode(j):createButton(j, j));} // スタートボタンからエンドボタンまで作成。
if (pageEnd == lastPageNo - 2) {buttunElems.push(createButton(lastPageNo - 1, lastPageNo - 1));} // エンドページと総ページ数の間に1ページしかないときは右ジャンプボタンは作成しない。
if (pageEnd < (lastPageNo - 2)) { // エンドページが総ページ数より3小さい時だけ右ジャンプボタンを作成。
var nextnumber = pageEnd + 1 + diff; // ジャンプボタンでジャンプしたときに表示するページ番号。
if (nextnumber > lastPageNo) {nextnumber = lastPageNo;} // 表示するページ番号が総ページ数になるときは総ページ数の番号にする。
buttunElems.push(createButton(nextnumber, nextText)); // 右ジャンプボタンの作成。
}
if (pageEnd < lastPageNo) {buttunElems.push(createButton(lastPageNo, lastPageNo));} // 総ページ番号ボタンの作成。
writeHtml(buttunElems); // htmlの書き込み。
}
function createPageNavi() { // URLからラベル名と現在のページ番号を得、その後総投稿数を得るためのフィードを取得する。
var thisUrl = location.href; // 現在表示しているURL。
if (/\/search\/label\//i.test(thisUrl)) { // ラベルインデックスページの場合URLからラベル名を取得。
vars.postLabel = /\/search\/label\/(.+)(?=\?)/i.exec(thisUrl)[1]; // 後読みは未実装の可能性あるので使わない。
}
if (!/\?q=|\.html$/i.test(thisUrl)) { // 検索結果や固定ページではないとき。
vars.currentPageNo = (/#PageNo=/i.test(thisUrl))?/#PageNo=(\d+)/i.exec(thisUrl)[1]:1; // URLから現在のページ番号の取得。
var url; // フィードを取得するためのURL。
if (vars.postLabel) { // 総投稿数取得のためにフィードを取得するURLの作成。ラベルインデックスのときはそのラベル名の総投稿数を取得するため。
url = '/feeds/posts/summary/-/' + vars.postLabel + "?alt=json-in-script&callback=PageNavi2_Blogger.callback.getTotalPostCount&max-results=1";
} else {
url = "/feeds/posts/summary?max-results=1&alt=json-in-script&callback=PageNavi2_Blogger.callback.getTotalPostCount";
}
writeScript(url);
}
}
function createButton(pageNo, text) { // redirectするボタンのノード作成。
var spanNode = createElem('div');
spanNode.className = "displaypageNum";
spanNode.appendChild(createElem('a'));
spanNode.firstChild.textContent = text;
spanNode.firstChild.href = "#";
spanNode.firstChild.name = pageNo; // redirect()の引数に使う。
return spanNode;
}
function createCurrentNode(j) { // 現在表示中のページのノード作成。
var spanNode = createElem('div');
spanNode.className = "pagecurrent";
spanNode.textContent = j;
return spanNode;
}
function writeScript(url) { // スクリプト注入。
var ws = createElem('script');
ws.type = 'text/javascript';
ws.src = url;
document.getElementsByTagName('head')[0].appendChild(ws);
}
function onclickEvent(e) { // Event bubblingで発火させる関数。
e=e||event; // IE sucks
var target = e.target||e.srcElement; // targetはaになる。// and sucks again // target is the element that has been clicked
if (target && target.parentNode.className=="displaypageNum") {
redirect(target.name);
return false; // stop event from bubbling elsewhere
}
}
function writeHtml(buttunElems) { // htmlの書き込み。
var divNode = createElem('div');
buttunElems.forEach(function(b){divNode.appendChild(b);}); // ボタンノードを新しいdivノードの子ノードに追加する。
divNode.style.display = "flex"; // flexの子要素はdivにする。spanはダメ。
divNode.style.justifyContent = "flex-end"; // ボタンを右寄せにする
divNode.style.alignItems = "center"; // これがないと現在のページのボタンがずれる。
divNode.style.flexWrap = "wrap"; // ボタンを折り返す。
var divNode2 = createElem('div'); // ボタンを折り返しても中央表示されるようにflexboxを入れ子にする。
divNode2.appendChild(divNode);
divNode2.style.display = "flex";
divNode2.style.justifyContent = "center";
var dupNode;
vars.elements.forEach(function(elem){
dupNode = divNode2.cloneNode(true); // ボタンノードを子ノードとするdivノードを複製する。デフォルトのプロパティしかコピーされない。イベントもコピーされない。
dupNode.firstChild.onclick = onclickEvent; // 複製したノードにイベントのプロパティを追加する。
elem.appendChild(dupNode); // 既存のノードに追加して表示させる。
}); // 要素を書き換え。
}
return pg; // グローバルスコープにだす。
}();
//デフォルト値を変更したいときは以下のコメントアウトをはずして設定する。
//PageNavi2_Blogger.defaults["perPage"] = 10 //1ページあたりの投稿数。
//PageNavi2_Blogger.defaults["numPages"] = 5 // ページナビに表示するページ数。
PageNavi2_Blogger.all(["blog-pager","blog-pager2"]); // ページナビの起動。引き数にHTMLの要素のidを配列で入れる。
モバイルサイトで横幅がせまいときはページナビボタンを右揃えで折り返したいのですが、そうすると折り返さない時も右端に寄ってかっこわるいのでflexboxを入れ子にしました。それでも入れ子のdiv要素の幅は折り返す前の幅が認識されていて少し右寄りに表示されてしまいます。
div要素の幅を取得してtransformで縮小させることも考えましたがdiv要素の幅を取得できませんでした。
よく考えたらまだウェブページに表示させる前の状態で幅を取得するのは不可能なのは当然でした。
PageNavi2_Blogger/pagenavi.css at 2b794c1b8479fb34f0b6058b885afd96a4191783 · p--q/PageNavi2_Blogger
各ボタンのCSSも右だけmargin5pxにしていたのを上下左右にそれぞれ10pxと5pxをmargin追加しました。
«と»はlinuxBean14.04(125)Bloggerで使うJavaScriptの開発環境:その2の方法でローカルサーバで表示させると問題ありませんでしたが、BloggerのHTML/JavaScriptガジェットに入れて表示させると文字化けしたので、Blogger:JavaScriptのpromptやalertで句読点を表示する方法の方法でUTF-16にエンコードしました。
linuxBean14.04(126)Closure CompilerでJavaScriptを圧縮するの方法で圧縮した場合は自動的にUTF-16にエンコードされていました。
参考にしたサイト
How to Maintain Correct Javascript Event After Using cloneNode(true) - Stack Overflow
onclick属性はcloneNodeではコピーされないQに対してイベントバブリングを利用するというA。
イベントハンドラ | JavaScript プログラミング解説
onclick属性はDOMレベル0ですがイベントバブリングで発火できました。
イベント | JavaScript プログラミング解説
targetとcurrentTargetの違いがわかりやすいです。
addEventListenerの第3引数の意味 | JavaScript プログラミング解説
イベントフローのわかりやすい解説。
三章第三回 イベントバブリング — JavaScript初級者から中級者になろう — uhyohyo.net
イベントバブリングの簡単な実行例が勉強になりました。
CSS3 Flexbox の各プロパティの使い方をヴィジュアルで詳しく解説 | コリス
flexboxについてコンテナとアイテムにわけてのプロパティの解説がわかりやすいです。
JavaScriptを使用したdisplayプロパティの実装メモ - Qiita
コンテナにelement.style.display = 'flex'で設定できました。
A Complete Guide to Flexbox | CSS-Tricks
改行した行ごとに左右寄せを制御する方法はわかりませんでした。

0 件のコメント:
コメントを投稿