読み書きプログラミング

日常のプログラミングで気づいたことを綴っています

Bootstrapのドロップダウンメニューのhover効果をiOS Safariで改善する

Bootstrap(3.3.5)は様々な場所でCSSのhoverを利用しています。
残念ながらiOS Safariではhoverに該当する状態がなく、ドロップダウンメニューでどれをタッチしたかわかりにくいなどという問題が発生します。

検索すると、hover対応の凝ったJavaScriptコードがたくさん見つかりますが、Bootstrapに限ればそう大層なことをしなくても改善できます。
例えば、Bootstrapのドロップダウンメニューはhoverとfocusに同じ設定がされているため、以下の5行でOK。

$(document).ready(function() {
    $('.dropdown-menu>li>a').on('touchstart', function() {
        $(this).focus();
    });
});

これだけ。touchstartと同時にfocusさせると、hoverと同じ色になってくれるので、どのメニューをタッチしたかがわかるようになります。

TAPi18nのページロード時の振る舞いの改善

Meteor + Iron Routerの国際化にiron-router-i18nとTAPi18nを使っています。
TAPi18nはなぜかデフォルト言語として英語を読み込み表示、その後setLangaugeで言語が設定されるとその言語に切り替えると仕組みになっています。
なので、ネットワーク遅延の目立つ環境では、最初英語版のページが表示され、その後所望の言語に切り替わります。ユーザーの立場から見ると国際化とはわかっても途中でページをすり替えているように気分はあまり良くないですね。

そこで、英語の状態のページの表示を抑制するコードを書きました。
まずbodyをデフォルトで非表示にします。

<body style="visibility: hidden;">
{{> Router}}
</body>

クライアント側の言語設定とbodyを表示にするコードを追加します。

# client

I18NConf.onLanguageChange (oldLang, newLang) ->
    TAPi18n.setLanguage(newLang).done ->
        document.body.style.visibility = 'visible'
        return
    return

やっぱりちらりと英語が見えることがない方が気持ちがいいですね。

ファイルの文字コードの自動判別

AtomシフトJISのファイルを扱うことがあって、自動判別忘れに苦しみました。
ググるとちゃんと解決してくれている人がいました。

ATOM でファイルを開いたら自動文字コード判定を行う

ところが、これだと手動でファイルを開いた時にはOKですが、フォルダ指定してatomを起動した時に、以前に開いていたファイルに関しては自動判別してくれません。
Atom discussionで聞いてみたらあっさり解決。onDidOpenの代わりにobserveTextEditorsを使えばいいとのこと。

fs = require('fs')

atom.workspace.observeTextEditors (editor) ->
  try
    filePath = editor.getPath()
  catch error
    return
  return unless fs.existsSync(filePath)

  jschardet = require 'jschardet'
  iconv = require 'iconv-lite'
  fs.readFile filePath, (error, buffer) =>
    return if error?
    {encoding} = jschardet.detect(buffer) ? {}
    encoding = 'utf8' if encoding is 'ascii' or encoding is 'windows-1252'
    return unless iconv.encodingExists(encoding)

    encoding = encoding.toLowerCase().replace(/[^0-9a-z]|:\d{4}$/g, '')
    editor.setEncoding(encoding)
  return

エンコードの自動判別でwindows-1252にフォールバックする場合もあるので、その場合もUTF-8にするようにしました。
とても快適です。

コードがencoding-selector packageのコード丸写しなのは、encoding-selectorのメソッドが公開されていないからですが、元々encoding-selectorが自動判別機能を持つことが筋が悪い。text-bufferが持つべきなのでissueを上げていますが、果たして取り上げてもらえるかどうか。

https://github.com/atom/text-buffer/issues/86

レスポンシブなページネーション

Bootstrapのページネーションボタン、長すぎると困ります。
で、レスポンシブにしてくれるjQueryプラグインがありました。

http://auxiliary.github.io/rpage/

素晴らしい。
ただページネーションの隣にインライン要素があると動かなかったり、コードが少しあれだったりしたので、CoffeeScriptで書き直してみました。

https://github.com/y-ich/rpage

検討時間的に一番頑張ったのは、ページネーションボタンの折り返しをなくして、widthの計算をボタンひとつひとつの和ではなくてページネーションそのもののwidthとしたところ。CSSの勉強になりました。

でもこれ、テストにある通りの複数のページネーションのresize時とても遅いんですよねぇ。どこかに変なタイマー入っているのかと思うほど遅い。DOMの計算の重さを実感しました。

Iron Routerのフックの覚書

  • Iron Routerには以下の5つのフックが定義できるようになっている。
  • onRun
  • onRerun
  • onBeforeAction
  • onAfterAction
  • onStop

その他に呼び出しのタイミングを考慮する上で、

  • subscriptions
  • waitOn

オプションがある。

ここまでで1つ覚えておくことは、onRun, onStop以外はリアクティブに再計算されるということ。

次に、呼び出される順序は、通常(waitOnで返すハンドルのready()が一旦はfalseを返すような場合)

  1. subscriptions #1
  2. waitOn #1
  3. onRun #1
  4. onAfterAction #1
  5. subscriptions #2
  6. waitOn #2
  7. onRerun #1
  8. onBeforeAction #1
  9. action(というよりpage rendering) #1
  10. onAfterAction #2
  11. onStop #1

これは解説が必要で、

  1. まずsubscriptions/waitOnがコールされる
  2. onRunがコールされる
  3. waitOnの戻り値のready()がfalseなら、onBeforeActionとactionが見送られて、onAfterActionがコールされる。
  4. Iron Router 1.0の仕様ではどうも、waitOnの戻り値のready()がtrueになるとリアクティブな再計算とみなされるようで、subscriptions/waitOnがコールされる(注1)
  5. 再計算とみなされているのでonRerunがコールされる
  6. onBeforeAction, action, onAfterActionの順にコールされる。

(注1) subscribeは重複してコールしても動作上無視するようなので問題なさそうだが、お手製のready()ハンドルを作るとここで色々困る。github レポジトリのイシューに上がっている。

で、各フックの使い方ですが、

  • リアクティブでなくていいものはonRun
  • subscriptions/waitOnに依存しないものはonAfterAction
  • ページレンダリングの前に実行したいものはonBeforeAction
  • ページレンダリングの後に実行したいものはonAfterActionでthis.ready()がtrueの時

onAfterActionの実装が名前のイメージと違ってしまっているおかげでonRerunは使い道がなさそうです。

このタイミング仕様はわかりにくいし使いにくいので近いうちに変わるような気がします。
ただ、Iron Router開発が活発とは言えなくなっていて…
Flow Routerにi18nパッケージができたらみんなそちらに乗り換えてしまうかもしれませんね。

がんばれ、Iron Router!

アプリを検索クローラーに見てもらう

作ったMeteorアプリをGoogleで検索すると、URLとタイトルは引っかかるのですが、中身が空白です。
ウェブアプリは仕方がないのかと思っていたのですが、ちゃんと枠組みがあるのですねぇ。

Making AJAX applications crawlable

Meteorにもspiderableというパッケージがありました。このパッケージはPhantomJSがMeteorサーバー上に必要なので、spiderable-remoteを使いました。

以下、備忘録。

herokuにPhantomJSのサーバーを用意する。

基本、phantomjs-remoteのサーバーコードを動かすのですが、herokuでTCPポートを使うには、Ruppel's Socketsを使います。

1. gitレポジトリを用意して、phantom-server.jsをコピー。

2. ビルドパックが複数使えるように設定

heroku buildpacks:set https://github.com/ddollar/heroku-buildpack-multi.git

. ファイル.buildpacksを用意。

https://github.com/heroku/heroku-buildpack-nodejs.git
https://github.com/stomita/heroku-buildpack-phantomjs.git

3. Ruppell's Socketsを追加。

heroku addons:create ruppells-sockets
git submodule add https://bitbucket.org/ruppells/sockets-connect.git lib/sockets-connect

4. ポート番号にprocess.env.RUPPELLS_SOCKETS_LOCAL_PORTを参照するように修正する。

...
if (process.argv.length < 2) {
...
var port = process.env.RUPPELLS_SOCKETS_LOCAL_PORT;
...

5. Procfileを用意する。

socket: ./lib/sockets-connect/rs-conn phantom-server.js
heroku ps:scale socket=1

6. クライアントが使うURIをメモしておく。

heroku config:get RUPPELLS_SOCKETS_FRONTEND_URI

7. herokuにデプロイ

git push heroku master

Meteorアプリ側

1. spiderable-remoteを追加する。

meteor add gadicohen:spiderable-remote

2. サーバー側で環境変数を追加するコードを追加する。

if (Meteor.isServer) {
  process.env.PHANTOMJS_REMOTE = <上でメモったURI>
}

完了!

ImageIdentifyをREST APIとして使う

Twitter友達からWolfram Language Image Identification Projectを教えていただきました。こんな記事がありますね。

http://gigazine.net/news/20150527-wolfram-language-image-identification-project/

で、これをREST APIとして使えるかどうか調査しました。結果は「できる」です。

Wolfram Language Image Identification Projectは、よくわかりませんが、Wolfram Programming Cloudの実力を示すデモサイトのようです。

https://programming.wolframcloud.com/app/

以下、登録が終わってサインインして、新しいノートブックが使えるようになった状態から、 Image Identification REST APIを作る手順です。

1. 画像URLから識別を行う関数を定義する

f[url_]:=Module[{image=Import[url]},ImageIdentify[image]]

(入力を評価するには、Shift + returnです。)

2. 定義した関数でREST APIをデプロイする

api = CloudDeploy[APIFunction[{"url"->"String"}, f[#url]&,"JPG"]]

入力を評価すると、デプロイされたURLが表示されます。

3. REST APIパーミッションをPublicにする
(デフォルトではPrivateです)

SetOptions[api, Permissions->"Public"]

以上で出来上がりです。
2で取得したURLに?url=<特定したい写真のURLをURLエスケープしたもの>を足してアクセスすると、特定結果が得られます。


直接、写真データをPOSTして得られるようにしたかったのですが、Wolfram言語は今のところ、RESET APIに関してGETしかサポートしていないようです。