はじめに
過去にご縁がありまして、タイトルのようなWebアプリケーションを作成する機会がありました。
実際納品した物は以下で説明する内容よりももっと複雑だったのですが、自分としてはとても勉強になったので、忘れないうちに記録に残しておこうと思います。
システムの仕様
以下の仕様で進めていきます。
実際のプロジェクトではもう少し複雑な仕様だったのですが、説明のため単純化しています。
- ブラウザ上からレイアウトをD&Dで変更できるようにする
- ブラウザ上からコンテンツを変更できるようにする
- コンテンツのレイアウトを決定する要素を階層化する
- 階層はシステム側で用意した固定のものとする
- 上位からArea, Grid, Blockの順
- Area……全体のレイアウト、基本的にユーザーは編集不可
- Grid……主にAreaの横方向分割
- Block……Grid1個分の縦方向のコンテナ、テキストや画像を格納する
- 画像や文章などの要素は最下層のBlockのみに登録可能
- AreaとGridはレイアウトの決定にのみ影響する
図にするとこんな感じです。
上記を元に最低限のサンプルとして作成したHTMLが以下になります。
中央に編集対象のHTMLを配置しています。
左パネルには追加可能なオブジェクト一覧を表示しています。
ヘッダ部分と右パネルにはその他よくありそうなものを配置していますが、今回はダミーとなります。
これをベースに配置に関する制御を実装していきます。
とりあえずD&D(sample-1)
D&Dにはjquery.pep.jsを使用します。
Block要素の構成はこのような形です。
<div class="le-block" id="block3">
<div class="le-block-core">
<h4><i class="material-icons">create</i> Lorem ipsum</h4>
</div>
<div class="le-block__menu">
<button class="mdl-button mdl-js-button mdl-button--icon">
<i class="material-icons">open_with</i>
</button>
<span class="le-block__menu_id">block3</span>
<button class="mdl-button mdl-js-button mdl-button--icon">
<i class="material-icons">delete</i>
</button>
</div>
</div>
大きくcoreとmenuに分かれます。
coreは表示コンテンツが入る部分です。WYSIWYGエディタで編集されるのはこの部分です。
menuはオブジェクトの移動や削除に必要なアイコンを表示します。
このBlockに対してpepを適用します。
le-blockのclassを持つ要素が対象です。
<script defer src="https://code.getmdl.io/1.1.1/material.min.js"></script>
<script src="//code.jquery.com/jquery-1.12.0.min.js"></script>
<script src="//rawgithub.com/briangonzalez/jquery.pep.js/master/src/jquery.pep.js"></script>
<script>
jQuery(function($) {
$('.le-block').children().on('MSPointerDown touchstart mousedown', function(e){
$(this).closest('.le-block').pep();
});
});
</script>
下図は適用後です。
Blockを自由に動かせるようになりましたが、配置制限が無いためやりたい放題です。
配置先をレイアウトに沿って限定(sample-2)
sample-1でpepを追加してBlockを動かせるようにしましたが、これに配置制限を加えることでレイアウトしやすくします。
まずBlockとBlockの境界に、配置可能表示のためのdiv(le-block-edge)を挿入します。
<div class="le-block-edge"></div>
<div class="le-block" id="block1">
<!-- 何かコンテンツ -->
</div>
<div class="le-block-edge"></div>
<div class="le-block" id="block2">
<!-- 何かコンテンツ -->
</div>
<div class="le-block-edge"></div>
<div class="le-block" id="block3">
<!-- 何かコンテンツ -->
</div>
<div class="le-block-edge"></div>
le-block-edgeは初期表示でdisplay=noneで隠しておきます(後述のCSSを参照)。
次に、pepの初期化時にこのdivにのみdrop可能となる設定を行います。
$('.le-block-menu__move').on('MSPointerDown touchstart mousedown', function(e){
$(this).closest('.le-block').pep({
shouldEase: false,
place: false,
droppable: ".le-block-edge", // dropを許可する要素
revert: true,
start: function(ev, obj) {……},
drag: function(ev, obj) {……},
stop: function(ev, obj) {……}
});
});
また、それぞれのイベントの際の処理を記述します。
- ドラッグ開始……le-block-edgeを表示します。
start: function(ev, obj) {
// ドロップ可能エリアを表示
$(".le-block-edge").show();
},
- ドラッグ中……ドロップされる箇所のみ特に強調して表示します。
drag: function(ev, obj) {
if (obj.activeDropRegions.length >= 1) {
$(obj.activeDropRegions).each(function(index) {
if (index) {
$(this).removeClass(obj.options.droppableActiveClass);
}
});
}
},
pepから取得できるactiveDropRegions(ドロップ可能領域の一覧)のうち、常に0要素目に対してドロップさせますので、ドラッグ中も0要素目のみ強調表示を残し、それ以外は強調表示をクリアするようにします。
- ドロップ……le-block-edgeの表示Offと、Block移動及びle-block-edgeの移動処理を行います。
stop: function(ev, obj) {
// ドロップ可能エリアを非表示に
$(".le-block-edge").hide();
// ドロップ可能エリアの後ろにドラッグしたブロックとその直後のドロップ可能エリアを挿入
obj.$el.next().insertAfter(obj.activeDropRegions[0]);
obj.$el.insertAfter(obj.activeDropRegions[0]);
}
ドラッグ中にle-block-edgeの位置をわかりやすくするために、色を変えます。
初期表示時の非表示も含みます。
.le-block-edge {
display: none;
height: 20px;
width: 100%;
opacity: 0.6;
background-color: yellow;
position: relative;
margin-top: -10px;
margin-bottom: -10px;
z-index: 1000;
}
.le-block-edge.pep-dpa {
background-color: goldenrod;
}
うまくいくとこんな感じになります。