manekineko倉金家ホームページ

趣味の部屋/ホームページ余話

画面のクリックアンカー

2015年8月15日
ページにリンクやボタンなどを設けてそれをクリックして何か表示を変えるようなとき、サーバーにデータを送らなくてはならない場合があります。その場合ページは再読み込みされて表示はページの頭に戻ってしまいます。これは意外と簡単な工夫によって元の位置を再表示させることができました。


ページにテキスト入力欄やボタン、あるいはリンクタグなどを設置してそれによって処理をしたりページをちょっと変えて同じページを再表示したいことがあります。
処理をjavascriptでブラウザ内だけで行えればいいのですが、サーバーにデータを送らなくてはならない場合は同じページだとページは再読み込みされるため表示は頭に戻ってしまいます。いちいちスクロールしてまた元の位置に戻らなくてはならないというのはめんどうです。

いい例が左のMENU(INDEX)フレームですが(PCブラウザの場合のみ。携帯端末ではフレームはつかっていません)、たとえばフォルダーの部分をクリックすると新しいデータをサーバーから受け取って、何もしないとまた頭から再表示され、かんじんのフォルダ内のページタイトルを見るにはまたスクロールしなくてはなりません。(すべてのデータを送ってjavascriptでやるという手もあるけど当初からそれはやらなかったし、やったとしたら検索ロボットにはサイトが見れない。)

以前は行ごとにアンカーを打って可視範囲から外れないようにはしていましたが、ちょっとした工夫によってクリックした部分の位置をきちんと再現できるようになりました。フォルダマークの部分をクリックしてみるとわかります。(スクロール範囲の制限で位置が変わる場合もあるけど。記事タイトルをクリックすると別のページにいっちゃいますのでやらないで!)

以下にその方法。名づけてクリックアンカー方式。

以下のjavascriptコードを読み込ませておきます。これは実際に左のMENUページのもの。
(<script type="text/javascript" src="..."></script>で読み込ませてもいいが無駄なアクセスを減らすためここではサーバー側で読み込んでしまっています。)
<script type="text/javascript"><!--
// cookieの書込、読出。
// サイトの中にさらに子サイトをつくったときなどのcookie識別用ヘッダー。不要なら空("")に。
cookie_head = "";
// cookieの保存。setcookie("name", "value", "3days");などと。
function setcookie(cookiename, cookievalue, timelimit) {
 cookienameorder = cookie_head + cookiename + "=";
 if(timelimit) {
  lifetime = parseInt(timelimit.toString().match(/^[\+\-]?[0-9]*/));
  if(lifetime != 0) {
   cookielife = new Date();
   lifeunit = timelimit.toString().match(/[A-Za-z]*$/).toString();
   if(lifeunit.match(/^min/i)) { lifetime *= 60; } // minutes
   else if(lifeunit.match(/^h/i)) { lifetime *= 60*60; } // hours
   else if(lifeunit.match(/^d/i)) { lifetime *= 60*60*24; } // days
   else if(lifeunit.match(/^w/i)) { lifetime *= 60*60*24*7; } // weeks
   else if(lifeunit.match(/^m/i)) { lifetime *= 60*60*24*31; } // months
   else if(lifeunit.match(/^y/i)) { lifetime *= 60*60*24*365; } // years
   else { lifetime *= 60*60*24; } // default unit: days
   cookielife.setTime(cookielife.getTime() + lifetime*1000);
   expiredate = cookielife.toGMTString();
   document.cookie = cookienameorder + cookievalue + ";expires=" + expiredate+";";
  } else {
   document.cookie = cookienameorder + cookievalue + ";";
  }
 } else {
  document.cookie = cookienameorder + cookievalue + ";";
 }
}
// cookieの読出。getcookie("name");で値を得る。cookieがない場合false。
function getcookie(cookiename) {
 cookiedata = " "+document.cookie+";";
 cookienameorder = " "+cookie_head+cookiename+"=";
 startpos = cookiedata.indexOf(cookienameorder);
 if(startpos >= 0) {
  endpos = cookiedata.indexOf(";", startpos);
  readdata = cookiedata.substring(startpos + cookienameorder.length, endpos);
  return(readdata);
 } else {
  return false;
 }
}
// 画面のスクロール値の取得。get_scroll()を呼んだ時点のスクロール値がscroll_left,scroll_top,...に入る。
scroll_left = scroll_top = scroll_height = scroll_width = 0;
function get_scroll() {
 (scroll_left   = document.documentElement.scrollLeft)   || (scroll_left   = document.body.scrollLeft);
 (scroll_top    = document.documentElement.scrollTop)    || (scroll_top    = document.body.scrollTop);
 (scroll_height = document.documentElement.scrollHeight) || (scroll_height = document.body.scrollHeight);
 (scroll_width  = document.documentElement.scrollWidth)  || (scroll_width  = document.body.scrollWidth);
}
// object_idで指定されるobjectの上座標を得る。
function get_object_top(object_id) {
  object_element = document.getElementById(object_id);
  if(object_element && (rect = object_element.getBoundingClientRect())) {
   return Math.round(rect.top); // rectの各値はなぜか必ずしも整数ではない。
  } else {
   return 0;
  }
}
// object_idで指定されるobjectの左座標を得る。
function get_object_left(object_id) {
  object_element = document.getElementById(object_id);
  if(object_element && (rect = object_element.getBoundingClientRect())) {
   return Math.round(rect.left); // rectの各値はなぜか必ずしも整数ではない。
  } else {
   return 0;
  }
}
// クリックの位置をアンカーとして記憶し再現。引数はいずれも文字列。空文字""は無指定。
// object_idが指定されればそのobjectをスクロール可能な範囲でできるだけ同じ位置にいくようにする。
// さらにpage_idが指定されれば同じpage_idのときのみ動作。(ページが変わったらアンカーは効かない。)
// アンカーのセットは画面内容が変わらない場合にはobjectを指定せず<body onCick="anchor_save('','PageName')">としてもよい。
// アンカーの適用は原則としてページをすべて書き出した後、<script>anchor_apply('PageName');anchor_erase();</script>の形で呼ぶ。
//
// anchor_idはフレームを使う場合最低限各フレームごとに設定。お互い他のフレームを書換えたときにへんな干渉をさけるため。
// その他ページの種別毎などで変えてもよいが、あまり小分けにするとcookieが増える。
// 同じanchor_idで保存されるcookieは1セットのみ。でも単独ページ表示のサイトなら概ねこれで充分。
anchor_id = "anc_menu";
// アンカーの記憶。
function anchor_save(object_id, page_id) {
  get_scroll();
  setcookie(anchor_id+"_pid", page_id, 0);
  setcookie(anchor_id+"_st", scroll_top, 0);
  setcookie(anchor_id+"_sl", scroll_left, 0);
  if(object_id) {
    object_top  = scroll_top  + get_object_top(object_id);
    object_left = scroll_left + get_object_left(object_id);
    setcookie(anchor_id+"_oid", object_id, 0);
    setcookie(anchor_id+"_ot",  object_top, 0);
    setcookie(anchor_id+"_ol",  object_left, 0);
  } else {
    setcookie(anchor_id+"_oid", "", -1);
    setcookie(anchor_id+"_ot",  "", -1);
    setcookie(anchor_id+"_ol",  "", -1);
  }
}
// アンカーの適用。
function anchor_apply(page_id) {
  if((saved_page_id = getcookie(anchor_id+"_pid")) && page_id != saved_page_id) { return; }
  get_scroll();
  if((anchor_st = getcookie(anchor_id+"_st")) != "" && (anchor_sl = getcookie(anchor_id+"_sl")) != "") {
    anchor_st = parseInt(anchor_st);
    anchor_sl = parseInt(anchor_sl);
  } else {
    anchor_st = scroll_top;
    anchor_sl = scroll_left;
  }
  if((object_id = getcookie(anchor_id+"_oid"))
   && (anchor_ot = getcookie(anchor_id+"_ot")) != ""
   && (anchor_ol = getcookie(anchor_id+"_ol")) != "") {
    offset_top  = (scroll_top  + get_object_top(object_id))  - parseInt(anchor_ot);
    offset_left = (scroll_left + get_object_left(object_id)) - parseInt(anchor_ol);
  } else {
    offset_top = offset_left = 0;
  }
  window.scrollTo(anchor_sl + offset_left, anchor_st + offset_top);
}
// アンカー消去。あえてとっておく必要がなければanchor_apply()後すぐ消去が望ましい。
function anchor_erase() {
  setcookie(anchor_id+"_pid", "", -1);
  setcookie(anchor_id+"_st",  "", -1);
  setcookie(anchor_id+"_sl",  "", -1);
  setcookie(anchor_id+"_oid", "", -1);
  setcookie(anchor_id+"_ot",  "", -1);
  setcookie(anchor_id+"_ol",  "", -1);
}
//--></script>
要は位置を再現したい要素にidを設定し、onCick="anchor_save();"でクリックしたときにその位置やそのときのスクロール値をcookieに記憶させ、それを使ってページの書換え時に位置を再現するというもの。

細かいstyle指定などは省きますが、たとえば左のメニューページでこのページをポイントしている部分は、
<h4 id="ID201" onClick="anchor_save('ID201','');"><a href="/index.html?id=201">画面のクリックアンカー</a></h4>
となっており、クリックしたとたんにcookieに位置情報が記憶されます。201というのはこのページのページ番号です。

ほとんどすべての要素にこのような指定が記載されていますがいちいち手書きで書いているわけではなく、プログラムで自動的に出力していますのでページが何千あろうと面倒でも何でもありません。

そしてページの最後の方(実質内容を書き出し終わったところ)に、
<script type="text/javascript"><!--
// アンカー。これは最後(実質内容がすべてが書き出されてから)。
if(getcookie(anchor_id+"_st") != "") {
// クリックアンカーがあればそれを適用。
anchor_apply("");
anchor_erase();
}
else {
// なければ自動アンカー(最初とリロード時)で表示中の記事タイトルが画面の上から1/3くらいにくるようにする。
get_scroll();
target_obj_top = scroll_top + get_object_top("ID201");
shift = Math.round(getWindowHeight()/3);
window.scrollTo(scroll_left, target_obj_top - shift);
}
//--></script>
などと出力します。これでクリックしたオブジェクトの位置が再現されます。

こちらの右の記事ページの方はもっと簡単です。
同様にクリックアンカー用のスクリプトを読み込みますが、最後のアンカーセットの部分は、
<script type="text/javascript"><!--
if(getcookie("anc_main_st") != "") {
anchor_apply("201");
anchor_erase();
}
//--></script>
そしてアクションの指定は<body>1箇所のみ。あとは特に何もしてません。
<body onClick="anchor_save('', '201');">
201はこのページのページ番号です。
ページ番号を指定するのは、ページが切り替わったとき前のページのアンカーが適用されないようにするためだけで、識別できれば番号でなくても何でもかまいません。

記事ページにも仕掛ける理由は、
(1)時折ボタンアクションなどを使う場合がある。
(2)ページ作成の際確認のため再読み込みしても編集している部分がすぐに見れるようにできる。(どこかクリックしてから再読み込みすればよい。)
などのメリットからです。すべてのページに設置しても別に害にはなりません。

例として(1)のボタンアクションをためしてみましょう。
通常<form>のボタンでサーバーにデータを送るとページは再読み込みされページのトップに戻ってしまいます。
でもこの簡易クリックアンカーのおかげでこのページではボタンの位置はさほど変わりません。(ボタンより前の内容が大きく変わるならさらにobject指定が必要ですが。)

ためしに毎回ページを再読込し、かつ3回クリックすると写真を表示するようにしてみます。
1回ごとに少しスクロールして位置を変えて試してみるとよいかと思います。
3回クリックすると写真を表示!

さらにしつこくスクロールやonBlurイベントでも位置を記憶するという手もありますがそこまではやっていません。


(参考)
ページの画像にmax-widthやmax-heightを使ったりwidthやheightにautoを指定したり(要は画面サイズに合わせて画像をリサイズしようと)すると、アンカー位置(スクロール値)をちゃんと計算できないだめブラウザがあるようで、一部のブラウザでこの場合へんな現象が出ます。アンカーが正しく動作しないだけでなく他にも問題が出るので、このようなブラウザに対しては画像サイズは固定しており画面に合わせてのサイズ縮小はされません。いろいろと面倒くさいCSSハック(八苦?)もあるようですがそこまではやりません。