jQuery UIのsortableを使ってtable要素を並び替える

sortableの基本的な使い方はこちらを参照。
jQuery UIのsortableを使ってドラッグアンドドロップで並び替え

間違った例

一番シンプルに書こうとするとこうなるかと思います。

HTML
<table id="sortable-table">
	<tr>
		<th>番号</th><th>項目</th><th>備考</th>
	</tr>
	<tr>
		<td>1</td><td>項目1</td><td></td>
	</tr>
	<tr>
		<td>2</td><td>項目2</td><td></td>
	</tr>
	<tr>
		<td>3</td><td>項目3</td><td>なし</td>
	</tr>
</table>
javascript
$(function(){
	$('#sortable-table').sortable();
});

これだとうまくいきません。

リンク先の例でもtheaderタグ、tbodyタグをきちんと書いた上で下記のようにやっていますが、どうやらtbodyが曲者のようです。

	$('#sortable-table tbody').sortable();

調べてみると、tableタグを扱う際にはtbodyが必要らしく、最近のブラウザはtableタグの直下にtrがある場合はtbodyを追加してtrをその下に移動させているようです。同様にjQueryでもtbodyを自動的に追加するようです。

で、sortable()の話になるんですが、並び替え対象となる項目はitemsオプションで指定するんですが、デフォルトでは"> *"です。つまり直下の子要素となります。
Sortable API(items)

これらを考えると、大きく二通りの方法で並び替えを実現できそうです。

  • tbodyをちゃんと書く
  • itemsオプションを指定する

以下にサンプルコードを載せておきます。

デモ画面

tbodyに対してsortable()

html
	<table id="sortable-table1">
		<thead>
			<tr>
				<th>番号</th>
				<th>項目</th>
				<th>備考</th>
			</tr>
		</thead>
		<tbody>
			<tr>
				<td>1</td><td>項目1</td><td></td>
			</tr>
			<!-- 項目2~は省略 -->
		</tbody>
	</table>
css
#sortable-table1 {
	width: 50%;
	border-collapse: collapse;
}
#sortable-table1 td, #sortable-table1 th {
	border: solid 1px black;
}
javascript
$(function(){
	$('#sortable-table1 tbody').sortable();
});

ポイントとしては、javascriptセレクタでtbodyを指定しているところくらいです。
おそらくこれが最もシンプルな方法かなと思います。

itemsオプションで指定

html、css

上の例と同じ(idは"sortable-table2"に変えています)

javascript
$(function(){
	$('#sortable-table2').sortable({"items": "tbody tr"});
});

これは上の例とは違って、セレクタはtableを指定しています。その上でitemsオプションで"tbody tr"を指定することで目的を達成できます。
この方法は、少し応用が利きます。

並び替え可能な項目にcssクラスを設定

html
	<table id="sortable-table3">
			<tr>
				<th>番号</th>
				<th>項目</th>
				<th>備考</th>
			</tr>
			<tr class="sortable-tr">
				<td>1</td><td>項目1</td><td></td>
			</tr>
			<!-- 項目2~4は省略 -->
			<tr>
				<td>5</td><td>並び替え不可</td><td></td>
			</tr>
	</table>

ここではあえてtheadとtbodyタグは削除しています。
代わりに項目1~4のtrに"sortable-tr"クラスを指定しています。

css

上の例と同じ(idは"sortable-table3"に変えています)

javascript
$(function(){
	$('#sortable-table3').sortable({"items": "tr.sortable-tr"});
});

こうすることによって、並び替えしたくない要素がある場合にも対応可能です。例えば、最後に「1行追加」ボタンを設置したい場合等はこのように実現できます。

もう一歩

デモ画面を見てもらって気になった人もいると思いますが、ドラッグしようとしているときの要素の幅が縮んでいます。
それについては続きにて

JavaScript 第6版

JavaScript 第6版

struts2でJSONを出力する

struts2でJSONを結果として出力したいとなったときのために、JSON Pluginがあります。

これとconvention pluginのアノテーションを使って実際に試してみました。

もちろんJSONとなる文字列を作って、InputStreamオブジェクトに設定してStream Resultとして返してもいいんですが、折角あるものを使わない手はないし何よりめんどくさいので、そんなことはしません。

Mavenに追加

まずはpom.xmlにpluginを追加します。

   <dependency>
       <groupId>org.apache.struts</groupId>
       <artifactId>struts2-json-plugin</artifactId>
       <version>STRUTS_VERSION</version>
   </dependency>

このjarファイルにあるstruts-plugin.xmlにresult type=jsonが定義されています。

ここではidとnameプロパティを持つオブジェクトのリストをあらわすJSONを返します。

Action
package kamegu.action.fwtest;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.struts2.convention.annotation.ParentPackage;
import org.apache.struts2.convention.annotation.Results;

import com.opensymphony.xwork2.Action;

@Results({
	@org.apache.struts2.convention.annotation.Result(name="success", type="json", params={"root", "list"})
})
@ParentPackage("json-default")
public class JsonAction implements Action {

	public List<Map<String, Object>> list = new ArrayList<>();
	@Override
	public String execute() throws Exception {
		list.add(createCustomer(10l));
		list.add(createCustomer(11l));
		list.add(createCustomer(12l));
		return SUCCESS;
	}

	private Map<String, Object> createCustomer(long id) {
		Map<String, Object> customer = new HashMap<>();
		customer.put("id", id);
		customer.put("name", "name" + id);
		return customer;
	}
}

クラスにアノテーションを2つ設定しています。

以上で対応するURLにアクセスしてみると、次のようなレスポンスが返ってくるかと思います。

[{"id":10,"name":"name10"},{"id":11,"name":"name11"},{"id":12,"name":"name12"}]
JSON resultの説明
  • デフォルトだとアクションクラスをJSON化します。rootパラメータを設定すればその名前のフィールドをJSON化します。
  • JSON化されるのはJavaBean出ないとダメっぽい(getter/setter)参考

詳しくはJSON pluginのページみてください

アノテーションを使わないような書き方

アノテーションを使わずにjson resultを使います。
struts2でresultの設定をxmlにもアノテーションにも指定しない方法を使います。

Action
package kamegu.action.fwtest;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.struts2.json.JSONResult;

import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.Result;

public class JsonAction implements Action {

	public List<Map<String, Object>> list = new ArrayList<>();
	@Override
	public String execute() throws Exception {
		return SUCCESS;
	}

	public Result test() {
		list = new ArrayList<>();
		list.add(createCustomer(1l));
		list.add(createCustomer(2l));
		list.add(createCustomer(3l));
		JSONResult result = new JSONResult();
		result.setRoot("list");
		return result;
	}

	private Map<String, Object> createCustomer(long id) {
		Map<String, Object> customer = new HashMap<>();
		customer.put("id", id);
		customer.put("name", "name" + id);
		return customer;
	}
}

これ、結構使える気がします。

struts2でresultの設定をxmlにもアノテーションにも指定しない方法

概要

struts2では基本的にはresultの設定はstruts.xmlに定義します。
(ここでは詳細は省きます)

ただ、最近はconvention-pluginを使ってxml設定せずに画面表示させたり、リダイレクト等が必要なときはアノテーションを使って設定していることが多いと思います。

しかし、struts2のコードを見てみると他の方法も使えることが分かりました。
それは、アクションで"success"のような文字列を返すのではなく直接Resultオブジェクトを返すという方法です。
どういうことか例を示します。

struts2とconvention-pluginの基本的な部分が分かっているという前提で進めます。
まずはコードから。

ResultAction.java
package kamegu.action.fwtest;

import org.apache.struts2.dispatcher.ServletRedirectResult;

import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.Result;

public class ResultAction implements Action {

	@Override
	public String execute() throws Exception {
		return SUCCESS;
	}

	public Result jump() {
		ServletRedirectResult result = new ServletRedirectResult();
		result.setLocation("./result");
		return result;
	}
}
result.jsp
<%@page contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>
<%@ taglib prefix="sj" uri="/struts-jquery-tags"%>
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Insert title here</title>
</head>
<body>
アクションクラス動作確認用
</body>
</html>

今回は次のようなリクエストを行うと、
/myapp/fwtest/result!jump 
次の画面にリダイレクトさせます。
/myapp/fwtest/result
ResultAction.javaのexecute()とjump()にブレークポイントを置くかSystem.out.println("分かりやすいメッセージ")みたいにすればリダイレクトしていることが分かると思います。

この後でフレームワークの説明をします

パーフェクトJava (PERFECT SERIES) (PERFECT SERIES 2)

パーフェクトJava (PERFECT SERIES) (PERFECT SERIES 2)

struts2上の動き

該当部分のみ説明します。

リクエストがあると、com.opensymphony.xwork2.DefaultActionInvocation#invokeAction()から対象のアクションクラスの対象のメソッドが実行され、結果がsaveResultメソッドに渡されます。

で、この結果オブジェクトはStringクラスではなくObjectクラスで、saveResultメソッド内ではこのオブジェクトがcom.opensymphony.xwork2.Resultインターフェースを実装しているかどうかを判断し、実装している場合はexplicitResultフィールドにそのオブジェクトをセットしてnullを返し、実装していない場合は結果オブジェクトをStringにキャストして返します。

この結果を元に処理を行うのがexecuteResultメソッドですが、そこで使用するResultオブジェクトはexplicitResultフィールドが!=nullの場合はその値が使用されます。
explicitResultがnullで文字列がアクションから返されている場合はそれを元に設定からResultインスタンスが導かれます。

つまり

アクションクラスでcom.opensymphony.xwork2.Resultを返せばそのオブジェクトを元に処理が行われ、文字列を返せばそれを元に設定どおりの処理が行われる

経産省の山田課長補佐、ただいま育休中

男性の育休について書いている本ってこれくらいしかないですね。
折角なので読んでみました。

経産省の山田課長補佐、ただいま育休中 (文春文庫)

経産省の山田課長補佐、ただいま育休中 (文春文庫)

内容は面白くてどんどん読み進められます。
経産省のキャリアの人だけあって文章能力があるんだな、と、ちょっとひねくれた感想を持ってみたり。
既に取り入れたり、今後の参考にしたいなというところもありました。
面白い中にも、男性の育児やそもそも育児がどれだけ大変かというのが垣間見えます。そういうのをいろんな人に知ってほしい。

うちは一日中大人2人対子供2人、著者の場合は昼間に関して言えば大人1人対子供1人。そういう意味では私の場合は夫婦で助け合える分楽な気もしますが、それでも相手が子供だと大変です。
二人以上の小さい子供の面倒を見ている女性は本当にすごい。

P.S. 子育てはたのしい

P.S. 本書には、子育てには「お金と時間が足りない」とありますが、それはそのとおりだと思います。

S2jdbcでエンティティ共通化とDao作成

前置き

DBを使ったプログラムをやってると、全部のテーブルに共通するカラムがあったりします。
例えば作成日時とか最新更新日時とか。
そういう場合にすべてのエンティティクラスでそれらを定義するんではなくて、親クラスで共通なものは定義すると各エンティティの定義が楽になります。
また、その親クラスを継承させておくことで処理も共通化でき、コードをシンプルにすることもできます。

AbstractEntityクラス(エンティティの基底クラス)とSimpleDaoクラス(DB共通処理)の作成例を示します。

エンティティ共通化

共通でこのようなカラムを持たせるものとします(下の例はPostgresql

CREATE TABLE hoge_hoge (
  id BIGSERIAL NOT NULL PRIMARY KEY,
  --ここに各テーブル独自カラム
  version BIGINT NOT NULL,
  create_ts TIMESTAMP,
  create_user INTEGER,
  update_ts TIMESTAMP,
  update_user INTEGER
);
  • idは識別子です。各テーブルには必ずユニークとなるようなIDが振られるものとします。この考え方に好みもあると思いますが、便宜上このようにしてます。
  • versionはバージョンチェック用のカラムです参考。更新時にバージョンチェックを行うため、必ず指定する必要があります。
  • create_ts、update_tsはそれぞれ作成時、最終更新時のタイムスタンプです
  • create_user、update_userはそれぞれ作成者、最終更新者をあらわすユーザの識別子です。単なるログ情報なので外部キーは張らないです。また、BIGINTでもいいんですが大抵の場合INTEGERの最大値を超えることはないと思うのでINTEGERにしてます。

S2Jdbcでは、@MappedSuperclassアノテーションを親クラスに設定することでその子クラスで共通のカラムを使用することができます。

kamegu.common.jdbc.AbstractEntity
package kamegu.common.jdbc;

import java.sql.Timestamp;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Version;

@MappedSuperclass
public abstract class AbstractEntity {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	public Long id;

	@Version
	public Long version;

	@Temporal(TemporalType.TIMESTAMP)
	public Timestamp createTs;

	public Integer createUser;

	@Temporal(TemporalType.TIMESTAMP)
	public Timestamp updateTs;

	public Integer updateUser;
}

これを使えばエンティティはこのようにかけます。

package kamegu.entity;

import javax.persistence.Entity;

import kamegu.common.jdbc.AbstractEntity;
import kamegu.enumerable.Sex;

@Entity
public class Customer extends AbstractEntity {
	public String name;
	public String email;
	public Sex sex;
}

Sexはenumです。

DBアクセス処理共通化

シンプルなDBアクセスを共通化します。

  • PKを元に1件取得
  • insert
  • update

これらは通常、それぞれ下記のように書く必要があります。

jdbcManager.from(Customer.class).id(customer.id).getSingleResult();
jdbcManager.insert(customer).execute();
jdbcManager.update(customer).execute();
jdbcManager.delete(customer).execute();

また、上記の処理に加えてinsert時にはupdate_ts,update_user、update時にはinsert_ts,insert_userを更新対象から外す必要があります。

これらをふまえて次のようなクラスを作ります。
ここでは、まだinsert_userとupdate_userは登録するようになってません。

package kamegu.common.jdbc;

import java.sql.Timestamp;

import javax.annotation.Resource;

import org.seasar.extension.jdbc.JdbcManager;

public class SimpleDao {

	@Resource
	private JdbcManager jdbcManager;

	public <T extends AbstractEntity> int insert(T entity) {
		entity.createTs = new Timestamp(System.currentTimeMillis());
		return jdbcManager.insert(entity).excludes("updateTs", "updateUser").execute();
	}

	public <T extends AbstractEntity> int update(T entity) {
		entity.updateTs = new Timestamp(System.currentTimeMillis());
		return jdbcManager.update(entity).excludes("createTs", "createUser").execute();
	}

	public <T extends AbstractEntity> int delete(T entity) {
		return jdbcManager.delete(entity).execute();
	}

	public <T> T find(Class<T> clazz, Object... id) {
		return jdbcManager.from(clazz).id(id).getSingleResult();
	}

	@SuppressWarnings("unchecked")
	public <T extends AbstractEntity> T find(T entity) {
		return jdbcManager.from((Class<T>)entity.getClass()).id(entity.id).getSingleResult();
	}
}

最後にこれをコンポーネント登録します。

app.dicon に追加
  <component name="simpleDao" class="kamegu.common.jdbc.SimpleDao" instance="singleton" />

動作確認

struts2とseasar2でCRUD処理CRUD処理(validation編)で作った画面に手を加えます。
ファイルの中身は全て再掲します。

JspCrudAction
package kamegu.action.fwtest;

import java.util.List;

import javax.annotation.Resource;

import kamegu.common.MyActionSupport;
import kamegu.entity.Customer;
import kamegu.enumerable.Sex;

import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.convention.annotation.Result;
import org.apache.struts2.convention.annotation.Results;
import org.seasar.extension.jdbc.JdbcManager;

import com.opensymphony.xwork2.validator.annotations.RequiredStringValidator;
import com.opensymphony.xwork2.validator.annotations.Validations;

@Results({
	@Result(name="list", type="redirect", location="./jsp-crud")
})
public class JspCrudAction extends MyActionSupport {

	public List<Customer> customers;
	public Customer customer = new Customer();

	public Sex[] sexs = Sex.values();

	@Resource
	private JdbcManager jdbcManager;

	public String execute() {
		customers = jdbcManager.from(Customer.class).orderBy("name").getResultList();
		return SUCCESS;
	}

	public String input() {
		if (customer.id != null) {
			customer = jdbcManager.from(Customer.class).id(customer.id).getSingleResult();
		}
		return INPUT;
	}

	@Validations(
			requiredStrings = {
					@RequiredStringValidator(fieldName="customer.name", message="必須項目です"),
					@RequiredStringValidator(fieldName="customer.email", message="必須項目です")
			}
	)
	public String save() {
		if (customer.id != null) {
			jdbcManager.update(customer).execute();
		} else {
			jdbcManager.insert(customer).execute();
		}
		return "list";
	}

	public String delete() {
		jdbcManager.delete(customer).execute();
		return "list";
	}
}
jsp-crud.jsp
<%@page contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>
<%@ taglib prefix="sj" uri="/struts-jquery-tags"%>
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <sj:head locale="ja" loadAtOnce="true"/>
  <link href="<s:url value="/bootstrap/css/bootstrap.min.css"/>" rel="stylesheet" media="screen">
  <script src="<s:url value="/bootstrap/js/bootstrap.min.js"/>"></script>
  <script>
    var bootstrapBtn = $.fn.button.noConflict();
    $.fn.bootstrapBtn = bootstrapBtn;
  </script>
</head>
<body>
  <table class="table table-condensed table-bordered">
    <thead>
      <tr>
        <th>ID</th><th>名前</th><th>E-Mail</th><th>性別</th><th></th>
      </tr>
    </thead>
    <tbody>
    <s:iterator value="customers">
      <tr>
        <td>${id}</td>
        <td><s:property value="name"/></td>
        <td>${email}</td>
        <td><s:property value="sex"/>(${sex})</td>
        <td>
          <a type="button" class="btn btn-small iframe cbox" href="jsp-crud!input?customer.id=${id}">編集</a>
        </td>
      </tr>
    </s:iterator>
    </tbody>
  </table>
  <a type="button" class="btn btn-small iframe cbox" href="<s:url method="input"/>">新規</a>
</body>

</html>
jsp-crud-input.jsp
<%@page contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>
<%@ taglib prefix="sj" uri="/struts-jquery-tags"%>
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <sj:head locale="ja" loadAtOnce="true"/>
  <link href="<s:url value="/bootstrap/css/bootstrap.min.css"/>" rel="stylesheet" media="screen">
  <script src="<s:url value="/bootstrap/js/bootstrap.min.js"/>"></script>
  <script>
    var bootstrapBtn = $.fn.button.noConflict();
    $.fn.bootstrapBtn = bootstrapBtn;
  </script>
</head>
<body>
  <s:form method="POST">
    <s:if test="customer.id != null">
      <s:hidden name="customer.id"/>
      <s:hidden name="customer.version"/>
    </s:if>
    <table>
      <tr>
        <td>名前</td>
        <td><s:textfield name="customer.name" placeholder="名前"/><s:fielderror fieldName="customer.name"/></td>
      </tr>
      <tr>
        <td>E-Mail</td>
        <td><s:textfield name="customer.email" type="email" placeholder="E-Mail"/><s:fielderror fieldName="customer.email"/></td>
      </tr>
      <tr>
        <td>性別</td>
        <td><s:select name="customer.sex" listKey="name()" list="sexs" value="customer.sex.name()"/></td>
      </tr>
    </table>
    <s:submit cssClass="btn btn-primary" value="登録" method="save"/>
    <s:if test="customer.id != null">
      <s:submit cssClass="btn btn-primary" value="削除" method="delete"/>
    </s:if>
  </s:form>
  <s:debug/>
</body>
</html>

ポイントとしては、更新時にidと一緒にversionもパラメータとして渡しています。

テーブル作成 (Postgresqlの場合)
CREATE TABLE customer (
  id BIGSERIAL NOT NULL PRIMARY KEY,
  name TEXT,
  email TEXT,
  sex INTEGER,
  version BIGINT NOT NULL,
  create_ts TIMESTAMP,
  create_user INTEGER,
  update_ts TIMESTAMP,
  update_user INTEGER
);

これで
/myapp/fwtest/jsp-crud
にアクセスすればCRUD処理ができて、DB上のversion, create_ts, update_tsが正しく登録されていることがわかるはずです。

以上

男の育休中です

先日(といっても半月ほど前)、第2子が生まれてそこから育休を取得しています。

男性の育児休業というのは取得率ですらあまり高くないみたいなんですが、今回は会社の理解もあってかなり長期的に取得することができてます。

始まる前は、家事に慣れて育児も落ち着いてくれば勉強する時間も取れるだろうと思っていましたが、今のところまだまだそんな状態ではありません。
しばらくは嫁さんにも楽してもらいたい、というのもあってなるべく家事するようにしてるし、なんといっても上の子の相手が大変でなかなか時間を見つけられません。
子供が寝てからというのが一番現実的なんですが、なかなか寝付かない日が多く、疲れるのもあってそんな日は一緒に寝ちゃう感じになってます。


取得するに当たっていろいろ調べたこともあるので整理したいなと思います。
また今度、、、

jQuery UI の.disableSelection()がdeprecated(非推奨)になってる件

メモ

のつもりがちょっと長くなりました。

jQuery UIのsortable()とかを使おうと思っていろいろ検索してみると、

$(".sortable").sortable();
$(".sortable").disableSelection();

みたいに書いている人が多くてそのまま使っちゃいそうなんですが、.disableSelection()はdeprecatedみたいです。

version1.6で追加されて、1.9からDeprecated。
http://api.jqueryui.com/disableSelection/


そもそも今使っているバージョン()に限っていえば、

$(".sortable").sortable();

だけでもテキストの選択はできない。
Deprecatedになる前のjQuery UI 1.8.1(+jQuery 1.7.1)でもテキストの選択はできません。(文字列上でダブルクリックすれば選択できますが、それはdisableSelection()を使っているかどうかは関係なさそう)

本家のサンプルでも使ってます。
何のために入れているんだろうか。。。わかんない。

Deprecatedにするチケット

これを訳すと

開発者にTextSelectionを扱わせるべきではない。これらのメソッドはそもそもInteractionプラグイン(sortableとかDraggbleとか)のためのものだが、それらは独自にそれらをハンドリングする機能を有している。

とでもなるんでしょうか。
となると、やっぱり要らない気がする。
「念のため」くらいなんだろうか?知っている方いたら教えてほしい。



文字列を選択できなくするならCSSでやれ、って話も。
stackoverflow

* {
   -ms-user-select: none; /* IE 10+ */
   -moz-user-select: -moz-none;
   -khtml-user-select: none;
   -webkit-user-select: none;
   user-select: none;
}

.selectable {
   -ms-user-select: auto;
   -moz-user-select: auto;
   -khtml-user-select: auto;
   -webkit-user-select: auto;
   user-select: auto;
}

コアjQuery+プラグイン/jQuery UI 開発実践技法 (Programmer’s SELECTION)

コアjQuery+プラグイン/jQuery UI 開発実践技法 (Programmer’s SELECTION)

以上