tumblr

tumblr(タンブラー)は、メディアミックスブログサービス。ブログとミニブログ、そしてソーシャルブックマークを統合したマイクロブログサービスである。アメリカのDavidville.inc(現: Tumblr, Inc.)により2007年3月1日にサービスが開始された。

jQueryのプラグインをつくってみよう


僕はiPhoneとかiPadとかmacみたいな流行りものを使用すると蕁麻疹が全身に出てさらに左腕が疼きだすという特異体質(別名:天邪鬼)のため、これまであまりjQueryは使ったことがなかったです。特異体質によるものもあったのですが、僕みたいなグズがjQueryのような便利なものに手を出したら、面倒なコードを書くにもjQuery頼りになって自分でコードを書く努力を怠る習慣が身についてしまうのでは、という懸念があったためです。最近になってようやく、本当に多少ではありますが独力である程度の処理は書けるし読めるようにもなったので、効率化のためにもjQueryをもっと使っていこうという気になったわけであります。

で、そうなるとやはりjQueryが提供する便利関数では全然足りなくて自分でプラグインを書きたくなるわけです。ちょうどjQueryの公式サイトにjQueryプラグインの作り方指南な記事(http://docs.jquery.com/Plugins/Authoring)があったので、これを勉強がてら超訳的なまとめ覚書+実際にプラグインを作ってみます。

プラグインの追加

jQueryプラグインを書くために、まずはプラグインと同名の関数をjQuery.fn オブジェクトに追加します。

jQuery.fn.myPlugin = function() {

  // Do your awesome plugin stuff here

};


上では「myPlugin」がプラグインの名前です。これで $("#element").myPlugin(); などのようにプラグインを呼び出すことが出来ます。
が、プラグインの名前はいいとして、上のコードの中には例の「$」がありません。なぜ「$.fn.myPlugin」としないのでしょうか。これはドル記号が他のドル記号をショートカットとしてグローバルに追加しているライブラリと衝突してしまう可能性があるからです。その場合、jQueryとは関係のないライブラリにfn.myPluginというメソッドが追加されてしまいます。(実際はfnが他のライブラリ側にないとエラー吐いて終わるけど)
とにかく、そういうわけでここではドル記号を使っていません。ただ、プラグイン定義の中でドル記号が使えないのは不便なので、以下のようにします。

(function( $ ) {
  $.fn.myPlugin = function() {
  
    // Do your awesome plugin stuff here

  };
})( jQuery );

これならドル記号も使えるし、グローバルを汚すこともありません。


ちなみに、この「fn」って何だろかと思ってjQueryのソースを調べてみたら

(略)
jQuery.fn = jQuery.prototype = {
	constructor: jQuery,
	init: function( selector, context, rootjQuery ) {
(略)

とあったので、prototypeらしいです。

コンテキスト

とりあえずはこれでプラグインが作れそうなので実際にコードを書いてみます。が、その前にコンテキストについて。プラグイン名のついたfunctionの中ではthisはjQueryオブジェクトにバインドされます。で、ちょっとややこしいのは、callbackを受け付けるようなjQueryのメソッドの場合、callbackの中のthisはjQuryオブジェクトではなく、DOM Elementにバインドされます。

(function( $ ){
  $.fn.myPlugin = function() {
    // there's no need to do $(this) because
    // "this" is already a jquery object
    // $(this) would be the same as $($('#element'));
    this.fadeIn('normal', function(){
      // the this keyword is a DOM element
    });
  };
})( jQuery );
$('#element').myPlugin();


コード中のコメントにもあるように、this = $('#element') なので、$(this)とかするといけないよ、ということです。ただし、その下のfadeInが第二引数としてとっているcallback functionの中ではthisはただのDOM Elementなので、$(this)とかできるわけです。

実際に書いてみる

というわけで、プラグイン作成の上で重要なところが分かったところで、実際に動くプラグインを作ってみます。
ページ内のdiv要素のうち、最も高いもののheightを返す簡単なプラグインです。

(function( $ ){
  $.fn.maxHeight = function() {
    var max = 0;
    this.each(function() {
      max = Math.max( max, $(this).height() );
    });
    return max;
  };
})( jQuery );
var tallest = $('div').maxHeight(); // Returns the height of the tallest div


most tallest div - jsdo.it - share JavaScript, HTML5 and CSS


メソッドチェーン

上の例では、プラグインの関数の返り値はheightでした。これでもまぁいいことはいいんですが、jQueryの人気の秘訣の一つはメソッドチェーンであり、これは例えば $('#element').css('color', 'red').fadeIn(); みたいに取得した結果に対してさらにメソッドを実行する感じでメソッドをつなげていくやつなんですが、この人気の秘訣ともなるチェーンをプラグインが途中でぶった斬ってほしくないっていうか空気読んで欲しいわけなんです。

というわけで、特に上の例は機能的に無理がありますが、特別何か値を返す必要がない場合はthisを返しましょう。もちろんこのthisはjQueryオブジェクトです。

(function( $ ){
  $.fn.lockDimensions = function( type ) {  
    return this.each(function() {
      var $this = $(this);
      if ( !type || type == 'width' ) {
        $this.width( $this.width() );
      }
      if ( !type || type == 'height' ) {
        $this.height( $this.height() );
      }
    });
  };
})( jQuery );
$('div').lockDimensions('width').css('color', 'red');

この場合、eachメソッドの返り値を返しています。$('element').each()の帰り値はjQueryオブジェクトなので、lockDimensionsメソッドの次に.cssでチェーンさせることが可能なわけです。

設定値の煩雑さを解消する

$('#element').myPlugin(height, width, top, left, right, bottom, position);

みたいな引数がたくさんな感じはイヤやで、ということです。プラグインによっては多くの設定値を引数として取る必要がある場合もあるでしょう。そのとき、引数が多いと、わざわざその順番に合わせて引数セットして呼び出し、というのが面倒なのです。
jQueryに限らず、こういう場合は煩雑な引数たちをオブジェクトにぶち込み、そのオブジェクトのみを引数として渡せばいいわけです。

(function( $ ){

  $.fn.tooltip = function( options ) {  

    // Create some defaults, extending them with any options that were provided
    var settings = $.extend( {
      'location'         : 'top',
      'background-color' : 'blue'
    }, options);

    return this.each(function() {        
      // Tooltip plugin code here
    });

  };
})( jQuery );
$('div').tooltip({
  'location' : 'left'
});

上ではsettingsというオブジェクトにデフォルト値をセットしておき、それをoptionsという引数でextendしています。
$.extend(target, obj)で、targetにobjを上書きコピーして書き換えられたtargetを返します。上の例では location: left をoptionsの値としているので、最終的にプラグインのsessingsは以下のような値が収まるのです。

{
  'location'         : 'left',
  'background-color' : 'blue'
}


extendの詳しい使い方についてはこちら参照
http://d.hatena.ne.jp/nitoyon/20110324/jQuery_extend_mania


ネームスペース

ネームスペースは他のプラグインにもしかしたら上書きされるかもしれないので、割と慎重に諸々決める必要があります。jQueryではプラグインのメソッドだけでなくイベントやデータにもネームスペースが関わってきます。

プラグインメソッド

プラグインによってはいくつもプラグイン内で使うメソッドが必要になると思います。で、そういう場合に以下のように書かれると無駄にメソッド追加することになるのでマジ最悪という感じです。

(function( $ ){
  $.fn.tooltip = function( options ) { 
    // THIS
  };
  $.fn.tooltipShow = function( ) {
    // IS
  };
  $.fn.tooltipHide = function( ) { 
    // BAD
  };
  $.fn.tooltipUpdate = function( content ) { 
    // !!!  
  };
})( jQuery );


1つのプラグインで$.fn namespaceに4つも新たなメソッドが追加されてしまいました。これはあかん。でもこの4つのメソッドはどうしても必要だし…という場合はクロージャを使ってこれを解決します。

(function( $ ){

  var methods = {
    init : function( options ) { 
      // THIS 
    },
    show : function( ) {
      // IS
    },
    hide : function( ) { 
      // GOOD
    },
    update : function( content ) { 
      // !!! 
    }
  };

  $.fn.tooltip = function( method ) { 
    // Method calling logic
    if ( methods[method] ) {
      return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
    } else if ( typeof method === 'object' || ! method ) {
      return methods.init.apply( this, arguments );
    } else {
      $.error( 'Method ' +  method + ' does not exist on jQuery.tooltip' );
    }    
  };

})( jQuery );

// calls the init method
$('div').tooltip(); 

// calls the init method
$('div').tooltip({
  foo : 'bar'
});

必要なメソッドをプラグインと同じスコープ内の methods という変数にまとめておけば、$.fn.tooltipからも問題なくアクセスできます。$.fnを汚さないで済むので良いですね。このメソッドのカプセル化jQueryプラグイン製作者の中ではスタンダードな方法らしく、jQueryUIもこの方法を使っているようです。

イベント

あまり知られていないっぽいですが、bindというメソッドはイベントに名前空間を付加することが出来ます。

$('.class').bind('click', function(){});
$('.class').unbind('click');

例えば上記のような場合、unbindすることで全てのハンドラが削除されてしまいますが、以下のようにすれば特定のハンドラだけ削除することができます。

$('.class').bind('click', function(){});
$('.class').bind('click.myPlugin', function(){});

$('.class').unbind('click.myPlugin');

bindの第一引数を eventType.namespace という感じで渡してやることで、名前空間を追加できるのです。
この場合、unbindで削除されるのは2番目にbindしているclick.myPluginのみです。
自分でも動作を確認するためにテストコードを作ってみました。


jQuery Namespaced Event のテスト - jsdo.it - share JavaScript, HTML5 and CSS


とにかく、この名前空間を使えば、もし他のプラグインが同じ要素に同じイベントタイプのハンドラを登録しても、他のプラグインの影響でunbindされてしまうということがなくなるわけです。

(function( $ ){

  var methods = {
     init : function( options ) {
       return this.each(function(){
         $(window).bind('resize.tooltip', methods.reposition);
       });

     },
     destroy : function( ) {

       return this.each(function(){
         $(window).unbind('.tooltip');
       })

     },
     reposition : function( ) { 
       // ... 
     },
     show : function( ) { 
       // ... 
     },
     hide : function( ) {
       // ... 
     },
     update : function( content ) { 
       // ...
     }
  };

  $.fn.tooltip = function( method ) {
    if ( methods[method] ) {
      return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
    } else if ( typeof method === 'object' || ! method ) {
      return methods.init.apply( this, arguments );
    } else {
      $.error( 'Method ' +  method + ' does not exist on jQuery.tooltip' );
    }    
  };

})( jQuery );

上のコードの場合、初期化時にinitメソッドが呼ばれてtooltipという名前空間の中でresizeイベントが登録されます。これで他のプラグインなどに勝手にresizeイベントがunbindされることはありません。
そして、tooltipが要らなくなった段階でdestroyメソッドを呼び出せば、安全にこのプラグインのresizeイベントは削除できます。もちろん他のプラグインが登録しているresizeイベントのハンドラを不意に消すことはありません。


データ

プラグインによっては要素ごとに状態を持ちたいとか、要素にデータを紐付けたい時があるかもしれません。そういう場合にdataメソッドを使います。

jQuery.data( element , key [, value ] );

dataメソッドは第一引数にデータを紐付けたい要素を、2番目にkey, 3番目にvalueを指定します。valueを省略することで、すでにその要素に紐付けられているデータを取得することもできます。
これもkeyが名前空間の役割を果たすということみたいです。

(function( $ ){

  var methods = {
     init : function( options ) {
       return this.each(function(){
         var $this = $(this),
             data = $this.data('tooltip'),
             tooltip = $('<div />', {
               text : $this.attr('title')
             });
         // If the plugin hasn't been initialized yet
         if ( ! data ) {
           /*
             Do more setup stuff here
           */
           $(this).data('tooltip', {
               target : $this,
               tooltip : tooltip
           });
         }
       });
     },

     destroy : function( ) {
       return this.each(function(){
         var $this = $(this),
             data = $this.data('tooltip');
         // Namespacing FTW
         $(window).unbind('.tooltip');
         data.tooltip.remove();
         $this.removeData('tooltip');
       })
     },

     reposition : function( ) { // ... },
     show : function( ) { // ... },
     hide : function( ) { // ... },
     update : function( content ) { // ...}
  };

  $.fn.tooltip = function( method ) {
    if ( methods[method] ) {
      return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
    } else if ( typeof method === 'object' || ! method ) {
      return methods.init.apply( this, arguments );
    } else {
      $.error( 'Method ' +  method + ' does not exist on jQuery.tooltip' );
    }    
  };

})( jQuery );


これもイベントの時の例と同じくinitで tooltip というkeyにデータを入れて、destroyでtooltipに紐付いていたデータを削除しているわけです。

まとめ

というわけで、jQueryプラグイン作成時に気をつけることをまとめると

  • プラグインは次のようなクロージャでラップする。 (function( $ ){ /* plugin goes here */ })( jQuery );
  • スコープによってthisが指すものがjQueryオブジェクトだったりDOMElementだったりするので気をつける。
  • 何か特定の値を返さなきゃいけないプラグインでなければ、チェーンをぶった切らないように、返り値はthisにする。
  • 引数を多く取る必要があるとき、煩雑にならないようにオブジェクトリテラルでまとめる。
  • プラグイン1つにつき、jQuery.fnへの追加メソッドは1つにする。


英語そこまで読めるほうではないのでかなり大雑把な超訳になってしまいましたが、ここ意味ちげーよボケとか説明よくわかんねーよカスとか早く働けデブとかあったらコメントください。


とりあえず作ってみた

すでにクソみたいに長い記事をさらに長くするため、自分でも簡単なプラグインをさくっと作ってみたわけです。
jquery.searchSelect.jsと名づけました。
ページに表示されてる文字を選択したらすぐさま「twitterへつぶやく」「googleで検索」みたいなボックスが選択した文字列の近くに出てきてうざいアレです。とりあえずgoogle検索またはwikipediaで検索できるようにしてあります。
上で解説したような項目(設定値をオブジェクトリテラルで引数に渡してextendとかメソッドをクロージャであくせすできるようにするとか)をいくつかこれみよがしに入れてあります。

(function($){
    
    function getSelectedTxt(){
    	var txt = null;
	if(window.getSelection){
	    txt = window.getSelection().toString();
	}else if(document.getSelection){
	    txt = document.getSelection().toString();
	}else{
	    txt = document.selection.createRange().text;
	}
	return txt;
    }
    
    $.fn.searchSelect = function(options){
        var settings = $.extend({
                'google':    true,
                'wikipedia': false,
            }, options);
        var $searchBox = $('<div></div>');
	$searchBox.css({
	    width: '150px',
	    height: '25px',
	    background: '#666',
	    position: 'absolute'
	});
        
        this.bind('mouseup', function(e){
            var txt = getSelectedTxt();
	    console.log(settings.google);
	    
	    if(settings.google){
	    	$('<a>google </a>').attr('target', '_blank')
		    .attr('href', 'https://www.google.co.jp/search?q='+encodeURI(txt))
		    .appendTo($searchBox);
	    }
	    if(settings.wikipedia){
	    	$('<a>wikipedia</a>').attr('target', '_blank')
		    .attr('href', 'http://ja.wikipedia.org/wiki/'+encodeURI(txt))
		    .appendTo($searchBox);
	    }
	    
	    $searchBox.css({
	    	left: e.pageX-5,
		top: e.pageY-40
	    }).appendTo('body');
        });
	$(document).bind('mousedown.searchSelect', function(){
	    $searchBox.remove();
	});
    };
})(jQuery);

デモ

jquery.searchSelect.js - jsdo.it - share JavaScript, HTML5 and CSS


今日も相変わらず長いですね。丸1日ブログ書くと疲れる。