タグ別アーカイブ: JavaScript

File API FileReaderを使った画像のドラッグ&ドロップアップロード

HTML5で新たに搭載されたAPIを利用して、WordPressのメディアアップロード画面のようなドラッグ&ドロップでの画像アップロード機能を実現します。File APIのFileReaderとData URLを使ったAjax POSTの手順は、以前OnlyCSViewを作成したエントリを参考にしていただくとして、今回はさらにドラッグ&ドロップAPIを使ったローカルファイルのセットと、PHP側での受け取ったData URLの保存方法を説明します。

STEP0:HTML5におけるページ内要素のドラッグ&ドロップ

HTML5では、ブラウザ上でのネイティブのドラッグ&ドロップを実現させるための属性として、draggableおよびdropzoneが用意されました。理想的には、draggable=”true”と指定されたページ上の要素をぐぐっと掴んで、dropzone属性の指定がされた要素の上に移動できるという動作の実現が期待されているのですが、dropzone属性に対応しているブラウザがほぼ無いため、ドロップ側の挙動はJavaScriptで実装するしかありません。とりあえずJavaScriptは置いておいて、属性指定だけした動作サンプルを見てみましょう。

draggable=”true”を指定した要素
(対応ブラウザならぐぐっと掴める)
dropzone=”move”を指定した要素
(対応ブラウザならドラッグした要素を
ドロップで受け付ける)

これがページ内要素のドラッグ&ドロップです。おそらく動作していないでしょう(笑)。ちなみにdropzone属性の値は、ドラッグ中の要素を移動させる”move”以外にも、複製する”copy”、ドラッグされた要素のリンクを取得する”link”などがあります。絵に描いた餅ですね。

STEP1:ローカルファイルのドラッグ&ドロップ

今回作成するプログラムのように、ローカルファイルをドラッグ&ドロップする場合には、ドラッグ側にdraggable属性を設定しなくてもOS標準機能でドラッグができています。ただし、通常ブラウザ上にファイルをドロップしたときの処理は、例えば画像ファイルやテキストファイルであればウィンドウ全体を使ったプレビュー表示になりますので、この標準処理をドロップ側でキャンセルし、別の処理をイベントに紐づける必要があります。もちろんJavaScriptを使うことになります。

STEP2:ドロップを受け付ける要素を用意してAjaxイベント付加

では、ドロップを受け付ける要素にdropzone属性の代わりとなるイベントをつけましょう。

//jQueryのCDNでのインポート。ヴァージョンはあまり気にせず。
<script src="http://code.jquery.com/jquery-1.7.2.min.js"></script>
.
.
<script type="text/javascript">
$(function(){
//jQueryに無いイベントdataTransferを付加
$.event.props.push("dataTransfer");
 
//ドロップを受け付ける領域は、適当にclass名をdropzoneとしておく。
var dropzone = $(".dropzone");
 
//FileReaderオブジェクトが無いとそもそも動作しない
//非対応ブラウザではdropzoneをそもそも表示しない心遣い(必須ではありません)
if(!window.FileReader){
          dropzone.hide();
          return false;
        } else {
//心遣いしないのなら、本処理はここから
		dropzone.bind("dragover",function(event){
			event.preventDefault();
			event.stopPropagation();
			return false;
		});
//領域にファイルが入ったら、灰色にハイライトさせる心遣い(必須ではない)
		dropzone.bind("dragenter",function(event){			
			$(this).css("background-color","#CCC");
			return false;
		});
		dropzone.bind("dragleave",function(event){
			$(this).css("background-color","#FFF");
			return false;
		});
//心遣い終わり
 
//本処理
		 var handleDroppedFile = function(event){
			 var thisarea = $(this);
          var file = event.originalEvent.dataTransfer.files[0];
		  var type = file.type;
          var fileReader = new FileReader();
		  fileReader.readAsDataURL(file);
          fileReader.onload = function(event){
 
//心遣い担当
            thisarea.css("background-color","#FFF");
//終わり
			$.ajax({
				type : "POST",
				url : "dropupload.php",
				data: {"type" : type,
"data" : event.target.result},
				success : function(data){
//成功処理を書くならここに。"成功しました"と表示させるとか			
				},
				dataType : "json"
			});
		  }
		  fileReader.onerror = function(stuff){
//心遣い担当
            thisarea.css("background-color","#FFF");
//終わり
		  }
		  event.preventDefault();
		  event.stopPropagation();
          return false;
        }
		   dropzone.bind("drop", handleDroppedFile);
		}
});
</script>

dropzoneには$.bindでもってイベントを結びつけます。ここでは”dragover”,”dragenter”,”dragleave”,”drop”の4種類が出てきますが、領域にファイルが重なったときに処理を行わないのなら、”dragover”と”drop”だけで良いでしょう。dragoverでは、ファイルをブラウザのウィンドウ上にドラッグしたときの標準処理をpreventDefaultでキャンセルし、stopPropagationでイベントが下位要素にも伝わってしまうのを止めます。ちなみに、IEの場合下から上にイベントが伝わっていくらしく、またメソッドではなくプロパティで指定する(event.returnValue = false;/event.cancelBubble = true;)ようです。IE10ではFileReaderがサポートされるみたいなので、対応の手間が増えますね。

fileReaderからreadAsDataURLでファイルをDataURLにする辺りは、以前OnlyCSViewを作成したエントリを参考。今回は$.postではなく$.ajaxを使っているので注意です。

STEP3:PHP側でアップロードの実装

PHP側では、Data URLにされた画像の解凍に一手間が要ります。

//dropupload.php
 
<?php
$filetype = htmlentities($_POST["type"], ENT_QUOTES, "UTF-8");
if(preg_match("/jpeg/",$filetype)){
$filesuffix = ".jpg";
$mimeprefix = "data:image/jpeg;base64,";
} else if(preg_match("/gif/",$filetype)){
$filesuffix = ".gif";
$mimeprefix = "data:image/gif;base64,";
} else if(preg_match("/png/",$filetype)){
$filesuffix = ".png";
$mimeprefix = "data:image/png;base64,";
}
 
//とりあえず一意の名前になってほしいので、日付時刻をコピー先ファイル名にする。
 
$filename = date("YmdHis").$filesuffix;
 
//まあ、仮にアップロード画像を格納するディレクトリがuploads/だったとしたら
$filepathname = "uploads/".$filename;
 
file_put_contents($filepathname,base64_decode(str_replace($mimeprefix, '', $_POST["data"])));
?>

エラーのコールバック処理を入れなければこういう感じです。
base64_decodeという関数でData URLのbase64エンコードを解凍するのですが、このときURLのファイルタイプ部分を取り除いてやらないと、PHP側でうまくコピーできないというのがキモですよ。


WordPressでプラグイン無しでThickBoxを使う方法

WordPressの比較的よく知られた隠し機能として、画像をクリックしたときにフロートウィンドウを出し拡大表示をするThickBoxを、プラグインなしで実現できるというものがあります。方法はまあまあ簡単で、<head>タグに囲まれた部分で、<?php wp_head(); ?>が呼ばれるより前に<?php add_thickbox(); ?>を呼んでおく。すると投稿中の画像の<a>タグにclass=”thickbox”を指定してやる事で、画像クリック→フロート拡大表示の機能が実現します。

ThickBox導入手順1

ThickBox導入手順1

ThickBox導入手順2

ThickBox導入手順2

LightBoxという呼び名が一般的な機能かもしれませんが、何故LightBoxと呼ばれているかというと、この機能を広く知らしめたのがLightBoxというプログラムだったからで、繰出鉛筆をシャープペンシルと呼ぶのに似ています。
標準でこの機能が有効化されていない理由は、ThickBox自体がディスコンになってしまっているためとのことです。WordPressの最近のアップデートが、プログラムのスリム化とそれに伴うサポート終了という傾向であるため、この機能もいずれはなくなってしまい、プラグイン導入が推奨されるようになるかもしれません。


Internet Explorerのみscriptが動かない→console.logの置き忘れを疑え

JavaScriptの便利なデバッグ手段、console.logについて以前のエントリで紹介しました。この命令は特にWebKitブラウザやFirefoxとの相性が良く、変数の中身の確認のためついつい頼りがちです。しかしながら、作成完了したページにこの命令を残したままにしておき、いざクライアントのPCでチェックを行うと、scriptが全く動作せずに恥をかく危険性があります。というのも、Internet Explorerはこの命令をデフォルトでは解釈せず、scriptを止めてしまうからです。

Internet Explorerの場合には、F12で起動する開発ツールが有効になっている場合に初めてconsoleオブジェクトが作成されます。したがって原因がconsoleオブジェクトにあることに気付かなければ、検証のため開発ツールウィンドウを開いた際には作動ということで、症状の発生が不規則→通信の問題ではないか、などとどんどん疑いの対象が広がり収拾がつかなくなります。実は私がこの問題に嵌ったときには、問題の発生したページがAjaxをフルに使ったものであったため、原因の所在が非同期通信の実行順序かIEのJavaScript実行速度の問題かなどと、いつもながらの遠回りに時間を取られてしまいました。

そこで教訓ですが、Internet Explorerだけ動かないという場合には真っ先にconsole.logを疑う。以前のエントリで挙げましたが、Firefoxだけ動かない時には宣言部、ヘッダ部を疑う。とりあえずこのように記憶しておきましょう。


さらっと理解しておきたいjQueryオブジェクト(後編)

前編からひっぱります。

JavaScriptのオブジェクトについて、組み込みオブジェクト、ブラウザオブジェクト、DOMオブジェクトの三項目について説明しましたが、これらのオブジェクトではピリオドで何個もの処理を繋ぐメソッドチェーンの書式が使えません。なぜならメソッドが返す返り値が文字列や数値などバラバラな型であるからで、それは手続き型プログラミング言語のメソッドとして何ら不思議なことではありません。

ただ、HTML文書の要素を対象とした操作をする場合、一度処理を行った要素に連続して処理を与えるというケースが非常に多く、その度に要素の検索を行うのがかなりのオーバーヘッドとなります。そこで、メソッドの結果がまたオブジェクトを参照することで連続して処理を与えることのできるメソッドチェーンの実現に意味があるのです。

jQueryの要素の多くは、自身jQueryオブジェクトである上に、処理の返り値としてもjQueryオブジェクトを返します。ではjQueryオブジェクトとは何かというと、セレクタにより検索、ヒットしたDOM要素の集合を、多数の便利なメソッドの定義されたオブジェクトでラップしている状態です。console.logなどでオブジェクトの中身を確認してみると、単なる文字列として格納されたHTML要素と、ずらりと並んだプロパティ、メソッドがあることがわかります。これらのセットがjQueryオブジェクトです。

メソッドチェーンの終端で、jQueryオブジェクトのメソッドでない、通常のDOMオブジェクトのメソッドを使いたい場合はどうすれば良いでしょうか。たとえば前編で出した動かないプログラムの例ですが、jQueryオブジェクトの正体を知っていれば、このようなアクセスが可能であると予想が立ちますね。

$(".fadeinout")[0].innerHTML = "Hi!";

このように、呪文のようなjQueryと言えども仕組みがわかれば何ということも無いのです。これはjQueryのセレクタエンジンにも言えることなのですが、それはまたいずれかの機会に記事にします。


さらっと理解しておきたいjQueryオブジェクト(前編)

CSSコーダにとって、サイトを動的にしたい時の救世主となるjQuery。CSSセレクタで要素を選択し、あとはメソッドの書式に従っていくつかの呪文を唱えるだけで、簡単にアニメーションや文書構造の変更ができます。たとえば、サイト訪問時にclass=”fadein”とした全要素をフェードインする場合、以下のようなソースで事足ります。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja" dir="ltr">
<head>
//meta宣言とtitleは省略
//jQueryのインポート(Google CDN)
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script>
$(function(){
$(".fadein").fadeIn();
//要素がフェードイン
});
</script>
</head>
//省略

もちろん$(“#main ul li”)のような指定にも対応していますので、解説サイトのサンプルをコピーして、セレクタ部分を変更するだけで好きな要素に処理を適用できます。
また、同じ要素に対して複数の処理を連続して適用する場合には、処理を順番にピリオドで繋いでいけばよいという特性も直感的です。メソッドチェーンと呼ばれるこの書き方を頭に入れておけば、JavaScriptについてほとんど知識が無くても、処理部分に手を加えることができます。

$(".fadeinout").fadeIn().fadeOut();
//フェードイン後フェードアウト

こうした直感的な文法をもつjQueryですが、本当に直感のままの記述を行っていると、ときどき反逆します(笑)。メソッドチェーンが有効にならず、途方に暮れてしまいます。たとえば次のような例です。

$(".fadeinout").attr("class").appendTo("#classname");
//クラス名を取り出し、classnameというidの領域に表示したい
//動きません

あるいは、JavaScriptについて少し知識のある人間が、jQueryもJavaScriptだからとチャンポンな書き方をして反逆されることもあります。

$(".fadeinout").innerHTML = "Hi!";
//動きません

何故こういったことが起こるのか、結論から言いますと、jQueryは独自メソッドをもつjQueryオブジェクトというオブジェクト形式を扱う為で、メソッドチェーンなどもメソッドの返り値がこのjQueryオブジェクトである為に実現しているのです。

jQueryオブジェクトを知る為に、JavaScriptのオブジェクトの種類について整理してみましょう。

組み込みオブジェクト 〜素のJavaScriptが持つオブジェクト

JavaScriptというと、WEBページを動的にする手段としてWEBと切っても切り離せない言語であると思われがちですが、実際のところ素のJavaScriptがそういった機能を有しているわけではありません。ブラウザ以外のJavaScript実行環境で、document.writeIn()などと命令しても、JavaScriptはそれがどういう命令なのか理解する事ができません。
それでは素のJavaScriptはどのような要求に応えるのかというと、数値や文字列の演算であったり、メモリへのアクセスであったりと、プログラミング言語の基本的な処理です。
しかしながら、素のJavaScriptにもオブジェクトやメソッドといった概念があり、既に組み込み済みのオブジェクトもあります。たとえばStringオブジェクトは文字列を操作するメソッドを提供するオブジェクトで、このオブジェクトは特に宣言をしなくても、文字列データに対して自動的に呼び出すことができるようになっています。

var movie = "演歌の花道";
var titlelength = movie.length;
//titlelengthの値は5

この例では変数movieに入った文字列をStringオブジェクトで自動的にラップしているため、Stringオブジェクトのlengthメソッドを使う事ができています。他にも組み込みオブジェクトとして、NumberやArrayなどがあります。

ブラウザオブジェクト 〜ブラウザメーカーによる勝手実装のオブジェクト

JavaScriptがWEBページを処理の対象として認識できるのは、WEBブラウザがJavaScriptのオブジェクトとしてページを定義し、処理の為のメソッドを提供しているからです。具体的にはページはWindowオブジェクトとして定義され、Windowオブジェクトの直下にdocument、location、navigatorなどのプロパティがあり、これらもまたオブジェクトとして定義され、メソッドが提供されているといった構造です。そして基本的にこのWindowオブジェクトは省略して書く事ができるため、おなじみのdocument.writeIn()という表記になっているわけです。
ちなみにWindowオブジェクト自体のメソッドとしては、setTimeOutなどのタイマー系やalertなどのウィンドウ表示系があります。これらは確かにdocument.alert()のようには書きませんね。

documentオブジェクトは下位にforms、images、anchorsといったオブジェクトを持ち、これらのオブジェクトはページ中のフォーム要素、イメージ、アンカー全てを配列に格納してくれているので、name属性やインデックスで個々の要素を取り出す事ができます。

var formvalue = document.フォーム名.要素名.value;
var formvalue = document.forms[インデックス].elements[インデックス].value;
var imagesource = document.イメージ名.src;

このように基本的なオブジェクトモデルをベンダーが定義してくれているおかげで、JavaScriptがWEB制作の言語として特待的なポジションを得ているわけです。ただ、ブラウザオブジェクトはIEとNetscapeのブラウザ戦争の過程で、独自仕様を実装しよう(そして囲い込みを行おう)という目論見のもとにルール無用に拡張されていった背景があります。そこで、そうした勝手実装でなくどのブラウザでも共通した記述が可能になるようなオブジェクトモデルが求められるに至りました。

DOMオブジェクト 〜XMLの汎用的な操作モデルを導入したオブジェクト

DOMとはDocument Object Modelの略で、HTMLの親戚であるXMLを操作するための汎用モデルです。ブラウザオブジェクトの勝手実装による混乱に収拾をつけるため、W3Cはこのモデルをベースにしたオブジェクトやメソッドを定義し、勧告としました。
DOMオブジェクトは文書中のhtmlタグとその属性、テキストなどに対応し、メソッドやプロパティを持ちます。また各タグへのアクセス方法として、タグ名、id、クラス名での検索メソッドを提供しています。これらはブラウザオブジェクトのdocumentオブジェクトをhtml文書の最上位の要素<html>の参照というかたちで実装されているので、これまでのブラウザオブジェクトのスクリプトと併存して使う事ができます。

document.getElementById("target").value = 1;
//idをもとに要素をオブジェクトとして取得するgetElementByIdメソッド
 
var element = document.lastChild;
//要素の親子関係(子要素を内包する親要素)をもとに要素を取得
 
element.setAttribute("id","idname");
//要素に属性を付加

今回はjQueryオブジェクトの説明まで辿り着かなかったので、次回に続きます。


Safari、Chromeで動くJavaScriptがFirefoxで動かない→宣言部、ヘッダ部を疑いましょう

JavaScriptが満載のWEBページを制作していて、ページの一部の機能がSafari、Chromeでチェックした際には動作するのに、Firefoxでは全く動作しないという状況に遭遇しました。
具体的には、ナビゲーションボタンのマウスホバー時の挙動として、透明度の指定をjQueryで行った箇所と、画像のLightBox的挙動のためjQuery.lightboxを使った箇所です。

マウスホバーの部分については、ホバー状態が終了する際に、FirebugのコンソールにremoveAttribute is not a functionというエラーが表示されます。lightboxのエラーは、画像が拡大表示された後ページに戻る事ができず背景が暗いままという不具合で、こちらは特にコンソールに表示はありません。

これはjQueryの動作に問題があるのだろうとあたりをつけ、様々な手段を使ってチェックを行いました。

ホバー部分のコードについては、以下のようなものでした。

<script>
$(function(){
$(".hover").hover(
function(){
$(this).fadeTo(0,0.5);
},function(){
$(this).fadeTo(0,1.0);
});
</script>

hoverメソッドは引数に関数を二つ取り、それぞれホバー時と終了時の処理を書く事ができます。処理の部分で、fadeToではなくcssメソッドを使ってopacityを指定するも動作せず。しかしながら、cssメソッドでwidthなどの他の値を指定する場合は動作します。つまり透明度関係の指定が上手く行っていないということが予想できます。
そこでブラウザのopacityの実装の違いではないかと考え、Firefoxについてのリファレンスを漁ったり、懐かしの-moz-opacityを引っ張り出してきたりと、色々と試みるも挙動が変わる事はありません。
次にプラグイン同士の相性を疑い、抜き差し抜き差しするも効果なし。jQueryのファイル名やヴァージョンの変更を行うも駄目。この時点で既に大量の時間を原因の究明に費やしています。

戯れに同じコードを別の製作中のページに貼付けてみると、ちゃんと動作しました。そのページはXHTMLではないHTML5のページだったので、jQueryやlightboxプラグインのドキュメントをあたり必須要件がHTML5なのかチェック、しかしながら特に指定はありません。

結局不具合の原因が何だったのかというと、

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja" dir="ltr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="Content-Style-Type" content="text/css;" />
<meta http-equiv="Content-Style-Type" content="text/javascript;" />

こんなに単純な事でした。本来Content-Script-Typeでtext/javascriptを指定しなければならないところを、上のcssの指定をコピーしてそのまま、Content-Style-Typeとしていたわけですね。
何故HTML5で無事に動いたのかというと、これらの部分が省略可だからでした。

つまりトラブルを一般化すると、SafariやChromeで動くJavaScriptがFirefoxで動かない場合、間違ったXHTMLの記述に対する、ブラウザの解釈の違いが原因である場合があるということです。
一字一句間違えてはならないDOCTYPE宣言についても、思わぬエラーの原因になりそうなものです。実際標準モードと互換モードでcssの解釈が異なるので、意図せず互換モードが呼び出され、jQueryでの表示不具合に直結している可能性があるのではないでしょうか。

毎度の事ですが、ニッチなエラーに足を取られ易い私の失敗談が、誰かの役に立てば幸いです。


console.logでJavaScriptの変数の中身を見る

JavaScriptのコーディング中、オブジェクトや変数の中身を確認したくなることは多々あります。PHPの場合であればprint_rやvar_dumpで間に合うのですが、JavaScriptの場合にはconsole APIに対応したブラウザで対話式にチェックを行うのが便利なようです。

具体的には、FirefoxにFirebugを搭載した状態もしくはSafari、Chromeなどのwebkitブラウザで開発メニューを有効化した状態にて、webコンソールあるいはwebインスペクタのコンソールの入力エリアに命令を投げ与えます。

console APIのコンソール

console APIのコンソール

変数やオブジェクトの中身を参照したいなら、console.log(対象);という命令を与えエンターを押すと、コンソール画面に表示してくれます。特に便利なのは、引数にjQueryのオブジェクトを与えても表示してくれるという点です。さらにwebkitのブラウザは至れり尽くせりで、jQueryのセレクタでヒットした要素をちゃんと入れ子状にまとめて表示してくれるので、この方法を知っているか知らないかで、jQueryでの開発効率は違ってくるはずです。

コンソールに投げられる命令は結構ありますが、オブジェクトのプロパティをリストアップしてくれるconsole.dir()なども便利そうです。
また、コード中の任意の場所で呼び出して、ブラウザのコンソールに任意の時点での変数の状態を吐き出させることもできるので、通常の標準出力のような使い方も可能です。

なんというか素晴らしい時代ですね。

(追記:IEではconsole.logの仕様に落とし穴があるようなので、こちらのエントリも参考に)