見出し画像

Twitter風入力文字数チェック


今日のjQueryTipsは、Twitter風文字数カウントです。

Twitterをやってる人ならご存知かもしれませんが、

文字を入力していくと、右下にドーナツ型のアイコンが出てきて、「あとどれくらい入力できるか」が視覚的にわかるようになってます。

スクリーンショット 2020-04-30 23.30.44


この機能が実装された当初は「わかりにくい」と否定的な意見もあったみたいですが、個人的にはお洒落で気に入ってます。


TwitterはReactで実装されてますがjQueryでもこの仕組みは作ることができます。ただ、完璧にコピーしようと思うと、もうちょっと頑張らないといけません。

気になる人は試してみてくださいな。


実装する機能を整理する

まず、どんな処理を書かないといけないのか、整理してみましょう。

・入力された文字数をリアルタイムで取得(半角は1全角は2で計算)
・取得した文字数に応じて、バーに色をつける
・未入力だったら送信ボタンは押せない
・既定の文字数を超えた場合、バーを赤色+送信ボタンを押せない



入力文字数をリアルタイムにチェックする


CSSでは特に特別なことはしておらず、TwitterのUIに近づけるのを頑張ったくらいです。

基本、jQueryをゴリゴリ書けばオッケー。

サクッとコピーしたい人はcodepenからどうぞ。


順番にみていきましょう。


入力された文字数をリアルタイムで取得(半角は1全角は2で計算)

ここでのポイントは、半角全角のチェックと入力テキストのリアルタイムチェックです。

まず、半角全角の判定用の関数を定義します。


  //カウント用の関数を定義
 function getLen(str){
   let result = 0;
   for(var i=0;i<str.length;i++){
     let chr = str.charCodeAt(i);
     if((chr >= 0x00 && chr < 0x81) ||
        (chr === 0xf8f0) ||
        (chr >= 0xff61 && chr < 0xffa0) ||
        (chr >= 0xf8f1 && chr < 0xf8f4)){
       //半角文字の場合は1を加算
       result += 1;
     }else{
       //それ以外の文字の場合は2を加算
       result += 2;
     }
   }
   //結果を返す
   return result;
 };


このコードはこちらからいただきました(コピペかよ!)


何をやっているかというと、charCodeAt() メソッドでUTF-16コード取得し、半角文字の判定を行います。

半角文字だった場合は1を返し、それ以外の場合は2を返してくれます。

これで半角全角のチェックはOK。


次に、入力文字のリアルタイムカウントです。

入力された文字をカウントするには、onメソッドのイベントタイプ「input」を使います。

$('.text').on('input', function(){
    // .textに入力された時に実行
});

この中でconsole.logを実行するとわかるのですが、指定された要素にテキストが入力された時に実行されます。

つまり、要素のvalueが変化するたびに発生するイベントです。


ざっくり変数宣言をしますが、ここで、さっき定義した関数を使って、半角全角のチェックをして文字数を取得しておきます。

     let cnt = getLen($(this).val())   //文字数を取得


リアルタイムに文字数を表示させる方法はこれでOK。

     $('.now_cnt').text(cnt);//現在の文字数を表示


入力するたびに、.now_cntの文字数が変化するはずです。



取得した文字数に応じて、バーに色をつける


さて、文字数を取得できたところで、肝心のTwitterのようなバーを表示させましょう。

この部分は、SVGを使って実装しています。

 <div class="circle_wrap">
   <svg height="100%" viewBox="0 0 20 20" width="100%" style="overflow: visible;">
     <circle cx="50%" cy="50%" fill="none" stroke-width="2" r="9" stroke="#38444D"></circle>
     <circle id="circle" cx="50%" cy="50%" fill="none" stroke-width="2" r="9" stroke="#1DA1F2" stroke-linecap="round" style="stroke-dashoffset: 56.3; stroke-dasharray: 56.5;"></circle>
   </svg>
 </div>


SVGについては、こちらの記事が参考になります。


LIGさんは流石という感じ。


こっちは英語ですが、動きがよくわかるのでイメージが掴みやすい。


svgのcircleで円を書いて、以下のスタイルを文字数に合わせて増減させることで、Twitterのようなバーガ表現できます。

style="stroke-dasharray: 56.5;"


SVGじゃなくても多分できますが、Twitterがsvgで作られてたので、本家に近づけてみました。


あとは、変数として、マックスの文字数がバーの最大値になるよう計算し、文字数単位の数字を割当てます。

     let cnt = getLen($(this).val())   //文字数を取得
     let cnt_bar = cnt * 0.1999;     //文字数を元に計算
     let dash_array = 56.5 + cnt_bar;  // グラフに反映する計算式
     let circle = $('#circle');
     let submit_btn = $('.submit_btn');
     /* 280文字でいい感じに100%になる計算 */


そしてこう書いてあげれば、valueが変更されるごとにinputイベントが実行され、その度にstroke-dasharrayの値が書き換わるという寸法です。

      // stroke-dasharrayにdash_arrayの数値を反映
     circle.css('stroke-dasharray',dash_array);


ここまでで、文字数をリアルタイムにカウントしつつ、バーに反映する動きが完成しました。

これだけじゃちょっと不十分なので、バリデーションチェックの処理も追加しておきましょう。


未入力、既定の文字数を超えた場合、送信ボタンを押せない

ちょっと長くなりますが、jQueryのif文の基礎を抑えておけば簡単です。一気に書いてしまいましょう。

慣れないうちは、日本語で整理してから書いていくと作りやすいです。

0文字
・ボタン無効
・circleは隠す
1文字以上280文字以内
・circle表示
・ボタン有効化

280文字以上
・circleを赤色
・文字色を赤色
・ボタン無効

これをif文にしたのがこちら。


      if(cnt == 0){                       //0文字
       submit_btn.prop('disabled', true);  // ボタン無効
       $('.circle_wrap').hide();           // circleを隠す

     } else if(cnt > 0 && 280 > cnt){            // 1文字以上、280文字以内
       $('.circle_wrap').fadeIn();               // circleを表示
       submit_btn.prop('disabled', false);       // ボタン有効化
       $('.cnt_area').removeClass('cnt_danger'); // 文字色を戻す
       circle.attr('stroke','#1DA1F2');          // errorから復帰した時、circleの色を青に戻す

     } else {                                  // 280文字を超える場合
       circle.attr('stroke','red');            // circleを赤色
       $('.cnt_area').addClass('cnt_danger');  // 文字色を赤色
       submit_btn.prop('disabled', true);      // ボタン無効
     }
   });


以上、これで完成かと思いきや、

入力が始まってからcircleを表示させたいのに、初めから表示された状態になってしまっています。

これは、inputイベントの中で「0文字の時はcircleを非表示にする」という処理が書かれているからです。

ただ画面を表示しただけでは、処理は実行されません。


ここで、triggerメソッドを使い、inputイベントを手動で実行させてあげます。

    });
   
   // triggerメソッドで、とりあえずinputイベントを手動で発生させる。
   // リロード時に初期文字列が入っていた時の対策 + circleを非表示にしたい
   $('.text').trigger('input');
 
});

こうすることで、画面が表示された時に、一旦inputイベントの中身が走ります。すると、.textのvalueは当然0なわけですから、circleは非表示になってくれる。


はい、以上です。

完成したコードをみてみましょう。


あとやってみたいのが、「制限文字数を超過したテキストに背景色をつける機能」ですが、これはtextareaでは難しそうな感じがしてます。

超過した文字数をチェックして、spanで囲うってのはできそうなのですが、textareaだと<span>がそのまま入ってしまいます。。

気になる方はぜひ挑戦してみてください!(そして僕に教えてくださいw)


一応、全てのコードを載せておきますね。


HTML

<div class="container">
 <textarea  class="text" placeholder='いまどうしてる?'></textarea>
 <div class="submit_wrap">
   <div class="circle_wrap">
   <svg height="100%" viewBox="0 0 20 20" width="100%" style="overflow: visible;">
     <circle cx="50%" cy="50%" fill="none" stroke-width="2" r="9" stroke="#38444D"></circle>
     <circle id="circle" cx="50%" cy="50%" fill="none" stroke-width="2" r="9" stroke="#1DA1F2" stroke-linecap="round" style="stroke-dashoffset: 56.3; stroke-dasharray: 56.5;"></circle>
     </svg>
 </div>
   <div class="cnt_area"><span class="now_cnt">0</span> / 280</div>
   <button type="button" class="submit_btn">ツイートする</button>
 </div>
</div>
 


SCSS

// リセット
button {
 display: inline-block;
 cursor:pointer;
 border:none;
 box-sizing:border-box;
}

textarea {
 width: 100%;
 box-sizing:border-box;
 border:none;
 resize: none;
}


//fontとbackgroundをtwitterっぽく
body {
 background-color: #16202C;
 font-family: Arial, "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", Osaka, メイリオ, Meiryo, "MS Pゴシック", "MS PGothic", sans-serif;
}

// コンテナ
.container {
 margin: 30px auto;
 width: 400px;
 padding: 10px;
 border:1px solid rgb(56, 68, 77);
 border-radius:16px;
}

//テキストエリア
.text {
 color:#fff;
 font-size: 20px;
 padding: 10px;
 background-color: #16202C;
 
   &::placeholder {
     color:#777;
   }

   &:focus {
     border:none;
     outline:none;

     &::placeholder {
     color:#eee;
   }
 }
}


//下部のwrap
.submit_wrap {
 display: flex;
 justify-content:flex-end;
 align-items:center;
 margin-top: 15px;
}

//文字数カウント
.cnt_area{
 text-align: center;
 width: 100px;
 color:#fff;
 
 &.cnt_danger{
   color: red;
 }
}

// グラフのwrap
.circle_wrap {
 position:relative;
 top: 0;
 right: 0;
 height: 30px;
 width: 30px;
 transform: rotate(-90deg);
}

//ツイートするボタン
.submit_btn {
 background-color: #1CA1F2;
 padding: 10px 20px;
 border-radius:50px;
 color:#fff;
 font-weight: bold;
 font-size: 16px;
 
   &:disabled {
   opacity:0.6;
 }
}


JavaScript(jQuery)

$(function(){

 //カウント用の関数を定義
 function getLen(str){
   let result = 0;
   for(var i=0;i<str.length;i++){
     let chr = str.charCodeAt(i);
     if((chr >= 0x00 && chr < 0x81) ||
        (chr === 0xf8f0) ||
        (chr >= 0xff61 && chr < 0xffa0) ||
        (chr >= 0xf8f1 && chr < 0xf8f4)){
       //半角文字の場合は1を加算
       result += 1;
     }else{
       //それ以外の文字の場合は2を加算
       result += 2;
     }
   }
   //結果を返す
   return result;
 };
 
 //入力時のイベント 
   $('.text').on('input', function(){
     let cnt = getLen($(this).val())   //文字数を取得
     let cnt_bar = cnt * 0.1999;     //文字数を元に計算
     let dash_array = 56.5 + cnt_bar;  // グラフに反映する計算式
     let circle = $('#circle');
     let submit_btn = $('.submit_btn');
     
     // stroke-dasharrayにdash_arrayの数値を反映
     circle.css('stroke-dasharray',dash_array);
     $('.now_cnt').text(cnt);//現在の文字数を表示

     if(cnt == 0){                       //0文字
       submit_btn.prop('disabled', true);  // ボタン無効
       $('.circle_wrap').hide();           // circleを隠す

     } else if(cnt > 0 && 280 > cnt){            // 1文字以上、280文字以内
       $('.circle_wrap').fadeIn();               // circleを表示
       submit_btn.prop('disabled', false);       // ボタン有効化
       $('.cnt_area').removeClass('cnt_danger'); // 文字色を戻す
       circle.attr('stroke','#1DA1F2');          // errorから復帰した時、circleの色を青に戻す

     } else {                                  // 280文字を超える場合
       circle.attr('stroke','red');            // circleを赤色
       $('.cnt_area').addClass('cnt_danger');  // 文字色を赤色
       submit_btn.prop('disabled', true);      // ボタン無効
     }
   });
   
   // triggerメソッドで、とりあえずinputイベントを手動で発生させる。
   // リロード時に初期文字列が入っていた時の対策 + circleを非表示にしたい
   $('.text').trigger('input');
 
});


少しでも参考になれば幸いです。

「ここ間違ってるよー!」というご意見ご感想大歓迎です。

サポートいただければ嬉しいです。