個人でWebサービス作る人の雑記ブログ

Webサイトを個人で作ってる人が人の役に立ちそうなことを書いていく雑記ブログです

【jqGrid】loadonce:trueでpaginateを動かそうにも、動的にサーバから2回目以降読み込もうにもロードされなくなった話と解決策

いきさつ

jqGridを2つ並べて、

1つ目のグリッドでレコードを選択すると、

それに紐付くデータが2つ目のグリッドに動的に表示される。

そんな機能を実装したんですが、

どうやら2つ目のグリッドでページネーションが動作してないっぽい。

 

どこか設定ミスってるんだろうと高をくくって調べ始めたら、

解決に3時間もかかってしまったのでビボウロクに。

 

 

調査

グリッドの右下を見るに必要な件数はサーバから取れてるっぽい。

f:id:pyamasan:20190320200954p:plain

 

ただページャが機能していない。2ページ目があるはずなのに。

 

ということで軽くググってみると、どうやらサーバから動的にデータを反映させる場合は、

グリッドのオプションにloadonce:trueを指定するべきらしい。

 

$("#cmn-master-tb").jqGrid({
url: '',
loadonce: true, // こんな感じ
datatype : "json",
cellEdit: false,

 

するとどうだろうか!

確かにページャが動作したではありませんか!!!

f:id:pyamasan:20190320201456p:plain

 

しかしさらなる問題が

これで万事解決かと思いきや、

1つ目のグリッドでほかのレコードを選択してデータを見ようにも

2つ目のグリッドで動的に読み込んでくれない。何も音沙汰なし。

ここからが長かった。

 

単刀直入に解決方法

1つ目のグリッドのレコードを選択して、2つ目のグリッドに動的に読み込ませる部分のソースが以下のような感じなんですけれども、

jQuery("#cmn-master-tb").jqGrid('setGridParam',
url:"https://xxx.com/row.cmn_master_grp_code",
page:1
})
.trigger('reloadGrid');

 

これ、明示的に相手のグリッドのdatatypeを指定してあげないとダメみたいなのです。

jQuery("#cmn-master-tb").jqGrid('setGridParam',
url:"https://xxx.com/row.cmn_master_grp_code",
datatype: 'json', //これ
page:1
})
.trigger('reloadGrid');

 

 

いやーこんなんわかんないっすよー笑

【JQuery】画像トリミングプラグイン「Croppic.js」が富豪コーディングだったので軽量化しなければいけなくなった話【重すぎ】

事件の経緯

運営しているサイトでWeb上で画像をトリミングさせる機能(Twitterのプロフィールアイコン設定のときのアレみたいなやつ)を実装しているのですが、

ある日サイトをご利用いただいているユーザ様から

 

「トリミングしようと思って画像をタッチして移動させようにも動かせないです。 in Android

 

というご連絡をいただきました。

はて、手持ちのスマホiPhone)で検証してみるも問題なく動かせる・・・何なのか。。

 

使ってるトリミングプラグイン

お洒落なサイトが印象的ですね。

www.croppic.net

 

自宅のテスト用Android実機で試してみる

「確かに動かせない。」

 

フロント側のメモリ不足あたりを疑いつつ、

なぜだろうなぜだろうと色々試行錯誤の上調べていった結果(プラグインごと乗り換えようともしたので丸3日かかりましたorz)、1件の記事にたどり着きました。

qiita.com

 

まさかねー!

だってほら、すごくお洒落なサイトだしブランド感も漂ってるし、新卒社会人みたいなコーディングになんてなってないよねー!(かく言う私も上の記事を見るまで意識してできてなかったですがw)

 

 

まさかでした。

重厚なmousemoveリスナ内のロジックがそこには広がってました、はい。

そこそこ潤沢なメモリを搭載した媒体なら耐えられそうですが、Android機とかそこらになると固まりそうなメモリ食いしん坊がそこにいました。

 

修正案

上記のサイトを参考にしつつ、

レンダリング用にフレームレートを設ける

・スクロール系リスナ内ではイベントの登録のみにする

ように修正してみました。(なお、修正したのはドラッグ系ロジックのみです。多分ここだけの修正で大丈夫だとは思っているのですが・・・お気づきの点があれば教えてくださいm(__)m)

 

修正箇所 #454あたり

initDrag: function() {
	var that = this;
	
	that.img.on("mousedown touchstart", function(e) {
				
		var pageX;
        var pageY;
        var userAgent = window.navigator.userAgent;
        if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i) || userAgent.match(/android/i) || (e.pageY && e.pageX) == undefined) {
            pageX = e.originalEvent.touches[0].pageX;
            pageY = e.originalEvent.touches[0].pageY;
        } else {
            pageX = e.pageX;
            pageY = e.pageY;
        }
		
		var z_idx = that.img.css('z-index'),
        drg_h = that.img.outerHeight(),
        drg_w = that.img.outerWidth(),
        pos_y = that.img.offset().top + drg_h - pageY,
        pos_x = that.img.offset().left + drg_w - pageX;
		
		that.img.css('z-index', 1000).on("mousemove touchmove", function(e) {
			
			var imgTop;
			var imgLeft;
			
			if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i) || userAgent.match(/android/i) || (e.pageY && e.pageX) == undefined) {
                imgTop = e.originalEvent.touches[0].pageY + pos_y - drg_h;
                imgLeft = e.originalEvent.touches[0].pageX + pos_x - drg_w;
            } else {
                imgTop = e.pageY + pos_y - drg_h;
                imgLeft = e.pageX + pos_x - drg_w;
            }
			
			that.img.offset({
				top:imgTop,
				left:imgLeft
			}).on("mouseup", function() {
				$(this).removeClass('draggable').css('z-index', z_idx);
			});
			
			if(that.options.imgEyecandy){ that.imgEyecandy.offset({ top:imgTop, left:imgLeft }); }
								
			if (that.objH < that.imgH) {
				if (parseInt(that.img.css('top')) > 0) { that.img.css('top', 0); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', 0);}}
				var maxTop = -( that.imgH - that.objH); if (parseInt(that.img.css('top')) < maxTop) { that.img.css('top', maxTop); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', maxTop); }}
			}else{
				if (parseInt(that.img.css('top')) < 0) { that.img.css('top', 0); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', 0); }}
				var maxTop =  that.objH - that.imgH; if (parseInt(that.img.css('top')) > maxTop) { that.img.css('top', maxTop);if (that.options.imgEyecandy) {that.imgEyecandy.css('top', maxTop); }}
			}

			if (that.objW < that.imgW) {
				if( parseInt( that.img.css('left')) > 0 ){ that.img.css('left',0); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', 0); }}
				var maxLeft = -( that.imgW-that.objW); if( parseInt( that.img.css('left')) < maxLeft){ that.img.css('left', maxLeft); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', maxLeft); } }
			}else{
				if( parseInt( that.img.css('left')) < 0 ){ that.img.css('left',0); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', 0); }}
				var maxLeft = ( that.objW - that.imgW); if( parseInt( that.img.css('left')) > maxLeft){ that.img.css('left', maxLeft); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', maxLeft); } }
			}
			if (that.options.onImgDrag) that.options.onImgDrag.call(that);
			
		});

	}).on("mouseup", function() {
		that.img.off("mousemove");
	}).on("mouseout", function() {
		that.img.off("mousemove");
	}).on("touchend", function() {
		that.img.off("touchmove");
	});
	
}

 

修正後

initDrag: function() {
	var that = this;

	// 独自に軽量に修正
	var fps = 30; // 30fpsに制限
	var frameTime = 1000 / fps;
	var isAnimated = false;
	var lastTouchInfo = null;
	var userAgent = window.navigator.userAgent;

	var z_idx = null,
	drg_h = null,
    drg_w = null,
    pos_y = null,
    pos_x = null;

	that.img.on("mousedown touchstart", function(e) {
		// 最初のデータを入れてアニメーションを開始
		e.preventDefault();
		if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i) || userAgent.match(/android/i) || (e.pageY && e.pageX) == undefined) {
	    	lastTouchInfo = e.originalEvent.touches[0];
		}
		else{
			lastTouchInfo = e;
		}
	    isAnimated = true;

	    pageX = lastTouchInfo.pageX;
        pageY = lastTouchInfo.pageY;

        z_idx = that.img.css('z-index');
        drg_h = that.img.outerHeight();
        drg_w = that.img.outerWidth();
        pos_y = that.img.offset().top + drg_h - pageY;
        pos_x = that.img.offset().left + drg_w - pageX;

	    dragAnimation();
	});

	that.img.on("mousemove touchmove", function(e) {
	    if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i) || userAgent.match(/android/i) || (e.pageY && e.pageX) == undefined) {
	    	lastTouchInfo = e.originalEvent.touches[0];
		}
		else{
			lastTouchInfo = e;
		}
	});

	that.img.on("mouseup touchend touchcancel", function() {
		// アニメーションの終了
	    isAnimated = false;
	    lastTouchInfo = null;
	});

	function dragAnimation(){
		// isAnimatedフラグが立ってなかったら終了
		if (!isAnimated) {
	        return; 
	    }

        var imgTop = lastTouchInfo.pageY + pos_y - drg_h;
        var imgLeft = lastTouchInfo.pageX + pos_x - drg_w;

        that.img.offset({
			top:imgTop,
			left:imgLeft
		}).on("mouseup", function() {
			$(this).removeClass('draggable').css('z-index', z_idx);
		});

		if(that.options.imgEyecandy){ that.imgEyecandy.offset({ top:imgTop, left:imgLeft }); }
							
		if (that.objH < that.imgH) {
			if (parseInt(that.img.css('top')) > 0) { that.img.css('top', 0); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', 0);}}
			var maxTop = -( that.imgH - that.objH); if (parseInt(that.img.css('top')) < maxTop) { that.img.css('top', maxTop); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', maxTop); }}
		}else{
			if (parseInt(that.img.css('top')) < 0) { that.img.css('top', 0); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', 0); }}
			var maxTop =  that.objH - that.imgH; if (parseInt(that.img.css('top')) > maxTop) { that.img.css('top', maxTop);if (that.options.imgEyecandy) {that.imgEyecandy.css('top', maxTop); }}
		}

		if (that.objW < that.imgW) {
			if( parseInt( that.img.css('left')) > 0 ){ that.img.css('left',0); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', 0); }}
			var maxLeft = -( that.imgW-that.objW); if( parseInt( that.img.css('left')) < maxLeft){ that.img.css('left', maxLeft); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', maxLeft); } }
		}else{
			if( parseInt( that.img.css('left')) < 0 ){ that.img.css('left',0); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', 0); }}
			var maxLeft = ( that.objW - that.imgW); if( parseInt( that.img.css('left')) > maxLeft){ that.img.css('left', maxLeft); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', maxLeft); } }
		}
		if (that.options.onImgDrag) that.options.onImgDrag.call(that);

        // 次のanimationを登録
		setTimeout(dragAnimation, frameTime);

	}
}

 

どなたかのお役に立てば光栄ですm(__)m

 

 

【追記:2019/2/28】

Android機でスクロールできない問題の根本原因違いました。。

どうやら

 

that.img.on("mousedown touchstart", function(e) {...
...
that.img.css('z-index', 1000).on("mousemove touchmove", function(e) {...

 

といった「that.img.on(...」というイベント登録のところに原因があるようです。

アプリケーションのつくりにもよると思いますが、レンダリングする画像データが重かったりすると、that.imgに正常にイベントが登録されないようです。

よって、私の場合は一つ上の上位divにイベントを登録するように修正することで、この問題を回避することができました。

Bootstrap3のブレークポイントを増やしたいときの処方箋

やりたいこと

Bootstrap3のグリッドシステムを利用してるけど、タブレット以下のブレークポイントが768px未満ってこれちょっと大雑把すぎませんかね?(Extra(特別な)smallっていう割には全然特別感のない幅感というか・・・)
ということで自力で
col-xs ~320px
col-ss 360px (これを追加)
col-sm 768px
col-md 992px
col-lg 1200px~
となるようにscssを改変->cssを生成したい!
というのがやりたいことです。
(特別なブレークレイアウトにしなきゃいけないのって個人的にはiPhoneSEなどの横幅320px系だと思っているので、こういう分け方にしてます。)

稼働中のサービスに反映させたいと言うことで、col-xsが一番小さいブレークポイントとしていたるところにクラス指定してるので、
途中に挟み込む形にしたいと言うわけです。

編集するscss

当方はHonoka v3.3.7を利用しているため、これを改変していきますが、
行数は違えど公式版bootstrap3でも役に立つかもしれません。

honokak.osaka
※バージョン指定でダウンロードしたい場合は、Gitページに飛んでからTagsをご参照ください。

また、デフォルトで生のbootstrap3のsassはhonokaには同梱されていないので、別途/honoka/bootstrap/~として配置する必要があります。
github.com
※これもバージョンをTagsからしっかり合わせるようにしてください。
※分かりづらいですが/assets/stylesheets/配下にあります。

編集するファイル

honoka/_variables.scss

#289あたり

// Extra small screen / phone
//** Deprecated `$screen-xs` as of v3.0.1
$screen-xs:                  320px !default; // !変更!
//** Deprecated `$screen-xs-min` as of v3.2.0
$screen-xs-min:              $screen-xs !default;
//** Deprecated `$screen-phone` as of v3.0.1
$screen-phone:               $screen-xs-min !default;

// !追加!Small screen / smart
//** Deprecated `$screen-sm` as of v3.0.1
$screen-ss:                  360px !default;
$screen-ss-min:              $screen-ss !default;
//** Deprecated `$screen-smart` as of v3.0.1
$screen-smart:              $screen-ss-min !default;

// Small screen / tablet
//** Deprecated `$screen-sm` as of v3.0.1
$screen-sm:                  768px !default;
$screen-sm-min:              $screen-sm !default;
//** Deprecated `$screen-tablet` as of v3.0.1
$screen-tablet:              $screen-sm-min !default;

#320あたり つながる様に追加&変更

$screen-xs-max:              ($screen-ss-min - 1) !default; /*!sm->ss!*/
$screen-ss-max:              ($screen-sm-min - 1) !default;/*!追加!*/
$screen-sm-max:              ($screen-md-min - 1) !default;
$screen-md-max:              ($screen-lg-min - 1) !default;

honoka/bootstrap/_grid.scss

#56あたり

@include make-grid(xs);

// !追加!
@media (min-width: $screen-ss-min) {
  @include make-grid(ss);
}

// Small grid
//
// Columns, offsets, pushes, and pulls for the small device range, from phones
// to tablets.

@media (min-width: $screen-sm-min) {
  @include make-grid(sm);
}

honoka/bootstrap/mixins/_grid.scss

#38おわりあたり

// !追加!Generate the smart columns
@mixin make-ss-column($columns, $gutter: $grid-gutter-width) {
  position: relative;
  min-height: 1px;
  padding-left:  ($gutter / 2);
  padding-right: ($gutter / 2);

  @media (min-width: $screen-ss-min) {
    float: left;
    width: percentage(($columns / $grid-columns));
  }
}
@mixin make-ss-column-offset($columns) {
  @media (min-width: $screen-ss-min) {
    margin-left: percentage(($columns / $grid-columns));
  }
}
@mixin make-ss-column-push($columns) {
  @media (min-width: $screen-ss-min) {
    left: percentage(($columns / $grid-columns));
  }
}
@mixin make-ss-column-pull($columns) {
  @media (min-width: $screen-ss-min) {
    right: percentage(($columns / $grid-columns));
  }
}

honoka/bootstrap/mixins/_grid-framework.scss

#6からの関数に2箇所追加

// [converter] This is defined recursively in LESS, but Sass supports real loops
@mixin make-grid-columns($i: 1, $list: ".col-xs-#{$i}, .col-ss-#{$i}, /*!追加!*/ .col-sm-#{$i}, .col-md-#{$i}, .col-lg-#{$i}") {
  @for $i from (1 + 1) through $grid-columns {
    $list: "#{$list}, .col-xs-#{$i}, .col-ss-#{$i},/*!追加!*/  .col-sm-#{$i}, .col-md-#{$i}, .col-lg-#{$i}";
  }
  #{$list} {
    position: relative;
    // Prevent columns from collapsing when empty
    min-height: 1px;
    // Inner gutter via padding
    padding-left:  ceil(($grid-gutter-width / 2));
    padding-right: floor(($grid-gutter-width / 2));
  }
}

honoka/bootstrap/_responsive-utilities.scss

#31

@include responsive-invisibility('.visible-xs');
@include responsive-invisibility('.visible-ss'); /*!追加!*/

#37あたり

.visible-xs-block,
.visible-xs-inline,
.visible-xs-inline-block,
.visible-ss-block,/*!追加!*/
.visible-ss-inline,/*!追加!*/
.visible-ss-inline-block,/*!追加!*/

#74あたり 真似して追加

/*!追加!*/
@media (min-width: $screen-ss-min) and (max-width: $screen-ss-max) {
  @include responsive-visibility('.visible-sm');
}
.visible-ss-block {
  @media (min-width: $screen-ss-min) and (max-width: $screen-ss-max) {
    display: block !important;
  }
}
.visible-ss-inline {
  @media (min-width: $screen-ss-min) and (max-width: $screen-ss-max) {
    display: inline !important;
  }
}
.visible-ss-inline-block {
  @media (min-width: $screen-ss-min) and (max-width: $screen-ss-max) {
    display: inline-block !important;
  }
}

#154あたり 真似して追加

/*!追加!*/
@media (min-width: $screen-ss-min) and (max-width: $screen-ss-max) {
  @include responsive-invisibility('.hidden-ss');
}

お疲れ様でした。コンパイルしましょう!

honoka/階層にある「bootstrap.scss」をコンパイルして生成されるcss
元のbootstrap.cssと置き換えて、ちゃんと動くか確認してみてください!

クリムゾンの迷宮を読んで(ネタバレなし)

先日読み終わった「クリムゾンの迷宮」について読書感想文を書いていきたいと思います。
(概要だけでネタバレはしないように書いていきます)

おすすめ度

★★★★☆

概要

ある日起きたら一面表紙のイラストのようなクリムゾンな砂丘に訳も分からず自分がいる。
この「バングル・バングル」と言われる地を主人公は、ある指令のもと彷徨い歩くことになる。

感想

目覚めたら非現実的な世界に自分がいる、
この時点で私の場合完全に主人公に感情移入を果たしてしまっていたわけですが、
そこからのストーリー展開が続きが気になる続きが気になるの連続で、
読書習慣があまりない私のような読書ビギナーでも飽きることなく最後まで読むことができると思います。

読み終わった後、なんでこんなにスラスラ読めたのかを考察してみたのですが、気づいたのが
『回想シーン』が全然ないのと主人公目線で最後まで話が進んでいくということです。

回想シーンが全然ない

ストーリーが面白いのは勿論なのですが、
回想シーンがあまりないので、話が逆戻りせず、捲ったページの分だけ時間が進みます。

小説の表現の仕方はいろいろあると思いますが、
この小説のストーリーの内容的に続きが気になるばかりなので、
時間が戻されることによる「しらけ感」がなく、1本の時間軸で話の臨場感を保ったまま話が進んでいきます。

主人公目線で最後まで話が進んでいく

これもストーリーの内容的にすごく読みやすかったです。
ドラゴン○ールで例えると、
『悟空とフリーザバチバチにやり合ってる途中で、「一方その頃…」と別のシーンに移動してしまう』
みたいな「時間に対する目線の平行移動」がないので、これまたしらけずに読み進められた1因だと思いました。

総評

「バトルロワイヤル」もののようなものが好きで、非現実的な世界に自分を置いてみたい。
そんな方は絶対に面白いと思える作品だと思います!(そうでない方も読みやすいので1本の映画を見る調子で是非。)
でも結末はもうちょっと情報が欲しかったなぁ…

Javaバージョンあげたらorg.hibernate.HibernateException: identifier of an instance of was altered from xxx to null

仕事の都合上、アプリケーションのJava実行環境を

oracle製のJRE6からopen系のJRE7にバージョンアップすることになった。

 

すると、とある検索ロジックの処理で以下のようなhibernateエラーが発生。

org.hibernate.HibernateException: identifier of an instance of SampleModel was altered from xxx to null
at org.hibernate.event.def.DefaultFlushEntityEventListener.checkId(DefaultFlushEntityEventListener.java:58)
at org.hibernate.event.def.DefaultFlushEntityEventListener.getValues(DefaultFlushEntityEventListener.java:164)
at org.hibernate.event.def.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:120)

解決法

当方はDBにpostgresqlを使っているが、
どうやらView系のテーブルに検索いった後、キャッシュを削除せずに次の検索にいくとエラーが出てるらしかった。
よってwebでいろいろ調べた結果、以下のようにView系の検索をsession.evict()してやると解決した。

// 検索(ViewSearchUtil内でView系テーブルを検索していると仮定)
ViewModel viewModel = ViewSearchUtil.searchView(session, viewCode);
if(viewMode != null){
    process(viewModel);
    session.evict(viewModel);
}
// evictしないとここでエラー
HogeModel hogeModel = HogeSearchUtil.search(session, hogeCode);

 

疑問

なぜJRE6->JRE7にあげたら再現するようになったのか。。

マジ神!JqueryUIを使っててイメージ式アイコンにイライラ!フォント化したい人必見!

お久しぶりです、ピー山です!

久々に記事にしたいことがありましたので書きます☆٩(。•ω<。)و

 

 JqueryUIのアイコンのimage式iconにイライラしたことありませんか?

僕はあります。

理由1

あれ、JqueryUIをインストールするときにcssディレクトリ直下にimagesディレクトリ配置しなきゃアイコンとして機能してくれないじゃないですか?

気持ち悪い。css直下に画像ファイルがあることがもう無理。

cssディレクトリとは別にimage専用のディレクトリを用意しているため)

 

理由2

 ローカルから本番環境にリリースするときに、

cssディレクトリをまるっとうpすると絶妙にイライラする時間がかかる。

なぜなら細かいアイコン(画像ファイル)が150個近くあるから。

 

何とかアイコンフォント化できないん?

FontAwesomeに置き換えてくれるjqueryUI用のcssを配布していただけてるとか、

だいたい自分が考えるようなイライラは先人の方が解決&記事にしてくれているものです。

 

 

・・・と思いきや、海外のサイト等を回ってもなかなか解決記事がない。

2時間ちかく徘徊しましたorz

 

 

ということでたどり着いた神をご紹介!

いやぁー見つけました。

有志でjqueryUIのアイコンをフォント化してくれているゼウスを。

 

github.com

 

神すぎる。しかもしっかりとjqueryUI1.11, 1.12ともにバージョンごとの対応もされてる。

 

適用も簡単

ファイルをダウンロードして、

①fontディレクトリ直下のフォントファイルを全てフォントを管理するディレクトリにコピー

②root直下にあるcssファイルを使ってるJqueryUIのバージョンに合わせてcssを管理するディレクトリにコピー(jquery.ui.cssのtopにバージョン表記されてます)

③【任意】②でコピーしたcssファイルの最初の10行目くらいまでにフォントファイルへのパス指定がされているので、①で置いた場所によっては適宜パスを変更

jquery-ui-1.11.icon-font(.min).cssjquery.ui.cssの次に呼び出して完了。

 

感動。

cssファイルの中にimagesアイコンファイルがないのにdatepickerなどでJqueryUIアイコンが表示されてマジ感動。

mkkeckさん本当にありがとう。

 

ということで、私みたいなストレスを抱えている方は、

導入も簡単なので試してみてはいかがでしょうか?

 

それではまたいつか_(:3」 ∠)_