背景を暗くするモーダルウィンドウで表示したい!(Control.Modalの使い方)その4

ヘルパメソッドを定義

おそらく、このままでは1ヶ月後に再びモーダルウィンドウを使おうと思った時に、JavaScriptに悩むことになりそうなので、ヘルパメソッドを定義してみた。

  • modal_window_class(modal_class, options={})
  • modal_window_id(modal_id, options={})
      • 注意:全てのオプション条件を思い通りに設定できるかは疑問です...。自分の使い方の範囲で楽できるように作ってあります。
app/helpers/application_helper.rb
ヘルパー
module ApplicationHelper
...(途中省略)...
  def modal_window_class(modal_class, options={})
    options[:width] = "link.getAttribute('width') || #{options[:width].to_i}"
    options[:height] = "link.getAttribute('height') || #{options[:height].to_i}"
    non_quoted = %w{width height beforeOpen afterOpen beforeClose afterClose beforeLoad onLoad afterLoad requestOptions} 
    options_string = options.to_a.map do |k,v| 
                       v_string = (non_quoted.include?(k.to_s) || v==true || v==false) ? v.to_s : "'#{v.to_s}'"
                       "#{k.to_s}:#{v_string}"
                     end.join(',')
    javascript_tag <<-END_javascript_tag
      $$('a.#{modal_class}').each(function(link){
        new Control.Modal(link,{
          #{options_string}
        });
      });
    END_javascript_tag
  end
  
  def modal_window_id(modal_id, options={})
    non_quoted = %w{beforeOpen afterOpen beforeClose afterClose beforeLoad onLoad afterLoad requestOptions} 
    options_string = options.to_a.map do |k,v| 
                       v_string = (non_quoted.include?(k.to_s) || v==true || v==false) ? v.to_s : "'#{v.to_s}'"
                       "#{k.to_s}:#{v_string}"
                     end.join(',')
    javascript_tag "new Control.Modal( $('#{modal_id}'),{#{options_string}} );"
  end

使い方

modal_window_class
  • modal_window_class('modal', :iframe=>true, :width=>500)のように宣言した場合...
    • link_toのオプションに:class=>"modal"を指定すると、モーダルウィンドウで表示される。(iframe利用、幅:500px、高さ:自動調整)
    • link_toのオプションで:width=>640, :height=>480と指定すれば、640x480のウィンドウサイズで表示される。(ウィンドウサイズのみ個別に指定できる。)
    • それ以上のウィンドウ属性を個別に指定したい場合は、modal_window_idを利用する。
<%= link_to '設定', {:controller=>'defaults', :action=>'edit_preference'}, :class=>"modal" %>
    => :class=>"modal"を追記するだけで、モーダルウィンドウ(iframe利用、幅:500px、高さ:自動調整)で表示される。

<%= link_to '設定', {:controller=>'defaults', :action=>'edit_preference'}, :class=>"modal", :width=>640, :height=>480 %>
    => ここで指定した:width=>640, :height=>480が優先される。(:width, :heightのみ属性として指定可能)
       さらに個別にオプションを指定したい場合は、modal_window_idを利用してid属性で指定する。

<%# ここより手前(上)で<a class="modal">リンクを設定しておく必要あり。 %>
<%= modal_window_class 'modal', :iframe=>true, :width=>500 %>
    => class属性が'modal'を持つリンクを、モーダルウィンドウ(iframe利用、幅:500px、高さ:自動調整)で表示する宣言。
       以下のように展開される...

       <script type="text/javascript">
       //<![CDATA[
             $$('a.modal').each(function(link){
               new Control.Modal(link,{
                 overlayCloseOnClick:false,height:link.getAttribute('height') || 0,width:link.getAttribute('width') || 500,iframe:true
               });
             });
       //]]>
       </script>
modal_window_id
  • modal_window_id('modal_preference', :width=>320, :height=>240, :opacity=>0.6)のように宣言した場合...
    • link_toのオプションに:id=>"modal_preference"を指定すると、モーダルウィンドウで表示される。(幅:320px、高さ:240px、オーバーレイの不透明度0.6)
    • :id=>"modal_preference"を設定したlink_toは、そのページの中で1箇所だけであること。
<%= link_to '設定', {:controller=>'defaults', :action=>'edit_preference'}, :id=>'modal_preference' %>
    => :id=>"modal_preference"を追記するだけで、モーダルウィンドウ(幅:320px、高さ:240px、オーバーレイの不透明度0.6)で表示される。

<%# ここより手前(上)で<a id="modal_preference">リンクを設定しておく必要あり。 %>
<%= modal_window_id 'modal_preference', :width=>320, :height=>240, :opacity=>0.6 %>
    => id属性が'modal_preference'を持つリンクを、モーダルウィンドウ(幅:320px、高さ:240px、オーバーレイの不透明度0.6)で表示する宣言。
       以下のように展開される...

       <script type="text/javascript">
       //<![CDATA[
       new Control.Modal( $('modal_user'),{opacity:'0.6',height:'240',width:'320'} );
       //]]>
       </script>

利用例

ヘルパーを定義したので、以下のように書けるようになった。簡潔になった!(一番下の modal_window_class)

app/views/layouts/_menu2.rhtml
レイアウト(メニューを描画する部分)
<div id="menu_update">

<div id="info_bar">
<% if session[:user] %>
  <span>
    ようこそ!
    <%= link_if_authorized "#{current_user.fullname}", {:controller => 'user', :action => 'edit'}, :class=>"modal" %> 
    さん
  </span>
  <span><%= "権限 = #{current_user.roles.map {|role| role.name}.join(', ')}" %></span>
  <span><%= "path = #{current_user.paths.map {|path| path.name}.join(', ')}" %></span>
<% else %>
  <span>ようこそ! ゲストユーザー さん</span>
<% end %>
</div id="info_bar">

<div id="navcontainer">
  <ul id="navlist">
  <li><%= link_if_authorized_current 'CSVリスト', {:controller=>'csvs', :action=>'list'} %></li>
  <li><%= link_if_authorized_current '設定', {:controller=>'defaults', :action=>'edit_preference'}, :class=>"modal" %></li>
  <li><%= link_if_authorized_current 'パスワード変更', {:controller=>'user', :action=>'change_password'}, :class=>"modal" %></li>
  <li id="active"><%= '<a href="#">管理</a>' if admin? %>
    <ul id="subnavlist">
    <%= link_if_authorized_current '権限', {:controller =>'role',:action=>'list'}, :wrap_in=>"li" %>
    <%= link_if_authorized_current 'ユーザー', {:controller=>'user', :action=>'list'}, :wrap_in=>"li" %>
    <%= link_if_authorized_current 'アクセス権', {:controller=>'permission', :action=>'list'}, :wrap_in=>"li" %>
    <%= link_if_authorized_current 'フィルターSQL', {:controller=>'filtersqls', :action=>'list'}, :wrap_in=>"li" %>
    <%= link_if_authorized_current 'フィルターSET', {:controller=>'filtersets', :action=>'list'}, :wrap_in=>"li" %>
    <%= link_if_authorized_current 'デフォルト', {:controller=>'defaults', :action=>'list'}, :wrap_in=>"li" %>
    </ul>
  </li>
  <%= link_if_authorized_current 'ログアウト', {:controller=>'user', :action=>'logout'}, :wrap_in=>"li", :id=>'logout' %>
  </ul>
</div id="navcontainer">

<%# モーダルウィンドウを利用するクラス宣言。ここより手前(上)で<a class="modal">リンクを設定しておく必要あり。 %>
<%= modal_window_class 'modal', :width=>500 %>

</div id="menu_update">

スタイルシート

モーダルウィンドウの下地をメインウィンドウと同じ縞模様にしたかったので、1行追加した。(ついでに、ソースから丸ごとコピーの状態だったので、不要な指定は取り除いた。)

#modal_container {
  padding:5px;
  _background-color:#fff;
  background: url(../images/stripe_white2.png) repeat; 
  border:1px solid #666;
  overflow:auto;
  _font-family:"Lucida Grande",Verdana;
  _font-size:12px;
  color:#333;
  text-align:left;
}

#modal_overlay {
  background-color:#000;
}
  • オーバーレイを赤くしたり、なんてことも簡単にできる。#modal_overlay {background-color:#f00;}

モーダルウィンドウからメインウィンドウを更新する場合の注意(evalScripts:trueの使いどころ)

以下、メニュー上のユーザー名をクリックして、ユーザー情報を更新する場合に悩んだこと。(ajax更新範囲に、JavaScriptが含まれている場合)

app/views/user/edit.rhtml
ビュー(ユーザー名の編集ページ)
  • ユーザー名を変更した時は、メニューの左上のユーザー名と、リスト中の担当者列のユーザー名の2箇所を更新する必要があるので、以下のようにしてみた。
<div title="<%= title_helper %>" class="form" id="user_update">
  <h2><%= _("Edit user") %></h2>
  <%= error_messages_for 'user' %>
  <%= form_remote_tag :update => 'user_update', 
                      :submit => 'user_params', 
                      :url => {:action => 'edit'}, 
                      :complete => "new Ajax.Updater('menu_update', '/csvs/menu_update');
                                    new Ajax.Updater('list_update', '/csvs/list_update');" %>
      <div id="user_params">
        <%= render_partial 'edit', :user => @user, :submit => true %>
      </div>
  <%= end_form_tag %>
</div>
  • 一見、うまく動きそうなんだが、問題が発生した...。ユーザー名を変更してメインウィンドウに戻ると、その後、モーダルウィンドウへのリンクをクリックしても、モーダルウィンドウでは表示されず、ページが移動してしまう。(再読み込みすれば、正常にモーダルウィンドウで表示される。)
  • 原因が分からない状態が続いたが、以下のようにRailsのヘルパメソッドを利用すれば、正常にモーダルウィンドウで表示されることが判明。
    <%= form_remote_tag :update => 'user_update', 
                        :submit => 'user_params', 
                        :url => {:action => 'edit'}, 
                        :complete => "#{remote_function(:update=>'menu_update', :url=>'/csvs/menu_update')};
                                      #{remote_function(:update=>'list_update', :url=>'/csvs/list_update')};" %>
  • remote_functionは、展開されると、以下のようなJavaScriptが出力される。
  • 正常に動かない時との違いは、オレンジ色の部分だ。
remote_function(:update=>'menu_update', :url=>'/csvs/menu_update')
  => new Ajax.Updater('menu_update', '/csvs/menu_update', {asynchronous:true, evalScripts:true});
  • 起こっていたこと
    • :complete => "#{remote_function(:update=>'menu_update', :url=>'/csvs/menu_update')}"で、メニューが更新される。
    • メニューには<%= modal_window_class 'modal', :width=>500 %>で定義されたJavaScriptが含まれている。
    • ところが、evalScripts:true を指定しないと、JavaScriptはロードされるが、実行しないという状態になるらしい。
  • 結局、iframeを利用した時も正常に動かすため、以下のコードに落ち着いた。(parentを追加)
<%= form_remote_tag(
      :update => 'user_update', 
      :submit => 'user_params', 
      :url => {:action => 'edit'}, 
      :complete => "new parent.Ajax.Updater('menu_update', '/csvs/menu_update', {asynchronous:true, evalScripts:true});
                    new parent.Ajax.Updater('list_update', '/csvs/list_update', {asynchronous:true, evalScripts:true});") %>

忘れたくない自分メモ

  1. ヘルパメソッドで指定するオプションのキー(ハッシュのキー)は、シンボルで指定する。(:width=>500 ---> OK 'width'=>500 ---> NG)
  2. modal_window_class、modal_window_idより手前(上)で、<a class="modal">リンクを設定しておく必要あり。おそらく、理由は以下...
    • $$('a.modal').each(function(link){では、ブラウザにロードされた順にJavaScriptが実行されるため。
  3. Event.observe( window,'load',function(){なら、宣言とリンクの前後関係は無視できる。おそらく、理由は以下...
    • ウィンドウがロードされてから、JavaScriptが実行されるため
  4. しかし、3.では、モーダルウィンドウからメインウィンドウのメニューを更新すると、モーダル状態を解除した直後、モーダルウィンドウが開かなくなる不具合が発覚。おそらく、理由は以下...
    • ajax更新では、ウィンドウがロードされたことにならないため
  5. よって、現状ではEvent.observe( window,'load',function(){は使わない。
  6. もう1つ、evalScripts:trueが大事。trueにすることで、ajax更新後に実行される。falseではJavaScriptは読み込まれるが、実行されない。
    • 以下、メニュー更新時のコード例...
form_remote_tag(
  :update => 'user_update', 
  :submit => 'user_params', 
  :url => {:action => 'edit'},
  :complete => "new parent.Ajax.Updater('menu_update', '/csvs/menu_update', {asynchronous:true, evalScripts:true});")

実現したい自分メモ

  • modal_window_classやmodal_window_idと、モーダルウィンドウへのリンクの前後関係なんて、気にしないようにしたい。