Webサービスのページ遷移をモーダルで表現する[開発メモ]

#はじめに
[トビデモ](http://tobidemo.com/　)を作った際に、機能のキモとして、
ページの画面遷移を作成する編集画面の実装方法をメモしています。

#はじめに

![5月-20-2018-14-35-39-compressor.gif](https://qiita-image-store.s3.amazonaws.com/0/162167/5494d7bd-0ce9-612c-e0f8-4a3c9df50a97.gif)


Webサービスのページ遷移を表現するために、スクリーンショットをモーダルで繋げて表示しています。
この画面は全てGUIから作れるようになっています。

![5月-20-2018-14-45-06-compressor (1).gif](https://qiita-image-store.s3.amazonaws.com/0/162167/5d7bcd62-db11-eac1-2894-14f14369fb61.gif)

#全体の流れ
1. ポップアップを配置
2. ポップアップアイコンの編集ボタンをクリック
3. 新しいHTML要素が生成される。表示階層以外はdisplay:none;
4. 保存ボタンを教えてAjaxで保存する。

当初は実現方法が思い浮かばずだいぶ悩みましたが、
後から見るとだいぶシンプルですね！以下主要なポイントを紹介しています。

考案時のメモ：

![IMG_20180523_223631.jpg](https://qiita-image-store.s3.amazonaws.com/0/162167/df3d450d-11b0-a015-cd7f-a5c2d209b8e2.jpeg)


#編集画面の設計
ページ遷移を編集する方法として、必要に応じてHTMLの要素を追加し、
それ以外はdisplay:none;にすることで、サーバーサイドとのやりとりを最低限にストレスなく編集できるよう気を使いました。
以下が実際に生成される要素です。


<img width="700" alt="スクリーンショット 2018-05-20 15.01.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/162167/0e73c317-1b8d-c3ff-bd21-9348e05b1c4e.png">


当初はその発想がなく、ページを増やすごとに今あるデータを非同期で保存する実装を考えていました。　
ページ遷移の度にリクエストが発生しますし、一つ前の階層に戻って編集し直すときも、
データベースから情報を取って再描画しなくてはならず、実装に手間がかかるので、今思っても前者の実装で正解だったと思います。


```javascript:

// ポップアップ先の要素を作成
	$('.edit_popup_target')
			.on(
					'click',
					function() {

						var _this = $('.flag_now_edit');

						var page_level_num = TRANSITION_MANAGER[TRANSITION_MANAGER.length - 1];
						var page_level = page_level_num.slice(0, 1);
						var next_page_level = Number(page_level) + 1;
						var page_num = page_level_num.slice(2, 3);
						var next_page_num = $('[data-dropzone^="'
								+ next_page_level + '"]').length;

						if ($(_this).attr("data-popuptarget") == undefined) {

							if (next_page_num != 0) {
								next_page_num += 1;
							} else {
								next_page_num = 1;

							}
							var this_dropzone_id = page_level + "." + page_num;
							var next_dropzone_id = next_page_level + "."
									+ next_page_num;

							$('.mapEreaWrapper')
									.append(
											'<div class="maperea" data-dropzone="'
													+ next_dropzone_id
													+ '"><img src ="./../../img/default-none.png"></div>');
							$(_this).attr("data-popuptarget", next_dropzone_id);
```


#階層管理の設計
<img width="682" alt="スクリーンショット 2018-05-20 15.17.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/162167/8e9fff86-0218-8db8-4557-00915ed2ac95.png">

それぞれのページは以下情報を保持します。
- ページ番号
- 遷移ターゲットページ番号
- アイテムの情報（メモなど画面を説明する）
- 背景画像

ページ番号は1.1から始まり同じ階層では1.2,1.3と増えます。
新しい階層では2.1のように冒頭の数字を繰り上げ、2.1,2,2とと続きます。
ページのアイテムは、2.1.1,2.1.2,のようにページ番号の後に連番が続きます。

この管理番号を元にして、保存されたデータを再描画することになります。

#戻るボタンの実装
編集画面ですので前の画面に戻れないとなりません！
<img width="213" alt="スクリーンショット 2018-05-20 15.32.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/162167/901036af-c8ad-d628-78d7-3f6a0c5f1f34.png">

頭をひねった結果、ページを移動する際に配列にページ番号を書き込むことで、
ページの遷移履歴を管理しています。

```javascript

// ------------グローバル変数--------------
	TRANSITION_MANAGER = [ '1.1' ]; // 現在地TRANSITION_MANAGER[TRANSITION_MANAGER.length
	// - 1]
	// ---------------------------------------

```

例えばページを一つ戻るときは、以下のようにページ管理用配列（TRANSITION_MANAGER）からデータを取得しています。

```javascript
// ページを一つ戻る
	$('.backToBeforePage')
			.on(
					'click',
					function() {

						if (TRANSITION_MANAGER.length != 1) {
							$(
									'[data-dropzone="'
											+ TRANSITION_MANAGER[TRANSITION_MANAGER.length - 1]
											+ '"]').hide();
							$(
									'[data-dropzone="'
											+ TRANSITION_MANAGER[TRANSITION_MANAGER.length - 2]
											+ '"]').show();
							// ACTIVE_PAGE_NUMBER =
							// TRANSITION_MANAGER[TRANSITION_MANAGER.length-2];
							TRANSITION_MANAGER.pop();
							changePageTransitionView();
						}
						;
						return false;
					});

```

#データの保存

- ページ番号
- 遷移ターゲットページ番号
- アイテムの情報（メモなど画面を説明する）
- 背景画像

などをAjaxで一気に保存します。
ネストすると綺麗に送付できたと思うのですが、

Jsonデータに対応したBeanを用意しておくと、Spring Bootがよしなに受け取ってくれるのですが、
ネスト構造の表現方法がわからなかったので、以下のように2,3項目だけ送付したいのに、
Beanで用意した全ての項目を用意しています。後ろ髪を引かれますが今回は回避策があったので諦めました。

また、保存したデータをアップデートする処理も書くつもりでしたが、
Spring boot では　データベースのテーブル側のキーに指定している情報が一致していれば
同じデータとして更新してくれるので、意図せず実装できてしまいました。


```javascript

  item_stat_array.push({
                                id : linkid + "_" + dropzone,
                                type : "drop-zone", // common
                                itemid : "0", // common
                                top : "0", // common
                                left : "0", // common
                                color : "0", // common
                                size : "0", // popup,tooltip
                                title : "0", // tooltip
                                target : "0", // popup
                                width : "0", // sticky
                                height : "0", // sticky
                                text : "0", // sticky
                                dropzone : dropzone, // dropzpne
                                img : img
                            // dropzone
```

ちょっと長いですが以下全文。


```javascript

$(document)
			.on(
					'submit',
					".save_to_server",
					function(e) {
						e.preventDefault();

						$(".maperea").show(); // disply:noneだとtop,left が取れない

						var demoname = $('.demo-name-val').val();
						if (demoname === undefined || demoname === null | demoname.replace(/[\t\s ]/g, '').length < 1){
							demoname = "-";
						}
						var item_stat_array = [];

						var maperea_array = $('.maperea');
						var maperea_leng = $('.maperea').length;
						$(maperea_array[i]).attr('data-dropzone');
						var linkid = $('#linkid').text();
						var xsrf = $.cookie('XSRF-TOKEN');

						for (var i = 0; i < maperea_leng; i++) {

							var dropzone = $(maperea_array[i]).attr(
									'data-dropzone');
							var img = $(maperea_array[i]).attr('data-img')
							if (img === undefined || img === null) {
								img = "0";
							}
							// var id = linkid + "_" +dropzone; //データベース保管時の主キー

							item_stat_array.push({
								id : linkid + "_" + dropzone,
								type : "drop-zone", // common
								itemid : "0", // common
								top : "0", // common
								left : "0", // common
								color : "0", // common
								size : "0", // popup,tooltip
								title : "0", // tooltip
								target : "0", // popup
								width : "0", // sticky
								height : "0", // sticky
								text : "0", // sticky
								dropzone : dropzone, // dropzpne
								img : img
							// dropzone

							});
						}

						var targetArray = $('.dropped');
						var item_num = targetArray.length;
						item_num - 1;

						for (var i = 0; i < item_num; i++) {
							var target = targetArray[i];

							if (target.className.indexOf('_tooltip') != -1) {

								var itemid = $(target).attr("data-item");
								var top = $(target).position().top;
								var left = $(target).position().left;
								var color = $(target).attr(
										"data-tooltipIconColor");
								var size = $(target).attr(
										"data-tooltipIconSize");
								var title = $(target).attr("title");
								// var id = linkid + "_" + itemid;
								// //データベース保管時の主キー

								item_stat_array.push({
									id : linkid + "_" + itemid,
									type : "_tooltip", // common
									itemid : itemid, // common
									top : top, // common
									left : left, // common
									color : color, // common
									size : size, // popup,tooltip
									title : title, // tooltip
									target : "0", // popup
									width : "0", // sticky
									height : "0", // sticky
									text : "0", // sticky
									dropzone : "0", // dropzpne
									img : img
								// dropzone
								});

							} else if (target.className.indexOf('_popup') != -1) {

								var top = $(target).position().top;
								var left = $(target).position().left;
								var itemid = $(target).attr("data-item");
								var color = $(target).attr("data-popupColor");
								var size = $(target).attr("data-popupSize");
								var target = $(target).attr("data-popuptarget");

								item_stat_array.push({
									id : linkid + "_" + itemid,
									type : "_popup", // common
									itemid : itemid, // common
									top : top, // common
									left : left, // common
									color : color, // common
									size : size, // popup,tooltip
									title : "0", // tooltip
									target : target, // popup
									width : "0", // sticky
									height : "0", // sticky
									text : "0", // sticky
									dropzone : "0", // dropzpne
									img : img
								// dropzone
								});

							} else if (target.className.indexOf('_sticky') != -1) {

								var top = $(target).position().top;
								var left = $(target).position().left;
								var width = $(target).width();
								var height = $(target).height();
								var itemid = $(target).attr("data-item");
								var color = $(target).attr("data-stickycolor");
								var text = $(target).children(".textsticky")
										.val();

								item_stat_array.push({
									id : linkid + "_" + itemid,
									type : "_sticky", // common
									itemid : itemid, // common
									top : top, // common
									left : left, // common
									color : color, // common
									size : "0", // popup,tooltip
									title : "0", // tooltip
									target : "0", // popup
									width : width, // sticky
									height : height, // sticky
									text : text, // sticky
									dropzone : "0", // dropzpne
									img : img
								// dropzone
								});

							}

						}

						$(".maperea").hide();
						$(
								'[data-dropzone="'
										+ TRANSITION_MANAGER[TRANSITION_MANAGER.length - 1]
										+ '"]').show();

						if (window.FormData) {

							var ajaxUrl = "/edit/uploaditemstat" + "?name=" + demoname ;

							$
									.ajax(
											{
												type : "POST", // HTTP通信の種類
												url : ajaxUrl, // リクエストを送信する先のURL
												dataType : "json", // サーバーから返されるデータの型
												data : JSON
														.stringify(item_stat_array),
												contentType : 'application/json; charset=utf-8',
												"beforeSend": function(xhr, setting) {
													$(".btn").attr('disabled', true);
													$(".item-uploading-animation").removeClass("nodispy");
												},
												headers : {
													'X-XSRF-TOKEN' : xsrf
												},
											})

									.done(
											function(data) { // Ajax通信が成功した時の処理

												var yourLink = "/show?id=";
												var yourLinkID = $("#linkid")
														.text();

												var demoname = $(".demo-name-val").val();
												if(demoname==null ||demoname== "") {demoname = "無題のトビデモ"}

												$(".yourLink").attr("href",
														yourLink + yourLinkID);
												$(".yourLink").text(
														demoname);
												$(".yourLink").attr('target',
														'_blank');
												$('.upload-item-done').slideDown(1500);

												$(".btn").attr('disabled', false);
												$(".item-uploading-animation").addClass("nodispy");


											}).fail(
											function(XMLHttpRequest,
													textStatus, errorThrown) { // Ajax通信が失敗した時の処理
												$(".btn").attr('disabled', false);
												$(".item-uploading-animation").addClass("nodispy");
												$('.upload-item-failed').slideDown(1500);
												return false;

											});
						} else {
							alert("アップロードに対応できていないブラウザです。");
						}

					});

```


#まとめ
以上です！

ここに書いた以外にもアイテムの座標や画面描画など細かいところも合わせて、
数週間かかってしまいました。
しかし、当初は無理かも...と思っていたのですが、やればできるものですね！