jQueryでtextareaのカーソル位置の行数を取得・設定(スクロール対応)

jQueryロゴtextarea内のカーソル位置や行数を取得したり、指定行のみ置換したりなどをJavaScriptでサラリと出来るかな?と思ったら、それなりに大変だったという記録です。

一部jQueryオブジェクトですがjQueryの機能は使っていないので依存しない改造も簡単だと思います。

 

カーソル位置までの行数

テキストエリア内のカーソルはキャレットという名称です。キャレット位置の行数を取得する関数は用意されておらず、キャレット位置までの文字数のみが取得できます。日本語も1カウントです。その後、改行コードの数を調べることで行数を割り出します。

まず、文字数から行数を取得できる関数をStringクラスに追加しておきます。ブラウザによって異なる改行コードの差異も吸収します。

String.prototype.getLinefromCount = function(start){
// 文字数から行数を取得
    var NotLF = /rn|r/g;
    var region_byCaret = this.slice(0, start);
    var CRLFs = region_byCaret.match(NotLF);
    if (CRLFs) {
        region_byCaret = region_byCaret.replace(NotLF, 'n');
    }

    var lines = region_byCaret.split('n');
    return lines.length;
}

次にjQueryでtextareaからキャレット位置を取得・設定する関数 caretPos を定義します。 IE8,Firefox,Chromeで動作確認してあります。

selectionStart が使えないIEの為に getSelectionCount関数を定義しています。正確なスクロール位置の算出の為には、textarea の line-height を CSS で設定しておく必要があります。

使い方
var linenum = $("textarea").caretPos(); // キャレット位置の行数取得
$("textarea").caretPos( 先頭からの文字数 ); // キャレット位置を設定(スクロール対応)
(function($) {
    var caretPos = function(pos) {
        var item = this.get(0);
        if (pos == null) {
            return get(item);
        } else {
            set(item, pos);
            return this;
        }
    };

    var get = function(item) {
        var CaretPos = 0;
        if (item.selectionStart || item.selectionStart == "0") { // Firefox, Chrome
            start = item.selectionStart;
        } else if (document.selection) { // IE
             start = getSelectionCount(item)[0];
        }
        
        if (isNaN (start)){
            return;
        }
        
        return item.value.getLinefromCount( start );
    };
    var set = function(item, pos) {
        if (item.setSelectionRange) {  // Firefox, Chrome
            item.setSelectionRange(pos, pos);
            
            var lineNum = item.value.getLinefromCount( pos );
            var lineHeight = item.style.lineHeight.slice(0,-2);
            item.scrollTop = (lineNum-1) * parseInt(lineHeight);
            item.focus();
        } else if (item.createTextRange) { // IE
            var range = item.createTextRange();
            range.collapse(true);
            range.moveEnd("character", pos);
            range.moveStart("character", pos);
            range.select();
        }
    };
    
    $.fn.extend({caretPos: caretPos});
})(jQuery);

function getSelectionCount(textarea) {
    var selectionRange = textarea.document.selection.createRange();

    if (selectionRange == null || selectionRange.parentElement() !== textarea) {
        return [ NaN, NaN ];
    }

    var value = arguments[1] || textarea.value;
    var valueCount = value.length;
    var range = textarea.createTextRange();
    range.moveToBookmark(selectionRange.getBookmark());

    var endBoundary = textarea.createTextRange();
    endBoundary.collapse(false);

    // endBoundary << range
    if (range.compareEndPoints('StartToEnd', endBoundary) >= 0) {
        return [ valueCount, valueCount ];
    }

    var normalizedValue = arguments[2] || value.replace(/rn|r/g, 'n');
    var start = -(range.moveStart('character', -valueCount));
    start += normalizedValue.slice(0, start).split('n').length - 1;

    // range << endBoundary << range
    if (range.compareEndPoints('EndToEnd', endBoundary) >= 0) {
        return [ start, valueCount ];
    }

    // range << endBoundary
    var end = -(range.moveEnd('character', -valueCount));
    end += normalizedValue.slice(0, end).split('n').length - 1;
    return [ start, end ];
}
[/code]


<h2>指定行のみ置換する</h2>
<p>JSのreplaceは最初にマッチしたものを置換しますが、指定行でマッチした場合のみ置換する関数を作りたいと思います。</p>
<p>まず指定行の文字列を取得する関数をStringクラスに追加しておきます。ブラウザによって異なる改行コードの差異も吸収します。</p>

[code]
String.prototype.getStrfromLine = function(line){
// 行数の文字列を取得
    var body = this;
    var NotLF = /rn|r/g;
    var CRLFs = this.match(NotLF);
    if (CRLFs) {
        body = this.replace(NotLF, 'n');
    }
    
    var lines = body.split('n');
    return lines[line-1];
}

次に正規表現文字をクオートする関数を追加しておきます。PHPのpreg_quoteと同じ仕事をしてくれます。

function preg_quote(str, delimiter){
    // Quote regular expression characters plus an optional character  
    // 
    // version: 1107.2516
    // discuss at: http://phpjs.org/functions/preg_quote
    // +   original by: booeyOH
    // +   improved by: Ates Goral (http://magnetiq.com)
    // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    // +   bugfixed by: Onno Marsman
    // +   improved by: Brett Zamir (http://brett-zamir.me)
    // *     example 1: preg_quote("$40");
    // *     returns 1: '$40'
    // *     example 2: preg_quote("*RRRING* Hello?");
    // *     returns 2: '*RRRING* Hello?'
    // *     example 3: preg_quote("\.+*?[^]$(){}=!<>|:");
    // *     returns 3: '\.+*?[^]$(){}=!<>|:'
    return (str + '').replace(new RegExp('[.\\+*?\[\^\]$(){}=!<>|:\' + (delimiter || '') + '-]', 'g'), '\$&');
}

最後に指定行のみを置換する関数です。Stringクラスに追加しました。

String.prototype.replaceTargetLine = function(org, dest, line){
// 何行目の置換かを指定できる
    var NotLF = /rn|r/g;
    var preg = new RegExp(preg_quote(org));
    if ( !line ) {
        return this.replace(preg, dest);
    }

    var str = this.getStrfromLine(line);
    if ( str ){
        var body = this;
        var CRLFs = this.match(NotLF);
        if (CRLFs) {
            body = this.replace(NotLF, 'n');
        }
        var lines = body.split('n');
        lines[line-1] = str.replace( preg , dest );
        return lines.join('n');
    } else {
        alert(line+"行目にn"+org+"nは存在しません。");
    }
}

使い方は下のようになります。str内の文章の10行目の最初のbeforeをafterに置換します。

str.replaceTargetLine( 'before' , 'after' , 10 );

 

プログラミングで悩んだ時は

93%の回答率が売りのエンジニアのための無料Q&Aサイト「teratail」。長く悩んでも答えが出ない時の為に、登録しておけば助かるかもしれません。