【knockout.js】 要素の外側のクリックにバインド 【カスタムバインディング】

以前公開した jQuery プラグインの「[skOuterClick](http://sukobuto.com/archives/261)」がなかなか需要があるようなので、outerClick の Knockout 版カスタムバインディングを公開します。

## カスタムバインディング本体
これを knockout.js と jQuery の後に読み込みます。

```js:ko.outerClick.js
/**
 * ko.outerClick.js
 * depend on jQuery
 */

(function() {
    var dummyCallback = function(){};
    var uo = ko.utils.unwrapObservable; // alias
    function searchNearestElements(selector, startingElement) {
		var candidates = startingElement.closest(selector);
		if (candidates.length > 0) return candidates;
		candidates = $(selector);
		if (candidates.length > 0) return candidates;
		return false;
	};

    /**
     * Custom binding [ outerClick ]
     * 要素の外側のクリックイベントに対してコマンドをバインドする
     *
     * binding-type: function/object
     * binding-object: {
     *  command:    [default:function] 要素の外側がクリックされた際に呼び出されるコールバック関数を指定する。
     *  enable:     true = commandの実行を可能にする。動的バインド可能。
     *              デフォルトは true
     *  inners:     内側として扱う要素をCSSセレクタで指定する。
     *              要素はまず近い親要素から検索され、見つからなければDOM全体から検索される。
     *              デフォルトは false（指定なし）
     *
     * }
     */
    ko.bindingHandlers['outerClick'] = {
        init: function(element, valueAccessor) {
            var value = uo(valueAccessor());
            var elm = $(element);
            var config = ko.utils.extend({
                command: dummyCallback,
                enable: true,
                inners: false
            }, (typeof value == 'function' ? {command: value} : value));
            var isInner = false;
            // Bind click event to suppress
            function onInnerClick(e){
                isInner = true;
            };
            elm.click(onInnerClick);
            if (config.inners) {
                var inners = searchNearestElements(config.inners, elm);
                if (inners.length > 0) inners.each(function(inner) {
                    $(inner).click(onInnerClick);
                });
            }
            // Bind click elsewhere
            $(document).click(function(e) {
                if (isInner) {
                    isInner = false;
                    return;
                }
                if (uo(config.enable)) config.command.call(element, ko.dataFor(element), e);
            });
        }
    };
})();
```

## 使い方

```html:view_1.html
<div data-bind="outerClick: showMessage">
    ここをクリックしてもなにも起きません。<br />
    この外側をクリックするとメッセージが表示されます。
</div>
```

```js:view-model_1.js
function AppViewModel() {
    this.showMessage = function() {
        alert("メッセージ")
    }
}
$(function() {
    ko.applyBindings(new AppViewModel());
}
```

## 使い方（アドバンスト）

```html:view_2.html
<div data-bind="outerClick: { command: showMessage,
                              enable: showMessageExecutable,
                              inners: '.inner_box' }">
    ここをクリックしてもなにも起きません。<br />
    この外側をクリックするとメッセージが表示されます。
</div>
<div class="inner_box">
    さらにここをクリックしてもなにも起きません。
    なぜならここも inners 指定によって「内側」として扱われるからです。
</div>
```

```js:view-model_2.js
function AppViewModel() {
    this.showMessage = function() {
        alert("メッセージ")
    }
    this.showMessageExecutable = ko.observable(true);
}
$(function() {
    var viewModel = new AppViewModel()
    ko.applyBindings(viewModel);
    // ...later
    viewModel.showMessageExecutable(false); // これでメッセージは表示されなくなる
}
```