location object と anchor element

JavaScript で Unit Test を書こうとした時、その関数が location object を
処理したり、URL の文字列からホスト名などを取得したい場合、anchor element を生成して location object の代わりに使うという方法が stack overflow にあった。

testem とか使ってテストをする場合、location.href に値を入れると画面遷移してしまうので、別のオブジェクトを用意して使えばよい。

例: URL 文字列からホスト名を取得

var mylocation = document.createElement('a');
mylocation.href = 'http://example.com/path/to/file';

var hostname = mylocation.hostname;

例: アクセスしているURLのパラメータを削除した文字列を返す関数とそのテスト

// Test
describe('callMyFuncTest', function(){
    beforeEach(function(){
        myNS.location = document.createElement('a');
    });

    afterEach(function(){
        myNS.location = window.location;
    });

    it('should return url without query string', function(){
        myNS.location.href = 'http://example.com/path?query=string';

        expect(myNS.removeQueryStr()).toBe('http://example.com/path');
    });
});

// Function
var myNS = {
    location: window.location
};

myNS.removeQueryStr = function(){
    var l = myNS.location;

    return l.protocol+'://'+l.hostname+l.pathname;
}

コードの方で少し工夫する必要はあるけど、一応これでテストできる。

参照: MDN

Advertisements
location object と anchor element

JavaScript で CSV をテンプレートにあわせて 1 行 1 ファイルに変換

例えば以下のような CSV から

title,date,text
Convert CSV into some files,2013-04-12,How to convert each record in CSV to...
Sample script,2013-04-11,Here is the sample script that you want...

以下の様なテンプレートを使って

<html>
<head>
    <title>{{title}}</title>
</head>
<body>
    <h1>{{date}} {{title}}</h1>
    <p>{{text}}</p>
</body>
</html>

以下のようなファイルを生成するスクリプト(各行につき 1 ファイル)を書いた。自分以外に必要とする人がいるか分からないが。。。

<html>
<head>
    <title>Convert CSV into some files</title>
</head>
<body>
    <h1>2013-04-12 Convert CSV into some files</h1>
    <p>How to convert each record in CSV to...</p>
</body>
</html>

Node.js と mu、fast-csv モジュールを使う。

$ npm install mu2
$ npm install fast-csv

convert.js

var mu  = require('mu2');
var csv = require('fast-csv');
var fs  = require('fs');

var jsondata = [];
var contents = [];
var datafile = 'data.csv';
var template = 'template.html';
var outfile  = 'page';

csv(datafile, {headers: true})
.on('data', function(data){
    jsondata.push(data);
})
.on('end', function(){
    for (var i=0, l=jsondata.length; i<l; i++) {
        render(template, jsondata[i], i);
    }   
})
.parse();

function render (template, data, contentIndex) {
    contents[contentIndex] = "";

    mu.compileAndRender(template, data)
    .on('data', function(output){
        contents[contentIndex] += output.toString();
    })
    .on('end', function(){
        fs.writeFileSync(outfile+'_'+contentIndex+'.html', contents[contentIndex]);
    });
}

ちょっと必要になったので作ってみた。そして後で少しいじって使うかもしれないので、メモとして残してみた。

JavaScript で CSV をテンプレートにあわせて 1 行 1 ファイルに変換

TDD で jQuery plugin を作ってみた

指定した element のサイズに合わせて 1 行ごとにフォントサイズを調整する jQuery のプラグインを書いた。結果はサンプルを見た方がわかりやすいと思う。

指定したフォントや、使っている文字とブラウザの組み合わせによっては、上手くサイズが合わなくて折り返しちゃったりする事があるので、完璧ではないけど。。一応、Mac の Safari、Chrome、Firefox でテストした。

これを作る前に テスト駆動JavaScript を読んでたので、JsTestDriver のテストを書きながら作ってみた。

ちょっとハマったのが、JsTestDriver の charset が iso-8859-1 で決め打ちされてるので、日本語のテストができなかった。一応 Accepted になってる からいつかは直ると思うけど、結構放置気味…。

TDD を初めて自分でやってみたんだけど、テストを先に書く良さというのが (説明はできないまでも) 何となく体感できたような気がする。

後は、JsTestDriver のテストは、”test…“ で始まらないものが無視されるようなので、考えた仕様をメモ的に “should ….” とかで空のテストとして思いついたものから書いておいて、1 つづつ順番にテスト → 実装を繰り返すようなこともした(邪道かもしれないが)。

TDD で jQuery plugin を作ってみた

JavaScript の関数呼び出しと this

Yehuda Katz のブログで 2011/08/11 に post された記事 の概要。

大本の関数呼び出し

Function の call メソッドは次のように動作する

  1. すべての引数のリストを作る (argList)
  2. 最初のパラメータが thisValue
  3. function が this を最初の引数、次の引数に argList を指定して呼び出される

例:

   function hello(thing) {
     console.log(this + " says hello " + thing);
   }

   hello.call("Yehuda", "world") //=> Yehuda says hello world

他の呼び出し方は、この形式に変換できる。

単純な関数呼び出し

例:

   function hello(thing) {
     console.log("Hello " + thing);
   }

   // これは:
   hello("world")

   // このように変換できる:
   hello.call(window, "world");

ECMAScript 5 (ES5)の strict モードでは、 hello.call(undefined, “world”) になる。

メンバ関数

例:

   var person = {
     name: "Brendan Eich",
     hello: function(thing) {
       console.log(this + " says hello " + thing);
     }
   }

   // this:
   person.hello("world")

   // desugars to this:
   person.hello.call(person, "world");

hello メソッドがどのような方法でオブジェクトにアタッチされているかは関係ない。動的に追加された場合を見ると、次のようになる。

   function hello(thing) {
     console.log(this + " says hello " + thing);
   }

   person = { name: "Brendan Eich" }
   person.hello = hello;

   person.hello("world") // これも person.hello.call(person, "world") と同じ意味になる

   hello("world") // これは "[object DOMWindow] says hello world" となる

Function.prototype.bind を使った場合

常に同じ this を持つように、closure を使うテクニックが良く使われる。

   var person = {
     name: "Brendan Eich",
     hello: function(thing) {
       console.log(this.name + " says hello " + thing);
     }
   }

   var boundHello = function(thing) { return person.hello.call(person, thing); }

   boundHello("world"); // "Brendan Eich says hello world"
</code></pre>
このテクニックをもう少し汎用的にして、次のように書くことができる。
<pre><code>   var bind = function(func, thisValue) {
     return function() {
       return func.apply(thisValue, arguments);
     }
   }

   var boundHello = bind(person.hello, person);
   boundHello("world") // "Brendan Eich says hello world"

これを理解するためのポイントは、arguments は配列に似たオブジェクトだということと、apply メソッドは引数を配列に似たオブジェクトとして扱う以外は call メソッドと同じだということ。

尚、ES5 には Function に bind メソッドが追加されている。

   var boundHello = person.hello.bind(person);
   boundHello("world") // "Brendan Eich says hello world"

これはもとの関数をコールバックとして渡す時に便利である。

   var person = {
     name: "Alex Russell",
     hello: function() { console.log(this.name + " says hello world"); }
   }

   $("#some-div").click(person.hello.bind(person));

   // div がクリックされたら、 "Alex Russell says hello world" が表示される
JavaScript の関数呼び出しと this

カレンダーを作った (iPhone向けウェブアプリ)

ここのところ、ウェブページなんだけど iPhone でアプリっぽく見せたり、フリック操作を扱ったり、オフラインでも動くようにキャッシュさせたりする方法を調べたので、ひとまずオフラインでも動くカレンダーを作った。

特徴:

  • オフラインで動く
  • 「ホーム画面に追加」したアイコンから起動すると、ちょっとアプリっぽく見える
  • 左右のフリックで月の移動
  • (日本の)祝日に対応(色が違うだけ)
  • 任意の日付にマークできる(3色から選択)
  • 色を消すには、同じ色を選択してもう一度日付をタップ

注意点:

  • 最初に祝日データを Google Calendar API から取得してくるので、最初はオンラインにする必要がある(過去1年から2年先の分を取得)
  • データはすべて Local Storage に保存
  • オフラインで動くと書いたように、application cacheも使ってる
  • Android 2.3系だと、メニューのアニメーション(CSS)が上手く動かない(4.0だったら動くかも知れないけど未確認)

自分が祝日表示と、有給休暇などの休みの日だけちょこっと確認する程度の機能を持ったカレンダーアプリが欲しかったのだが、探してみてもピンとくるのが無かったので作ってみた。

iPhoneアプリの Yearsがかなり理想に近かったが、祝日も自分で入れなきゃいけないのが面倒だなーと思ったのと、まあ勉強も兼ねて。

参考:

カレンダーを作った (iPhone向けウェブアプリ)

はじめての jQuery Plugin

前回のpostにフリックのイベントについて書いたが、イベントを検知する jQuery Plugin にしてみた。jQuery Touchwipeとほとんど一緒なんだけど、jQuery Mobileも参考に、touchだけじゃなくてmouseでも使えるようにした。

Pluginのテンプレート

(function($){
    $.fn.myplugin = function(option){
        // default values
        var config = {
            config1: "value"
        };

        // override config with the give option(s)
        if (option) {
            $.extend(config, option);
        }

        // "this" is a jQuery object
        this.each(function(){
            // in the function, "this" is a DOM element
            // set shorthand of jQuery object if necessary
            var $this = $(this);

            // do something
        });

        // return "this" not to break method chain
        return this;
    };
})(jQuery);

シンプルな Plugin なら、これの必要な箇所に必要な処理を入れてけばいい。作り方を解説するみたいなタイトル付けたけど、あんまり解説する事なくて、テンプレメモだ。。

参考:

今回作ったやつ (jQuery.swipeListener)

jQuery.swipeListener (GitHub)

(function($){

    $.fn.swipeListener = function(option){
        var config = {
            duration: 1000, // within this period
            minX: 20, // swipe L/R if touch position moved more than this horizontally
            minY: 20, // swipe U/D if touch position moved more than this vertically
            swipeUp: undefined,   // callback function invoked when swipe up
            swipeDown: undefined, // or swipe down,
            swipeLeft: undefined, // or swipe left,
            swipeRight: undefined // or swipe right
        };

        if (option) {
            $.extend(config, option);
        }

        this.each(function(){
            var start = undefined;
            var stop  = undefined;
            var $this = $(this);
            var isTouchDevice   = typeof this.ontouchstart !== "undefined";
            var touchStartEvent = isTouchDevice ? "touchstart" : "mousedown";
            var touchMoveEvent  = isTouchDevice ? "touchmove" : "mousemove";
            var touchEndEvent   = isTouchDevice ? "touchend" : "mouseup";

            $this.bind(touchStartEvent, touchStart);

            function touchStart(event){
                var e = isTouchDevice ? event.originalEvent.touches[0] : event;
                start = {
                    x: e.pageX,
                    y: e.pageY,
                    time: (new Date()).getTime()
                };
                event.stopPropagation();

                $this.bind(touchMoveEvent, touchMove).one(touchEndEvent, touchEnd);
            };

            function touchMove(event){
                if (!start) {
                    return;
                }

                event.preventDefault();

                var e = isTouchDevice ? event.originalEvent.touches[0] : event;
                stop = {
                    x: e.pageX,
                    y: e.pageY,
                    time: (new Date()).getTime()
                };
            };

            function touchEnd(event){
                $this.unbind("touchmove mousemove", touchMove);

                if (start && stop && stop.time-start.time < config.duration) {
                    diffX = start.x - stop.x;
                    diffY = start.y - stop.y;

                    if (Math.abs(diffX) > config.minX) {

                        if (diffX > 0 && config.swipeLeft) {
                            config.swipeLeft();
                        } else if (config.swipeRight) {
                            config.swipeRight();
                        }

                    } else if (Math.abs(diffY) > config.minY) {

                        if (diffY > 0 && config.swipeUp) {
                            config.swipeUp();
                        } else if (config.swipeDown) {
                            config.swipeDown();
                        }
                    }
                }

                start = stop = undefined;
            };
        });

        return this;
    };

})(jQuery);
はじめての jQuery Plugin

JavaScript のフリックイベントでカードをめくる

参考:

フリック(またはスワイプ)イベント

ちょっと検索してみると、onFlickとかonSwipeとかいう都合のいいイベントハンドラは無いので、touch関係のイベント

  • touchstart
  • touchmove
  • touchend
  • touchcancel
  • gesturestart
  • gesturechange
  • gestureend

を使って、タッチしてから放すまでの間に、どのくらい指が動いたかをチェックして、指定した距離以上移動したら(場合によっては何秒以内に、という条件をつけて)、フリック(スワイプ)したと判断する。

シンプルにイベントハンドラだけ実装してるjQuery Touchwipe Pluginは、touchstartとtouchmoveだけチェックしてる。フリックしてエレメントを動かす(切り替える)のが目的のjQuery.flickSimpleはtouchendもチェックしている。「実装してみる」の記事やjQuery Mobileでは、それに加えてtouchstartからtouchendまでの時間もチェック(素早く動かした時をフリックと判断)。

カードを捲る

表用と裏用のコンテンツを用意して重ねて表示しておき、回転させた時にそれぞれの裏面が見えないように backface-visiblity を hidden にする。裏面用のコンテンツはあらかじめ rotateY(180deg) として裏にひっくり返しておく。

横向きのフリックイベントが発生した時に、カードに対して class を適用してアニメーションさせる。

サンプル

HTML:

<!DOCTYPE HTML>
<html lang="ja-JP">
<head>
    <meta charset="UTF-8">
    <title>flick test</title>
    <link rel="stylesheet" type="text/css" href="flicktest.css" media="all" />

</head>
<body>

<div id="card">
    <div id="face">
        <div id="front">F</div>
        <div id="back" class="reversed">B</div>
    </div>
</div>

<a href="http://code.jquery.com/jquery-1.7.1.min.js">http://code.jquery.com/jquery-1.7.1.min.js</a>
<a href="http://flicktest.js">http://flicktest.js</a>
</body>
</html>

CSS:

#card {
    border: 1px solid #999;
    width: 240px;
    height: 320px;
    -webkit-perspective: 1000;
    -webkit-transform-style: preserve-3d;
}

#face {
    padding: 5px;
    width: 240px;
    height: 320px;
}

#front {
    background: #ccd;
    position: absolute;
    width: 230px;
    height: 310px;
    -webkit-backface-visibility: hidden;
}

#back {
    background: #dcc;
    width: 230px;
    height: 310px;
    position: absolute;
    -webkit-backface-visibility: hidden;
}

.reversed {
    -webkit-transform: rotateY(180deg);
}


/** Animation: Turn Left **/
.turnleft {
    -webkit-animation-duration: 300ms;
    -webkit-animation-name: turnleft;
}

@-webkit-keyframes turnleft {
    0% { -webkit-transform: rotateY(0deg); }
    100% { -webkit-transform: rotateY(180deg); }
}

/** Animation: Turn Right **/
.turnright {
    -webkit-animation-duration: 300ms;
    -webkit-animation-name: turnright;
}

@-webkit-keyframes turnright {
    0% { -webkit-transform: rotateY(0deg); }
    100% { -webkit-transform: rotateY(-180deg); }
}

JavaScript:

$(document).ready(function(){
    var cardEl  = $("#card");
    var frontEl = $("#front");
    var backEl  = $("#back");
    var touchDevice = typeof cardEl[0].ontouchstart !== "undefined";

    var startX = null;
    var minX   = 40;

    cardEl.bind("touchstart mousedown", touchStart);

    function touchStart(e) {
        e.stopPropagation();

        var ev = touchDevice ? e.originalEvent.touches[0] : e;
        startX = ev.clientX;

        cardEl.bind("touchmove mousemove", touchMove);
    }

    function touchMove(e) {
        e.preventDefault();
        var ev = touchDevice ? e.originalEvent.touches[0] : e;
        var diffX = ev.clientX - startX;

        if (Math.abs(diffX) > minX) {
            startX = null;
            cardEl.unbind("touchmove mousemove");
            turnCard(diffX);
        }
    }

    function animationEnd(e) {
        cardEl.removeClass();
        frontEl.toggleClass("reversed");
        backEl.toggleClass("reversed");
        cardEl.unbind("webkitAnimationEnd");
    }

    function turnCard(diffX) {
        cardEl.bind("webkitAnimationEnd", animationEnd);

        if (diffX < 0) {
            cardEl.addClass("turnleft");
        } else {
            cardEl.addClass("turnright");
        }
    }
});
JavaScript のフリックイベントでカードをめくる