はじめに
トビデモを作った際に、機能のキモとして、
ページの画面遷移を作成する編集画面の実装方法をメモしています。
はじめに
Webサービスのページ遷移を表現するために、スクリーンショットをモーダルで繋げて表示しています。
この画面は全てGUIから作れるようになっています。
全体の流れ
- ポップアップを配置
- ポップアップアイコンの編集ボタンをクリック
- 新しいHTML要素が生成される。表示階層以外はdisplay:none;
- 保存ボタンを教えてAjaxで保存する。
当初は実現方法が思い浮かばずだいぶ悩みましたが、
後から見るとだいぶシンプルですね!以下主要なポイントを紹介しています。
考案時のメモ:
編集画面の設計
ページ遷移を編集する方法として、必要に応じてHTMLの要素を追加し、
それ以外はdisplay:none;にすることで、サーバーサイドとのやりとりを最低限にストレスなく編集できるよう気を使いました。
以下が実際に生成される要素です。

当初はその発想がなく、ページを増やすごとに今あるデータを非同期で保存する実装を考えていました。
ページ遷移の度にリクエストが発生しますし、一つ前の階層に戻って編集し直すときも、
データベースから情報を取って再描画しなくてはならず、実装に手間がかかるので、今思っても前者の実装で正解だったと思います。
// ポップアップ先の要素を作成
$('.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);
階層管理の設計

それぞれのページは以下情報を保持します。
- ページ番号
- 遷移ターゲットページ番号
- アイテムの情報(メモなど画面を説明する)
- 背景画像
ページ番号は1.1から始まり同じ階層では1.2,1.3と増えます。
新しい階層では2.1のように冒頭の数字を繰り上げ、2.1,2,2とと続きます。
ページのアイテムは、2.1.1,2.1.2,のようにページ番号の後に連番が続きます。
この管理番号を元にして、保存されたデータを再描画することになります。
戻るボタンの実装
頭をひねった結果、ページを移動する際に配列にページ番号を書き込むことで、
ページの遷移履歴を管理しています。
// ------------グローバル変数--------------
TRANSITION_MANAGER = [ '1.1' ]; // 現在地TRANSITION_MANAGER[TRANSITION_MANAGER.length
// - 1]
// ---------------------------------------
例えばページを一つ戻るときは、以下のようにページ管理用配列(TRANSITION_MANAGER)からデータを取得しています。
// ページを一つ戻る
$('.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 では データベースのテーブル側のキーに指定している情報が一致していれば
同じデータとして更新してくれるので、意図せず実装できてしまいました。
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
ちょっと長いですが以下全文。
$(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("アップロードに対応できていないブラウザです。");
}
});
まとめ
以上です!
ここに書いた以外にもアイテムの座標や画面描画など細かいところも合わせて、
数週間かかってしまいました。
しかし、当初は無理かも...と思っていたのですが、やればできるものですね!