Annotated Time Lineを使ってみた
グラフ表示をするために今まではflotを使っていたが、時系列なデータをドラッグしたり表示範囲をぐりぐりやりたいなと思ってGoogle Visualization APIのAnnotated Time Lineを使ってみた。
今までExt JS等も使っていたのでサンプルソースを見てすぐに動かすことができた。こういったやつの考え方や使い方はどれも似たり寄ったりなのでわかりやすい。
しかし想像していたよりは万能では無かった。APIを眺めた限りではデータの更新ができない。つまり非同期で差分データを取ってでデータを追加していくことができない。この辺りは今後に期待でいいのかなぁ。
SAStrutsでJSONを返す
SAStrutsでDBから取得した情報をJSON形式で返したいと思ったのでJSON-libを使ってみた。単純に取得したEntityを変換してやれば終わりだろうと思っていたらJSON-libがpublicフィールドに対応していない。だからといってgetter、setterを追加するのも馬鹿らしいと思ったのでMapに変換してからJSON形式に変換して回避。
ソースは以下のような感じになった。
private static final String JSON_RES_HEADER = "application/json"; @Resource protected EntityService entityService; @Execute(validator = false) public String index() { Map<String, Object> user = newHashMap(); // ここでMapに変換 BeanUtil.copyProperties(entityService.findById("entity"), user); ResponseUtil.write(JSONObject.fromObject(user).toString(), JSON_RES_HEADER, "utf-8"); return null; }
publicフィールドは楽だが外部ライブラリを使うとなったときにまだまだ不便だなぁ。クラスロード時に動的にgetter、setterをつけるようにすればいいのだろうか。でもどれをつける、つけないが問題になったり、そのためにアノテーションや設定を書くのもなんだかなぁということになりそう。
S2Maiでの「X-」ヘッダの追加方法
SAStrutsの勉強アプリでメール送信をするためにS2Maiを導入。ここを参考にセットアップは簡単にできた。ただ、テンプレートでのpublicフィールドには対応していない(?)ようなのでDtoにgetterを追加。残念。特に問題も無くメール送信完了。なるほど、これは簡単だ。
そしてX-Faceヘッダをつける方法を模索してみたが本家には特にやり方は書いていなかった。以下の様にテンプレートにSubjectと一緒に書いてもまったく無視された。
Subject: test mail X-Face: [X-Faceの値] 本文
ozacc-mail libraryとS2Maiのソースを見てDtoのXHeaderという変数をMailに設定してくれていることが判明。
というわけでDtoに以下のフィールドを追加。getterは追加していない。
public Map<String, String> XHeader = Maps.newHashMap();
後はActionでヘッダ情報を設定して送信。
dto.XHeader.put("X-Face", xface);
mai.sendMail(dto);
と、ここまで自力で調べた後にふと、検索してみたら作者様のブログに書いてあった・・・。試してはいないがMapで無くてもStringでもいけるようだ。う〜む、こういった情報は是非本家に乗せて頂きたいものである。
jQuery UI TabsとjQuery Form Plugin2
前の時に書いたjQuery UI TabsとjQuery Form Pluginではタブの遷移先のHTMLやjspに特定のJavaScriptを埋め込む必要があった。やはりこれだと面倒なので親画面(タブがある画面)で読み込み先をajaxForm化する方法を考えてみた。Aタグもonclickでloadするようにしてみた。loadでnoCacheというパラメータを指定しているのはPOSTで画面を取得するため(loadはパラメータを1つでも指定するとPOSTでリクエストする仕様)。GETでリクエストするとキャッシュが表示されてしまうことが多かった。
var currentPanel; function ajaxLink() { $(currentPanel).load(this.href, {'noCache': '1'}, function() { $('a', currentPanel).click(ajaxLink); $('form', currentPanel).ajaxForm({target: currentPanel, complete: ajaxForm}); }); return false; } function ajaxForm() { $('a', currentPanel).click(ajaxLink); $('form', currentPanel).ajaxForm({target: currentPanel, complete: ajaxForm}); } function tabChanged(e, ui) { currentPanel = ui.panel; ajaxForm(); } $(document).ready(function() { $("#tab > ul").tabs( {fx : {opacity :'toggle'}, load: tabChanged}); });
これで後はタブの中のコンテンツ自体はタブ化されていることを意識しなくても作成できるようになった(はず)。
Google カレンダー風味なカレンダを作ってみた
Google カレンダーのようなカレンダーに色々登録したりするのが欲しかったのでjQueryの勉強がてら作ってみた。
とりあえず実装してみたレベルのなので改善点等は色々あるし、色々なブラウザで動くかどうかは試していない。それでも月移動とか、日付のボックスをクリックした時のコールバック関数も渡せるので、とりあえずは使える感じだと思う。(この上に自分なりのレンダリングをするとなると頑張る必要があるけど)
突っ込みどころは満載だと思うので色々突っ込んで貰えると嬉しいです。
ファイル構成は以下のようにする。画像(combined.gif)はGoogle本家のをダウンロードして使用した。
日付処理でDate.jsを使っているので別途ダウンロードが必要。
ディレクトリ構成
webapp +-css +-jquery.calendar.css +-image +-combined.gif +-js +-jquery.calendar.js +-date.js
画像
本家よりダウンロードしてcombined.gifとして保存する。
http://calendar.google.com/googlecalendar/images/combined_v3.gif
jquery.calendar.css
.calendar-day { background-color: #ffffff; position: absolute; border-top: 1px solid #bbbbbb; border-left: 1px solid #bbbbbb; } .calendar-today { background-color: #FFFBA4; position: absolute; border-top: 1px solid #bbbbbb; border-left: 1px solid #bbbbbb; } .calendar-target-month { color: #6A6A6B; } .calendar-not-target-month { color: #BABAC4; } .calendar-head { position: absolute; background-color: rgb(232, 238, 247); text-align: right; } .calendar-hover { background-color: #ffffee; } .calendar-body { position: relative; } .calendar-navbutton { cursor:pointer; } img.calendar-navback { background-image:url(../image/combined.gif); background-position:-148px -17px; padding-left:2px; padding-right:0; vertical-align:middle; } img.calendar-navforward { background-image:url(../image/combined.gif); background-position:-148px 0px; padding-left:0px; padding-right:2px; vertical-align:middle; } .calendar-dateunderlay { color:#000000; font-size:13px; padding-left:5px; white-space:nowrap; }
jquery.calendar.js
$.fn.calendar = function(options, callback) { if (typeof options == 'function') { callback = options; } if (callback == null) { callback = function(){}; } options = $.extend({ width: options.width || window.innerWidth, height: options.height || window.innerHeight, head: options.head || 15, target: options.target || Date.today().toString('M/1/yyyy'), type: this.attr('type') || 'month', navi: options.nav || function() {}, click: options.click || null }, options || {}); var targetMonth = Date.parse(options.target).toString('yyyyM'); var now = Date.parse(options.target).last().sunday(); var endDate = Date.parse(options.target).moveToLastDayOfMonth(); // 最終日が土曜日の場合は次とやってしまうと1週間先になってしまう if (endDate.is().sat() == false) { endDate.next().saturday(); } var weekOfYear = now.getWeekOfYear(); var weekcnt = endDate.getWeekOfYear() - weekOfYear + 1; // 年をまたいだ場合はgetWeekOfYearで取れた値をそのまま使用 if (weekcnt < 0) { weekcnt = endDate.getWeekOfYear() + 1; } // 位置の定数定義 var colheader = 20; var leftmargin = 10; var bottommargin = 10; var navmargin = 30; // 1つ分の日付のボックスの幅 var dayBoxWidth = (options.width - leftmargin) / 7; // 1つ分の日付のボックスの高さ var dayBoxHeight = (options.height - colheader - bottommargin - 1) / weekcnt; // コンテキストパスを取得 var context = getContextPath(); // 月移動ボタン作成 $('<table/>').css({ 'position': 'absolute' , 'top': '0px' , 'left': '0px' }).append( $('<tbody/>').append( $('<tr/>').append( $('<td/>').append( $('<table/>').append( $('<tbody/>').append( $('<tr/>').append( $('<td/>').append( $('<img class="calendar-navbutton calendar-navback" height="17" width="29" src="' + context + '/image/blank.gif"/>') .click(function(e){ options.navi(Date.parse(options.target).add(-1).months().toString('M/1/yyyy'), e); }) ) ).append( $('<td/>').append( $('<img class="calendar-navbutton calendar-navforward" height="17" width="29" src="' + context + '/image/blank.gif"/>') .click(function(e){ options.navi(Date.parse(options.target).add(1).months().toString('M/1/yyyy'), e); }) ) ).append( $('<td/>').append( // 今日ボタンは当月の場合は無効化するがそれは下でやる $('<input type="button" value="今日" id="calendar-today" />') .click(function(e){ options.navi(Date.now().toString('M/1/yyyy'), e); }) ) ).append( $('<td/>').append( $('<span class="calendar-dateunderlay"/>').text(Date.parse(options.target).toString('yyyy年 M月')) ) ) ) ) ) ) ) ).appendTo(this); // 当月は「今日」ボタンを無効化 if (targetMonth == Date.now().toString('yyyyM')) { $('#calendar-today').attr('disabled', 'disabled'); } // 角の丸み用div $('<div id="top_container"/>') .css({ 'background-color': '#c3d9ff', 'margin-left': '1px', 'margin-right': '1px', 'position': 'absolute', 'top': navmargin + 'px'}) .height(1) .width(options.width - 2) .appendTo(this); $('<div id="bottom_container"/>') .css({ 'background-color': '#c3d9ff', 'margin-left': '1px', 'margin-right': '1px', 'position': 'absolute', 'top': (navmargin + options.height + 1 ) + 'px'}) .height(1) .width(options.width - 2) .appendTo(this); var container = $('<div id="container"/>').css({ 'background-color': '#c3d9ff' , 'position': 'absolute' , 'top': (1 + navmargin) + 'px' , 'float': 'left'}) .height(navmargin + options.height) .width(options.width); // 曜日ヘッダ作成 for (i = 0; i < 7; i++) { $('<div />') .width(dayBoxWidth) .height(colheader - 1) .css({ 'position': 'absolute' , 'top': (1 + navmargin) + 'px' , 'left': (i * dayBoxWidth + leftmargin) + 'px' , 'vertical-align': 'middle' , 'color': 'rgb(17, 42, 187)' , 'font-size': '80%' , 'z-index': 999 }) .text(Date.CultureInfo.shortestDayNames[i]) .appendTo(this); } // 日付ボックス作成 while (now.compareTo(endDate) != 1) { // カレンダの何行目か var lineNum = now.getWeekOfYear() - weekOfYear; // 年をまたいだ場合は負の値になってしまうのでgetWeekOfYearで取れた値をそのまま使用 if (lineNum < 0) { lineNum = now.getWeekOfYear(); } // 描画対象日 var day = now.getDay(); // 今日だったかどうかでスタイルを変更 var className = 'calendar-day'; if (Date.today().compareTo(now) == 0) { className = 'calendar-today'; } // 描画対象の月かどうかで文字のスタイルを変更 var colorClass = 'calendar-target-month'; if (targetMonth != now.toString('yyyyM')) { colorClass = 'calendar-not-target-month'; } var cursor = options.click ? 'pointer' : 'auto'; $('<div />') .css({ 'position': 'absolute' , 'width': dayBoxWidth + 'px' , 'height': dayBoxHeight + 'px' , 'top': (dayBoxHeight * lineNum + colheader) + 'px' , 'left': (day * dayBoxWidth + leftmargin) + 'px' , 'cursor': cursor}) .attr('id', 'cal_' + now.toString('yyyyMMdd')) .attr('className', className) .hover(function() { $(this).toggleClass('calendar-hover'); }, function() { $(this).toggleClass('calendar-hover'); }) .click(function(e) { if (options.click) { options.click(Date.parseExact(this.id.substring(4), 'yyyyMMdd'), e); } }) .appendTo(container).append( $('<div />').width(dayBoxWidth) .height(options.head) .addClass('calendar-head') .addClass(colorClass) .html(now.toString('d') + ' ') ) .append( $('<div />').width(dayBoxWidth) .height(dayBoxHeight - options.head) .addClass('calendar-body') .addClass(colorClass) .css({'top': options.head + 'px'}) ); now.addDays(1); } this.append(container); // 作成が終了したらコールバック関数を呼ぶ callback(); } // 簡易コンテキストパス取得用関数 function getContextPath() { var path = './'; var e = document.createElement('span'); e.innerHTML = '<a href="' + path + '" />'; url = e.firstChild.href; var p = url.split('/'); return '/'+p[3]; }
使用サンプル
<html> <head> <link rel="stylesheet" href="css/jquery.calendar.css" type="text/css"></link> <script src="http://www.google.com/jsapi"></script> <script> google.load("jquery", "1.2"); </script> <script type="text/javascript" src="js/jquery.calendar.js"></script> <script type="text/javascript" src="js/date.js"></script> <script type="text/javascript"> $(document).ready(function() { showCalendar(Date.now()); }); function showCalendar(targetDate) { // カレンダ作成 $('#calendar').empty().calendar({ width: 600 , height: 400 , target: targetDate.toString('M/1/yyyy') , nav: showCalendar }); } </script> </head> <body> <div id="calendar"></div> </body> </html>
jQuery UI TabsとjQuery Form Plugin
jQuery UI/Tabsを使用してタブを作成し、タブの内容をAjaxで取得するようにしてみた。
やり方は単純に本家のサンプルを見ればわかるとおり、書き方としては単純にリンクを張っただけ。
<script type="text/javascript"> $(document).ready(function() { $("#tab > ul").tabs( {fx : {opacity :'toggle'}}); }); </script> // ~省略~ <div id="tab" class="flora"> <ul> <li><a href="#fragment-1"><span>トップ</span></a></li> <li><a href="#fragment-2"><span>日記</span></a></li> <li><s:link href="/image/"><span>画像アップロード</span></s:link></li> <- ここがAjaxなコンテンツ </ul> <div id="fragment-1">静的なトップの内容</div> <div id="fragment-2">静的な日記の内容</div> </div>
その先に何かの入力画面でそこから確認、完了画面へ進む機能を作りたい。
ただ、せっかくタブの内容に入力画面を表示したのに通常の<form>タグでの遷移を行ってしまうと確認画面の方にもタブを作成しなければならなくなってしまう。それかタブの中に表示した画面はすべてAjax等で遷移しない作りにしないといけなくなる。
そこでjQuery Form Pluginと組み合わせれば通常の遷移する機能を作ってその<form>タグをAjax化すれば良いということに気がついた。
が
更新するtarget指定がjQuery UI Tabsで作るとタブの数などで動的に変わってid属性で指定できない。
<s:form styleId="targetForm"> <input type="text" name="email" value=""> <input type="submit" value="確認"/> </s:form> <p> <script> <!-- $('#targetForm').ajaxForm({target: 'ここに何を指定するのが問題'}); --> </script> </p>
そこで何とかタブの数や並び順が変わっても大丈夫な指定方法は無いかと色々見ていたところ、jQuery UI Tabsではタブのコンテンツ部分にはCSSのクラスとして「ui-tabs-panel」というのが付くようになっているようだ。そして表示されていないタブのコンテンツにはさらに「ui-tabs-hide」というのが付いている。
ということで以下のようにすればとりあえず期待通りに動いた。
$('#targetForm').ajaxForm({target: '.ui-tabs-panel:not(.ui-tabs-hide)'});
こんなことをしなくても別に正しいやり方があるのかなぁ。
今回の件でChromeとFirefoxの挙動が違うところに気がついた。通常の遷移する画面で以下のような画面を作っていたのだがFirefoxでは動いたがChromeでは動かなかった。
<html> <head> // 最初はheadタグの中に書いていた <script type="text/javascript"> $(document).ready(function()) { $('#targetForm').ajaxForm({target: '.ui-tabs-panel:not(.ui-tabs-hide)'}); } </script> </head> <body> // コンテンツ </body> </html>
挙動としてはChromeで上記のHTMLをAjaxで取ってきて表示とやると<body>タグの中しか評価されない感じ。結果として<head>の中に書いたJavaScriptがうまく動かなかった。それを回避するために<body>の中に<script>タグを書いて対処した。
いつものことだがこれが正しいやり方なのか分からない。
<html> <head> </head> <body> // コンテンツ <p> // bodyタグの中に記述するように修正 <script type="text/javascript"> <!-- $(document).ready(function()) { $('#targetForm').ajaxForm({target: '.ui-tabs-panel:not(.ui-tabs-hide)'}); } --> </script> </p> </body> </html>
ログイン情報をセッションに格納する方法
少しずつSAStrutsの勉強中。
ログイン情報をセッションに格納する場合は機能リファレンスを見る限り以下のようにするようだ。
@Component(instance = InstanceType.SESSION) public class UserDto implements Serializable { public String givenName; public String mail; } public class IndexAction { @ActionForm @Resource protected LoginForm loginForm; @Resource protected LoginService loginService; @Resource protected UserDto userDto; }
そしてDIされたフィールドのuserDtoにログイン情報を設定するんだと思う。それは通常ならDBから取得してentityをBeansクラスを使ってdtoに変換するのが筋なのだろう。
わざわざentityを作りたくなくてserviceがdtoを直接返すようにしたい場合は以下のようになるのだろうか。(DBではなくてActiveDirectoryに認証しに行って情報を取得している)
@Execute(input = "index.jsp") public String login() { // フィールドのDIされたuserDtoにコピー Beans.copy(loginService.search(loginForm.name, loginForm.password), userDto).execute(); return "top.jsp"; }
もしくは
@Execute(input = "index.jsp") public String login() { // 設定してもらうdtoをserviceに渡す loginService.search(userDto, loginForm.name, loginForm.password); return "top.jsp"; }
または
@Resource protected HttpSession session; @Execute(input = "index.jsp") public String login() { // 自分で設定する session.setAttribute("userDto", loginService.search(loginForm.name, loginForm.password)); return "top.jsp"; }
どれもカッコ悪いなぁ。
やはりentityを素直に作るか設定してもらうdtoをserviceに渡すのだろうか。