コンピュータクワガタ

かっぱのかっぱによるコンピュータ関連のサイトです

jQuery.trimで使っているJavaScriptの\sに全角スペースを含むかどうか。

コアjQuery+プラグイン/jQuery UI 開発実践技法 (Programmer’s SELECTION)

コアjQuery+プラグイン/jQuery UI 開発実践技法 (Programmer’s SELECTION)

を読んでいて、jQuery.trimに言及があったのですが全角スペースがどうなるかについて言及がなかったので調べてみました。

jQueryのtrimでは、正規表現で「\s\uFEFF\xA0」をtrimしています。FEFFやA0はともかく\sはブラウザにより(というよりIEのバージョンにより)、全角スペースが対象になるかどうかが違っていました。

確認に使用したコードは以下です。


gist9376145

結果としては、IE8までは\sに全角スペースは含みません、IE9以降は\sに全角スペースを含みます。
その他のモダンブラウザの最新版(ChromeFirefoxSafari)は\sに全角スペースを含みます。

IE9以降のみを対象とする環境では動作は変わりませんが、IE8以前を考慮しないといけない環境では動作が変わるので注意が必要です。

時刻の入力補助をするjQueryプラグイン

あけました、おめでとうございました。
今年もよろしくお願い致します。

さて、新年一発目はJavaScriptによる日付の入力補助プラグインの紹介です。
jQuery UIには日付を選択するdatepickerプラグインはありますが、時刻を選択するコンポーネントはありません。

Web画面において、時刻の入力は結構面倒なものです。最近のAndroidGoogleカレンダーの予定の登録では、以下のようなコンポーネントが使われています。

f:id:kuwalab:20140101084449p:plain

この画面はスマホやタブレットのタッチ可能な端末だとすごく選択がしやすいのですが、同じようなUIをPC上で作っても操作性がよくありません。

そこで、色々と探してみた中で個人的に一番使いやすかったjQuery UI Timepickerを紹介します。

紹介と言っても以下のページを見てくださいという程度のものです。

http://kuwalab.github.io/jQuery/Timepicker/index.html

datepickerとほとんど同じように使用でき、国際化もされていますので日本語で表示もできます。

実際に使ってもらうとわかるのですが、時間と分を両方選択すると自動でポップアップが閉じるのもいい感じです。

ということで、Timepickerでした。

jQueryクックブック

jQueryクックブック

LABUtilにしました

LABjsを使った動的で同期的なscriptの読み込み - コンピュータクワガタ

昨日の今日ですが、LAB_sampleという名前とサンプルそのものを変更しないといけない使い勝手があまりにも手抜きだったのです、LABUtilとして、ライブラリー部分とそうでない部分を分離しました。

https://github.com/kuwalab/LABUtil

LABのwaitでソースをすべて読み込むため、かなり遅いですがライブラリーの管理の手間と、あとから共通してライブラリーを入れ込めるメリットも大きいと思います。

LABjsを使った動的で同期的なscriptの読み込み

HTMLプロトタイプを作成する際にJavaScriptでちょっとしたユーティリティを使うことがあります。

普通はjQuery等のライブラリーを用いるのですが、読み込むライブラリーのバージョンを統一したいことが多いです。 また、同じようなscriptの読み込みを何箇所も書くのは面白くありません。

個人的に、キャッシュの意味もありJavaScriptのライブラリーにはバージョン番号をつけて管理することが多いです。 バージョン番号を記載することで、いざ違うバージョンのライブラリーを導入した場合には、すべての箇所の修正が必要になります。

最初は自作でJavaScriptの動的ロードを考えていましたが、LABjsという優秀なライブラリーを見つけたので、それを用いた共通のライブラリーのロードの仕組みを作成しました。

https://github.com/kuwalab/LAB_sample

これで少しは、作業が楽になる感じです。

CoffeeScriptのclassでprivateなメソッド等を作る

自分のメモ用に、張っておく。
要するに、classの中に、オブジェクトのプロパティでなくてクロージャとして閉じ込めるということ。

class Test
  # private
  internal = 0

  # private method
  reset = () ->
    internal = 0

  inc: ->
    internal++
  view: ->
    alert "#{internal}"
  newView: ->
    reset()
    @view()

test = new Test()
test2 = new Test()
test.view()
test2.view()
test.inc()
test.view()
test2.view()
test.newView()

ServletとjQueryによる非同期ファイルアップロード

最近色々なところで見かける、ファイルをアップロードする際にプログレスバーを表示する方法を色々調べてみました。
以前調べていた時には、インラインフレームへのサブミットとCommons FileUploadを組み合わせて行う方法しか見つからなかったのですが、今はJavaScript側での仕組みが整って来ました。
ちなみにChromeFirefoxの最新版でしか確認していません。IEは動きません。
とりあえずコードです。アップロードを受信するサーブレットはいつもどおり、丸パクリの使いまわしです。この部分はサーブレットでなくてもサーバーサイドでファイルのアップロードを受信できればなんでも構いません。

package net.kuwalab.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

@WebServlet(name = "Upload", urlPatterns = { "/upload" })
@MultipartConfig(fileSizeThreshold = 5000000, maxFileSize = 700 * 1024 * 1024, location = "e:/temp")
public class UploadServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        request.setCharacterEncoding("utf-8");
        Part part = request.getPart("file");
        String name = getFilename(part);
        part.write(name);

        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json");

        PrintWriter pw = response.getWriter();
        pw.println("{\"result\":\"ok\"}");
        pw.flush();
        pw.close();
    }

    private String getFilename(Part part) {
        for (String cd : part.getHeader("Content-Disposition").split(";")) {
            if (cd.trim().startsWith("filename")) {
                return cd.substring(cd.indexOf('=') + 1).trim()
                        .replace("\"", "");
            }
        }

        return null;
    }
}

重要なアップロードするHTMLです。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>トップページ</title>
 </head>
 <body>
  <input type="file" id="file"><input type="button" id="stop" value="停止">
  <div id="fileInfo"></div>
  <div id="ajaxStatus"></div>
  <div id="status"></div>
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
  <script type="text/javascript">
(function() {
    var ajax; // 途中で停止する場合に必要。
    var ajaxSettings = {
        xhr: function() {
            var xhr = new window.XMLHttpRequest();
            // アップロードの進捗状況を取得
            xhr.upload.addEventListener("progress", function(event) {
                if (event.lengthComputable) {
                    $('#status').html(parseInt(parseInt((event.loaded / event.total) * 100)) + '%');
                }
            }, false);
            // 中止の際に何かしたい場合
            xhr.addEventListener("abort", function(event) {
            }, false);
            
            return xhr;
        },
        type: 'post',
        url: 'upload',
        processData: false,
        contentType: false, // 送信するデータをFormDataにする場合、こうしないといけない。
        cache: false,
        dataType: 'json',
        success: function(data) {
            if (data && data.result == 'ok') {
                $('#ajaxStatus').html('アップロード完了');
            } else {
                $('#ajaxStatus').html('アップロード失敗');
            }
            ajax = null;
        },
        error: function(xhr, textStatus, errorThrown) {
            alert('エラー\n' + textStatus);
            ajax = null;
        }
    };
    
    // ファイルが選択されたらアップロードする。
    $('#file').bind('change', onChangeFile);

    function onChangeFile() {
        // ファイルの情報を取得
        var uploadFile = $('#file')[0].files[0];
        $('#fileInfo').html(uploadFile.name + ' サイズ:' + uploadFile.size);
        var formData = new FormData();
        formData.append("file", uploadFile);
        ajaxSettings.data = formData;

        ajax = $.ajax(ajaxSettings);
    }

    $('#stop').click(function() {
        if (ajax) {
            // abortで送信を停止。
            ajax.abort();
        }
    });
}());
  </script>
 </body>
</html>

FormData

非同期でmultipartリクエストするためにはFormDataオブジェクトを利用する必要があります。送信するファイルデータは以下のようにFormDataオブジェクトにappendして設定します。

var formData = new FormData();
formData.append("file", uploadFile);
ajaxSettings.data = formData;

contentType

jQueryで$.ajaxするパラメータも非常に重要です。まず重要なのがcontentTypeパラメータで、falseにする必要があります。
ajaxのリクエストがFormDataの場合には適切なcontentType(multipart/form-data; ○○ boundary=△△)になるため、contentTypeをfalseにしないといけません。false以外の場合には設定を上書きしてしまい、送信がうまくいきません。このあたりは仕様に記載されています。

アップロードの進捗状況

送信状況を取得するためには、$.ajaxのパラメータのxhrを設定する必要があります。ここでは生のXMLHttpRequestオブジェクトを操作してxhr.uploadオブジェクトのprogressイベントを処理します。progressイベントは定期的に呼び出されますのでそのタイミングで進捗状況を処理します。

アップロードを途中で止める

最後にアップロードを途中で止める方法です。$.ajaxの戻り値でXMLHttpRequestが返されますので、そのメソッドabortを呼び出します。

うん。簡単。

JavaScriptのthisをjQueryを含めて検証してみた

JavaScriptのthisは使用するタイミングによって、その内容が変わります。JavaScriptを記述する上でthisは基本的な要素であり、かつ理解が必須の要素でもあります。自分自身があまり整理できていなかったこともありましたので、確認の意味も込めて検証して見ました。

さて、thisそのものに注目するとthisはJavaScriptにおいてキーワードであって変数ではありません。そのため、thisの内容を参照することはできますが、その値を任意に変更することはできません。

今回はJavaScriptそのもののthisが参照する値がどうなるのか、またjQueryにおいても色々と変化するためそのパターンをひと通り検証し、整理します。

プレーンなJavaScriptの場合

最初にjQueryを考慮せずにプレーンなJavaScriptのthisを確認します。

単純にthisを照会するとwindowオブジェクトであることがわかります。以下のサンプルでは this === window はtrueになります。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <script type="text/javascript">
console.log('this === window : ' + (this === window));
  </script>
 </body>
</html>

次に関数呼び出しの例を確認します。関数の中のthisもwindowオブジェクトを参照している事がわかります。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <script type="text/javascript">
// 関数の例
function func() {
    console.log('# func() function');
    console.log('this === window : ' + (this === window));
}

console.log('### 関数');
func();
  </script>
 </body>
</html>
コンストラクタ関数

ここまでのthisはすべてwindowオブジェクトを参照してましたが、ここからが違ってきます。

以下の例ではコンストラクタ関数内でのthisの動作を検証しています。コンストラクタ関数をnew演算子を使って呼び出すと、空のオブジェクトが生成されそれがthisとして設定されます。ここで重要なのはコンストラクタ関数(と言ってもそういう用途として作成した関数)はnew演算子を使用しないとthisにオブジェクトが設定されないということです。以下の例では同じ関数をnew演算子を用いた呼び出しと、通常の関数呼び出しを検証しています。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <script type="text/javascript">
// コンストラクタ関数の例
function Point(x, y) {
    this.x = x;
    this.y = y;
    console.log('# Point() function');
    console.log('this === window : ' + (this === window));
    console.log('this instanceof Point : ' + (this instanceof Point));
}

Point.prototype.print = function() {
    console.log('Point(this.x,this.y)=(' + this.x + ',' + this.y + ')');
}

console.log('### コンストラクタ関数');
console.log('## newする');
var point = new Point(1, 2);
point.print();
console.log("window.hasOwnProperty('x')=" + window.hasOwnProperty('x'));
console.log('\n');

console.log('## newしない');
Point(3, 4);
console.log("window.hasOwnProperty('x')=" + window.hasOwnProperty('x'));
  </script>
 </body>
</html>

実行すると以下のようなログが出力されます。newした場合にはwindowオブジェクトではなくPointインスタンスであることがわかります。

### コンストラクタ関数
## newする
# Point() function
this === window : false
this instanceof Point : true
Point(this.x,this.y)=(1,2)
window.hasOwnProperty('x')=false

## newしない
# Point() function
this === window : true
this instanceof Point : false
window.hasOwnProperty('x')=true

具体的に確認してはいませんが、新しいブラウザではJavaScriptをstrictモードで実行することができます。strictモードの場合、newを付けないコンストラクタ関数の呼び出しではthisキーワードは未定義値になります。

そのためnewを付けないコンストラクタ関数の呼び出しを抑止できないことはありませんが、古い環境との共存を考慮した場合には完全な抑止は難しくなります。

strictモードについては、https://developer.mozilla.org/ja/JavaScript/Strict_modeを確認してください。

イベント

イベントハンドラで関数を指定すると、その関数内のthisではそのイベントの発生元となった要素がセットされます。また、eventオブジェクトのcurrentTargetプロパティにも呼び出し元の要素がセットされます。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <input type="button" id="button1" value="ボタン">
  <script type="text/javascript">
// イベントの例
console.log('### イベント');
document.getElementById('button1').onclick = function(event) {
    console.log('## button1#click');
    console.log('# this');
    console.log('this=' + this.tagName);
    console.log('id=' + this.getAttribute('id'));
    console.log('this instanceof HTMLElement : ' + (this instanceof HTMLElement));
    console.log('this.constructor=' + this.constructor);

    // event.currentTarget
    console.log('# event.currentTarget');
    console.log('event.currentTarget=' + event.currentTarget.tagName);
    console.log('event.currentTarget.id=' + event.currentTarget.getAttribute('id'));
};
  </script>
 </body>
</html>

実行すると以下のようなログが出力されます。thisもevent.currentTargetでも同じイベントの対象となる要素が取得できていることが確認できます。

### イベント
## button1#click
# this
this=INPUT
id=button1
this instanceof HTMLElement : true
this.constructor=function HTMLInputElement() { [native code] }
# event.currentTarget
event.currentTarget=INPUT
event.currentTarget.id=button1

jQueryの場合

jQueryを使用した場合も基本的には、上記のように動作するがjQuery固有の注意すべき点がいくつかあります。

each

jQueryオブジェクトをeachメソッドでループさせた場合のthisは、対象の(jQueryオブジェクトではない)DOMオブジェクトとなります。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <div id="title">JavaScript this テスト</div>
  <div id="contents">jQueryのテスト</div>
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
  <script type="text/javascript">
// eachのthis
console.log('### eachのthis');
$('div').each(function(i) {
    console.log('# div(' + i + ')=' + $(this).html());
    console.log('this === window : ' + (this === window));
    console.log('this.tagName=' + this.tagName);
    console.log('this.id=' + this.getAttribute('id'));
    console.log('this instanceof HTMLElement : ' + (this instanceof HTMLElement));
    console.log('this.constructor=' + this.constructor);
    console.log("this.hasOwnProperty('length')=" + this.hasOwnProperty('length'));
    console.log("$(this).hasOwnProperty('length')=" + $(this).hasOwnProperty('length'));
    console.log('\n');
});

console.log('### jQueryオブジェクト');
console.log("$('div') instanceof jQuery.fn.init : " + ($('div') instanceof jQuery.fn.init));
console.log("$('div').constructor=" + $('div').constructor);
  </script>
 </body>
</html>

実行後の出力は以下のようになります。

### eachのthis
# div(0)=JavaScript this テスト
this === window : false
this.tagName=DIV
this.id=title
this instanceof HTMLElement : true
this.constructor=function HTMLDivElement() { [native code] }
this.hasOwnProperty('length')=false
$(this).hasOwnProperty('length')=true

# div(1)=jQueryのテスト
this === window : false
this.tagName=DIV
this.id=contents
this instanceof HTMLElement : true
this.constructor=function HTMLDivElement() { [native code] }
this.hasOwnProperty('length')=false
$(this).hasOwnProperty('length')=true

### jQueryオブジェクト
$('div') instanceof jQuery.fn.init : true
$('div').constructor=function (a,b){return new e.fn.init(a,b,h)}

イベント

jQueryでも通常のJavaScriptと同様にイベント発生元のDOMオブジェクトがthisに設定されあます。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <input type="button" id="button1" value="ボタン">
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
  <script type="text/javascript">
// イベントのthis
$('#button1').click(function() {
    console.log('### イベントのthis');
    console.log('# click')
    console.log('this === window : ' + (this === window));
    console.log('this=' + this.tagName);
    console.log("this.hasOwnProperty('length')=" + this.hasOwnProperty('length'));
    console.log('id=' + this.getAttribute('id'));
});
  </script>
 </body>
</html>

実行結果は以下のようになります。

### イベントのthis
# click
this === window : false
this=INPUT
this.hasOwnProperty('length')=false
id=button1

each内部での関数呼び出し

each内部でそこで定義した関数を呼び出した場合には、その関数内部ではthisはwindowオブジェクトになります。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <div id="title">JavaScript this テスト</div>
  <div id="contents">jQueryのテスト</div>
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
  <script type="text/javascript">
// eachと関数の組み合わせ
console.log('### 関数のテスト');
$('div').each(function(i) {
    console.log('div(' + i + ')=' + $(this).html());
    (function() {
        console.log('inner function i=' + i);
        console.log('div(' + i + ')=' + $(this).html());
        console.log('this === window : ' + (this === window));
        console.log('\n');
    })();
});
  </script>
 </body>
</html>

実行結果は以下のようになります。

### 関数のテスト
div(0)=JavaScript this テスト
inner function i=0
div(0)=null
this === window : true

div(1)=jQueryのテスト
inner function i=1
div(1)=null
this === window : true

このあたりはスコープチェーンを理解していればさほど難しくはありません。スコープチェーンについては、ハイパフォーマンスJavaScriptのp.16のスコープの管理がわかりやすいと思います。

まとめ

JavaScriptは定番といえる本がオライリーから出ているので、それを読めばこのあたりはパーフェクトです。具体的には以下の3冊がおすすめです。

通称サイ本。これが難しいのであれば、初めてのJavaScript 第2版から入るといいかと思います。

JavaScript 第5版

JavaScript 第5版


サイ本を読んだ後に、これを読むと目からウロコというか、JavaScriptの作法がよくわかります。絶対読むべき本だと思います。
JavaScriptパターン ―優れたアプリケーションのための作法

JavaScriptパターン ―優れたアプリケーションのための作法


jQueryのバージョンとしては古いのですが、jQuery全般について例を交えてよく書かれています。end()メソッドの使い方とか非常に有効です。
jQueryクックブック

jQueryクックブック