【Onsen UI for Vue】スライダーメニューで削除ボタンの表示を実装してみた

Onsen UI for Vueで、リストのスライダーメニューを実装しました。
最近のUIで左スライドで削除というアイテムの削除が流行っています。

本記事では、リストのスライダーメニューで削除要素の表示、
クリック後、リストのアイテムの削除。の実装をご紹介します。

もしかしたら私のリサーチ不足で、スライダーメニューで削除のライブラリなりがあったかも・・しれませんが、一先ずgsapを使用して作成しました。

完成例

下のGIF画像のように作成しました。
OnsenUIなだけあって、ネイティブアプリのようなUIが簡単に作成できます。

削除

要件

・左スライドで削除要素の表示。
・左スライド時点からドラッグ終了時の差が最大値の場合、削除要素は開く。
・左スライド時点からドラッグ終了値の差が最大値以外の場合、削除要素を閉じる。
・他要素をクリックした場合、スライドされたメニューは閉じる。
・削除要素をクリックした場合、リストのアイテムを削除する。

上のような要件に基づき実装しました。
では、ソースを記していきます。

ソース

slider.vue

<template>
 <v-ons-page>
   <v-ons-list>
    <v-ons-list-item
     class="swipeListWrap"
     lock-on-drag
     v-for="item in items"
     :key="item.id"
     >
    <v-ons-row
      class="swipeList row px-0 mg-auto"
      :style="{ left: 0 - animatedNumber(item.id) + 10 + '%' }"
    >
     <v-ons-col
      class="item swipeListTarget"
      :data-order-id="item.id"
      width="70%"
    >
     <v-ons-row
      class="row px-0 mg-auto swipeListTarget"
      :data-order-id="item.id"
     >
      <v-ons-col
       class="col-12 swipeListTarget"
       :data-order-id="item.id"
      >
       {{ item.name }}
      </v-ons-col>
     </v-ons-row>
    </v-ons-col>
    <v-ons-col
      class="itemDeleteWrap"
      width="30%"
      @click="deleteFnc(item)"
    >
    <div class="itemDelete">
     <span class="itemDeleteText">削除</span>
    </v-ons-col>
   </v-ons-row>
  </v-ons-list-item>
 </v-ons-list>
</v-ons-page>
</template>
<script>
import gsap from "gsap";

export default {
 data() {
  return {
  number: 0,
  tweenedNumber: 0,
  dragstartX: 0,
  initialPosition: 30,
  targetOrderId: 0,
  animateLocked: false,
  movementPosition: 0,
  items: [
   {
    id: 0,
    name: "おれんじ",
   },
   {
    id: 1,
    name: "いちご",
   },
  {
   id: 2,
   name: "ぶどう",
   },
  ],
 };
},
 mounted: function () {
   let _this = this;
   this.$ons
   .GestureDetector(this.$el)
   .on("dragstart drag dragend touch", (e) => {
    if (e.target.matches(".swipeListTarget") && !_this.animateLocked) {
     _this.targetOrderId = e.target.getAttribute("data-order-id");
     var range = _this.dragstartX - e.gesture.center.pageX;
     if (e.type == "dragstart") {
      _this.dragstartX = e.gesture.center.pageX;
     } else if (e.type == "dragend") {
     _this.number = range < 0 + 80 ? 0 : _this.initialPosition;
     } else if (e.type == "drag") {
      _this.number =
      range < 0
      ? 0
      : range && _this.initialPosition <= range
      ? _this.initialPosition
      : range;
     }
    } else {
     _this.number = 0;
    }
   });
  },
 methods: {
  deleteFnc: function (item) {
   //削除処理
   this.items = Object.values(this.items).filter((el) => {
    return el.id != item.id;
   }, 0);
   this.animateLocked = false;
   },
  },
 computed: {
  animatedNumber: function () {
   return function (uid) {
    this.movementPosition = this.tweenedNumber.toFixed(0);
     if (uid != this.targetOrderId) {
      return 0;
     } else if (this.movementPosition == this.initialPosition) {
      this.animateLocked = true;
      return this.movementPosition;
     }
   if (this.movementPosition == 0) {
    this.animateLocked = false;
   }
    return this.movementPosition;
  };
 },
},
 watch: {
 number: function (newValue) {
  gsap.to(this.$data, {
   duration: 0.4,
   tweenedNumber: newValue,
  });
 },
},
};
</script>
<style scoped>
.swipeListWrap {
  position: relative;
  height: 50%;
  min-height: 70px;
  width: 130%;
}
.swipeList {
  position: absolute;
  width: 100%;
  height: 100%;
  align-items: center;
}
.item {
 position: relative;
 display: flex;
 align-items: center;
 height: 100%;
}
.itemDeleteWrap {
 height: 100%;
}
.itemDelete {
 display: block;
 height: 100%;
 background: red;
 width: 100%;
}
.itemDeleteText {
 position: relative;
 display: flex;
 align-items: center;
 justify-content: center;
 height: 100%;
 background: red;
 color: white;
 width: 100%;
}
</style> 
▼dataについて
・number
スライドメニューの右端を示しています。
どれぐらいメニューが開いているか座標が返ります。・tweenedNumber
numberの値が変化するとアニメーションが実行され、
tweenedNumberが変化します。
tweenedNumberはアニメーション後の値を代入するので、
numberを遅れて代入するという認識です。

・dragstartX

リスト内でdragを開始したポイントを記録します。

・initialPosition
削除要素を表示した際の移動範囲です。

・targetOrderId
イベントのある要素のカスタムデータを取得、代入します。

data-order-idにより、ページのIDを代入しています。

・animateLocked
連続してアニメーション実行のイベントが発生した場合、それらを受け付けるかどうかを判定するフラグです。

・movementPosition

移動した距離です。

mounted: function () 
{ 
let _this = this; 
this.$ons.GestureDetector(this.$el)
.on("dragstart drag dragend touch", (e) => 
{ 
 if (e.target.matches(".swipeListTarget") && !_this.animateLocked) 
{ _this.targetOrderId = e.target.getAttribute("data-order-id");
var range = _this.dragstartX - e.gesture.center.pageX;
 if (e.type == "dragstart")
 { _this.dragstartX = e.gesture.center.pageX; }
 else if (e.type == "dragend")
 { _this.number = range < 0 + 80 ? 0 : _this.initialPosition; }
 else if (e.type == "drag")
 { _this.number = range < 0 ? 0 :
 range && _this.initialPosition <= range 
? _this.initialPosition : range; }
 } else { _this.number = 0; 
} 
}
); },

dragstart drag dragend touchのイベントが発生した場合に実行されるコードです。
タッチしたポイントからドラッグ終了をrangeとして、
それぞれのポイントに応じ処理を変更しています。

computed: { animatedNumber: function () 
{ 
return function (uid) 
{ 
this.movementPosition = this.tweenedNumber.toFixed(0);
 if (uid != this.targetOrderId) {
 return 0; 
} else if (this.movementPosition == this.initialPosition)
 { this.animateLocked = true; return this.movementPosition;
 } if (this.movementPosition == 0)
 { this.animateLocked = false;
 } return this.movementPosition; }; },

それぞれのリストアイテムで持っているanimatedNumberにおける処理です。

イベントの発生した要素とそれ以外の要素で分けて、スライドした要素のみスライドするように設定してあります。
また、前回イベント時のスライドの状態から処理を分けています。

おわりに

今回は、スライダーメニューで削除要素の表示及び削除を実装しました。
簡単に実装できるのでは?と思っていましたが、所々で躓いてしまいました。

例えば、複数リストアイテムに対して
computed内で行ってる処理が共通化されているため、変数の管理をするため、
変数の状態管理で苦労しました。
アイテムAをスライドメニューを開いているときにアイテムBのスライドメニューに触れると、アイテムAは閉じ、アイテムBが開いてしまうという事象です。しかもアニメーションが適用されませんでした。

改めて自ら作成してみると、大手SNSのUIは最適化されているなと思いました。
ストレスのなく操作ができますよね。
ただ、個人的にはInstagramよりTwitterの方が洗練されていると感じます。
スクロールの手の動きって上下ではなく、斜めです。
人間工学に基づいたTwitterは、操作ミスが少ないです。
一方でInstagramは不意に画面スワイプや画面をクローズしてしまいます。

こういう小さな違いが多くのユーザを獲得するか否か。
サービス内容と同様に重要なポイントですね。

 

以上、ありがとうございました。

コメント

スポンサーリンク
スポンサーリンク
タイトルとURLをコピーしました