SafariとChromeの判別があやしくなったので改善してみる2011年08月25日 20時10分51秒

夏も終わり間近というこのタイミングで今年最初のエントリになっちゃうんだけども、デスクトップ版Safariが5.1になったことで以前書いた「SafariとChromeを判別してみる」の方法が通用しなくなっちゃったのであわてて更新してみる。ほんとは中華パッド3号とか4号とかのこと書きたかったんだけども。

前回のおさらい

前回のアプローチは、SafariとChromeでwindow.constructorが違っている(SafariはObject、ChromeはDOMWindow)ことを利用してこんなアプローチをしてた。

// Safariはtrue、Chromeはfalseを返す関数
<script type="text/javascript">
window.check1 = function() {
  try {
    return window.constructor.prototype.alert == undefined;
  } catch(e) {
    // IEとかエラーでるし。
    alert(e.message || e.description);
  }
}
</script>

<button type="button" onclick="alert(check1())">check1</button>

ところが、Safari5.1からはwindow.constructorがDOMWindowConstructorとやらに変わっちゃったので、Chromeと判別されちゃう。さて、困りました。

Safari Developer Libraryを探したら

んで、急いでAppleのSafari Developer LibraryDocument Additions Referenceプロパティを確認したところ、「webkitCurrentFullScreenElement」「webkitFullScreenKeyboardInputAllowed」「webkitIsFullScreen」なんてのが見つかった。

念のためChromeのJavaScriptコンソールで「document.webkit」まで入力してタブキーで候補をさらってみたところ「Current~」とか「FullScreen~」、「Is~」とかは表示されなかったのでなんとなくで「webkitIsFullScree」を採用することに決定。

ただし、プロパティの名前からしてbooleanを返すだろうから、単純に

!document.webkitIsFullScreen

なんて試しても判別がつかないので、undefinedかどうかをテストすることにして、こんな感じに。
<script type="text/javascript">
window.check2 = function() {
  try {
    return window.constructor.prototype.alert == undefined ||
           document.webkitIsFullScreen !== undefined;
  } catch(e) {
    return e.message || e.description;
  }
}
</script>

<button type="button" onclick="alert(check2())">check2</button>

で、こうなりました。

この変更を適用すると、こんな感じに。前回同様is_webkitとis_mobileの判定が出来る前提で。

<script type="text/javascript">
// Safariかどうか
window.is_safari2 = (function() {
    // WebKitじゃなきゃ当然Safariじゃないわな
    if(! window.is_webkit) return false;
    if(window.is_mobile) {
        // モバイル版の場合、SVGをサポートしてたらSafari
        return !! window.SVGColor;
    } else {
        // デスクトップ版の場合、windowコンストラクタのプロトタイプで判別
        try {
            // * ココ修正!! *
            // どっちかtrueならSafari
            return window.constructor.prototype.alert == undefined ||
                   document.webkitIsFullScreen !== undefined;
        } catch(e) {
            // そもそもwindow.constructor とか そのプロトタイプにアクセスできない
            // ブラウザもあるので try - catch しとく
            return false;
        }
    }
})();

// Chromeかどうか
window.is_chrome2 = (function() {
    // WebKitじゃなきゃ(ry
    if(! window.is_webkit) return false;
    // 後は基本的にSafariと逆
    if(window.is_mobile) {
        return ! window.SVGColor;
    } else {
        try {
            // * ココ修正!! *
            return window.constructor.prototype.alert != undefined &&
                   document.webkitIsFullScreen === undefined;
        } catch(e) {
            return false;
        }
    }
})();

</script>

<button type="button" onclick="alert(window.is_safari2)">Safariっすか?(改)</button>
<button type="button" onclick="alert(window.is_chrome2)">Chromeっすか?(改)</button>

Chrome 13まではこれで大丈夫だけど、フルスクリーンがどうのこうのなんて機能はOS X Lion向けっぽいのでいずれこれでも判別できなくなっちゃいそうではあるが。

つか、そろそろ中華パッドネタ書いときたいなぁ。もうすでに旬をすぎちゃってるんだけども。

追記

Safari且つChromeとか判定されたり、前回の記事と同時に表示すると上書きされちゃったりなど細々と考慮漏れがあったりしたのでちくちく修正してました。もう平気かな。

追記2(2011.12.28)

ありゃ、気付いたらChromeもdocument.webkitIsFullScreenとかサポートしちゃってるしorz さて、どうするべ…

→ window.chrome が使えそう。なぜこれに気付かないし。

ブラウザで開いているページのURLをEvernoteにメールするブックマークレット2010年12月18日 06時26分37秒

需要があるかはわかりませんが...

半年くらい前からちょろちょろとEvernote使ってるんだけども、dara-j的にはWebの内容をクリップするのが主目的なんです。たとえばTumblrのダッシュボードに流れてきたQuoteの元記事見に行って気に入った部分があったらクリップするとか。FirefoxのWebクリッパーなんかはかなり出来がいいので、なかなか快適に使えるわけです。

が、、内容をじっくり呼んでる時間がないときなんかは後回しにするためにちゃっちゃとタイトルとURLだけクリップしときたい、ってケースもままあるわけで。そんな時はメールで飛ばしちゃうのが手っ取り早いので、ページタイトルとURLをクリップボードに入れるJSActionスクリプトとか作ってみてたんだけど、これだと既定のノートブックに入るだけでしばらくすると後で探しづらかったり。

そこはEvernote、ちゃんとサブジェクトの書き方でターゲットのノートブックやタグを指定はできるんだけど、いちいち書くのも結構面倒になってくるわけで。

で、こういう用途ならブックマークレットを経由してmailtoで開いてやれば投稿用のtoアドレスやらタグをつけたサブジェクトを指定できて結構クイックに扱えるかな、と思って作ってみました。前置きが長かったな。

まずはソース

こんな感じ。

(function(opt){
    var a = '', t = document.title, u = location.href, e = encodeURIComponent, s = ' ';
    location.href =
        'mailto:' + a +
        '?subject=' + e(t + s + opt) +
        '&body=' + e([t, u].join('\n'));
})(prompt('input #tag and/or @notebook', '') || '');


で、実際のブックマークレットはこんなの。

Evernoteにメール
クリックしたらソースを表示

「クリックしたらソースを表示」のチェックボックスをOnにすると、クリック時にpromptでソースをポップアップするので、iPhoneなんかのドラッグドロップでブックマークレット登録できないブラウザ使ってる場合にちょっぴり便利。

ざっと解説

メインの関数式に渡す引数はprompt()の実行結果から受け取るようにしてて、これをページタイトルとくっつけたものをサブジェクトに指定してます。なので、「未読」ってタグをつけたい場合はprompt()に「#未読」って入れてやればOKと。

タグやノートブックを複数指定する場合はそれぞれの間をスペースで区切ります。ノートブック「個人用」にタグ「未読」と「url」をつけるなら、「#未読 #url @個人用」みたいな感じで。

改造とか

関数内の変数'a'はtoアドレスに使われるので、ここに自分のEvernoteメールアドレスをあらかじめ入れておけば送信先を選択やら入力やらしなくてもよくなります。さらに関数式への引数もリテラルで書いておけば、ブックマークレットを起動すればメーラーがすぐに起動するので後は送信するだけで目的を果たせるようになります。

ただし引数をリテラル記述する場合、マルチバイト文字はそのまま使えないので、あらかじめescapeしたものを使ってください。

改造が面倒だろうからジェネレータ

って、ブックマークレットのソースを改造するのはちとめんどくさいので、ジェネレータを作って起きました。自分が良く使うパターンで生成してブックマークしておくと便利じゃないかと。

  • toアドレス:
  • ターゲットのノートブック:
  • タグ(スペース区切り):
  • リンクタイトル:
(ここにブックマークレットが作成されます)
クリックしたらソースを表示

使い方は、

  • toアドレスに自分のEvernoteメールアドレス(@m.evernote.com)を入力
  • ターゲットのノートブックやタグを入力。@や#は不要、タグは半角スペースで区切って複数指定可能
  • リンクタイトルに入れた文字列が、生成されるブックマークレットの見出しになる
てな感じ。ジェネレータがうまく動かなかったらコメントください。

追記

あー、ジェネレータでノートブック名やタグにマルチバイト文字使うと、IEじゃ化けちゃうなぁ...

SafariとChromeを判別してみる2010年12月18日 04時32分45秒

※:Safari 5.1がChromeと区別できなくなったので修正版作ってみました。(11.08.25)

はい、またも3週間もほったらかしにしてたわけですが、ようやく前回の続きで、レンダリングエンジンがWebKitだった場合にAppleのSafariなのか、GoogleのChromeなのかを判別してみようかと。

当初の考えでは、まずSafariなのかChromeなのかを判断して、それからモバイル版なのかを見ようと思ってたんだけど、「SafariとモバイルSafariにはあって、ChromeとモバイルChromeには存在しないプロパティ」またはその逆ってのが見つからなかったのよね。

仕方ないのでデスクトップ版同士、モバイル版同士の差異を探して、

  • まずはモバイル版かどうか判断し
  • その後にそれぞれSafariかChromeかをチェックする
という方法に落ち着いた。

あ、この記事では便宜上、Androidの標準ブラウザを「モバイル版Chrome」とか呼んじゃってるけど、これたぶん正式な名前じゃないみたい。あくまで便宜上の呼称と思ってくださいな。

まずはモバイル版かどうか見分ける

これは前回の最後にふれてたけど、モバイル版ブラウザにはデバイスの向きを示す「window.orientation」というNumberなプロパティがあり、これはデスクトップ版には今のところ実装されてないのでこれを使ってみる。

<script type="text/javascript">
// WebKitかどうか。
window.is_webkit = (function() {
    try {
        return window.navigator.taintEnabled == undefined;
    } catch(e) {
        return false;
    }
})();

// モバイルブラウザかどうか
window.is_mobile = (function() {
    return window.is_webkit && ! isNaN(window.orientation);
})();
</script>

事前にWebKitであるかもあわせて判別しておく。

あとはこの情報を参照すればいいだけ。
<button type="button" onclick="alert(window.is_mobile)">モバイル版のWebKit?</button>

Safariであるかどうか - モバイル編

まずモバイル版の違い。エミュレータベースでしか確認してないけど、Android 2.3(Gingerbread)になってもモバイル版のChromeはまだSVGをサポートしてない模様。しかもありがたいことにWebKitはDOMとかブラウザ由来のオブジェクトのコンストラクタがちゃんとwindowのプロパティとしてアクセスできるので、SVG関連のコンストラクタの有無をチェックすればいいわけだ。

が、モバイルSafariのほうもSVGの実装が段階的に進んでいるため、バージョンによって存在するものとしないものがあるので、なるべく基本的なオブジェクトを使う必要がある。

ここではSVGColorを使うことにする。要はwindow.SVGColor が undefined かどうかを見るというわけ。コードは後述。

Safariであるかどうか - デスクトップ編

で、今度はデスクトップ版の場合だが、これがなかなか苦しかった。さすがに各ブラウザともHTML5街道へまっしぐら状態なのでなかなかブラウザ固有のものが見つからないのだ。windowのプロパティをfor..inで列挙してみると、たまに片方にしか存在しないプロパティ名が見つかるのだが、単にDontEnum属性がついてるだけでオブジェクトとしては存在してたりするし。

そんなわけで単純な存在の有無のチェックじゃ実現できないんだけど、windowプロパティの列挙時に値までチェックしてみたところちょっと面白いことに気づいた。たとえばデベロッパコンソール上でwindowのコンストラクタを出力してみると、Safariの場合は

> window.constructor
  function Object() {
      [native code]
  }

Chromeの場合は
> window.constructor
  function DOMWindow() { [native code] }

と、コンストラクタ関数がまったく異なっているようなのだ。

Safariの場合、コンストラクタがObject()ってことは、window固有のプロパティはコンストラクタから継承してるわけではないと推測できるし、ChromeのほうはモロにDOMWindowという名前なのでこれにwindow固有プロパティが実装されてそうだと推測できる。ってことで、Chromeで試してみた。

> window.constructor.prototype.alert
  function alert() { [navite code] }

お!いい感じですよ?じゃ、Safariでは?
> window.constructor.prototype.alert
  undefined

よし!予想通り!!要するに、window.constructor.prototype.alert が undefined ならSafari、そうでないならChromeと判断して間違いない、と。

これ試したのはChrome 8だったので、以前のバージョンがどういう実装になっていたのか、USBブート可能なポータブル版のChrome 4やら5やらを探し出してきて試してみたところ、まったく同じ状況だったのでこれで問題なさそう。

で、こうなる、と。

ようやく判別方法が固まったので、以下のような感じで。あ、先ほどのis_webkitとis_mobileの判断がでてることが前提でね。

<script type="text/javascript">
// Safariかどうか
window.is_safari = (function() {
    // WebKitじゃなきゃ当然Safariじゃないわな
    if(! window.is_webkit) return false;
    if(window.is_mobile) {
        // モバイル版の場合、SVGをサポートしてたらSafari
        return !! window.SVGColor;
    } else {
        // デスクトップ版の場合、windowコンストラクタのプロトタイプで判別
        try {
            // undefinedならSafari
            return window.constructor.prototype.alert == undefined;
        } catch(e) {
            // そもそもwindow.constructor とか そのプロトタイプにアクセスできない
            // ブラウザもあるので try - catch しとく
            return false;
        }
    }
})();

// Chromeかどうか
window.is_chrome = (function() {
    // WebKitじゃなきゃ(ry
    if(! window.is_webkit) return false;
    // 後は基本的にSafariと逆
    if(window.is_mobile) {
        return ! window.SVGColor;
    } else {
        try {
            return window.constructor.prototype.alert != undefined;
        } catch(e) {
            return false;
        }
    }
})();

</script>

<button type="button" onclick="alert(window.is_safari)">Safariっすか?</button>
<button type="button" onclick="alert(window.is_chrome)">Chromeっすか?</button>

そのうちライブラリにするよ。多分。

ということで、ようやく当初の目的どおりの判別ができるようになりましたとさ。

ここまでのスクリプトはすべて、window.onloadを待たずに判断できるので、単独ライブラリで実装して一番最初に読み込むスクリプトに仕立て上げちゃえばいいんだけど、ちょっとまとめるのがめんどくさいので、まぁそのうち。

追記

ちなみに、window.constructor.prototype のプロパティでの判断は、モバイル版では通用しません。モバイル版ChromeはSafariと同じで window.constructor.prototype.alert は undefined になります。と思いました。確か。

<button type="button" onclick="try { alert(window.constructor.prototype.alert); } catch(e) { alert(e); }">window.constuctor.prototype.alert は?</button>

JSでブラウザを判別してみよう2010年11月25日 05時07分32秒

油断してたらまた4ヶ月も更新してなかったのだが、なんとなくJSでブラウザ判別を行うという、わりとありがちな事に挑戦してみる。

ライブラリとしてまとめようと思ってたんだけど、そろそろ眠いので、今日のところは断片的な記述にしておこう。

何を判別する?

メジャーどころだったらたいていはレンダリングエンジン≒ブラウザが成立しそうなもんなんだけど、WebKitの場合はSafariとChromeの両方がなかなか普及してるので、「レンダリングエンジンが何であるか」と「ブラウザが何であるか」の2種類を判別することにする。で、Safari/Chromeの場合はモバイル版かどうかも見ないとね。

ということで、レンダリングエンジンは

  • Trident
  • Gecko
  • Presto
  • WebKit
、ブラウザは
  • IE
  • Firefox
  • Opera
  • Safari
  • Chrome
を判別してみる。

まずは、Presto = Opera。

めちゃくちゃ手抜きなんだけど、window.operaがundefinedじゃなければPrestoっつーことで。しかもPrestoだったら何も考えずにOperaっつーことで。

<button type="button" onclick="alert(!! window.opera)">レンダリングエンジンははPresto?</button>

<button type="button" onclick="alert(!! window.opera)">ブラウザはOpera?</button>

次はGecko = Firefox。

まぁ、GeckoをみんなFirefoxとみなすのは乱暴なのはわかってるんだけども、こちらも手抜き。

昔、MooToolsの1.2.2のブラウザ判定部分のソース見たときは、window.document.getBoxObjectForがundefinedでなければGeckoてな判断してたみたいなんだけど、Firefox 3.6の環境で試したらこれが成立しなくなってたのでwindowのプロパティを総当りで調べたところ、window.mozInnerScreenXあたりが使えそうなので組み合わせで判別してみる。

<button type="button" onclick="alert(window.document.getBoxObjectFor != undefined || window.mozInnerScreenX != undefined);">レンダリングエンジンはGecko?</button>

<button type="button" onclick="alert(window.document.getBoxObjectFor != undefined || window.mozInnerScreenX != undefined);">ブラウザはFirefox?</button>

IEかどうか、までは簡単。

IEかどうかを判別するのは、window.ActiveXObjectをチェックするのが定番なんだけども、念のためMSDNを見てみたら、なんかIE9で廃止になっちゃいそうな記述が。

びっくりしてIE9ベータで確認したらnullは返してこなかったんだけど、ちょっと怖いのでもうひとつの定番「window.document.all」を使うことに。でもこれ、Operaも実装してるんだったよな、確か。 っつーことで、こんな感じに。

<button type="button" onclick="alert(!!((window.ActiveXObject || window.document.all) && !window.opera))">レンダリングエンジンはTrident?</button>

IEのバージョンは細かく見てみたい。

で、IEの場合は他のブラウザよりもバージョンの違いをなるべく厳密に判別したくなるので、

  • IE6よりも前
  • IE6
  • IE7
  • IE8
  • IE9
をなんとか判別してみようかといろいろ探してみたところ、これまたMSDNにDetecting Internet Explorer More Effectivelyなんてぴったりの記事が見つかった。冒頭のほうではユーザエージェントで判別する方法を記載してるんだけど、これじゃ心もとないのでもう少し読み進めてみたら、「Another IE version detector snippet」と題するコメントが
Another IE version detector snippet
function getIEVersion(odoc){
if (odoc.body.style.scrollbar3dLightColor!=undefined)
{
if (odoc.body.style.opacity!=undefined) {return 'IE9';}
else if (odoc.body.style.msBlockProgression!=undefined) {return 'IE8';}
else if (odoc.body.style.msInterpolationMode!=undefined) {return 'IE7';}
else if (odoc.body.style.textOverflow!=undefined) {return 'IE6'}
else {return 'IE5.5 or lower';}
}
}
なるほど、styleプロパティでチェックするのね。ただこのサンプルどおりbody.styleをチェックする方法だとページロードが完了しないと判定できないので不便なので、head要素でチェックをしてみることに。あ、あとIE9かどうかの判別はwindow.msPerformanceでチェックしたほうがよさげ。

で、こんなコードを実行して、

<script type="text/javascript">
window.detected_ie_name = (function() {
    if(window.ActiveXObject == undefined && window.document.all == undefined) return "IEじゃない";
    if(window.msPerformance != undefined) return "IE9";
    var h = document.getElementsByTagName("head")[0];
    if(h.style.msBlockProgression != undefined) return "IE8";
    if(h.style.msInterpolationMode != undefined) return "IE7";
    if(h.style.textOverflow != undefined) return "IE6";
    return "IE5.5かそれ以前";
})();
</script>

こんな感じのボタンを貼ってみる。
<button type="button" onclick="alert(window.detected_ie_name)">IEのバージョンは?</button>

WebKitか否か。

MooTools 1.2.2では、navigator.taintEnabledのチェックでもって判別してた。これなんぞ?と思ったら、よい解説が見つかった。

navigator.taintEnabled() というのは、「ユーザーに非通知でデータ送信が可能かどうか(データテイント機能の使用有無)を返すメソッド」です。「何それ?」て思われた方、ご安心ください。Netscape Navigator 3.x の時代の、古い古ーい仕様で、データテイント機能は現在使われておりません。多くのブラウザは、ただfalseを返すだけです。
ただしSafari(WebKit)では、このメソッドそのものを定義していません。
だそうで、まんま使わせてもらおう。

<button type="button" onclick="try { alert(window.navigator.taintEnabled == undefined) } catch(e) { alert(false); }">レンダリングエンジンはWebKit?</button>

って、IETester使ってるせいか、デフォルトIE以外だとwindow.navigator.taintEnabledにアクセスしただけで例外吐くのでtry catchで囲まなきゃ。

Safariか、Chromeか。

これはあんまりいい方法が見つからなかったので、両方でSafari5とChrome7の両方でwindowのプロパティを洗いざらい列挙してめぼしいところをいくつか試してみたところ、window.TouchListの有無がよさそうだった。

と思って今試したら、Safariでもwindow.TouchListがundefined返してきた。あれぇ?会社で試したときと違うなあ。

ということで、ちょっと別の方法探さなきゃ。ちなみにモバイル版かの判別は今のところwindow.orientationがNaNじゃなければモバイルってな判定でよさげ。

簡易パスワード生成ブックマークレット2010年01月21日 05時10分08秒

たいした話じゃないのだが

ちょっとWEPキーを生成したくなったので、ずいぶん前に取り上げた、Number#toStringで生成してみた。とりあえずはFirebugのコンソールでちょろっと実行して済ませたのだが、ちょっと思いつきがあったのでブックマークレットにしてみることに。

このリンクをクリックすると、数字とアルファベットの小文字のランダムな組み合わせで長さ16の文字列を生成。クリップボードにコピーできるよう、生成結果表示にはprompt()を使用。

Firefox3.5/IE 5.5~8(IETester使用)/Opera9.6/Safari 4/Google Chrome 3で動作確認してます。

生成部分のコードは展開するとこんな感じ。

(function(p) {
  var s = '';
  with(Math) {
    while(s.length < p)
      s += floor(random() * pow(2, 32)).toString(36)
  }
  return s.slice(-1 * p);
})(16);

思いつきってのは、生成部分でfloor/random/powと、Mathメソッドの利用頻度が高いので「withでMath囲えば短くなるんじゃね?」ってとこだったんだけど、やってみたら3文字しか稼げてないし。まぁ可読性はいいような気がするんだけども。

36進数の生成は、以前capture.tumblr.jsの改造ときはMath.random()をnew Date().valueOf()で持ち上げてからピリオドを落とすやり方してたんだけど、今回は2^32で持ち上げた結果の実数部を切り捨てにしてみた。そのほかは、まぁシンプルですな。

もうちょっとだけ強度を上げてみる

これでも間に合わせにはいいかなと思うんだが、どうせならアルファベットは大文字小文字の混在にしたいな、と。で、やってみた。

展開すると、こんな感じ。
(function(p) {
  with(Math) {
    var s = '', r = random, f = floor, i;
    while(s.length < p)
      s += f(r() * pow(2, 32)).toString(36);
    s = s.slice(-1 * p).split('');
    for(i = 0;i < p;i++)
      if(f(r() * 10) % 2)
        s[i] = s[i].toUpperCase()
  }
  return s.join('')
})(16);

複数回出現するMathメソッド(floorとrandom)のエイリアスを作っちゃってるのでちょっと比較しづらいんだけど、手を入れた部分は
for(i = 0; i < p; i++)
の部分で、ここで1文字づつスキャンして、約50%の確率でtoUpperCase()による大文字変換をしてます。あと文字数短縮のためにvarを1回で済ませてたり。

実用性があるかはわからんのだが

これらのリンクをブックマーク登録しておけば、思い立ったときにランダムな文字列を生成できます。て、そうしょっちゅう需要があるものでもないんだろうけど。