JavaScript : してログ

12

前回は CSS のみでテーブルヘッダを固定する方法を紹介しました。ただ、ページ構成によっては position: sticky; が効かない場合があるため、今回は JavaScript を使う方法を紹介します。jQuery には高機能なプラグインがありますが、実装が複雑なためか動作が不安定になることがありますが、余計な機能はいらないならこれで十分です。

見かけの等級星座バイエル符号名称距離 (光年)絶対等級英名
-26.75太陽0.0000164.82
-1.46おおいぬ座α星シリウス8.61.434Sirius
-0.74りゅうこつ座α星カノープス309-5.624Canopus
-0.1ケンタウルス座α星4.394.365α Centauri
-0.05うしかい座α星アークトゥルス36.7-0.307Arcturus
0.01ケンタウルス座α星Aリギル・ケンタウルス4.394.365Rigil Kentaurus
0.03こと座α星ベガ25.030.604Vega
0.08ぎょしゃ座α星Aカペラ42.78-0.510Capella
0.13オリオン座β星リゲル862.43-6.983Rigel
0.37こいぬ座α星プロキオン11.462.641Procyon
0.42オリオン座α星ベテルギウス642.22-5.499Betelgeuse
0.46エリダヌス座α星アケルナル139.38-2.695Achernar
0.6ケンタウルス座β星ハダル391.83-4.799Hadar
0.667みなみじゅうじ座α星アクルックス320Acrux
0.76わし座α星アルタイル16.722.21Altair
0.86おうし座α星アルデバラン66.61-0.692Aldebaran
0.91さそり座α星アンタレス553.48-5.239Antares
0.97おとめ座α星スピカ249.62-3.450Spica
1.14ふたご座β星ポルックス33.771.064Pollux
1.16みなみのうお座α星フォーマルハウト25.111.727Fomalhaut
1.25はくちょう座α星デネブ1411.26-6.932Deneb
1.25みなみじゅうじ座β星ミモザ278.39-3.407Mimosa
1.33ケンタウルス座α星Bトリマン4.395.685Toliman
1.4しし座α星Aレグルス79.26-0.529Regulus
1.5おおいぬ座ε星アダーラ404.97-3.971Adara
1.58ふたご座α星カストル50.840.615Castor
1.62さそり座λ星シャウラ570.93-4.597Shaula
1.64みなみじゅうじ座γ星ガクルックス88.51-0.529Gacrux
1.64オリオン座γ星ベラトリックス252.32-2.804Bellatrix
1.65おうし座β星エルナト133.83-1.417Elnath
1.69りゅうこつ座β星ミアプラキドゥス113.12-1.012Miaplacidus
1.69オリオン座ε星アルニラム1975.76-7.223Alnilam
1.71つる座α星アルナイル100.96-0.745Al Na'ir
1.714ほ座γ星スハイル・ムーリフ、レゴール1116.44-5.843Al Suhail al-Muhlif
1.77おおぐま座ε星アリオト82.51-0.246Alioth
1.79オリオン座ζ星アルニタク735.89-4.978Alnitak
1.79おおぐま座α星ドゥーベ122.83-1.090Dubhe
1.79ペルセウス座α星ミルファク506.21-4.166Mirfak
1.84おおいぬ座δ星ウェズン1605.91-6.623Wezen
1.85いて座ε星カウス・アウストラリス143.23-1.364Kaus Australis
1.86おおぐま座η星アルカイド103.89-0.657Alkaid
1.862さそり座θ星サルガス300.18-2.959Sargas
1.9ぎょしゃ座β星メンカリナン81.07-0.078Menkalinan
1.918くじゃく座α星ピーコック178.73-1.777Peacock
1.92ふたご座γ星アルヘナ109.25-0.706Alhena
1.92みなみのさんかく座α星アトリア390.42-3.472Atria
1.95ほ座δ星Alsephina80.51-0.013Alsephina
1.953りゅうこつ座ε星アヴィオール604.82-4.389Avior
1.97おおいぬ座β星ミルザム492.45-3.926Murzim
1.97うみへび座α星アルファルド180.21-1.743Alphard
2.01おひつじ座α星ハマル65.780.486Hamal
2.01くじら座β星ディフダ96.28-0.342Diphda
2.02こぐま座α星ポラリス(現北極星)432.36-3.593Polaris
2.05ケンタウルス座θ星メンケント58.790.77Menkent
2.05アンドロメダ座β星ミラク197.34-1.860Mirach
2.06オリオン座κ星サイフ646.83-4.428Saiph
2.06アンドロメダ座α星アルフェラッツ96.97-0.307Alpheratz
2.067いて座σ星ヌンキ227.65-2.153Nunki
2.07へびつかい座α星ラス・アルハゲ48.561.205Rasalhague
2.08こぐま座β星コカブ130.87-0.938Kochab
2.1アンドロメダ座γ星アルマク392.77-3.305Almach
2.11つる座β星Tiaki176.89-1.562Tiaki
2.12ペルセウス座β星アルゴル89.88-0.082Algol
2.13しし座β星デネボラ35.861.923Denebola
2.17ケンタウルス座γ星ムリファイン130.09-0.835Muhlifain
2.21ほ座λ星スハイル544.24-3.903Suhail
2.23りゅう座γ星エルタニン154.21-1.144Etamin
2.23カシオペヤ座α星シェダルSchedar
2.23はくちょう座γ星サドル1831.46-6.518Sadr
2.24かんむり座α星アルフェッカ75.010.43Alphecca
2.25とも座ζ星ナオス1083.06-5.357Naos
2.26りゅうこつ座ι星アスピディスケ765.26-4.593Aspidiske
2.27おおぐま座ζ星ミザール85.770.169Mizar
2.27カシオペヤ座β星カフ54.721.146Caph
2.286おおかみ座α星464.39-3.482
2.29さそり座ε星Larawag800.4Larawag
2.3ケンタウルス座ε星427.26-3.287
2.31ケンタウルス座η星305.53-2.549
2.32さそり座δ星ジュバ490.96-3.569Dschubba
2.37おおぐま座β星メラク79.710.429Merak
2.37しし座γ星アルギエバ130.04-0.634Algieba
2.37ほうおう座α星アンカア84.680.297Ankaa
2.386さそり座κ星ギルタブ482.96-3.467Girtab
2.39うしかい座ε星イザール202.48-1.576Izar
2.39ペガスス座ε星エニフ689.22-4.236Enif
2.39カシオペヤ座γ星612.78-3.980γ Cassiopeiae
2.41オリオン座δ星ミンタカ692.14-4.225Mintaka
2.42ペガスス座β星シェアト195.91-1.474Scheat
2.42へびつかい座η星サビク88.320.256Sabik
2.44おおぐま座γ星フェクダ83.140.407Phecda
2.45おおいぬ座η星アルドラ1987.8-6.476Aludra
2.46ケフェウス座α星アルデラミン49.021.574Alderamin
2.473ほ座κ星マルカブ570-3.7Markab
2.48はくちょう座ε星Aアルジェナー72.670.739Aljanah
2.48ペガスス座α星マルカブ133.28-0.578Markab

HTML

<div class="scrollbox">
	<div class="innerbox">
		<table class="tablefix">
			<thead>
				<tr><th></th></tr>
			</thead>
			<tbody>
				<tr><td></td></tr>
			</tbody>
		</table>
	</div>
</div>

CSS

.scrollbox {
	position: relative;
	width: 100%;
	height: 30em;
	overflow: scroll;
	border: 1px solid gray;
}

.innerbox {
	padding: 1em;
	display: table-cell;
}

.tablefix {
	border-collapse: separate;
	border-spacing: 0px;
	border-bottom: 1px solid #dddddd;
	border-right: 1px solid #dddddd;
}

.tablefix thead th {
	position: relative;
	white-space: nowrap;
	background-color: #e3e3e3;
}

.tablefix th,
.tablefix td {
	border-top: 1px solid #dddddd;
	border-left: 1px solid #dddddd;
	padding: 0.5em;
}

JavaScript(要 jQuery)

$(function(){

	$('.scrollbox').scroll(function(e){
		let th_top = $(this).find('.tablefix').position().top;
		let filter;

		if (th_top>=0) {
			th_top = 0;
			filter = 'unset';
		} else {
			th_top = -th_top-1;
			filter = 'drop-shadow(0px 4px 0px #00000040) drop-shadow(0px 2px 0px #00000040)';
		}

		$(this).find('.tablefix thead th').css({
			top: th_top,
			filter: filter
		});
	});

});

解説

ロジック

scrollbox のスクロールイベントで、テーブルヘッダがスクロールアウトしていた場合、位置を計算して更新しています。位置計算でマイナス1しているのは、枠線分の背景が透けて見えるのを防いでいるためです。

また、ヘッダが固定状態になったときに下側に影を落としています。box-shadow ではなく filter を使っているのは、影が左右へはみ出すのを防ぐためです(同じ理由でぼかしを入れられない)。スタイルの変更はお好みに合わせて変更してください。

Firefox のバグに対応

テーブルを border-collapse: collapse; にして th に position: sticky; を設定すると、firefox 系で枠線(の右側と下側)が消えるバグが発生します。このためテーブルを border-collapse: separate; border-spacing: 0px; にしています。この場合、枠線の太さを 2px にする場合と 1px にする場合とで工夫が必要になります。

枠線の太さを 2px にする場合

table, th, td の3つに 1px の枠線を付けます。

.tablefix {
	border-collapse: separate;
	border-spacing: 0;
	border: 1px solid #dddddd;
}

.tablefix th,
.tablefix td {
	border: 1px solid #dddddd;
}
枠線の太さを 1px にする場合

table の右と下に 1px の枠線、th, td の左と上に 1px の枠線を付けます(サンプルの方法)。

innerbox について

scrollbox 内でスクロールさせたとき、padding(もくしはテーブルの margin)がうまく適用されない点について修正しています。これ(display: table-cell;)が無いと、右側へスクロールさせても余白がありません。また、Firefox と Chrome で下側へのスクロールで違いがあります。

なお、スクロールボックス内に入れない場合や、余白が必要ない場合は必要ありません。

複数の REST API の呼び出しなどで、すべての通信が終わった後に何か実行したいとき地味に困りますよね。非同期通信なので、終了時のコールバック関数で処理するのですが、他の通信がまだ終わったのをどうやって知るのがベストでしょうか。様々な手があるかと思いますが、今回は記述がシンプルで分かり易い方法を考えてみました。他にもっといい手がある、という方はコメント欄にて教えて下さい。

考案した方法は、必要な処理数を各終了時関数内でカウントダウンしていき、ゼロを引いた時点ですべての完了関数を呼び出すというものです。カウントダウンとゼロ判定、完了関数の呼び出しまで1行で記述しており、可読性も高いと思います。

let standby = 2;

$.getJSON(URL1,function(json){
	allReady();
});

$.getJSON(URL2,function(json){
	allReady(); 
});

function allReady() {
	if (--standby) { return; }
	console.log('All Ready !');
}

OpenLayers2 と OpenLayers3 以降とではだいぶ構成が違うので慣れるまでが大変です。

イベントハンドラの代替案2つ

Feature にはクリックイベントが無いようです。似たような動作をしたい場合は、マップオブジェクトのクリックイベントで行えます。以下の例では、クリックした座標から forEachFeatureAtPixels を使ってヒットした Feature をすべてコンソールに出力しています。

map.on('click',function(e){
	map.forEachFeatureAtPixel(e.pixel,function(feature,layer){
		console.log(feature);
	})
});

セレクトツール(インタラクションというらしいが)で良ければ、下記のようなコードでも実現できます。こちらの場合、選択時のスタイルも設定しておくか、選択されたら即解除するような処理を入れておかないとおかしな動作に見えると思います。

var selectSingleClick = new ol.interaction.Select();
map.addInteraction(selectSingleClick);
selectSingleClick.on('select',function(e) {
	console.log(e.target.getFeatures().getLength());
});

ポップアップ(吹き出し)の作り方

OpenLayers2 のときあった OpenLayers.Popup.FramedCloud も無いみたいです。CSS で吹き出し形状にするのって、after などの疑似要素やボーダーの変態活用が絡むのであまりやりたくない。標準のポップアップくらい用意しておいてくれよと思います。

枠だけで良いなら下記のように jQuery でスタイルも埋め込んでやれば CSS 要りません。ただし地図においては、三角形がないとどこを示しているポップアップなのか分からなくなるので、素直に CSS に定義したほうが良さそうです。

popup_container =
	$('<div>')
	.css({
		'position':'absolute',
		'background-color':'white',
		'box-shadow':'0 1px 4px rgba(0,0,0,0.2)',
		'padding':'15px',
		'border-radius':'10px',
		'border':'1px solid #cccccc',
		'bottom':'12px',
		'left':'-50px',
		'min-width':'280px'
	})
	.append($('<div>',{class:'pp_content'}));

popup_overlay = new ol.Overlay({
	element: popup_container[0],
	autoPan: true,
	autoPanAnimation: {
		duration: 250
	}
});

マップのクリックイベントハンドラを下記のように書けばポップアップを表示できます。

map.on('click',function(e){
	map.forEachFeatureAtPixel(e.pixel,function(feature,layer){
		var geom = feature.getGeometry();
		var coord = geom.getCoordinates();
		popup_overlay.setPosition(coord);
		var obj = $(popup_overlay.element);
		obj.find('.pp_content').text(feature.getProperties()...);
	})
});

ポップアップを閉じるには下記のようにします。

popup_overlay.setPosition(undefined);

PHP + JavaScript でシステムを組む場合、PHP 側の設定値を JavaScript 側に渡したいときがあります。いくつも方法があり簡単ではありますが、どうもスマートでないやり方になってしまうのでどうにかしたいと思っていました。

代表的な方法は、テンプレートエンジンに埋め込む(JavaScript を埋め込むか、input タグに入れる、class 属性などに突っ込む)、ajax で json を取得させるなどがあります。前者はカッコ悪いの一言。後者は非同期通信になってロード完了するまで、それらの値にアクセスできないなどイマイチです。そこで少し検討してみました。

JavaScript を吐き出す PHP を書いてしまう

JavaScript のコードを吐き出す PHP を書いて、ヘッダーの script タグでロードするという方法です。これならテンプレートが複雑にならないし、ロードのタイミングで悩むこともありません。ただし、PHP 側のコードが汚くなるのが目に見えるので、もう少し検討してみました。

せっかくなので JSON を受け渡しに使いたい

PHP 側で JSON を作り JavaScript 側は ajax で受け取る、この流れ少し変えて JSON データをデコードするだけの JavaScript を書いてロードしてもらう、というふうにします。

  1. PHP で受け渡したいデータを配列などにまとめる
  2. データを JSON にエンコードして base64 エンコードする(エスケープしなくて良くするため)
  3. JavaScript で base64 → JSON の順でデコードするコードに加工する
  4. JavaScript として返却する

このような流れでコードを書けば、PHP、JavaScript でコードが入り乱れることは無くなり非常にすっきりすると思います。簡単なのでコードを例示する必要も無いかと思いますが、一応書いておきます。

PHP 側のコード($data に受け渡すものは準備済みとする)
$json = json_encode($data);
$b64 = base64_encode($json);
$js = "var data=JSON.parse(window.atob('".$b64."'));";
header('Content-Type: text/javascript; charset=UTF-8');
echo $js;
HTML(JavaScript)側のコード
<script type="text/javascript" src="config.js.php"></script>

ネットで探しても、シンプルで使い勝手が良いカレンダー作成関数が無かったので自作してみました。そのまま使えて、余計なコードが入って無くて、カスタマイズもしやすい、そんな関数になっていると思います。

カレンダー作成関数
function build_calendar(mon,cell) {

	var cur = new Date(mon.getTime());
	var lst = new Date(mon.getTime());

	cur.setHours(0,0,0,0);
	cur.setDate(1);
	cur.setDate(-cur.getDay()+1);

	lst.setMonth(mon.getMonth()+1,0);
	lst.setDate(lst.getDate()+7-lst.getDay()-1);

	var tbody = '';

	tbody += '<table>';
	tbody += '<tr><th>日</th><th>月</th><th>火</th><th>水</th><th>木</th><th>金</th><th>土</th></tr>';

	while (cur<=lst) {

		tbody += (cur.getDay()==0 ? '<tr>' : '');
		tbody += '<td>'+cell(cur)+'</td>';
		tbody += (cur.getDay()==6 ? '</tr>' : '');

		cur.setDate(cur.getDate()+1);
	}

	tbody += '</table>';

	return tbody;
}

使い方

第1引数に作成したい月の Date オブジェクト(月内であれば何でも良い)を、第2引数で日付セルのコールバック関数を渡してください。セルをコールバック関数にしたことで、予定を入れたり、月齢を入れたり、自由にカスタマイズができます。

var html = build_calendar(new Date('2019/1/1'),function(cur){
	return cur.getDate();
});
出力結果
303112345
6789101112
13141516171819
20212223242526
272829303112

コールバック関数は引数で Date オブジェクトを渡してくるので、シンプルに getDate() で日付を返せば出力結果のようになります。その Date オブジェクトを別の関数(たとえば月齢計算など)に渡せば、情報をカレンダーに埋め込むことが可能になります。オプションを増やして、デフォルトの表示を付けたり、月内の日付以外は出さないようにしたり、汎用的に拡張してもいいかも知れません。

ロジック

コードを見れば分かる程度のことですが、簡単にロジックを説明しておきます。

  1. 渡された日付からその月の1日を求め、更にその週の日曜日(A)を求める
  2. 渡された日付からその月の末日を求め、更にその週の土曜日(B)を求める
  3. (A)から(B)までループさせながらテーブルタグを構成していく
  4. 各日付の中身は、渡されたコールバック関数を呼び出して任せる

シンプルにするための工夫としては、半端な前月と次月の日付を含めてループさせることで余計なパディングをしていないこと、<tr> タグの入れ方により変数の数を減らした点です。カレンダーのカスタマイズは CSS とコールバック関数でほとんどクリアできると思います。

jQuery UI Dialog で modal: true としたダイアログで、再表示したときに操作不能になるという現象に遭遇しました。 操作不能というのは、ダイアログに配置したセレクトボックスは選択リストが表示されなくなり、テキストボックスはクリックしてもキャレットすら表示されないという状態です。 ただし、フォーカスは入っているらしく Chrome などで動かすと、枠の色が変わるのが分かります。 ちなみに、モーダルでなければそういうことにはなりません。

色々と調べた結果、作りおきのダイアログがあるとダメなことが分かりました。 問題のあったページでも、そのダイアログとは別に、autoOpen: false として作っておいて、必要なときに dialog('open') しているものがありました。 これを、ボタンやリンクがクリックされた際に、都度作るようにしてみたところ、問題が解決しました。 この現象でお悩みの方は、作りおきのダイアログが他に無いか調べてみてください。

OpenLayers をバージョンアップしたところ、jQuery の draggable を適用したフローティング・パネルの動きが変になってしまいました。ゆっくりと動かして、ドラッグハンドルをマウスが外れなければ正しく動きますが、速く(といっても普通に)動かして、ドラッグハンドルをマウスが少しでも外れると、ドラッグが解除されたり、おかしな動作が始まります。マウスダウンからアップまで、ハンドリングできない状態になり、低スペックマシンではかなり操作しづらくなります。

なかなか原因が分からなくて、旧バージョンにロールバックしようかと思いましたが、分かってしまえば至って単純な原因でした。OpenLayers の新しいバージョンは、fallThrouth というオプションがデフォルトで false になっています。これを true にするだけでした。

	map = new OpenLayers.Map({
		fallThrough:       true
	});

FormData があると Ajax でファイルアップロードできるようになって大変便利なのですが、困るのが IE8 と IE9 の対応です。 IE10 からは FormData が使えますが、未だに IE8 の要求は来ます。 かつての IE6 のポジションを受け継いでいる臭が漂っています。

諦めて POST で画面リロードするところでしたが、ちゃんと調べて見ると代替策がありました。 使うのは POST ですが、見せ方としては非同期通信っぽく動きます。 サンプルコードは載せませんが、要点を箇条書きにしますので、参考にしてください。

  • 親ページに iframe を配置して見えないようにします(display:none だと Safari でまずいみたいなので、横幅&縦幅をゼロにします)
  • フォームのターゲット要素(target="<iframe名>")で、配置した iframe を指定します
  • サーバー側で POST を受け取ったら、JavaScript で親ページの関数を呼んで通知するようにします
  • 親ページでは関数が呼ばれたら、アップロード完了などのメッセージを提供できます

最近のブラウザは、showModalDialog で開いたウィンドウ内の遷移は普通に行えます。 しかし、IE8 では新しいウィンドウが開いてしまします。 ググると、iframe を入れて面倒なことして回避している輩もおられるようですが、そんなことしたくないですよね。 いろいろと調べてみると、海外のサイトに解決方法を見つけました。

IE8 はどうやらベースとなるターゲットが親ウィンドウかなんかに指定された状態で、ダイアログがオープンされているようです。 html のヘッダー部分に、ベース・ターゲットを自分自身にするよう指定すれば良いみたいです。

ベースターゲットの指定
<base target="_self" />
12