Browse Source

implement context menu on planning

tripeur 4 years ago
parent
commit
0036aadf06

+ 1 - 1
package-lock.json

@@ -6913,7 +6913,7 @@
       "dev": true
     },
     "jc-timeline": {
-      "version": "git+http://gitlab.jaquin.fr/clovis/jc-timeline.git#700acd245a76a3ff67529e6ea312dcb4ed4a84d9",
+      "version": "git+http://gitlab.jaquin.fr/clovis/jc-timeline.git#97e0fe591514b2f4be6ffd8091098fd120c320d0",
       "from": "git+http://gitlab.jaquin.fr/clovis/jc-timeline.git",
       "requires": {
         "dayjs": "^1.10.4",

+ 5 - 1
src/PlannerApp.vue

@@ -1,5 +1,9 @@
 <template>
-  <c-header title="BDLG Planner" title-to="/planner" :tabs="tabs" />
+  <c-header title="BDLG Planner" title-to="/planner" :tabs="tabs"
+    ><div class="floating burger" @click="save">
+      <i class="material-icons">save</i>
+    </div></c-header
+  >
   <div class="main-container">
     <router-view
       @export="exportStateToJson"

+ 72 - 0
src/components/ContextMenu.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="context-menu" :class="{ active: active }" :style="{ top: y + 'px', left: x + 'px' }">
+    <div v-for="button in actions" :key="button.id" @click="button.onClick" class="item">
+      <i v-if="button.icon" class="material-icons">{{ button.icon }}</i
+      >{{ button.name }}
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from "vue";
+
+export interface MenuAction {
+  id: string;
+  onClick: (e: MouseEvent) => void;
+  icon?: string;
+  name: string;
+}
+export default defineComponent({
+  data() {
+    return { active: true };
+  },
+  props: {
+    actions: { type: Array as PropType<Array<MenuAction>>, default: () => [] },
+    x: { type: Number, default: 0 },
+    y: { type: Number, default: 0 },
+  },
+  methods: {
+    open() {
+      this.active = true;
+      window.addEventListener("click", this.close);
+    },
+    close() {
+      this.active = false;
+      window.removeEventListener("click", this.close);
+    },
+  },
+});
+</script>
+
+<style scoped>
+.context-menu {
+  position: fixed;
+  z-index: 10000;
+  width: 150px;
+  background: var(--color-neutral-100);
+  border-radius: 5px;
+  transform: scale(0);
+  transform-origin: top left;
+  overflow: hidden;
+}
+.context-menu.active {
+  transform: scale(1);
+  transition: transform 100ms ease-in-out;
+}
+.context-menu .item {
+  padding: 8px 10px;
+  font-size: 15px;
+  color: #eee;
+}
+.context-menu .item:hover {
+  background: var(--color-neutral-200);
+}
+.context-menu .item i {
+  display: inline-block;
+  margin-right: 5px;
+}
+.context-menu hr {
+  margin: 2px 0px;
+  border-color: var(--color-neutral-200);
+}
+</style>

+ 12 - 2
src/components/EditeurCreneauGroup.vue

@@ -3,14 +3,21 @@
     <h3>Modifier une ligne</h3>
     <div class="actions">
       <button class="btn small primary" v-on:click="emitCreationOrder">
-        <i class="material-icons right">create</i>Ajouter une ligne
+        <i class="material-icons right">create</i>Nouvelle
+      </button>
+      <button
+        class="btn small primary"
+        :class="{ disabled: creneauGroup === undefined }"
+        v-on:click="emitDuplicateOrder"
+      >
+        <i class="material-icons right">content_copy</i>Dupliquer
       </button>
       <button
         class="btn small error"
         :disabled="creneauGroup === undefined"
         v-on:click="emitDeleteOrder"
       >
-        <i class="material-icons">delete_forever</i>Supprimer la ligne
+        <i class="material-icons">delete_forever</i>Supprimer
       </button>
     </div>
 
@@ -111,6 +118,9 @@ export default defineComponent({
       const value = (e.target as HTMLInputElement).value;
       this.updateCreneau("title", value);
     },
+    emitDuplicateOrder(): void {
+      this.$emit("duplicate");
+    },
     emitCreationOrder(): void {
       this.$emit("create");
     },

+ 1 - 0
src/components/Header.vue

@@ -14,6 +14,7 @@
         {{ item.name }}
       </router-link>
     </nav>
+    <slot></slot>
     <div class="floating show-sm burger" @click="showMenu = true" v-if="tabs.length > 0">
       <i class="material-icons">menu</i>
     </div>

+ 146 - 52
src/views/Planning.vue

@@ -2,7 +2,7 @@
   <div class="timeline">
     <div class="timelime-control">
       <h1>Planning global de l'événement</h1>
-      <div class="actions" @click="(e) => e.stopPropagation()">
+      <div class="actions" jc-timeline-keep-select>
         <button
           class="btn small secondary"
           @click="createCreneau"
@@ -39,20 +39,22 @@
     v-if="currentCreneau"
     :creneau="currentCreneau"
     @create="createCreneau"
-    @delete="deleteCreneau"
+    @delete="deleteCurrentCreneau"
     @duplicate="duplicateCreneau"
     @edit="updateCreneau"
-    @click="(e) => e.stopPropagation()"
+    jc-timeline-keep-select
   ></editeur-creneau>
   <editeur-ligne
     class="planning"
     v-else
     :creneauGroupId="currentCreneauGroupId"
     @create="createRessource"
-    @delete="deleteRessource"
+    @delete="deleteCurrentRessource"
+    @duplicate="duplicateCurrentRessource"
     @edit="updateCreneauGroup"
-    @click="(e) => e.stopPropagation()"
+    jc-timeline-keep-select
   ></editeur-ligne>
+  <context-menu :x="menuX" :y="menuY" :actions="contextActions" ref="menu" />
 </template>
 
 <script lang="ts">
@@ -69,8 +71,9 @@ import { MutationTypes } from "@/store/Mutations";
 import Selectable from "node_modules/jc-timeline/lib/utils/selectable";
 import toast from "@/utils/Toast";
 
-import * as dayjs from "dayjs";
+import dayjs from "dayjs";
 import "dayjs/locale/fr";
+import ContextMenu, { MenuAction } from "@/components/ContextMenu.vue";
 dayjs.locale("fr");
 
 type changePayload = {
@@ -82,9 +85,14 @@ export default defineComponent({
   components: {
     EditeurCreneau,
     EditeurLigne,
+    ContextMenu,
   },
   data() {
     return {
+      contextActions: [] as Array<MenuAction>,
+      menuX: 0,
+      menuY: 0,
+
       currentCreneau: undefined as Creneau | undefined,
       currentCreneauGroup: undefined as Ressource | undefined,
       zoomLevel: 14,
@@ -107,34 +115,8 @@ export default defineComponent({
         : false;
       if (groupId) {
         const start = new Date(this.timeline.start);
-        const e = this.timeline.addEvent(
-          new jcEvent({
-            id: uuidv4(),
-            start: start,
-            end: new Date(start.getTime() + 1000 * 60 * 60),
-            ressourceId: groupId,
-            title: "Nouveau creneau",
-          })
-        );
-        if (e) {
-          this.timeline.clearSelectedItems();
-          e.selected = true;
-          const creneau = new Creneau({
-            event: e,
-            penibility: 12,
-            minAttendee: 1,
-            maxAttendee: 1,
-            benevoleIdList: [],
-            competencesIdList: [],
-            description: "",
-            isMeal: false,
-            fixedAttendee: false,
-          });
-          this.$store.commit(MutationTypes.addCreneau, creneau);
-
-          this.currentCreneau = this.$store.getters.getCreneauById(creneau.id);
-          this.currentCreneauGroup = undefined;
-        }
+        const end = new Date(start.getTime() + 1000 * 60 * 60);
+        this.registerCreneau(start, end, groupId);
       } else {
         toast({
           html:
@@ -143,14 +125,47 @@ export default defineComponent({
         });
       }
     },
+    registerCreneau(start: Date, end: Date, groupId: string): void {
+      const e = this.timeline.addEvent(
+        new jcEvent({
+          id: uuidv4(),
+          start: start,
+          end: end,
+          ressourceId: groupId,
+          title: "Nouveau creneau",
+        })
+      );
+      if (e) {
+        this.timeline.clearSelectedItems();
+        e.selected = true;
+        const creneau = new Creneau({
+          event: e,
+          penibility: 12,
+          minAttendee: 1,
+          maxAttendee: 1,
+          benevoleIdList: [],
+          competencesIdList: [],
+          description: "",
+          isMeal: false,
+          fixedAttendee: false,
+        });
+        this.$store.commit(MutationTypes.addCreneau, creneau);
+
+        this.currentCreneau = this.$store.getters.getCreneauById(creneau.id);
+        this.currentCreneauGroup = undefined;
+      }
+    },
     createRessource(): void {
       const ressource = new Ressource({
         id: uuidv4(),
         title: "Nouvelle ligne",
       });
-      this.$store.commit(MutationTypes.addCreneauGroupAt, { pos: 0, r: ressource });
+      this.registerRessource(ressource);
+    },
+    registerRessource(ressource: Ressource, pos = 0) {
+      this.$store.commit(MutationTypes.addCreneauGroupAt, { pos, r: ressource });
       this.timeline.clearSelectedItems();
-      this.timeline.addRessource(ressource).selected = true;
+      this.timeline.addRessource(ressource, pos).selected = true;
       this.currentCreneauGroup = ressource;
     },
     duplicateCreneau(payload: Creneau) {
@@ -158,28 +173,40 @@ export default defineComponent({
         ...payload.toJSON(),
         event: new jcEvent({ ...payload.event, id: uuidv4() }),
       });
-      newCreneau.title = "Copy de " + newCreneau.title;
+      newCreneau.title = "Copie de " + newCreneau.title;
       this.timeline.addEvent(newCreneau.event);
       this.$store.commit(MutationTypes.addCreneau, newCreneau);
       this.currentCreneau = newCreneau;
     },
-    deleteRessource(): void {
-      if (this.currentCreneauGroup) {
-        const contentRemoved = this.timeline.removeRessourceById(this.currentCreneauGroup.id);
-        contentRemoved.ressources.map((r) =>
-          this.$store.commit(MutationTypes.removeCreneauGroup, r)
-        );
-        contentRemoved.items.map((e) => {
-          const crenenau = this.$store.getters.getCreneauById(e.id);
-          if (crenenau) this.$store.commit(MutationTypes.removeCreneau, crenenau);
-        });
-      }
+    duplicateCurrentRessource() {
+      if (this.currentCreneauGroup) this.duplicateRessource(this.currentCreneauGroup);
     },
-    deleteCreneau(): void {
-      if (this.currentCreneau) {
-        this.timeline.removeEventById(this.currentCreneau.id);
-        this.$store.commit(MutationTypes.removeCreneau, this.currentCreneau);
+    duplicateRessource(ressource: Ressource): void {
+      let newRessource = new Ressource({ ...ressource.toJSON(), id: uuidv4() });
+      if (ressource.parent) {
+        newRessource.parent = ressource.parent;
       }
+      this.registerRessource(newRessource, this.creneauGroupList.indexOf(ressource) + 1);
+
+      this.timeline.updateRessource(newRessource.id, "title", "Copie de " + newRessource.title);
+    },
+    deleteRessource(ressource: Ressource): void {
+      const contentRemoved = this.timeline.removeRessourceById(ressource.id);
+      contentRemoved.ressources.map((r) => this.$store.commit(MutationTypes.removeCreneauGroup, r));
+      contentRemoved.items.map((e) => {
+        const crenenau = this.$store.getters.getCreneauById(e.id);
+        if (crenenau) this.$store.commit(MutationTypes.removeCreneau, crenenau);
+      });
+    },
+    deleteCurrentRessource(): void {
+      if (this.currentCreneauGroup) this.deleteRessource(this.currentCreneauGroup);
+    },
+    deleteCreneau(creneau: Creneau) {
+      this.timeline.removeEventById(creneau.id);
+      this.$store.commit(MutationTypes.removeCreneau, creneau);
+    },
+    deleteCurrentCreneau(): void {
+      if (this.currentCreneau) this.deleteCreneau(this.currentCreneau);
     },
     selectionChangeHandler(ev: CustomEvent) {
       const elts = ev.detail.items as Array<Selectable>;
@@ -278,6 +305,72 @@ export default defineComponent({
       this.slotwidth = slotWidths[this.zoomLevel];
       this.legendspan = legendSpans[this.zoomLevel];
     },
+    openContextMenu(e: MouseEvent) {
+      e.preventDefault();
+      this.menuX = e.clientX;
+      this.menuY = e.clientY;
+      (this.$refs.menu as typeof ContextMenu).open();
+    },
+    rightclick(e: MouseEvent) {
+      if (this.timeline) {
+        const node = this.timeline.shadowRoot?.elementFromPoint(e.clientX, e.clientY);
+        if (node) {
+          if (node.className == "jc-timeslots" || node.className.includes("jc-slot")) {
+            const arr = this.timeline.shadowRoot
+              ?.elementsFromPoint(e.clientX, e.clientY)
+              .filter((o) => o.tagName == "TD");
+            if (arr) {
+              const slot = arr[0];
+              const startTime = dayjs(slot.getAttribute("start") as string);
+              const ressourceId = slot.parentElement?.getAttribute("ressourceid");
+              const endTime = startTime.add(this.slotduration, "m");
+              if (startTime.isValid() && ressourceId) {
+                const onClick = () => {
+                  this.registerCreneau(startTime.toDate(), endTime.toDate(), ressourceId);
+                };
+                this.contextActions = [{ id: "1", onClick: onClick, name: "Inserer un créneau" }];
+                this.openContextMenu(e);
+              }
+            }
+          } else if (
+            node.className.startsWith("jc-timeslot-") ||
+            node.classList.contains("jc-timeslot")
+          ) {
+            const timeslot = node.classList.contains("jc-timeslot") ? node : node.parentElement;
+            const creneau = this.$store.getters.getCreneauById(
+              timeslot?.getAttribute("eventid") ?? ""
+            );
+            if (creneau) {
+              this.contextActions = [
+                { id: "1", onClick: () => this.duplicateCreneau(creneau), name: "Dupliquer" },
+                { id: "2", onClick: () => this.deleteCreneau(creneau), name: "Supprimer" },
+              ];
+              this.openContextMenu(e);
+            }
+          }
+          if (node.className.startsWith("jc-ressource")) {
+            const ressourceId = node.parentElement?.getAttribute("ressourceid");
+            const ressource = this.$store.getters.getCreneauGroupById(ressourceId ?? "");
+
+            if (ressource) {
+              const pos = this.creneauGroupList.indexOf(ressource);
+              const newRessource = (pos: number) => {
+                const r = new Ressource({ id: uuidv4(), title: "Nouvelle ligne" });
+                if (ressource.parent) r.parent = ressource.parent;
+                this.registerRessource(r, pos);
+              };
+              this.contextActions = [
+                { id: "1", onClick: () => this.duplicateRessource(ressource), name: "Dupliquer" },
+                { id: "2", onClick: () => newRessource(pos), name: "Inserer au dessus" },
+                { id: "3", onClick: () => newRessource(pos + 1), name: "Inserer dessous" },
+                { id: "4", onClick: () => this.deleteRessource(ressource), name: "Supprimer" },
+              ];
+              this.openContextMenu(e);
+            }
+          }
+        }
+      }
+    },
   },
   computed: {
     timeline(): Timeline {
@@ -333,6 +426,7 @@ export default defineComponent({
     background: #fbca32;
 }`;
     this.timeline.clearSelectedItems();
+    this.timeline.addEventListener("contextmenu", this.rightclick);
   },
 });
 </script>