マウスオーバーすると親要素の高さが変化する IE のバグ

a タグにマウスを重ねると、それを内包する要素の高さがベコッと広がったまま戻らなくなるバグは、古代より受け継がれてきた由緒ある IE のバグだ。「IE hover 高さ」などでググると、このバグに悩まされている人たちがたくさん引っかかる。このバグは、 IE 10 の現代に於いても現存していて、生きた化石と言ってもよいかと思う。

このバグは再現条件が限られるし、厄介な割にはつい忘れがちなものなので、プロジェクトも終わりに差し掛かっているところで発見されることも少なくない。しかも色々な組み合わせで発生するみたいで、どのサイトも書いてある再現条件が違う。今回は全然違った条件で引っかかりました。以下が再現された HTML 。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>IEでガタつく</title>
<style>
  #wrapper {
    width: 200px;
    padding-bottom: 50px;
    min-height: 100px;
    box-sizing: border-box;
    float: left;
  }

  a:hover {
    color: red;
  }
</style>
</head>
<body>
<div id="wrapper">
  <div>
    <a href="#">hogehoge</a>
  </div>
</div>
<div style="clear:left;">-----------------------------</div>
</body>
</html>

発生条件

書かれているスタイル指定はすべて再現条件に含まれます。つまり、

  • 幅が指定されていて
  • 上下の padding もしくは border がどちらか一方、または両方指定されていて
  • min-height が指定されていて
  • box-sizingborder-box
  • float が指定されている

という条件を満たす要素の孫以下の要素で、

  • マウスオーバー時に見た目の変化がある

と、 #wrapper に当たる要素の高さが、 padding-top + padding-bottom + border-top-width + border-bottom-width の分だけ伸びる模様。なんともややこしい……。なお、コンテンツ量が min-height を超えるほどあった場合、バグは発生しない。こういう複雑な条件を満たさないと発現しないバグは、気をつけていてもなかなか取りきれない。 min-heightbox-sizing: border-box は同時に指定する時は要注意ということだ。

解決策

いくつかの解決策がある。

発現条件を回避する

一番わかりやすい解決方法は、 #wrapper に当たる要素から、発現条件であるプロパティのいずれかを除去すること。 box-sizing は必須じゃないかもしれないし、 padding-top, padding-top は別の要素のマージン等で代用できるかもしれない。しかし、 HTML 構造によってはこういう融通を利かせられないことがある。

::before, ::after を使う

これはなかなかスマートな手法かもしれない。 ::before::after の擬似要素を用いることで、上下 padding の指定を除去しようとする試み。

#wrapper {
  width: 200px;
  /* padding: 20px 0 30px; */
  padding: 0; /* 上下 padding の指定は無し */
  min-height: 100px;
  box-sizing: border-box;
  float: left;
}

#wrapper:before {
  content: "";
  display: block;
  height: 20px;
}

#wrapper:after {
  content: "";
  display: block;
  height: 30px;
}

これが一番楽かもしれない。だけど、上下に border が指定されていた場合は使えないですね。

JS でむりやり

かなりアホンダラなやり方。自分はこのやり方を試して、いちおう出来たけど、その後 ::before, ::after を使う手法を思いついたので採用しなかった。当然、ピクセルを使ってレイアウトしている時に限る。

$(function () {
  var $main, paddingTop, paddingBottom, borderTopWidth, borderBottomWidth, minHeight;
  if (isIE) {
    $main = $('#wrapper');
    paddingTop = parseInt($main.css('paddingTop'), 10);
    paddingBottom = parseInt($main.css('paddingBottom'), 10);
    borderTopWidth = parseInt($main.css('borderTopWidth'), 10);
    borderBottomWidth = parseInt($main.css('borderBottomWidth'), 10);
    minHeight = parseInt($main.css('minHeight'), 10);
    $main.css({
      'box-sizing': 'content-box',
      'width': $main.width(),
      'min-height': minHeight - paddingTop - paddingBottom - borderTopWidth - border-BottomWidth
    });
  }
});

どうしても他に対応策がないときの最終手段といえましょう。

input type="file" なやつを独自にデザインする方法

デモ

HTML

<div class="file">
  ファイルを選択
  <input type="file"/>
</div>

CSS

.file {
  display: inline-block;
  overflow: hidden;
  position: relative;
  padding: .5em;
  border: 1px solid #999;
  background-color: #eee;
}

.file input[type="file"] {
  opacity: 0;
  filter: progid:DXImageTransform.Microsoft.Alpha(opacity=0);
  position: absolute;
  right: 0;
  top: 0;
  margin: 0;
  font-size: 100px;
  cursor: pointer;
}

対応ブラウザ

以下のブラウザで確認済み。 IE 6 もたぶん動くんじゃないだろうか。

簡単な解説

type 属性が fileinput 要素は厄介なもので、 button 要素などと違い CSS でスタイルを調整できない。しかしデフォルトの表現を変えたいという機会はままあるようで、このような置換テクニックが必要になってくる。ここで紹介するものは JavaScript は不要なもの。

基本的な考え方は、「input 要素を透明にして、上にかぶせる」というもの。これで、見た目は好きに調整できるし、クリックした時はデフォルトの動作が呼び出されるようになる。

ミソなのは以下の部分。

  right: 0;
  font-size: 100px;

これは IE 対策。IEでは <input type="file"/> の向かって左側をシングルクリックしただけでは、ファイル選択ダイアログは立ち上がらない。そのため、向かって右側をむりやりクリックさせるための努力が上記の CSS 指定。

取るに足らないテクニックだけれども、ネットで得た情報だと JavaScript まで使って超めんどい感じに実装してたりしたので、メモった。

もっと良いまとめがあった

http://devadjust.exblog.jp/16866976

上記記事だと、 JavaScript を使ってキーボード操作まで出来るようにしていた。なるほど。

Backbone.js で events とか defaults とかを親クラスから継承する方法

events とか default とは何か

Backbone.js のクラス定義において、特殊なパラメーターがいくつか存在している。たとえば eventsdefaults は、 Backbone.js のチュートリアル的文書ではまっさきに触れられている特殊なパラメーターだ。

例えば、 defaults は Model の特殊パラメーターで、指定しておくことでクラスをインスタンス化した際に、デフォルトの値として使われるようになる。

var Person = Backbone.Model.extend({
  defaults: {
    name: 'John Doe',
    age: 25
  }
});

var person = new Person({ age: 100 });
person.get('name'); // 'John Doe'
person.get('age');  // 100

この events とか defaults は、実はオブジェクトを返す関数に置き換えることもできる。

var Person = Backbone.Model.extend({
  defaults: function () {
    return {
      name: 'John Doe',
      age: 25
    }
  }
});

var person = new Person({ age: 100 });
person.get('name'); // 'John Doe'
person.get('age');  // 100

関数はクラスがインスタンス化される時(initialize メソッドが呼ばれる前)に実行される。ではこの「関数に置き換えることができる」という特性を何に使うかというと、たとえば、インスタンスごとのユニークな ID を割り振りたい時なんかに使える。下記の例は ID ではなく年齢をランダムにするコードだけど、雰囲気は伝わるかと思う。

var Person = Backbone.Model.extend({
  defaults: function () {
    return {
      name: 'John Doe',
      age: 20 + Math.floor(Math.random() * 30)
    }
  }
});

var person = new Person();
person.get('age');  // 20 以上 50 未満のランダムな整数

var person2 = new Person();
person2.get('age');  // 20 以上 50 未満のランダムな整数

直面する問題

ここからが本題。クラスの継承を使っている場合、これらの特殊パラメーターは、親が持つ同名のパラメーターをうまいこと継承してくれないという問題にぶち当たる。 Person を継承した SuperMan は、 Person のデフォルト値を継承して欲しい。

var SuperMan = Person.extend({
  defaults: {
    power: 530000
  }
});

var superMan = new SuperMan({ name: 'Clark Kent' });
superMan.get('name');  // 'Clark Kent'
superMan.get('age');   // undefined (!)
superMan.get('power'); // 25

このように、 age は継承されずに undefined となってしまう。そこで継承のやり方を考えた。

events とか defaults を継承するスニペット

こうです。

var SuperMan2 = Person.extend({
  defaults: function () {
    return _.defaults({
      power: 530000
    }, _.result(Person.prototype, 'defaults'));
  },
  say: function () {}
});

ここで使っている特殊な関数は、 _.defaults および .result の2つ。いずれも Underscore.js が持っているメソッドだ。 _.defaults はあるオブジェクトのデフォルト値を設定したいときに役に立つメソッド。_.defaults の公式ドキュメント_.result は指定したプロパティの示す値が関数ならば実行した返り値を、そうでなければそのまま返してくれるメソッド。_.result の公式ドキュメント。そんなに難しいことはしていないです。

こんなふうにして、特殊パラメーターを親クラスから継承しつつ、値を設定することができた。

使いどころ

そもそもこういう、「オブジェクトを指定するところだけど、実は関数も指定できる」箇所ってどこなの? という疑問が湧く。実際のところ、以下の部分で使える。

  • Model.defaults
  • Model.urlRoot
  • Model.url
  • Collection.url
  • View.events
  • View.options
  • View.attributes
  • View.id
  • View.className
  • View.tagName
  • View.el

こんな感じに出てきた。これらには、オブジェクトや文字列ばかりじゃなくて、関数が指定できるっていうことですね。知っておくと細かい融通が利かせられるようになりそう。

ここまでのまとめ

フロントエンド技術者が会社とどう関わっていけばいいのかという話

今日はフロントエンドの仕事について考えていることを書く。フロントエンドの人間としてどのように会社と関わればいいのか、あるいはフロントエンド以外の人達に何を求めるか、そのような人たちとどうコミュニケーションをとって行ったらいいのか、などという事を中心に。

結論としては、やっぱりチームとしてモノを作っている意識を、みんなで持とうよということだった。

わたくしダーシマはネコメシに入る前、たぶん平均的な受託系ウェブ制作会社で2年半くらい仕事をした。フロントエンドとバックエンドが主で、ほんの少しだけディレクション業務もかじった。そこでは分業制が基本だった。ディレクターがクライアントと要件を詰め、デザイナーが絵を作り、場合によってはプログラマーがバックエンドをつくり、コーダーがコーディングをする。いわば、きちんとしたウォーターフォールでプロジェクトを進めていた。一人で複数の役割を担うことはあれど、世の中のウェブ制作会社としてはたぶん当たり前の制作フローで作っていたのではないかな。

では実際に、このやり方が全く滞りなく遂行されていたかというと、やっぱりそんなことはなかった。前職場の強みは、ウォーターフォールの中にも更に手戻りが起こらない工夫を盛り込んでいるという点だが、それでもやり直しは発生していた。これはどうにか改善できるのだろうか。

今の自分の考えでは、まあやりようによってはある程度の改善はできるかもしれないが、だいたいの部分は「しかたのないこと」なんじゃないかと思っている。開発フローのモデルはいくつもあり、それぞれ案件に対する向き不向きはあるわけだけど、案件に最も向いているモデルを選択したところで、それで仕事が完璧に滞りなく進むようになるかというとそうではない。予期せぬことは必ず起きる。じゃあひたすら予防策をはりめぐらせる方にコストを割くのかっていうと、そんなことをしだしたら本末転倒だ。もはやものづくりではないよね。「もはやものづくりではない。」これは大事な意識のひとつだと思う。

そのような現実的な事情の中、フロントエンドの人間が一体どのように立ち回っていくのがよいのか、という話に至る。

第一は、作業者になってはいけないということだ。指示されたことだけをこなすのは簡単なことなんだけれども、少しでも軽くなるようにとか、運用が容易なようにとか、再利用性を確保しつつとか考えると、目には見えない所でできることはいくらでもある。既存のサイトの修正だったら、修正箇所とは違うけどなんか不整合を見つけたので「ついでに修正しときました」というのは素敵なことだと思う。そういうのを「よしなにする」ことこそ、フロントエンダーとしての(というよりむしろ、チームメイトとしての)価値なのではないか。

個人的な話に逸れるけども、前職に入社し、自分がフロントエンドの部署についた時、「よしなに」とは真逆の「作業者たれ」というのが部署の空気としてあった。砕いて言うと「問題をディレクターが把握できていないのが悪い」「余計なことをすることでコーダーの価値を下げるな」のような空気。余計なことをすることで価値が下がるという理論はつまり、あれもこれもやってくれるとディレクターに思わせるなと(=コーダーの仕事単価が安くなる)いうことなんだけども、この空気は自分にはなじまなかった。「もはやものづくりではない。」だった。話を戻します。

第二に、責任をとるということ。我々フロントエンド技術者は、うつりゆく技術パラダイムの中で常により良い選択をしていかなければならないわけだけど、何か社内の規定や慣習に無い、新しい技術を導入するときにはリスクが発生するのが普通だ。たとえば、実装を開始してみたけど特定のブラウザでうまく動かなかったいとか。ちょっと技術的な検証が必要でそれだけ余分に稼働しなければならないとか。リスクの見込みにもよるが、自分の中でそれらのリスクを受け止めるある程度のバッファを持つことが肝要だと思う。サビ残しろということなのかと問われると、ある意味ではそうなる。あらゆる責任を放棄して、「やってダメだったのは指示したお前のせい」にするのはチームメイトとして反則だと思う。

そして啓蒙するということ。ディレクターもデザイナーもだいたい、フロントエンド技術のことについては門外漢だ。印刷技術を知らない人がDTPデザインをできるワケがないように、HTMLのことを知らない人がウェブデザインをできるワケがないというのは正論。しかし現実、技術のことをまったくワカラン人がそのような職種で飯を食っている以上は、誰かが何かしらの手段で蒙を啓いていなかなければならない。社内に対して啓蒙活動ができるフロントエンダーはもはや神だ。


しかし、とも思う。フロントエンダー全員に上のような意識を持てと強要するのもまた違うと思う。というのも、会社とどう関わっていくかとか、自分の人生の中の仕事とはなんぞやということは、まったく人によるからだ。生活水準を落としてでも良いものを作りたいと思う人もいれば、堅実にキャリアを固めたい人もいるし、生活のために仕方なくやっている人もいる。だからいろんな人がいて、そこにべき論なんてものはないのだ。チームの中で自分がどういう立ち位置に居たいか、あるいは居るべきかと考えて、自分こそそういう役割だと思った人が上述したような心得を噛み砕いて、自分の中にハメ込めばいいものなんだと思う。そういう意味で、チームとしてモノを作っている意識を持ち、チーム内での自分のあるべき立場というものを認識しようよというリード文に戻るというわけでした。

ここまで書いてきて、まあわたくしごときは業界3年たらずのヒヨッコだし、前職でそのように動けていたのかというと35%くらいの出来だし、春日井おやびんCOOのような実装ディレクションは出来るようになりたいと思いつつ思っているだけで、説得力があるかというと微妙なところ。とはいえ今後の自分のためでもあるし、このへんの立ち回りで悩んでる知人のためでもあるし、見ず知らずの誰かの背中をそっと押してあげられるような文章になっているといいのだけど。

株式会社ネコメシでは意識の高いフロントエンド技術者を求めています!!

グラデーションな背景に乗っているモノを比較的綺麗に切り抜く小技

デザインカンプが上がってきたけど、 PSD じゃなくて PNG だった……。とか、 PSD だけど肝心のレイヤーが結合されてて……。みたいなことは、業務でマークアップに携わったことのある人ならば経験があるはず。常に時間のないマークアップエンジニアにとって、こういう窮地に立たされた場合にもへこたれず、敢然と立ち向かう勇気が必要です。

この間の社内勉強会で少し評判のよかった画像切り抜き方法をひとつ紹介します。思いつく人はいるとおもうけど、自分で編み出した手法なので、広く知られた技術ではないと思う。

やりたいこと

f:id:tsmd:20130219020121p:plain

↑これを

f:id:tsmd:20130223013114p:plain

↑こうしたい。ちょっとジャギっていますが問題なし。なぜ問題ないかというと、大体のケースでこれで十分だからです。背景をごっそり差し替えるケースでジャギがあると問題ですが、そういう想定ではないということで。それに、中途半端にアルファチャンネル(半透明)を使って切り抜きをしようとすると、肝心の前景がちょっと汚くなったりすることが多い。

どう切り抜くか。愚直にやれば自動選択ツールでちまちま選択範囲を広げていく手法をとるけれども、今回紹介する手法を使えば、条件さえ合えば綺麗にかつ素早く切り抜けます。

手順

  1. 切り抜きたい画像を開く。開いた画像は背景レイヤーとなっているので、通常のレイヤーに変換する。レイヤーを複製しておく。

    f:id:tsmd:20130223013950p:plain

  2. グラデーションを構成するパターンに当たる部分を選択して、コピー。

    f:id:tsmd:20130223014743p:plain

  3. コピーしたものを貼りつけると新しいレイヤーができる。自由変形ツール(Ctrl+T)を使って、貼りつけたパターンを引き伸ばす。今回は縦方向のグラデーションなので横に引き伸ばした。

    f:id:tsmd:20130223013020p:plain

  4. ここがミソ。引き伸ばしたパターンのあるレイヤーの描画モードを「差の絶対値」にする。そうすると、グラデーションだった部分が綺麗に真っ黒になる。

    f:id:tsmd:20130223014031p:plain

  5. あとは簡単。4番で描画モードを設定したレイヤーと、その下のレイヤーを結合する。自動選択ツールで許容値0、アンチエイリアスオフ、隣接オフとし、真っ黒の部分を選択。

    f:id:tsmd:20130223014032p:plain

    f:id:tsmd:20130223014041p:plain

  6. 不要な部分を削除。できあがり。

    f:id:tsmd:20130223014046p:plain

    f:id:tsmd:20130223013114p:plain

注意点など

条件が整った場合にのみ使えるテクニックです。

  • 元となる画像が可逆圧縮系のフォーマット(つまりPNGかGIF)であること。
  • グラデーションが縦か横90度単位で進行していること。
  • グラデーションや減色時にディザが使われていないこと。

ぼやけてたり影が落ちている前景には向きません。

あくまでも、背景のグラデーションと同じ色の部分を消す技術です。

前景で真っ黒(#000000)が使われている場合、自動選択がうまくいきません。

最初にレベル補正などで真っ黒の部分をなくしてから上記手順を行い、選択範囲を保存してからやり直せばオッケイ。

ていうかPSDをもらいましょう。

この技は最終手段として使いましょう。