knowledge base

マークアップ/フロントエンドエンジニアのWEB制作における備忘録です。平日はWEB屋、休日は社会人劇団の主宰・劇作家をしています。

関数内でその関数自身を取得実行する

関数内で、その関数自身の情報を得たいなと思っていました。

次の方法で可能となります。

var functionA = function(){
   console.log(arguments);
}

argumentsというオブジェクトは、本来名前の通り関数に渡された引数を参照できる配列に似たものです。

もし関数に3つの引数が渡されたなら、arguments[0], arguments[1], arguments[2]という具合に参照ができます。

arguments.callee

さて、argumentsにはlengthとcalleeというプロパティがあり、lengthは引数の数、calleeは現在実行している関数の関数本体を示します。

今回用いるのは、まさしくこのarguments.calleeというプロパティです。

このarguments.calleeにはいくつかプロパティがあり、関数名などを取得することができます。

var functionA = function(){    
   console.log(arguments.callee); /* 関数そのものを出力 */
   console.log(arguments.callee.name); /* 関数名(functionA)を出力 */
}

arguments.calleeは関数本体を参照するため、呼び出すことが可能です。

たとえば、無名関数内で再帰処理をしなければいけない時などに利用できます。

ちなみに下記コードは再帰処理を終わらせるための分岐を書いていないので、無限ループになります。あしからず。

function(){
  /* 関数名を与えなくても再帰呼び出しできる */ arguments.callee(); }

また、自分自身をイベントにon/offすることもできます。

下記例ではmyEventというカスタムイベントに一度関数を紐づけたのち、その関数実行後にイベントとの紐づけを解除しています。

myEventというイベントは何度も起こりえますが、それに紐づけた関数onMyEventFuncは一度のみの実行でよい場合に使えます。 (※ちなみにカスタムイベント作成のプログラムは省略しています)

var onMyEventFunc = function(){
    $(window).off('myEvent', arguments.callee );
}
$(window).on('myEvent', onMyEventFunc );

余談

しかしながら、strictモードの際にこのプロパティを参照するとエラーになるそうです。

紹介した割に、最近は非推奨になっているという悲しい結末で申し訳ありません。

どうやら引数に対して操作を行えるということが、あまりよろしくないそうです。

strictモードでは、関数名を与えたり、引数を別の変数で受け取る等の代替案をとらなければいけません。

JavaScriptを用いずにクリックを無効にする

クリックを無効にするには、JavaScriptを用いる手法をよく見かけますが、CSSでも実装が可能です。

a{
  pointer-events: none;
}

対応ブラウザはモダンブラウザが中心となっているそうです。

なぜこのようなプロパティがあるのだろうと思ったのですが、どうやらSVGと併用するといろいろなことが実現できるそうです。

以下で解説されているように、SVGのためのプロパティがいくつか用意されています。

developer.mozilla.org

背景色や背景画像も印刷したい

ブラウザのもつ印刷機能では、デフォルトで背景色や背景画像を印刷しないことになっています。

画像をHTML上に直書きするのも一つの手段ですが、セマンティックなHTMLドキュメントを書こうとするとどうしても限界があります。

IEの各バージョン / Firefox / Opera ではブラウザ側の設定で印刷が可能ですが、ユーザー側の設定に関わらず必ず印刷させたい場合があります。

そんなときは、webkit系でのみ下記スタイル指定によって可能となります。

@media print {
   body {
      -webkit-print-color-adjust: exact;
   }
}

FireFoxでinline-blockがカラム落ちする

inline-blockとletter-spacingを同時指定すると崩れる

たとえば下図のようなレイアウトを実装すると想定します。

f:id:ShinImae:20160107205014j:plain

リストを横並びにした、シンプルなレイアウトです。

これをinline-blockを用いて横並びにすると、なぜかFireFoxだけカラム落ちしてしまいました。

f:id:ShinImae:20160107205029j:plain

実はこれ、inline-block間の隙間を削除するためにlette-spacingで調整をしています。

HTMLは下記の通りです。

<ul class="menu">
    <li class="menu__item">Nav Lorem</li>
    <li class="menu__item">Nav Lorem</li>
    <li class="menu__item">Nav Lorem</li>
    <li class="menu__item">Nav Lorem</li>
</ul>

そして、CSSは下記の通りです。

.menu{
    letter-spacing: -0.4em;
}
  .menu__item{
      display: inline-block;
      letter-spacing: normal;
  }

inline-blockを横並びにすると、改行コードが原因で存在しないはずの隙間が生まれてしまいます。

そのため、letter-spacingを利用して要素同士を隙間なく接するようにするテクニックが存在します。

ところがinline-blockのみの指定では崩れず、どうやらletter-spacingと併用するとこのようなカラム落ちが発生してしまいます。

ブラウザによってinline-blockの実装方法が違うという説もありますが、実際のところ明確な理由ははっきりしていないそうです。

そこで用いられるのが、word-spacingというプロパティです。

単語の間隔を指定するためのプロパティなのですが、letter-spacingと同時に指定することで、崩れを防ぐことができるようです。

指定するのは0.1emでなくてもよいです。とりあえず正の値を指定すれば崩れることがなくなるようです。

.menu{
    letter-spacing: -0.4em;
    word-spacing: 0.1em;
}
  .menu__item{
      display: inline-block;
      letter-spacing: normal;
  }

もし同時に指定することに抵抗があれば、FireFoxのみ有効なCSSハックを用いて指定すればよいでしょう。

.menu{
    letter-spacing: -0.4em;
}
  .menu__item{
      display: inline-block;
      letter-spacing: normal;
  }
@-moz-document url-prefix() {
  .menu{
      word-spacing: 0.1em;
  }
}

transitionのコールバックを実装する

transitionが終了したら実行されるようにしたい

CSS3のプロパティであるtransitionを用いてアニメーションをさせるとき、transition終了時にコールバックを実装したいときがあります。

jQueryのanimateメソッドならば、簡単にコールバック関数を指定できるのですが、transitionを用いた場合はどのようにコールバックを実装したらよいでしょうか。

スタイルシート上では設定できませんが、JavaScriptにて制御ができます。 transitionendというイベントにバインドさせることで、それが可能です。

下記は、水平方向に300px移動した後にアラートを出力するプログラムです。(ベンダープレフィックスを取得するgetPrefix関数を独自に作成していると仮定します)

/* ベンダープレフィックスを得る */
var prefix = getPrefix();

/* cssメソッドにわたすスタイルを格納 */
var animObj = {};
animObj[prefix + 'transform']  = 'translate3d(300px, 0px, 0px)';
animObj['transition'] = prefix + 'transform 400ms swing';

/* アニメーション */
$(ELEMENT).css(animObj);

/* transition終了時 */
$(ELEMENT).on('transitionend', function(){
   alert('moved');
});

このイベント、ブラウザによって実装が異なるため、より確実にバインドしたいならば、以下のように各ブラウザでのイベント名を列挙してしまったほうが無難です。

webkitTransitionEnd 
MozTransitionEnd 
mozTransitionEnd 
msTransitionEnd 
oTransitionEnd 
transitionEnd 
transitionend

ちなみに、別の文脈において同じ要素に複数transitionが指定されているときは注意が必要です。

他の文脈でtransitionが終了したときも同じ処理が実行されてしまうため、以下のようにコールバック処理の中でいちどアンバインドさせてください。

/* ベンダープレフィックスを得る */
var prefix = getPrefix();

/* cssメソッドにわたすスタイルを格納 */
var animObj = {};
animObj[prefix + 'transform']  = 'translate3d(300px, 0px, 0px)';
animObj['transition'] = prefix + 'transform 400ms swing';

/* アニメーション */
$(ELEMENT).css(animObj);

/* transition終了時 */
$(ELEMENT).on('webkitTransitionEnd MozTransitionEnd mozTransitionEnd msTransitionEnd oTransitionEnd transitionEnd transitionend',function(){
   alert('moved');
   $(this).off('webkitTransitionEnd MozTransitionEnd mozTransitionEnd msTransitionEnd oTransitionEnd transitionEnd transitionend');
});

Android 2.3でスクロールを実装する

Android2.3デフォルトブラウザにはiframeに関連するバグだけでなく、他にもスクロールにまつわるバグがあります。

今回はoverflow:scrollにまつわる現象について、その解決方法をご紹介したいと思います。

要素からあふれた部分を表示するoverflowプロパティ

有名な話ですが、Android2.3でoverflow: hiddenは未対応です したがって要素からあふれた部分は問答無用で非表示になり、大きくユーザビリティを損なってしまいます。

スマートフォン表示時の構成を再考するのが抜本的な解決策ですが、たとえばモーダルウィンドウ内のように限られたスペース内でスクロールさせなければいけない場合も少なくはないでしょう。

そのようなときは、JavaScriptの力を借りれば辛うじて対応が可能です。

touchstart/touchmoveを利用する

幸いなことに、Android 2.3でもtouchイベントには対応しているため、これを利用します。

タッチしたときの位置と、スワイプ中の位置を常に取得することで、疑似的なスクロールを生み出します。

※ここではスクロールさせたい要素(本来ならばoverflow:scrollを指定する要素)に「overflowArea」というIDを付与しています。

var scroll_start_x = 0;
var scroll_start_y = 0;
$('#overflowArea').on({
   'touchstart': function(e) {
      scroll_start_x = e.originalEvent.touches[0].pageX;
      scroll_start_y = e.originalEvent.touches[0].pageY;
   },
   'touchmove': function(e) {
      var scroll_end_x = e.originalEvent.touches[0].pageX;
      var scroll_end_y = e.originalEvent.touches[0].pageY;
      $(this).scrollTop($(this).scrollTop() - (scroll_end_y - scroll_start_y));
      $(this).scrollLeft($(this).scrollLeft() - (scroll_end_x - scroll_start_x));
   }
});

これでスクロールは実装できるのですが、ところが上記のコードだけでは動きがどことなく固くなってしまいます。

どうやらtouchmoveイベントにはクセがあるため、その調整をする必要があります。

こちらの記事で解説されているとおり、連続的に発生すると思われているtouchmoveイベントはAndroidでは一回しか発生しないそうです

 

FFBlog Webサイトでスマホ・タブレットのスワイプ動作(touchmove)イベントを確実に取得するには | 株式会社KSK フレックス・ファームビジネスユニット

 

そこで、もともとのスワイプ動作を抑止する必要があるため、上記コードのtouchmove時の処理を次のように変更します。

var scroll_start_x = 0;
var scroll_start_y = 0;
$('#overflowArea').on({
   'touchstart': function(e) {
      scroll_start_x = e.originalEvent.touches[0].pageX;
      scroll_start_y = e.originalEvent.touches[0].pageY;
   },
   'touchmove': function(e) {
      e.preventDefault();
      var scroll_end_x = e.originalEvent.touches[0].pageX;
      var scroll_end_y = e.originalEvent.touches[0].pageY;
      $(this).scrollTop($(this).scrollTop() - (scroll_end_y - scroll_start_y));
      $(this).scrollLeft($(this).scrollLeft() - (scroll_end_x - scroll_start_x));
   }
});

このようにすることで、スムーズなスクロールが実装可能です。

ちなみにAndroid4からはoverflow:scrollに対応しているため、できればバージョンが4以下のときのみ上記の処理を実行するようにすると、なおよいかと思います。

Androidのユーザーエージェントは以下のとおりなので、ここからバージョンを取得します。

Mozilla/5.0 (Linux; Android 4.4.2; ・・・

Androidという文字列から8文字分取得し、比較演算子によるバージョン比較をしやすいように文字列を数値に変換します。

var ua = navigator.userAgent.toLowerCase(); 
if(ua.indexOf('android') != -1 ){
   var version = parseFloat(ua.slice(ua.indexOf('android')+8));
   if(version < 4){
       return true; 
   }
}

上記コードにおいてtrueを返している箇所に、さきほど紹介した疑似スクロールのコードを書くことで、デバイスのOSバージョンが4以下のときのみスクロールを実装できるようになります。

iScrollは使用しない

最後に余談ですが、iOS4以下で固定表示やスクロールを実装することで有名なiScrollというJavaScriptプラグインがあります。

Android2.3の実機で使用した際にブラウザがクラッシュしてしまったことがあるので、ご利用は自己責任でお願いいたします。

Android 2.3で画面がフリーズする

iframeを使っていませんか

レガシーなIEと同じくらい不具合の多い旧式Androidデフォルトブラウザですが、まれにファーストビューのまま画面がフリーズしてしまうことがあります。

そのようなときはまず、iframeを使用していないか疑ってください。

iframeを使用していると、次のいずれかの条件でスクロールが固まってしまうそうです。

  • iframeそのものが、cssで非表示になっている。
  • iframeを包含する要素が、cssで非表示なっている。
  • iframeそのものが、JSによりロード時に非表示にされている。
  • iframeを包含する要素が、JSによりロード時に非表示にされている。

たとえば複数のiframeをボタン押下で表示切り替えするときなど、このバグが発現する格好の条件になってしまいます。

解決策は、display:noneやhide()を使用しないことです。

やりかたは様々ですが、非表示にする代わりにユーザーから見えない場所に配置する等すれば、このバグは解決できます。