ほんじゃらねっと

ダイエット中プログラマのブログ

Node.jsとwebshotパッケージでWebサイトのスクリーンショットリスト作成を自動化する

Web開発を仕事にしていると

「マニュアルやプレゼンに使うのでWebアプリのスクリーンショットをくれ」

と依頼されることがよくある。

ChromeのFireshotのようなブラウザの拡張機能を使ったりすれば

1画面ずつ撮っていけるのだけど、

chrome.google.com

たくさんのページのスクリーンショットを、

変更がある度に撮り直すのは面倒なのでなんとか自動化したい。

「node-webshot」モジュールを使う

探してみると、

Node.jsの「node-webshot」パッケージを使えば

手軽にスクリーンショット撮影スクリプトが作れそうだ。

www.npmjs.com

Node.jsでWebKitブラウザの機能を利用可能にするPhantomJSの

スクリーンキャプチャ機能をラップしたものらしい。

PhantomJS | PhantomJS

PhantomJSはブラウザで行っている処理を

色々自動化するのに使えそうなので、また調査してみよう。

「node-webshot」パッケージは

下記のnpmコマンドでアプリにインストールできる。

npm install webshot --save-dev

URLのリストを作って、連続でスクリーンショットを撮るスクリプトを作成する

早速このwebshotパッケージを使って

スクリーンショット自動撮影スクリプトを作ってみる。

URLのリストを読み込んで

各URLのスクリーンショットを保存するようなものを作っておけば

画面に変更があってもコマンド1つで再作成できるので良さそうだ。

下記のようなスクリプトを作成してみた。

capture_screen.js

var webshot = require('webshot');
var fs = require('fs');

// 保存ファイル名とURLのリストを作成する
var baseUrl = 'http://test-domain.com';
var links = { 
    '01_login': baseUrl + '/login.html',
    '02_top': baseUrl + '/top.html',
    '03_shohin_list': baseUrl + '/shohin_list.html',
    '04_shohin_edit': baseUrl + '/shohin_edit.html'
};

// 各URLのスクリーンショットを指定したファイル名で「shots」フォルダに保存する
Object.keys(links).forEach(function (key) {
    var href = links[key];
    webshot(href, 'shots/' + key + '.png', {}, function () {}); 
}); 

URLリストの部分は撮影したいWebサイトのアドレスに合わせて変更する。

今回はデフォルトの設定で撮影を行っているが、

生成される画像のサイズ等はwebshot関数のオプションで指定できる。

サーバサイドJavaScript Node.js入門 (アスキー書籍)

サーバサイドJavaScript Node.js入門 (アスキー書籍)

  • 作者: 清水俊博,大津繁樹,小林秀和,佐々木庸平,篠崎祐輔,高木敦也,西山雄也,Jxck
  • 出版社/メーカー: KADOKAWA / アスキー・メディアワークス
  • 発売日: 2014/02/27
  • メディア: Kindle版
  • この商品を含むブログを見る

treeコマンドで出力したJSONをExcelで階層表示できるように変換する

あるプロジェクトのソース分析を行う必要があったので、

まずはソースファイルの一覧をExcelに出力してみることにした。

treeコマンドがいい感じで階層出力してくれるのだけど、

それをそのままExcelに持っていくと文字化けするので、

treeコマンドから一旦JSON形式で階層情報を出力し、

それをnodejsスクリプトで変換してcsvに出力する方法をとった。

treeコマンドで「-J」オプションを指定するとJSON形式で出力してくれる。

$ cd <プロジェクトディレクトリ>
$ tree -J > file_tree.json

こうやって出力したjsonを下記のスクリプトでCSVにコンバートする。

階層毎に列がインデントされるようにして、

Excel上で拡張子でフィルタリングできるようにファイルの拡張子を出力する。

filetree2csv.js

//treeから出力したJSONをrequireで読み込み
var fileTreeJson = require('./file_tree.json');
var path = require('path');

//カンマの繰り返し出力のためのfunctionをStringに追加
String.prototype.repeat = function (num) {
    for (var str = ''; (this.length * num) > str.length; str += this);
    return str;
};

var parseFileList = function (level, fileNo, fileList) {
    fileList.forEach(function (file) {
        if (file.type != 'report') {
            var line = ''; 
            var type = ''; 
            if (file.type == 'file') {
                fileNo += 1;
                line += fileNo;
                type = path.extname(file.name);
            } else {
                type = 'dir';
            }   
            line += ',' + type;
            line += ','.repeat(level) + file.name;
            console.log(line);
            if (file.type == 'directory') {
                fileNo = parseFileList(level + 1, fileNo, file.contents);
            }   
        }   
    }); 
    return fileNo;
};

parseFileList(0, 0, fileTreeJson);

これを、下記のコマンドで実行する。

$ node filetree2csv.js > file_tree.csv

出力されたCSVをExcelで開き、タイトル行や罫線を整えたら、

ファイル一覧資料として使える。

Java8のJavascriptエンジンNashornでパスワード付きExcelを開く

以前Groovyで作成したパスワード付きExcelを開くスクリプトJavascriptでも 書いてみよう、ということでnode.jsで色々試してみたところ、パスワード付き Excelに対応しているライブラリが見つからなかった。 (普通にExcelを操作するだけならxlsjs等良さげなライブラリはあった)

他の手段がないかと調べてみると、Java SE 8に新JavascriptエンジンNashornが搭載され、 JavascriptからJavaのライブラリが使えるとのことだったので試してみた。

excel_decryption_test.js

var XLS_PATH= 'encrypted_file.xls';

var Biff8EncryptionKey = Java.type('org.apache.poi.hssf.record.crypto.Biff8EncryptionKey');
var WorkbookFactory = Java.type('org.apache.poi.ss.usermodel.WorkbookFactory');
var FileInputStream = Java.type('java.io.FileInputStream');

Biff8EncryptionKey.setCurrentUserPassword('<パスワード>');

var fis = new FileInputStream(XLS_PATH);
var workbook = WorkbookFactory.create(fis);
var sheet0 = workbook.getSheetAt(0);
var companyName = sheet0.getRow(4).getCell(1);
print('company Name: ' + companyName);

fis.close();

pom.xml

<project xmlns='http://maven.apache.org/POM/4.0.0' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
    xsi:schemaLocation='http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd'>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>sample</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>Maven Quick Start Archetype</name>
    <url>http://maven.apache.org</url>
    <dependencies>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.11</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.11</version>
        </dependency>
    </dependencies>
</project>

mavenで必要なjarをダウンロードしておき、 jdkのbinフォルダに含まれる「jjs」コマンドでclasspathを指定して Javascriptファイルを指定して実行する。

$ mvn dependency:copy-dependencies
$ jjs -scripting -cp target/dependency/poi-3.11.jar:target/dependency/poi-ooxml-3.11.jar:target/dependency/poi-ooxml-schemas-3.11.jar excel_decryption_test.js

うまくいった。

nashhornにはいくつかJavascript拡張関数が追加されており、 たとえばJavaのライブラリはJava.typeという関数を呼び出すことで簡単に使える。 クラスパス指定さえもうちょっと楽にできるようになれば、もっと使い勝手はよくなるはず。

他に、Java.load関数を使えばほかのjavascriptファイルを読み込むことができる。 これを使えば、jQuery等のライブラリを使って処理することも可能だろう。

Nashornの日本語ユーザーガイド

JavascriptライブラリとJavaライブラリを組み合わせて使えるのは、 色々と夢が広がって楽しみ。 文法としてJavascriptさえ使えるようになっておけば、Nashhorn、Node.js、JScript、ブラウザJavascriptなどの 実装を使い分けることで何でもできるようになりそう。

CakePHPとjQueryで汎用的なリンククリックカウンターを作成する

ページに記載した外部リンクが何回クリックされたかを知りたかったので、 リンククリックをカウントするスクリプトを作成した。 Google Analyticsのイベントトラッキング機能の簡易版。

リンクにあらかじめ決めたCSSクラスとリンクを識別するための属性を 設定しておき、リンクがクリックされた時にAjax処理でサーバに属性情報を 送信して記録する。

サーバ側はCakePHPで作成したけど、 大したことはしていないので他の言語でも実現できるはず。 1つコントローラを作成しておけばどのページからのリンクでもカウントできる。

HTMLソース

<html>
    <head>
        <script src='//ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js'></script>
        <script type='text/javascript' src='/js/click_counter.js'></script>
    </head>
    <body>
        ...
        <a href='http://honjala.net/' class='click-count' data-linkname='ブログ'>ほんじゃら堂</a>
        <a href='https://twitter.com/honjala' class='click-count' data-linkname='Twitter'>Twitter</a>
        ...
    </body>
</html>

click_counter.js

$(document).ready(function () {
    (function($) {
         var ClickTrackService = {
            track: function (name, url, options) {
                var callerUrl = location.href;
                $.ajax({
                    'type': 'POST',
                    'dataType': 'json',
                    'url': '/click_tracks/track/',
                    'data': {
                        'name': name,
                        'url': url,
                        'caller_url': callerUrl
                    },
                    'success': options.success || function (data) {},
                    'error': options.error || function (xml, status, e) {}
                });
            }
        };

        $('a.click-count').live('click', function (e) {
            var linkName = $(this).attr('data-linkname');
            var url = $(this).attr('href');
            if (linkName != null &amp;&amp; linkName != '') {
                ClickTrackService.track(linkName, url);
            }
        });
    })(jQuery);
});

保存用テーブル定義

CREATE TABLE IF NOT EXISTS click_tracks(
    id integer not null auto_increment,
    name varchar(100),
    url text,
    caller_url varchar(255),
    ip varchar(20),
    created datetime,
    PRIMARY KEY (id)
) ENGINE=InnoDB;

click_tracks_controller.php

<?php
class ClickTracksController extends AppController {
    public $name = 'ClickTracks';
    public $uses = array(
        'ClickTrack',
    );  

    public function track() {
        $url = null;
        $name = null;
        $caller_url = null;
        if (array_key_exists('form', $this->params)) {
            if (array_key_exists('url', $this->params['form'])) {
                $url = $this->params['form']['url'];
            }
            if (array_key_exists('name', $this->params['form'])) {
                $name = $this->params['form']['name'];
            }
            if (array_key_exists('caller_url', $this->params['form'])) {
                $caller_url = $this->params['form']['caller_url'];
            }
        }

        $ip = $_SERVER['REMOTE_ADDR'];

        $this->ClickTrack->create();
        $track = array(
            'url' => $url,
            'name' => $name,
            'caller_url' => $caller_url,
            'ip' => $ip
        );  
        $this->ClickTrack->save($track);

        //JSONレスポンス処理
    }
}

本当はGoogle Analyticsのイベントトラッキングを使いたかったのだけど、 うまくクリックを認識してくれなかった。

jQuery MobileでiPhone用Webサイトを作成する

jQuery Mobile
http://jquerymobile.com/


いろいろはまったので、メモ。
バージョンは 1.0 ALPHA 2。

page毎にHTMLを分けて作成する

これは好みだと思うのだけど、URL直指定でそれぞれのページを呼び出せたりするようにするなら、分けた方が簡単な気がした。


構成としては、下記のような形にしてる。

<html>
<head>
    <!-- 全ページ共通のリソース読み込み -->
    <link rel="stylesheet" type="text/css" href="sample_base.css" />
    <script type="text/javascript" src="sample_base.js"></script>
  </head>
<body>
<div data-role="page" class="user-home-page">
<!-- このページ専用のリソース読み込み -->
<link rel="stylesheet" type="text/css" href="sample_user_home.css" />
<script type="text/javascript" src="sample_user_home.js"></script>
<!-- ページレイアウト定義 -->
<div data-role="header">
</div>
<div data-role="content">
</div>
<div data-role="footer">
</div>
</div>
</body>
</html>


jQuery Mobile はページ遷移時に遷移先の[data-role="page"]なタグだけを読み込むらしく、pageタグの外で定義されたcssやjsは読み込んでくれないので、pageタグ内でページ固有のリソースを読み込むようにしてる。


最初にURLを指定して呼び出したページのみ、完全なHTMLとして読み込んでくれるようなので、各ページを上記のような形にしておくことで直URL指定とページ遷移の両方に対応できるらしい。


ただ、ドキュメントを読んだところでは、JSやCSSについては1つにまとめておいて、表示するページ毎に処理を切り替えるのがオススメらしい。


pageタグの名前定義にclassを使っているけど、これはID属性が定義されているとページ遷移で呼び出した時にjQuery MobileがdivタグでラップしてIDを別途つけちゃうから。これをされるとページ遷移時とURL直指定時とでDOM構造が変わって色々面倒なのでclassを使用する。ドキュメントにも「classで指定すりゃいいよ」的なことが書いてあった。

ページ初期化処理

ページ遷移時にJSで初期化処理を行ないたい時は、$(document).ready(function () {}) ではなく、$(".ページクラス").live("pagecreate", function () {}) を使う。readyは最初のページ表示時点で呼び出し済みなので、2ページ目以降では呼び出されないっぽい。

$(".home-page").live("pagecreate", function () {
var page = $.homePage();
});

formタグに設定されたlive submitイベントが強敵

formタグにはあらかじめliveなsubmitイベントのハンドラが設定されていて、submitするとajax処理でページ遷移しようとしちゃう。

$('form').live("submit", function () { ... });


な感じで定義されてるので、特定のフォームのみsubmit処理をしないようにしたくてもうまくいかなかった。dieしたら他のページのフォームが効かなくなるし。設定($.mobile.ajaxFormsEnabled)で無効にした場合もやっぱり全フォームの送信処理が効かなくなっちゃう。


これに関してはまだ解決策は見つかっておらず、フォームを送信したくない場合はsubmit以外のイベントで処理するようにしてる。

Google AJAX Search API でシンプルにローカル検索

Google AJAX Search API
http://code.google.com/intl/ja/apis/ajaxsearch/


ドキュメントに掲載されているサンプルでは備えつけのフォームや検索結果を表示する
ようになっているのだけど、Javascript内で検索処理を実行して
結果をJSONで処理する方法もあったので、そのサンプルを掲載しておく。


jQueryも使用している。

// 地図生成
var center = new google.maps.LatLng(37.67008923368245, -223.881025375);
var map = new google.maps.Map(document.getElementById("map"), {
zoom: 4,
center: center,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
// ローカル検索用サーチャーオブジェクト生成
var searcher = new google.search.LocalSearch();
// 検索範囲を設定
searcher.setCenterPoint(map.getCenter());
// 検索結果処理を定義(ここではマーカーを配置してる)
var markers = [];
searcher.setSearchCompleteCallback(self, function () {
/* このへんに既存のマーカー削除処理 */
$.each(searcher.results, function (i, result) {
var marker = new google.maps.Marker({
position: new google.maps.LatLng(parseFloat(result.lat), parseFloat(result.lng)),
map: map
});
result.marker = marker;
markers.push(marker);
});
});
// 検索を実行
searcher.execute('大阪城');

jQuery UI でダイアログ表示

別の記事でも書いたけど、個人で開発しているWebアプリでは、だいたいjQuery UIを使用してる。


今回はその中でもよく利用しているdialogの使い方をまとめておく。
dialog機能を使うと、割と簡単にエラーダイアログやフォーム表示用のサブウィンドウを作成することができる。
http://jqueryui.com/demos/dialog/


僕の使い方としては、あらかじめ使用するダイアログやサブウィンドウをHTML内に非表示で埋め込んでおき、
必要な時にダイアログとして表示する、という方法をとっている。
1つのファイルにまとまってると、本体のHTML、ダイアログ、サブウィンドウを区別なく編集できるので結構楽だと思う。


OKダイアログ(通知やエラー表示用)、確認ダイアログ、フォームダイアログを
それぞれ表示できるサンプルを書いておく。
よく使うOKダイアログや確認ダイアログは関数化しておくと便利。


sample.html

...
<head>
...
<link rel="stylesheet" href="/css/sunny/jquery-ui-1.8.custom.css" type="text/css" />
<link rel="stylesheet" href="/css/sample.css" type="text/css" />
<script type="text/javascript"> google.load("jquery", "1.4.2"); </script>
<script type="text/javascript"> google.load("jqueryui", "1.8.0"); </script>
<script type="text/javascript" src="/js/sample.js"></script>
...
</head>
<body>
...
<a href="#" id="show-error-dialog-btn">エラーダイアログを表示</a>
<a href="#" id="show-confirm-dialog-btn">確認ダイアログを表示</a>
<a href="#" id="show-form-dialog-btn">フォームダイアログを表示</a>
...
<div class="dialog" id="delete-confirm-dialog" title="メッセージ削除確認">
<div class="ui-state-highlight">
<span class="ui-icon ui-icon-info"></span>
本当にメッセージを削除してもよろしいですか?
</div>
</div>
<div class="dialog" id="message-update-failed-dialog" title="サーバエラー">
<div class="ui-state-error">
<span class="ui-icon ui-icon-alert"></span>
メッセージの更新に失敗しました
</div>
</div>
<div class="dialog" id="message-edit-dialog" title="メッセージを編集">
<form id="message-edit-form" method="POST">
<input type="text" id="message-field" />
</form>
</div>
...


sample.css

.dialog {
display: none;
}


sample.js

var Page = {
show_ok_dialog: function (dialog_id) {
$(dialog_id).dialog({
bgiframe: true,
modal: true,
buttons: {
"OK": function () {
$(this).dialog("destroy");
}
}
});
},
show_confirm_dialog: function (dialog_id, ok_func, cancel_func) {
$(dialog_id).dialog({
bgiframe: true,
modal: true,
buttons: {
"OK": ok_func,
"キャンセル": cancel_func
}
});
}
};
$(document).ready(function () {
$("#show-error-dialog-btn").click(function (e) {
e.preventDefault();
Page.show_ok_dialog("#message-update-failed-dialog");
});
$("#show-confirm-dialog-btn").click(function (e) {
e.preventDefault();
Page.show_confirm_dialog("#delete-confirm-dialog",
function () {
// OKボタンをクリックした時の処理
...
$(this).dialog("destroy"); // ダイアログを削除
},
function () {
// キャンセルボタンをクリックした時の処理
...
$(this).dialog("destroy");
}
);
});
$("#show-form-dialog-btn").click(function (e) {
e.preventDefault();
$("#message-edit-dialog").dialog({
bgiframe: true,
modal: true,
buttons: {
"更新": function () {
// 更新ボタンを押した時の処理
var message = $("#message-field").val(); // 同じHTML内なのでフォームの値も取得できる
// サーバに送信したり処理したり
...
$(this).dialog("destroy");
},
"キャンセル": function () {
// キャンセルボタンを押した時の処理
...
$(this).dialog("destroy");
}
}
});
});
});

jQuery UI.Layout Plugin を使って単一ページUIを作る

Google App Engineを使うようになって、リクエスト時間の節約のために
重いページ(HTML)のロードは最初の1回だけにして、以降はページ遷移なしで
AjaxJSONのやりとりをしてビューを変更するような方法をとることが多くなった。


UIについても毎回デザインを考えるのも面倒だしロクなものができなかったので、
jQueryUIとjQuery UI.Layout Pluginを利用するようになった。
この2つを利用するだけでアプリケーション風のUIが実現できて、
UI関連の悩みをほぼ解消してくれた。


jQueryUIはボタンやタブ、アコーディオンメニューなどのUIウィジェットを、
jQueryUIのサイト上で作成したデザインテーマに合わせて表示してくれるので、
統一感のあるデザインがてっとり早くできる。
自分でカスタマイズした設定を再編集できるようにしてくれたらもっと便利なのだけど。
http://jqueryui.com/


jQuery UI.Layout Plugin はjQueryUIのプラグインで、ページを上下左右中央に
分割してアプリケーション風に表示できるようにしてくれる。入れ子にしたり
細かくオプションを設定して調整できるので、便利。
http://layout.jquery-dev.net/


今後も上記の構成でAppEngine用アプリを作ると思うので、
再利用できる雛形を作っておく。


まず、CSSとJSのライブラリは、下記のものを読み込んでおく。
blueprintは必要ない気がしてきてる。
jQueryとjQueryUIのライブラリはGoogleのSDNを使用する。
作成したデザインテーマのCSSとイメージフォルダを配置してCSSを読み込んでおけば、
ちゃんとデザインテーマは反映される。

<link rel="stylesheet" href="/css/blueprint/screen.css" type="text/css" media="screen, projection" />
<link rel="stylesheet" href="/css/blueprint/print.css" type="text/css" media="print" />
<!--[if IE]><link rel="stylesheet" href="/css/blueprint/ie.css" type="text/css" media="screen, projection" /><![endif]-->
<link rel="stylesheet" href="/css/jquery-ui-1.8.custom.css" type="text/css" />
{% block sublayoutcss %}{% endblock %}
{% block extracss %}{% endblock %}
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript"> google.load("jquery", "1.4.2"); </script>
<script type="text/javascript"> google.load("jqueryui", "1.8.0"); </script>
<script type="text/javascript" src="/js/jquery/jquery.layout.min.js"></script>
<script type="text/javascript" src="/js/main.js"></script>


HTMLのコンテンツは、jQuery UI.Layout Plugin用にdivを作成しておく。

よく使うパターンは、
上にヘッダー(.ui-layout-north)を配置して、
左にメニュー(.ui-layout-west)、
中央に切り替わるメインコンテンツ(.ui-layout-center)、
フッターなし、
という形。

左のメニューはさらに分割して、
中央(.west-center)にメニュー表示エリア、
下部(.west-south)に広告用エリアを作成しておく。

中央のメインコンテンツもさらに分割するが、
ここでは様々なレイアウトを切り替えるので、個々のビュー用にdiv(.center-view)を
作成してその中に個別のレイアウトを作成する。
今回の例では、
初期表示用ビュー(#index-view)は上部(.center-north)と中央(.center-center)に分割し、
ヘルプビュー(#help-view)は上部(.center-north)と中央(.center-center)、下部(.center-south)に分割
している。

<div class="ui-layout-north">
</div>
<div class="ui-layout-west">
<div class="west-center">
<a class="ui-state-default ui-corner-all index-btn" href="#"><span class="ui-icon ui-icon-home"></span>インデックス</a>
<a class="ui-state-default ui-corner-all help-btn" href="#"><span class="ui-icon ui-icon-check"></span>ヘルプ</a>
</div>
<div class="west-south">
</div>
</div>
<div class="ui-layout-center">
<div class="center-view" id="index-view">
<div class="center-north">
<h3 class="ui-widget-header">インデックス</h3>
</div>
<div class="center-center">
</div>
</div>
<div class="center-view" id="help-view">
<div class="center-north">
<h3 class="ui-widget-header">ヘルプ</h3>
</div>
<div class="center-center">
</div>
<div class="center-south">
</div>
</div>
</div>


最後にJavascriptでレイアウトを設定する。
ビュー毎にJavascriptのオブジェクトを使用するようなフレームワークにしてみた。
prototypeを使うべき場面と使うべきでない場面は正直よく分かってない。

(function ($) {
$.tsPlatform = function (options) {
return new Platform(options);
};
var Platform = function (options) {
this.init(options);
};
$.extend(Platform.prototype, {
init: function (options) {
var self = this;
self.options = $.extend({
"doc_layout": null,
"west_layout": null,
"view_map": {
"index": new IndexView(self),
"help": new HelpView(self)
},
"current_view": null
}, options || {});
self.setup_layouts();
self.setup_events();
self.load_view("index");
},
setup_layouts: function () {
var self = this;
self.options.doc_layout = $('body').layout({
name: "mainLayout",
defaults: {
},
north: {
resizable: true,
slidable: false,
closable: false,
spacing_open: 0,
size: 60
},
west: {
size: 200,
resizable: false,
slidable: false,
closable: false,
spacing_open: 0,
},
center: {
}
});
self.options.west_layout = $("div.ui-layout-west").layout({
center: {
paneSelector: ".west-center"
},
south: {
paneSelector: ".west-south",
resizable: false,
slidable: false,
closable: false,
spacing_open: 0,
size: 100
}
});
},
setup_events: function () {
var self = this;
$(".index-btn").click(function (e) {
e.preventDefault();
self.load_view("index");
});
$(".help-btn").click(function (e) {
e.preventDefault();
self.load_view("help");
});
},
get_current_view: function () {
var self = this;
if (self.options.current_view) {
return self.options.current_view;
} else {
return new BaseView();
}
},
load_view: function (view_code) {
var self = this;
self.get_current_view().unload();
$.each(self.options.view_map, function (key, view) {
if (key === view_code) {
view.load();
self.options.current_view = view;
}
});
}
});
/* Abstract Base View Class */
var BaseView = function () {};
$.extend(BaseView.prototype, {
init: function (platform, options) {},
load: function () {},
unload: function () {},
update_view: function () {}
});
/* Index View Class */
var IndexView = function (platform, options) {
this.init(platform, options);
};
$.extend(IndexView.prototype, BaseView.prototype, {
init: function (platform, options) {
var self = this;
self.platform = platform;
self.options = $.extend({
"center_layout": null,
"view_node": "#index-view"
}, options || {});
self.view_node = $(self.options.view_node);
self.setup_events();
},
load: function () {
var self = this;
$(self.view_node).show();
if (!self.options.center_layout) {
self.setup_layouts();
}
self.update_view();
},
unload: function () {
var self = this;
$(self.view_node).hide();
},
update_view: function (note_key) {
var self = this;
},
setup_layouts: function () {
var self = this;
self.options.center_layout = $(self.view_node).layout({
north: {
paneSelector: ".center-north",
resizable: false,
slidable: false,
spacing_open: 0
},
center: {
paneSelector: ".center-center"
}
});
},
setup_events: function () {
var self = this;
}
});
/* Help View Class */
var HelpView = function (platform, options) {
this.init(platform, options);
};
$.extend(HelpView.prototype, BaseView.prototype, {
init: function (platform, options) {
var self = this;
self.platform = platform;
self.options = $.extend({
"center_layout": null,
"view_node": "#help-view"
}, options || {});
self.view_node = $(self.options.view_node);
self.setup_events();
},
load: function () {
var self = this;
$(self.view_node).show();
if (!self.options.center_layout) {
self.setup_layouts();
}
self.update_view();
},
unload: function () {
var self = this;
$(self.view_node).hide();
},
update_view: function (note_key) {
var self = this;
},
setup_layouts: function () {
var self = this;
self.options.center_layout = $(self.view_node).layout({
north: {
paneSelector: ".center-north",
resizable: false,
slidable: false,
spacing_open: 0
},
center: {
paneSelector: ".center-center"
},
south: {
paneSelector: ".center-south"
}
});
},
setup_events: function () {
var self = this;
}
});
})(jQuery);
$(document).ready(function () {
var platform = $.tsPlatform({});
});


今後はこのフレームワークを改良していこう。

地図上で場所を選択すると選択した地点の座標をフォームのhiddenフィールドに入力するプラグイン


jQueryプラグインの作成方法を学びつつ、色々作ってみる。


今回は、HTMLのフォームで場所の情報を登録する際に座標情報を入力してもらうのに使えそうなプラグインを作った。
表題のままだけど、地図で地点を選択するとその座標情報が指定したフォームフィールドに入力される。


Google Maps API の V3 で作ってみた。
http://code.google.com/intl/en/apis/maps/documentation/v3/


初期表示場所をクッキーに保存するのに、jquery.cookie.js プラグインを使用してる。
http://plugins.jquery.com/project/cookie


map_selector.js

(function($) {
$.fn.mapPointSelector = function (options) {
var elements = this;
return elements.each(function () {
new MapPointSelector(this, options);
});
};
var MapPointSelector = function (map_node, options) {
this.init(map_node, options);
};
$.extend(MapPointSelector.prototype, {
init: function (map_node, options) {
var self = this;
self.options = $.extend({
"lat_field": "#id_lat",
"lng_field": "#id_lng"
}, options || {});
self.map_node = $(map_node);
self.map = null;
self.center_marker = null;
self.lat_field = $(self.options.lat_field);
self.lng_field = $(self.options.lng_field);
self.setup_map();
},
update_point_fields: function (center) {
var self = this;
self.lat_field.val(center.lat());
self.lng_field.val(center.lng());
},
setup_map: function () {
var self = this;
var map_status = self.read_map_cookie();
var zoom = 4;
var center = null;
if (self.lat_field.val().length > 0) {
center = new google.maps.LatLng(self.lat_field.val(), self.lng_field.val());
} else if (map_status) {
zoom = map_status.zoom;
center = new google.maps.LatLng(map_status.lat, map_status.lng);
} else {
center = new google.maps.LatLng(37.321429, -122.015791);
}
self.map = new google.maps.Map(document.getElementById(self.map_node.attr("id")), {
zoom: zoom,
center: center,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
self.center_marker = self.create_marker("ここの座標を登録します", center);
self.setup_map_events();
},
setup_map_events: function () {
var self = this;
google.maps.event.addListener(self.map, 'drag', function () {
var center = self.map.getCenter();
self.center_marker.setPosition(center);
self.update_point_fields(center);
});
google.maps.event.addListener(self.map, 'idle', function () {
var center = self.map.getCenter();
var zoom = self.map.getZoom();
self.save_map_cookie(center, zoom);
});
},
save_map_cookie: function (center_latlng, zoom) {
var val = center_latlng.lat() + "," + center_latlng.lng() + "," + zoom;
$.cookie("map-status", val, {"path": '/', "expires": 30});
},
read_map_cookie: function () {
var status = $.cookie("map-status");
if (status) {
var state_list = status.split(",");
if (state_list.length == 3) {
return {"lat": state_list[0] - 0, "lng": state_list[1] - 0, "zoom": state_list[2] - 0};
}
}
return null;
},
create_marker: function (title, latlng) {
var self = this;
var marker = new google.maps.Marker({
position: latlng,
map: self.map,
title: title
});
return marker;
}
});
})(jQuery);


test.js

$(document).ready(function() {
var map_selector = $("#map").mapPointSelector();
});


test.css

#map {
width: 100%;
height: 350px;
}


test.html

...
<head>
...
<link rel="stylesheet" href="/css/test.css" type="text/css" />
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript"> google.load("jquery", "1.4.2"); </script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript" src="/js/jquery/jquery.cookie.js"></script>
<script type="text/javascript" src="/js/map_selector.js"></script>
<script type="text/javascript" src="/js/test.js"></script>
...
</head>
...
<form ...>
...
<input type="hidden" name="lat" id="id_lat" />
<input type="hidden" name="lng" id="id_lng" />
...
</form>
<div id="map-block">
<div id="map"></div>
</div>
...


ちょっと変更して、ズームレベルも保存できるようにした方がいいかな。

jQueryでリスト・テーブル操作 - 行の上下移動

ここ数件のプロジェクトでリストやテーブルの行追加、削除やソート、上下移動などの処理を書く事があったので、メモしておく。


今回は行の上下移動。
リストの各行に「上へ」「下へ」というボタンなりリンクなりアイコンなりを配置しておき、それをクリックすると対象の行が上下に移動するというもの。


test.html

...
<script type="text/javascript" src="/js/jquery/jquery-1.3.1.min.js"></script>
<script type="text/javascript" src="/js/test.js"></script>
...
<ul id="test-list">
<li class="test-row">
<input type="hidden" name="data" value="1:id1" />
<span class="title">データ1</span>
<a href="#" class="control up-control">上へ</a>
<a href="#" class="control down-control">下へ</a>
</li>
<li class="test-row">
<input type="hidden" name="data" value="2:id2" />
<span class="title">データ2</span>
<a href="#" class="control up-control">上へ</a>
<a href="#" class="control down-control">下へ</a>
</li>
<li class="test-row">
<input type="hidden" name="data" value="3:id3" />
<span class="title">データ3</span>
<a href="#" class="control up-control">上へ</a>
<a href="#" class="control down-control">下へ</a>
</li>
</ul>
...


hidden要素はフォームで変更結果を保存する場合に使う。
value値は「インデックス:データのID」で、インデックスを上下移動する時にjavascriptで更新する。
インデックスデータの保持についてはもう少し洗練された方法がありそうな気がする。


test.js

function update_row_index() {
$("#test-list li").each(function (i) {
var data_input = $('input[name="data"]', this);
var vals = data_input.val().split(":");
data_input.val((i + 1) + ":" + vals[1]);
});
}
$(document).ready(function() {
$("#test-list .up-control").click(function (e) {
e.preventDefault();
var row = $(this).parent();
if (row.not(":first")) {
row.insertBefore(row.prev());
update_row_index();
}
});
$("#test-list .down-control").click(function (e) {
e.preventDefault();
var row = $(this).parent();
if (row.not(":last")) {
row.insertAfter(row.next());
update_row_index();
}
});
});


これで動くはず。
各行が複雑な構造の場合は、parent().parent()l... みたいにしてるけど、もっと簡単に親ノードを呼ぶ方法はないものかな。

Thickboxで呼び出し元ウィンドウとデータのやりとり

他に方法が思いつかなかったので、親ウィンドウ側に関数を用意して
それをThickbox側から呼び出す方法をとった。


iframe版しか考えていないので、ajax版の使い方だとまた別の方法があると思う。


parent.html

<head>
  ...
  <script type="text/javascript" src="jquery-1.3.1.min.js"></script>
  <link rel="stylesheet" href="thickbox.css" type="text/css" />
  <script type="text/javascript" src="thickbox-compressed.js"></script>
  <script type="text/javascript" src="parent.js"></script>
  ...
</head>
<body>
...
<a href="/path/to/child/?pre_callback=pre_func&post_callback=post_func&TB_iframe=True&height=400&width=400" class="thickbox" title="サブウィンドウ">サブウィンドウを開く</a>
...
</body>


parent.js

var remote_func = {
pre_func: function () {
return ["param_data1", "param_data2", "param_data3"];
},
post_func: function (returned_data) {
...
}
};


views.py

...
def show_child(request):
pre_callback_func = request.GET["pre_callback"]
post_callback_func = request.GET["post_callback"]
return render_to_response('child.html', {"pre_callback_func": pre_callback_func, "post_callback_func": post_callback_func}, context_instance=RequestContext(request))
...


child.html

<head>
  ...
  <script type="text/javascript" src="jquery-1.3.1.min.js"></script>
  <script type="text/javascript" src="child.js"></script>
  ...
</head>
<body>
...
<div id="pre_func">{{ pre_callback_func }}</div>
<div id="post_func">{{ post_callback_func }}</div>
...
</body>


child.js

$(document).ready(function () {
$("#exec-btn").click(function (e) {
var callback_func = $("#post_func").val();
if (callback_func.length > 0) {
window.parent.remote_func[callback_func](return_data);
}
window.parent.tb_remove();
});
var pre_func = $("#pre_func").val();
if (pre_func.length > 0) {
var param_data = window.parent.remote_func[pre_func]();
...
}
});

django+jQuery Form Pluginでファイルアップロードしてみた


前回発見したjQuery Form Pluginを使って実際にファイルをアップロードするサンプルを作った。
http://d.hatena.ne.jp/piro_suke/20080615/1213498024


ファイルをアップロードする場合のみ、iframeが使用される関係でサーバ側から
返却するデータをtextareaタグで囲まないといけないことに気づかず、苦労した。


とりあえずこのサンプルがあればまた思い出して応用できるだろう。


アップロードフォームHTML

...
<script type="text/javascript" src="/static/javascripts/jquery/jquery-1.2.6.min.js"></script>
<script type="text/javascript" src="/static/javascripts/jquery/jquery.form.js"></script>
<script type="text/javascript" src="/static/javascripts/upload.js"></script>
<form id="upload-form" action="/image/upload/" method="POST" enctype="multipart/form-data">
<div>
<label>Title</label>
<span><input type="text" name="title" id="id_title" /></span>
</div>
<div>
<label>Image</label>
<span><input type="file" name="image" id="id_image" /></span>
</div>
<div><input type="submit" id="upload-btn" value="Upload" /></div>
</form>
...


フォーム送信用Javascript(upload.js)

$(document).ready(function() {
$("#upload-form").ajaxForm({
dataType: "json",
success: function (data) {
if (data.errors) {
var err_list = "";
for (var i in data.errors) {
err_list += data.errors[i] + "\n";
}
if (err_list.length > 0) {
alert("Error\n" + err_list);
}
return;
}
alert(data.message);
},
error: function (xml, status, e) {
alert("Server Error:" + e);
}
});
});


django側処理(views.py)
Photoクラスはアップロードした画像用のModelクラス。
PhotoAddFormは画像アップロード用のModelFormクラス。

from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.http import HttpResponseRedirect, HttpResponse
from django.utils import simplejson
from django.utils import encoding
from verdjnlib.fields import PhotoField, CROP, FIT
from testapp.main.models import *
from testapp.main.forms import *
@login_required
def photo_upload(request):
json_data = ""
res = {}
if request.method == "POST":
form = PhotoAddForm(request.POST, request.FILES)
if form.is_valid():
form_data = form.cleaned_data
photo = Photo()
photo.user = request.user
photo.image = request.FILES["image"]["filename"]
photo.name = form_data["name"]
photo.save_image_file(photo.image, PhotoField.resize(request.FILES["image"]["content"], 500, 500, FIT))
photo.save()
res = {"status": "success", "message": "photo saved"}
else:
error_list = []
for field in form:
if field.errors:
error_list.append(field.label + ":" + field.errors.as_text())
res = {"status": "error", "message": "form not valid.", "errors": error_list}
else:
res = {"status": "error", "message": "forbidden method."}
try:
json_data = simplejson.dumps(res, ensure_ascii=False)
except Exception,e:
print e
# For iframe(needed for ajax file upload)
res_data = "<textarea>%s</textarea>" % (json_data)
return HttpResponse(res_data, mimetype="text/html; charset=UTF-8")

jQuery Form Pluginでファイルアップロード


通常のフォームを画面遷移なしで送信可能にしてくれる
jQuery Form Pluginを使ってみた。


jQuery Form Plugin
http://malsup.com/jquery/form/


フォームに何も加えなくてもjsファイルを読み込んで
ajaxForm関数でサブミット後の処理を定義するだけで
サブミット時にajax通信を行うようにしてくれるようだ。


ファイルアップロードもiframeを生成してうまくやってくれる
ようなので、試してみる。

FlexとJavascriptの連携

Actionscript3で作成したSWFからActionscript2で
作成したSWFは呼び出せないようだ。


YoutubeAPIで提供される動画再生用のSWFがActionscript2で
作成されたものらしく、Flex2(Actionscript3)から呼び出しても
うまく動いてくれなかった。
(やり方がおかしかったのかな?)


そこで、YoutubeのJavascriptAPIを使って、Actionscript3-Javascript(-Actionscript2)
という形で連携させてみる。この方法をマスターすれば、Flexコンポーネント
Javascriptを組み合わせて使えるようになりそう。


Flex側からJavascriptを呼び出すには、flash.external.ExternalInterfaceクラスの
callメソッドを使用するのが簡単らしい。
引数も渡せるし、返り値も受け取れる。


Flex

import flash.external.*;
//引数なし返り値なしのJavascript関数を呼び出す
ExternalInterface.call("playVideo");
//引数ありのJavascript関数を呼び出す
var movieId:String = "aaaaa";
ExternalInterface.call("loadVideo", movieId);
//返り値ありのJavascript関数を呼び出す
var volume:String = ExternalInterface.call("getVolume");


Javascript

function playVideo() {
...
}
function loadVideo(movie_id) {
...
}
function getVolume() {
...
return volume;
}


Javascript側からFlexを呼び出す時はFABridgeを使用する。
http://labs.adobe.com/wiki/index.php/Flex_Framework:FABridge

Flex3の場合はライブラリに最初から含まれているのかな?
JavascriptファイルとActionscriptファイルが手に入るはず。


Javascript側で返り値を受け取れるかどうかは未テスト。


Flex

import bridge.*;
var bridge:FABridge = new FABridge();
bridge.rootObject = this; //Javascriptから呼び出し可能な関数を持つオブジェクトを指定する
public function handlePlayerReady():void {
...
}
public function handleVideoStateChange(status:String):void {
...
}


Javascript

<script type="text/javascript" src="FABridge.js"></script>
var flexApp = FABridge.flash.root();
flexApp.handlePlayerReady();
flexApp.handleVideoStateChange(state);


これだけ覚えておけば色々できそうだ。

jQueryでselectメニューで選択中のoptionの表示テキストを取得する

これまたもっといい方法があると思われるが、一応メモしておく。

<script type="text/javascript" src="/static/javascripts/jquery-1.1.3.1.pack.js"></script>
<script type="text/javascript">
  $(function() {
      $("#add_customer_btn").click(function() {
        if ($("#customer_select").val() != "") {
          var customer_id = $("#customer_select").val();
          alert($("#customer_select/option[@value=" + customer_id + "]").text());
        }
      });
  });
</script>
<div style="margin: 3px 0 3px 0;">
<select id="customer_select">
<option value="">選択して下さい</option>
{% for customer in customer_list %}
<option value="{{ customer.id }}">{{ customer.name }}</option>
{% endfor %}
</select>
<input type="button" id="add_customer_btn" value="追加" />
</div>


Vimにもスーパーはてな記法にも「htmldjango」なんていうシンタックスがあった。
いやー、何でも用意されてるもんだ。