Hatena::ブログ(Diary)

ある1つのサンプル

2011-05-16

jQM1.0 Alpha4.1への対応と留意点

3月からの一連のjQuery Mobileの記事 (jQuery Mobileについての雑感 ページ構成篇など)では、jQuery Mobile1.0 Alpha3を元にしていましたが、 既に3月末にAlpha4、4月7日にはAlpha4.1がリリースされています。

そこで前回までの記事でも多少言及していましたが、Alpha4での(興味が有る個所だけの;)変更点をざっと確認すると共に、 Blogaraスマートフォン用ページのAlpha4.1への対応を行ってみます。


Listview中のリンクの扱い

以前の記事でも触れましたが、Listview中のリンクに有無を言わさずに自動処理が適用される動作に関して変更が行われています。

Listviewのリストアイテム中のリンクを抽出するセレクタが、任意のanchor要素からリストアイテム直下の子のanchor要素に限定されるように変更されています。

var a = item.find( ">a" );

Alpha3では、この問題により想定するUI構成が実現不可となっていた為コードの変更も行いましたが、 Alpha4ではHTMLの変更だけで実装出来、わざわざjQuery Mobileのコードを弄る必要も無くなったのは非常に良い感じです。


また、Listview中のリンクの扱いの変更としては、自動追加CSS(.ui-btn-inner a.ui-link-inherit)にdisplay:blockとpaddingが設定されるようになっています。 よってそこに関連するCSSの設定を変更するなどの細かい対応も必要アリ。


リストアイテムをクリックした時の動作

上記のような簡単なHTMLとCSSの修正で、見た目的にはAlpha3と同一にする事が出来て一安心と思いきや、 ブログの元記事へのリンクが設定されているはずのリストアイテムをclickしても無反応という、機能面ではかなり拙い状態となっています。

Alpha4のリリースノートにもNew list markup conventionsとして書かれていますが、 コードを読んで確認してみたところListviewでのリンク(anchor)の扱いが後方互換性の無い仕様に変わっています。


Alpha3ではリストアイテムのanchorでは無い任意の場所をクリックした場合には、リストアイテム中に存在する先頭のanchor要素を探し、そのa要素にclickイベントを発行していました。

    // tapping the whole LI triggers click on the first link
    $list.delegate( "li", "click", function(event) {
      if ( !$( event.target ).closest( "a" ).length ) {
        $( this ).find( "a" ).first().trigger( "click" );
        return false;
      }
    });

が、Alpha4ではその処理は無くなり、さらにanchorのclickイベントハンドラでの外部サイトやtarget指定のあるリンクの場合の手動のページ移動処理(window.openやlocation.href指定)も下記のようなAlpha3のコードから消えています。

    if( isExternal || hasAjaxDisabled || hasTarget || !$.mobile.ajaxEnabled ||
      // TODO: deprecated - remove at 1.0
      !$.mobile.ajaxLinksEnabled ){
      //remove active link class if external (then it won't be there if you come back)
      removeActiveLinkClass(true);

      //deliberately redirect, in case click was triggered
      if( hasTarget ){
        window.open( url );
      }
      else if( hasAjaxDisabled ){
        return;
      }
      else{
        location.href = url;
      }
    }

要するに前述のa.ui-link-inheritのブロック要素化やpadding設定と合わせて考えると、リストアイテムにリンクを設定したい場合にはスクリプトの処理に頼らずに、HTML上でanchorをキチンと記述しろという方針の模様。
# HTML4のようにanchorの中はインライン要素のみが許可されていたのとは異なり、
# HTML5ではその制限が無くなっていた、と今回始めて知ったというオチ。;(


ただ、Blogaraのブログ更新情報ページでは、全体が元記事へのリンクとなっているリストアイテムの中にボタン形式の詳細ページへのリンクも設置しているので、この仕様変更は地味に痛い所。
リストアイテムのリンク


ページのtitle要素の自動更新

前回の記事ではGoogle Analytics(以下GA)に記録されるページのタイトル名に言及しましたが、 jQuery MobileのAjaxナビゲーションでは、Ajaxによるページ更新の場合にはページ自体のtitie要素が元ページのままなので、 GAのタイトル別のコンテンツ欄で正確な分類が出来ていませんでした。

これがAlpha4では変更され、Ajaxナビゲーションでページを取得した場合には、取得したページのtitleを抽出し表示しているdocumentのtitleに反映されるようになっています。

タイトル名云々については、ブログ更新情報ページから人物の詳細ページをAjaxナビゲーションで呼び出した時に、 詳細ページを表示しているにも拘わらずtitleはブログ更新情報ページのまま、というのが課題でした。 そこでAlpha4ではどうなっているかを確認してみた所、キチンとtitleが詳細ページのタイトルに変わっています。

と、それで済めば素晴らしい改善という所ですが、ここもまたクセのある動作となっています。


title変更の副作用

ブログ更新情報ページは、SKE48 メンバー一覧とブログ更新情報 - BlogaraSというようなtitleが設定されています。

Alpha3では当然このtitleのままタイトルバーやbookmarkの登録名として表示されていますが、何故かAlpha4ではこの表示上のタイトルがBlogaraになっています。

一体、この"Blogara"表示はどこから来たものかと疑問に思いつつコードを見てみると、$.mobile.changePageの中のpageChangeCompleteといういかにも関係アリげな関数のこの部分。

        //if title element wasn't found, try the page div data attr too
        var newPageTitle = to.jqmData("title") || to.find(".ui-header .ui-title" ).text();
        if( !!newPageTitle && pageTitle == document.title ){
          pageTitle = newPageTitle;
        }

まず、ブログ更新情報ページ(http://blogara.jp/t/ske48/sp1.html)が最初に表示された時には、pageTitleにはdocument.titleの文字列が入っています。 この例では、"SKE48 メンバー一覧とブログ更新情報 - BlogaraS"。

このままならAlpha3と同じく、この文字列がタイトルとして表示されますが、問題はnewPageTitleを取得する個所。

to.jqmData("title")のtoは現在表示しようとしているPageのデータが入り、そのPageのdata-titleというカスタムデータ属性の値を取得しています。 ブログ更新情報ページでは特にそのような記述は無いのでundefinedが返り、boolean false判定で次のto.find(".ui-header .ui-title" ).text()へ。

to.find(".ui-header .ui-title" ).text()では、取得しようとするページのjQuery Mobileのheader指定(data-role="header")がされている要素の中のh要素の中のテキストノードの値を取り出す。


ブログ更新情報ページや詳細情報ページなどのBlogaraのスマートフォン用ページは、
Blogaraヘッダ
というようなdata-role="header"でh1要素がある共通のヘッダとなっており、背景はCSS3グラデーションで文字はテキストとなっています。

で、問題の"Blogara"という文字列は見ての通り、このヘッダ部分のテキストでした。

初期ページではpageTitle==document.titleなので、このヘッダ部分のテキストが表示すべきtitleとして新たに設定され、訳判らんうちにタイトルが変更されていたという顛末です。


title強制変更への対応

HTMLのtitle要素は、検索サイトでの検索結果に表示されるなど単なる文字列以上の重要な要素となっていますので、ページ上で表示したいタイトルとは異なる場合が多々あります。

検索エンジンBOTは大抵JavaScriptを処理しないのでこのAlpha4での変更は直接的な関係は無い模様ですが、本来のtitleが変更されてしまうというのは少々不都合な感じは否めません。

とりあえずこの動作への簡単な対応としては、data-role="header"で指定されるheaderを無くすかheader内のh要素を別の要素に置き換えるようにすればtitleが変更されるのを防ぐ事が出来ます。


ただ、これではあまりにも安直過ぎる対応なので、jQuery Mobileのコードを読むついでに何故このような動作となっているのかを見てみます。

まずtitleの変更が行われるのは、Pageの表示完了のタイミングで行われる処理のpageChangeCompleteで。

        //if title element wasn't found, try the page div data attr too
        var newPageTitle = to.jqmData("title") || to.find(".ui-header .ui-title" ).text();
        if( !!newPageTitle && pageTitle == document.title ){
          pageTitle = newPageTitle;
        }

        //add page to history stack if it's not back or forward
        if( !back && !forward ){
          urlHistory.addNew( url, transition, pageTitle, to );
        }

        //set page title
        document.title = urlHistory.getActive().title;

ブログ更新情報ページが最初に表示されたタイミングでは、pageTitleはHTMLに記述された通りのdocument.title要素のテキスト、toはブログ更新情報ページに記述されているPage、backとforwardはfalse、urlは空文字列となっています。

newPageTitleを取得する処理は前述した通り。その後のbackでもforwardでも無い場合にはhistoryに新たに追加します。 追加したhistoryの情報がactiveな項目として設定され、それが最後のurlHistory.getActive()で返され、そのtitleプロパティ値がページのタイトルとして表示されます。


次に、Ajaxナビゲーションで別ページの情報を取得する場合。Blogaraでは同一のブログ更新情報ページ間でのページ切替やDialog形式で表示される詳細情報ページのリンクで使われています。

Ajaxナビゲーションでの移動先ページのタイトル名の取得は、Ajaxの成功時のcallbackで処理が行われ、

      $.ajax({
        url: fileUrl,
        type: type,
        data: data,
        dataType: "html",
        success: function( html ) {
          //pre-parse html to check for a data-url,
          //use it as the new fileUrl, base path, etc
          var all = $("<div></div>"),
              redirectLoc,

              //page title regexp
              newPageTitle = html.match( /<title[^>]*>([^<]*)/ ) && RegExp.$1,

// 中略

          //finally, if it's defined now, set the page title for storage in urlHistory
          if( newPageTitle ){
            pageTitle = newPageTitle;
          }

Ajaxで取得した移動先ページのHTMLをまず文字列として処理し、正規表現でtitleのテキストを抜き出します。 pageTitleは前述のpageChangeCompleteのコードでの変数と同一の実体であり、その後のpageChangeCompleteでの処理でdocument.titleに設定されます。


Alpha段階のコード

このように新規表示ページでは、本来のHTMLよりもdata-titleやdata-role="header"のテキストノードの文字列が優先され、 一方Ajaxリクエストで取得した移動先のページの場合は、data-titleなどよりもtitle要素が優先されています。 本来どちらを優先したいのかイマイチ不明であり、要するにまだ仕様が確定していないようにも見受けられます。

この未確定らしい動作という事に関しては、既にAjaxリクエストで取得済みでありDOM上に存在しているPageを再び表示する場合にも窺えます。


このtitle名変更の動作の例として、公式ドキュメントの Dialogページ(pages/docs-dialogs.html)を取り上げてみます。

まずこのページをブラウザで開いた直後には、このHTMLのtitle要素に記述されているjQuery Mobile Docs - Pagesがタイトルバーに表示されます。

その後、ページの表示が完了したらしいタイミングで、タイトルがheaderのh1要素に指定されているDialogsに変更されます。


このDialogの説明ページの上部には、Open dialogというボタンが有り、リンク先のページをAjaxリクエストで取得しDialog形式で表示します。

リンクをクリックしAjaxリクエストでページを取得しダイアログのサンプルが表示された状態では、titleにはそのダイアログサンプルページ(pages/dialog.html)のtitle要素に記述されている jQuery Mobile Framework - Dialog Exampleがページ自体のtitleに設定されます。これは上記のコードを見た通りの動作。

任意のボタンをクリックしダイアログを閉じ、元のDialogページに戻ると、元ページのheaderの文字列を反映しtitleが再びDialogsに変わります。


ここで、既にDOM上に存在し新たにAjaxリクエストを行う必要の無いダイアログサンプルページを、Open dialogボタンクリックでもう一度開いてみます。

すると今度は、ダイアログサンプルページのtitle要素にあるjQuery Mobile Framework - Dialog Exampleでは無く、headerのh1要素のDialogがtitleとして表示されています。


このように、初期ページとして表示するか、Ajaxナビゲーションで表示するか、それともDOM上に存在するPageを表示するかでtitleの内容が変わってきます。

もし動作を統一するのであれば、Pageのdata-titleには必ずページ自体のタイトルとして表示する為の文字列を保持するようにするなどの変更を行う事が良さそうです。

        //if title element wasn't found, try the page div data attr too
        //var newPageTitle = to.jqmData("title") || to.find(".ui-header .ui-title" ).text();
        pageTitle = to.jqmData("title") || (to.jqmData("title",pageTitle) && pageTitle);

セレクトメニューのデフォルト

<select>のセレクトメニューは、Alpha3ではjQuery Mobile独自のカスタムセレクトメニューをブラウザネイティブのセレクトメニューに置き換えて表示していましたが、 Alpha4ではdefaultでブラウザネイティブのセレクトメニューを利用するようになっています。

リリースノートには、ブラウザネイティブのUIの方が利便性が高いと判断したとありますが、 只でさえ自動追加要素の多いjQuery Mobileなので、さすがに躊躇するようになったのかと思ったりしますが。

とりあえずカスタムセレクトメニューはdefault扱いでは無くなったので、 こちらもdefault動作に従ってブラウザネイティブのセレクトメニューを使う事にします。


セレクトメニューとボタン

メニュー自体はネイティブの物を使用する場合でも、セレクトメニューの現在選択されている項目はjQuery Mobileのボタン形式で表示されます。(2ページと表示されているのがセレクトメニュー)
セレクトメニュー表示

セレクトメニューの実体と表示されている要素が異なる為に、実際に選択されている項目と表示上のボタンのテキストを一致させる必要があります。


jQuery Mobileでは、下記のようにネイティブのセレクトメニューが選択変更された場合にはchangeイベントから再描画処理であるrefreshが呼ばれます。

    //events on native select
    select
      .change(function(){
        self.refresh();
      });

また、jQuery Mobileのカスタムセレクトメニューの場合には、選択が変更された場合にネイティブのセレクトメニューに対し、changeイベントを発行します。

        // trigger change if value changed
        if( oldIndex !== newIndex ){
          select.trigger( "change" );
        }

refresh処理では、ネイティブセレクトメニューのselectedIndexとその選択状態のoptionに設定された文字列を取得し、自動的にボタンのテキストを更新します。

が、このrefresh処理が呼ばれるのは、ネイティブ/カスタム共にUI上で選択状態が変更された場合となり、手動で(他のスクリプトから)プログラム的にselectedIndexを変更した場合には当然反映されません。

そんなケースについても当然jQuery Mobileでは考慮されており、jQuery Mobile Docs - Selectに書かれている通り、

var myselect = $("select#foo");
myselect[0].selectedIndex = 3;
myselect.selectmenu("refresh");

手動でrefreshを呼び出し、表示を更新する事が出来ます。


ちなみに以前の記事でも書きましたが、Blogaraのブログ更新情報ページでのページ切替のセレクトメニューは、ページ切替機能と共に現在表示しているページ番号の表示機能を兼ねているので、 セレクトメニューでの変更直後に、本来のページ番号に戻す処理を加えています。

      sel.val( selfPage ).selectmenu( 'refresh' );
      $.mobile.changePage( 'sp'+targPage+'.html', $(ev.target).data('transition'), (selfPage<targPage?false:true) );