From 3b1d94e23f32f5dc83107437f7080f32df9ff129 Mon Sep 17 00:00:00 2001
From: Bram Wiepjes <bramw@protonmail.com>
Date: Tue, 16 Jul 2019 20:43:03 +0200
Subject: [PATCH] fixed element not found bug and fire less events in Context
 component

---
 web-frontend/components/Context.vue     | 91 +++++++++++++++++--------
 web-frontend/config/nuxt.config.base.js |  6 +-
 web-frontend/directives/clickOutside.js | 19 ------
 web-frontend/directives/moveToBody.js   | 18 -----
 web-frontend/plugins/directives.js      |  7 --
 5 files changed, 62 insertions(+), 79 deletions(-)
 delete mode 100644 web-frontend/directives/clickOutside.js
 delete mode 100644 web-frontend/directives/moveToBody.js
 delete mode 100644 web-frontend/plugins/directives.js

diff --git a/web-frontend/components/Context.vue b/web-frontend/components/Context.vue
index f96ec1fdb..89a206594 100644
--- a/web-frontend/components/Context.vue
+++ b/web-frontend/components/Context.vue
@@ -1,10 +1,5 @@
 <template>
-  <div
-    v-move-to-body
-    v-click-outside="hide"
-    class="context"
-    :class="{ 'visibility-hidden': !open }"
-  >
+  <div class="context" :class="{ 'visibility-hidden': !open }">
     <slot></slot>
   </div>
 </template>
@@ -26,15 +21,30 @@ export default {
    * element and in the child element we need to register the child with their parent to
    * prevent this.
    */
-  beforeMount() {
+  mounted() {
     let $parent = this.$parent
     while ($parent !== undefined) {
       if ($parent.registerContextChild) {
         $parent.registerContextChild(this.$el)
-        break
       }
       $parent = $parent.$parent
     }
+
+    // Move the rendered element to the top of the body so it can be positioned over any
+    // other element.
+    const body = document.body
+    body.insertBefore(this.$el, body.firstChild)
+  },
+  /**
+   * Make sure the context menu is not open and all the events on the body are removed
+   * and that the element is removed from the body.
+   */
+  destroyed() {
+    this.hide()
+
+    if (this.$el.parentNode) {
+      this.$el.parentNode.removeChild(this.$el)
+    }
   },
   methods: {
     /**
@@ -62,33 +72,56 @@ export default {
       }
 
       if (value) {
-        const css = this.calculatePosition(target, vertical, horizontal, offset)
+        this.show(target, vertical, horizontal, offset)
+      } else {
+        this.hide()
+      }
+    },
+    /**
+     * Calculate the position, show the context menu and register a click event on the
+     * body to check if the user has clicked outside the context.
+     */
+    show(target, vertical, horizontal, offset) {
+      const css = this.calculatePosition(target, vertical, horizontal, offset)
 
-        // Set the calculated positions of the context.
-        for (const key in css) {
-          const value = css[key] !== null ? Math.ceil(css[key]) + 'px' : 'auto'
-          this.$el.style[key] = value
-        }
+      // Set the calculated positions of the context.
+      for (const key in css) {
+        const value = css[key] !== null ? Math.ceil(css[key]) + 'px' : 'auto'
+        this.$el.style[key] = value
       }
 
       // If we store the element who opened the context menu we can exclude the element
       // when clicked outside of this element.
-      this.opener = value ? target : null
-      this.open = value
-    },
-    hide(event) {
-      // Checks if the click is inside one of our children. In that code the context
-      // must stay open.
-      const isChild = this.children.some(element =>
-        isElement(element, event.target)
-      )
+      this.opener = target
+      this.open = true
 
-      // Checks if the context is already opened, if the click was not on the opener
-      // because he can trigger the toggle method and if the click was not in one of
-      // our child contexts.
-      if (this.open && !isElement(this.opener, event.target) && !isChild) {
-        this.open = false
+      this.$el.clickOutsideEvent = event => {
+        if (
+          // Check if the context menu is still open
+          this.open &&
+          // If the click was outside the context element because we want to ignore
+          // clicks inside it.s
+          !isElement(this.$el, event.target) &&
+          // If the click was not on the opener because he can trigger the toggle
+          // method.
+          !isElement(this.opener, event.target) &&
+          // If the click was not inside one of the context children of this context
+          // menu.
+          !this.children.some(element => isElement(element, event.target))
+        ) {
+          this.hide()
+        }
       }
+      document.body.addEventListener('click', this.$el.clickOutsideEvent)
+    },
+    /**
+     * Hide the context menu and make sure the body event is removed.
+     */
+    hide() {
+      this.opener = null
+      this.open = false
+
+      document.body.removeEventListener('click', this.$el.clickOutsideEvent)
     },
     /**
      * Calculates the absolute position of the context based on the original clicked
@@ -147,8 +180,6 @@ export default {
     /**
      * A child context can register itself with the parent to prevent closing of the
      * parent when clicked inside the child.
-     *
-     * @param element HTMLElement
      */
     registerContextChild(element) {
       this.children.push(element)
diff --git a/web-frontend/config/nuxt.config.base.js b/web-frontend/config/nuxt.config.base.js
index f090cdcef..5bdb09e02 100644
--- a/web-frontend/config/nuxt.config.base.js
+++ b/web-frontend/config/nuxt.config.base.js
@@ -25,11 +25,7 @@ export default {
   /*
    ** Plugins to load before mounting the App
    */
-  plugins: [
-    { src: '@/plugins/auth.js' },
-    { src: '@/plugins/directives.js' },
-    { src: '@/plugins/vuelidate.js' }
-  ],
+  plugins: [{ src: '@/plugins/auth.js' }, { src: '@/plugins/vuelidate.js' }],
 
   /*
    ** Nuxt.js modules
diff --git a/web-frontend/directives/clickOutside.js b/web-frontend/directives/clickOutside.js
deleted file mode 100644
index 6d1c7bf3b..000000000
--- a/web-frontend/directives/clickOutside.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { isElement } from '@/utils/dom'
-
-/**
- * This directive calls a custom method if the user clicks outside of the
- * element.
- */
-export default {
-  bind: (el, binding, vnode) => {
-    el.clickOutsideEvent = event => {
-      if (!isElement(el, event.target)) {
-        vnode.context[binding.expression](event)
-      }
-    }
-    document.body.addEventListener('click', el.clickOutsideEvent)
-  },
-  unbind: el => {
-    document.body.removeEventListener('click', el.clickOutsideEvent)
-  }
-}
diff --git a/web-frontend/directives/moveToBody.js b/web-frontend/directives/moveToBody.js
deleted file mode 100644
index c51404d09..000000000
--- a/web-frontend/directives/moveToBody.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * This directive moves the whole element to the document body so that it can be
- * positioned over another element.
- */
-export default {
-  inserted: el => {
-    const body = document.body
-
-    // The element is added as first child in the body so that child contexts
-    // are being shown on top of their parent.
-    body.insertBefore(el, body.firstChild)
-  },
-  unbind: el => {
-    if (el.parentNode) {
-      el.parentNode.removeChild(el)
-    }
-  }
-}
diff --git a/web-frontend/plugins/directives.js b/web-frontend/plugins/directives.js
deleted file mode 100644
index 64a9c8c3f..000000000
--- a/web-frontend/plugins/directives.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import Vue from 'vue'
-
-import moveToBody from '@/directives/moveToBody'
-import clickOutside from '@/directives/clickOutside'
-
-Vue.directive('move-to-body', moveToBody)
-Vue.directive('click-outside', clickOutside)