@blog.justoneplanet.info

日々勉強

overflowがscrollの時にアンカーにsmooth scrollできるjQueryプラグインを作ってみた

久しぶりのjQueryでのちょっとしたスクリプト。


■クライアントコード

// a[href="#target"]をクリックすると#targetに#mainがスムーズスクロールする。
$('a[href="#target"]').click(function(e){
    e.preventDefault();
    $('#main').boxScroll($("#target-wrapper"), $('#target'));
});

こんな感じで使う。気が向いたらブラッシュアップする。

File APIを使ってドロップでファイルをアップロードできるjQueryプラグインを作ってみる

■ダウンロード

クリックしてダウンロードして下さい。

■動作ブラウザ

Firefox 3.6+, Google Chrome 6+

■使い方

1.HTMLでプラグインを読み込む

jQuery本体を読み込んだ後にプラグインのJavaScriptを読み込む。

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script> 
<script type="text/javascript" src="jquery.dropUploader.js"></script>

2.JavaScriptでプラグインを設置する

以下のようにして、p.uploaderにファイルをドロップすると、ファイルをアップロードすることができる。

$(function(){
    $('p.uploader').dropUploader({
        "action" : "./save.php",
        "allowedMimetypes" : [
            'image/png',
            'image/jpeg',
            'image/gif'
        ],
        /*
        "onDragstart" : function(elm){
            //alert('dragstart');
        },
        "onMouseover" : function(elm){
            //alert('mouseover');
        },
        */
        "onProgress" : function(value){
            //console.log(value);
        },
        "onPartialError" : function(){
            alert('onPartialError');
        },
        "onComplete" : function(){
            alert('finished');
        }
    });
});

3.サーバーサイド(./save.php)でファイルを保存する

以下のようにするとPHPでファイルを保存することができる。但し、以下のコードはチェックを行っていないので、実際に使用する際にはファイルサイズや拡張子などのチェックを必ずしなくてはならない。

file_put_contents(basename($_GET['filename']), fopen('php://input', 'r'));

プラグイン本体

ワクワクする部分★

(function($){
    $.fn.dropUploader = function(options){
        var self    = this;
        var options = options;
        
        /**
         * _onDragstart
         * @param {Object} elm
         */
        var _onDragstart = function(elm){
            if(typeof options['onDragstart'] === 'function'){
                options['onDragstart']();
            }
            else{
                $(elm).css({
                    "border" : "5px dotted #cccccc"
                });
            }
        }
        
        /**
         * _onMouseover
         * @param {Object} elm
         */
        var _onMouseover = function(elm){
            if(typeof options['onMouseover'] === 'function'){
                options['onMouseover']();
            }
            else{
                $(elm).css({
                    "border" : "5px dotted #cccccc"
                });
            }
        }
        
        /**
         * _onMouseout
         * @param {Object} elm
         */
        var _onMouseout = function(elm){
            if(typeof options['onMouseout'] === 'function'){
                options['onMouseout']();
            }
            else{
                $(elm).css({
                    "border" : "none"
                });
            }
        }
        
        /**
         * _getAction
         * @param {string} action
         * @param {string} filename
         */
        var _getAction = function(action, filename){
            var url = '';
            if(action.indexOf('?') > -1){
                url = action + '&filename=' + encodeURIComponent(filename);
            }
            else{
                url = action + '?filename=' + encodeURIComponent(filename);
            }
            if(options['params']){
                for(var i = 0, n = options['params'].length; i < n; i++){
                    for(param in options['params'][i]){
                        url += '&' + encodeURIComponent(param) + '=' + encodeURIComponent(options['params'][i][param]);
                    }
                }
            }
            return url;
        }
        
        var result = this.each(function(){
            var elm = this;
            if(window.addEventListener){
                elm.addEventListener(
                    'dragstart',
                    function(e){
                        _onDragstart(this);
                        e.preventDefault();
                    },
                    false
                );
                elm.addEventListener(
                    'dragenter',
                    function(e){
                        _onMouseover(this);
                        e.preventDefault();
                    },
                    false
                );
                elm.addEventListener(
                    'dragover',
                    function(e){
                        _onMouseover(this);
                        e.preventDefault();
                    },
                    false
                );
                elm.addEventListener(
                    'dragout',
                    function(e){
                        _onMouseout(this);
                        e.preventDefault();
                    },
                    false
                );
                elm.addEventListener(
                    'drop',
                    function(e){
                        e.preventDefault();
                        var files    = e.dataTransfer.files;
                        var total    = 0;
                        var loaded   = 0;
                        var progress = 0;
                        var counter  = 0;
                        var xcounter = 0;
                        var response = [];
                        $(this).addClass('wait');
                        for(var i = 0; i < files.length; i++){
                            if($.inArray(files[i].type, options['allowedMimetypes']) > -1){
                                var xhr = new XMLHttpRequest();
                                total = total + files[i].size;
                                xhr.upload.onprogress = function(e){
                                    loaded = loaded + e.loaded;
                                    if(typeof options['onProgress'] === 'function'){
                                        options['onProgress']((loaded / total) * 100 + "%");
                                    }
                                }
                                xhr.upload.onload = function(e){
                                    counter++;
                                    if(i === counter){// upload is finished
                                        if(typeof options['onComplete'] === 'function'){
                                            options['onComplete']();
                                        }
                                        $(this).removeClass('wait');
                                        if(typeof options['onProgress'] === 'function'){
                                            options['onProgress'](0);
                                        }
                                    }
                                    else{
                                    }
                                }
                                var filename = files[i].name;
                                var action = _getAction(options['action'], files[i].fileName);
                                xhr.open('post', action);
                                xhr.onreadystatechange = function(e){
                                    if(this instanceof XMLHttpRequest && this.readyState === 4){
                                        xcounter++;
                                        try{
                                            response.push(this.responseText);
                                            if(i === xcounter){// upload is finished
                                                $(this).removeClass('wait');
                                                if(typeof options['onProgress'] === 'function'){
                                                    options['onProgress'](0);
                                                }
                                                options['onComplete'](response);
                                            }
                                            else{
                                            }
                                        }
                                        catch(e){
                                            if(typeof options['onError'] === 'function'){
                                                options['onError']();
                                            }
                                        }
                                    }
                                }
                                xhr.send(files[i]);
                            }
                            else{
                                if(i === counter){// upload is finished
                                    if(typeof options['onComplete'] === 'function'){
                                        options['onComplete']();
                                    }
                                    $(this).removeClass('wait');
                                    if(typeof options['onProgress'] === 'function'){
                                        options['onProgress'](0);
                                    }
                                }
                                counter++;
                            }
                        }
                    },
                    false
                );
            }
        });
        return result;
    }
})(jQuery);

クロスブラウザにしないとなー♪((O(〃⌒∇⌒〃)O))♪

document.querySelectorをつかう

CSSセレクタは凄く分かりやすくjQueryでも頻繁に使用する。

$('h3.hoge');

これだけのためにjQueryを使いたくはないので、querySelectorメソッドを使用する。

document.querySelector('h3.hoge');

上述だと最初の要素しか選択されないので、下述のようにquerySelectorAllメソッドを使用する。

document.querySelectorAll('h3.hoge');

textareaでmaxlengthを使えるようにする

以下のようにする。

var textarea = $('textarea[maxlength]');
textarea.bind(
    "keydown keyup click",
    function(){
        var max = $(this).attr('maxlength');
        if($(this).val().length > max){
            $(this).val($(this).val().substr(0, max));
        }
    }
);

面倒なのでjQueryを使用してます。ちなみにHTML5ではネイティブで使えるので以上のコードは必要ない。

IEはradioボタンをappendChildしてもつかえない

IE6~7でラジオボタンをappendChildした場合、そのラジオボタンはクリックできない。ラジオボタンとして致命的なバグである。

■失敗例

以下のようにDOM要素を操作する。

var input = document.createElement('input');
var p     = document.createElement('p');
p.appendChild(input);

ちなみにjQueryを使うと以下のようになる。

var input = document.createElement('input');
var p     = document.createElement('p');
$(p).append(input);

■解決策

IEだけinnerHTMLを使う。

var input = document.createElement('input');
var p     = document.createElement('p');
if(!!(!window.opera && window.attachEvent)){
    p.innerHTML = '<input type="radio" name="id" />';
}
else{
    p.appendChild(input);
}

ちなみにjQueryを使うと以下のようになる。

var input = document.createElement('input');
var p     = document.createElement('p');
if($.browser.msie){
    $(p).append('<input type="radio" name="id" />');
}
else{
    $(p).append(input);
}

JavaScriptで画像を縦横比を維持しつつ指定サイズに丸める

SVGやcanvasを使えば確かそのままトリミングもできた気がするが、別にそこまでしたくない用の関数。画像のオリジナルサイズを取得し計算する感じだ。面倒なのでjQueryを使う。

var frameWidth  = 700;
var frameHeight = 400;
$('li').css({
    "overflow" : "hidden"
});
$('div.resize img').each(function(){
    $(this).load(function(){
        var nWidth  = this.naturalWidth  || getNaturalSize(this).width;
        var nHeight = this.naturalHeight || getNaturalSize(this).height;
        if(nHeight < nWidth * (frameHeight / frameWidth)){
            this.width  = nWidth * frameHeight / nHeight;
            this.height = frameHeight;
            $(this).css({
                "width"  : nWidth * frameHeight / nHeight + 'px',
                "height" : frameHeight + 'px',
                "position" : "relative",
                "left" : -((nWidth * frameHeight / nHeight) - frameWidth) / 2 + 'px'
            });
        }
        else{
            this.width  = frameWidth;
            this.height = nHeight * frameWidth / nWidth;
            $(this).css({
                "width"  : frameWidth + 'px',
                "height" : nHeight * frameWidth / nWidth + 'px',
                "position" : "relative",
                "top" : -((nHeight * frameWidth / nWidth) - frameHeight) / 2 + 'px'
            });
        }
    });
});

以下の部分でブラウザ分岐をしている。IEとOpera以外はimgオブジェクトにオリジナルのサイズが格納されたプロパティ(naturalWidth、naturalHeight)を持つ。

var nWidth  = this.naturalWidth  || getNaturalSize(this).width;//ff : ie
var nHeight = this.naturalHeight || getNaturalSize(this).height;//ff : ie

getNaturalSize関数は以下のようになる。

var getNaturalSize = function(image){
    var w, h, key = "actual", run, mem;
    if(window.opera){
    }
    if (image[key] && image[key].src === image.src) {
        return image[key];
    }
    run = image.runtimeStyle;
    mem = {
        "w" : run.width,
        "h" : run.height
    }; // keep runtimeStyle
    run.width  = "auto"; // override
    run.height = "auto";
    w = image.width;
    h = image.height;
    run.width  = mem.w; // restore
    run.height = mem.h;
    image[key] = {
        "width"  : w,
        "height" : h,
        "src"    : image.src
    };
    return image[key]; // bond
};

ちなみにOperaには対応していない。

tinyMCEはjQueryのcloneで複製できない

jQueryのtinyMCEプラグインを使用する。

■cloneメソッド

そっくりそのままコピーができるが、コピーされたtinyMCEは機能しないはずだ。この不具合は、tinyMCEをsortable要素にした時にも生じる。恐らく、tinyMCEが内部的にiframeを使用していることに起因するのではないだろうか。

<!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">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>無題ドキュメント</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.js"></script>
<script type="text/javascript" src="tiny_mce/tiny_mce.js"></script>
<script type="text/javascript" src="tiny_mce/jquery.tinymce.js"></script>
<script type="text/javascript">
$(function(){
	$('textarea.tinymce').tinymce({
		theme : "advanced",
		plugins : "safari,pagebreak,style,layer,table,save,advhr,advimage,advlink,emotions,iespell,inlinepopups,insertdatetime,preview,media,searchreplace,print,contextmenu,paste,directionality,fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras,template",
		theme_advanced_buttons1 : "save,newdocument,|,bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,styleselect,formatselect,fontselect,fontsizeselect",
		theme_advanced_buttons2 : "cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|,outdent,indent,blockquote,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|,insertdate,inserttime,preview,|,forecolor,backcolor",
		theme_advanced_buttons3 : "tablecontrols,|,hr,removeformat,visualaid,|,sub,sup,|,charmap,emotions,iespell,media,advhr,|,print,|,ltr,rtl,|,fullscreen",
		theme_advanced_toolbar_location   : "top",
		theme_advanced_toolbar_align      : "left",
		theme_advanced_statusbar_location : "bottom",
		theme_advanced_resizing : true,
		init_instance_callback : function(){
			var clone = $('div.parts').clone(true);
			$('div.parts').after(clone);
		}
	});
});
</script>
</head>
<body>
<div class="parts">
<form method="post" action="">
<textarea class="tinymce" name="" rows="15" cols="60"></textarea>
</form>
</div>
</body>
</html>

■解決策

document.createElementでtextareaから生成したtextareaに対して、tinymce()する。

<!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">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>無題ドキュメント</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.js"></script>
<script type="text/javascript" src="tiny_mce/tiny_mce.js"></script>
<script type="text/javascript" src="tiny_mce/jquery.tinymce.js"></script>
<script type="text/javascript">
$(function(){
	$('textarea.tinymce').tinymce({
		theme : "advanced",
		plugins : "safari,pagebreak,style,layer,table,save,advhr,advimage,advlink,emotions,iespell,inlinepopups,insertdatetime,preview,media,searchreplace,print,contextmenu,paste,directionality,fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras,template",
		theme_advanced_buttons1 : "save,newdocument,|,bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,styleselect,formatselect,fontselect,fontsizeselect",
		theme_advanced_buttons2 : "cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|,outdent,indent,blockquote,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|,insertdate,inserttime,preview,|,forecolor,backcolor",
		theme_advanced_buttons3 : "tablecontrols,|,hr,removeformat,visualaid,|,sub,sup,|,charmap,emotions,iespell,media,advhr,|,print,|,ltr,rtl,|,fullscreen",
		theme_advanced_toolbar_location   : "top",
		theme_advanced_toolbar_align      : "left",
		theme_advanced_statusbar_location : "bottom",
		theme_advanced_resizing : true,
		init_instance_callback : function(){
			var div      = document.createElement('div');
			var form     = document.createElement('form');
			var textarea = document.createElement('textarea');
			$(div).attr('class', 'parts');
			$(div).append(form);
			$(form).append(textarea);
			$(textarea).attr('class', 'tinymce');
			$(textarea).attr('rows', '15');
			$(textarea).attr('cols', '60');
			$('div.parts:last').append(div);
			$(textarea).tinymce({
				theme : "advanced",
				plugins : "safari,pagebreak,style,layer,table,save,advhr,advimage,advlink,emotions,iespell,inlinepopups,insertdatetime,preview,media,searchreplace,print,contextmenu,paste,directionality,fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras,template",
				theme_advanced_buttons1 : "save,newdocument,|,bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,styleselect,formatselect,fontselect,fontsizeselect",
				theme_advanced_buttons2 : "cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|,outdent,indent,blockquote,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|,insertdate,inserttime,preview,|,forecolor,backcolor",
				theme_advanced_buttons3 : "tablecontrols,|,hr,removeformat,visualaid,|,sub,sup,|,charmap,emotions,iespell,media,advhr,|,print,|,ltr,rtl,|,fullscreen",
				theme_advanced_toolbar_location   : "top",
				theme_advanced_toolbar_align      : "left",
				theme_advanced_statusbar_location : "bottom",
				theme_advanced_resizing : true
			});
		}
	});
});
</script>
</head>
<body>
<div class="parts">
<form method="post" action="">
<textarea class="tinymce" name="" rows="15" cols="60"></textarea>
</form>
</div>
</body>
</html>

しっかり機能するtinyMCEが生成されるはずだ。

init_instance_callback

エディタが完成するとコールされる。tinymce()から完成までは時間がかかり、気をつけなくてはいけないのは、その間にscript処理が止まらない。従って、完成した(直後の)エディタに対してscriptからアクセスする場合は、init_instance_callbackを使用する必要がある。