TOBAGO-2012: Replace popup implementation from bootstrap to own

* first implementation
* wip!
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/ext-bootstrap.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/ext-bootstrap.ts
deleted file mode 100644
index e796f3a..0000000
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/ext-bootstrap.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Utility to "not" use jQuery and Bootstrap directly in the Tobago code.
-
-export class BootstrapUtils {
-
-  static modal(element: Element, data?: any, options?: any): void {
-    jQuery(element).modal(data, options);
-  }
-
-}
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-all.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-all.ts
index b8cb52b..fb506e0 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-all.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-all.ts
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import "./ext-bootstrap";
 import "./tobago-listener";
 import "./tobago-core";
 import "./tobago-dropdown";
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-popup.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-popup.ts
index ba306cb..c10c340 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-popup.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-popup.ts
@@ -15,16 +15,78 @@
  * limitations under the License.
  */
 
-import {BootstrapUtils} from "./ext-bootstrap";
+const ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key
+
+const Default = {
+  backdrop: true,
+  keyboard: true,
+  focus: true,
+  show: true
+};
+
+const DefaultType = {
+  backdrop: "(boolean|string)",
+  keyboard: "boolean",
+  focus: "boolean",
+  show: "boolean"
+};
+
+const Event = {
+  HIDE: "hide.bs.modal",
+  HIDE_PREVENTED: "hidePrevented.bs.modal",
+  HIDDEN: "hidden.bs.modal",
+  SHOW: "show.bs.modal",
+  SHOWN: "shown.bs.modal",
+  FOCUSIN: "focusin.bs.modal",
+  RESIZE: "resize.bs.modal",
+  CLICK_DISMISS: "click.dismiss.bs.modal",
+  KEYDOWN_DISMISS: "keydown.dismiss.bs.modal",
+  MOUSEUP_DISMISS: "mouseup.dismiss.bs.modal",
+  MOUSEDOWN_DISMISS: "mousedown.dismiss.bs.modal",
+  CLICK_DATA_API: "click.bs.modal.data-api"
+};
+
+const ClassName = {
+  SCROLLABLE: "modal-dialog-scrollable",
+  SCROLLBAR_MEASURER: "modal-scrollbar-measure",
+  BACKDROP: "modal-backdrop",
+  OPEN: "modal-open",
+  FADE: "fade",
+  SHOW: "show",
+  STATIC: "modal-static"
+};
+
+const Selector = {
+  DIALOG: ".modal-dialog",
+  MODAL_BODY: ".modal-body",
+  DATA_TOGGLE: "[data-toggle='modal']",
+  DATA_DISMISS: "[data-dismiss='modal']",
+  FIXED_CONTENT: ".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",
+  STICKY_CONTENT: ".sticky-top"
+};
 
 export class Popup extends HTMLElement {
 
-  static close(button: HTMLElement): void {
-    BootstrapUtils.modal(button.closest(".modal"), "hide");
-  }
+  TRANSITION_END: string = "bsTransitionEnd";
+
+  _dialog: HTMLElement;
+  _backdrop;
+  _isShown;
+  _isBodyOverflowing;
+  _ignoreBackdropClick;
+  // _isTransitioning;
+  _scrollbarWidth;
+  _clickDismiss: (event: Event) => void;
 
   constructor() {
     super();
+    this._dialog = this.querySelector(Selector.DIALOG);
+    this._backdrop = null;
+    this._isShown = false;
+    this._isBodyOverflowing = false;
+    this._ignoreBackdropClick = false;
+    // this._isTransitioning = false;
+    this._scrollbarWidth = 0;
   }
 
   connectedCallback(): void {
@@ -35,12 +97,485 @@
       for (const backdrop of document.querySelectorAll(".modal-backdrop")) {
         backdrop.parentNode.removeChild(backdrop);
       }
-
-      BootstrapUtils.modal(this, "show"); // inits and opens the popup
+      this.show(); // inits and opens the popup
     } else {
-      BootstrapUtils.modal(this, "hide"); // inits and hides the popup
+      this.hide(); // inits and hides the popup
     }
   }
+
+  // Public
+
+  // toggle(relatedTarget) {
+  //   return this._isShown ? this.hide() : this.show(relatedTarget)
+  // }
+
+  show():void {
+    if (this._isShown /*|| this._isTransitioning*/) {
+      return;
+    }
+
+/*
+    if (this.classList.contains(ClassName.FADE)) {
+      this._isTransitioning = true;
+    }
+*/
+
+    const showEvent = new CustomEvent(Event.SHOW, /*{*/
+      // detail: relatedTarget // TBD: detail or anything other
+    /*}*/);
+
+    this.dispatchEvent(showEvent);
+
+    if (this._isShown || showEvent.defaultPrevented) {
+      return;
+    }
+
+    this._isShown = true;
+
+    // this._checkScrollbar();
+    // this._setScrollbar();
+    //
+    // this._adjustDialog();
+    //
+    // this._setEscapeEvent();
+    // this._setResizeEvent();
+
+    this._clickDismiss = (event: Event) => {this.hide();};
+    // this._clickDismiss = (event: Event) => {this.hide(event)};
+    if (this.classList.contains(Selector.DATA_DISMISS)) {
+      this.addEventListener(Event.CLICK_DISMISS, this._clickDismiss);
+    }
+
+    this._dialog.addEventListener(Event.MOUSEDOWN_DISMISS, () => {
+      // $(this._element).one(Event.MOUSEUP_DISMISS, (event) => {  // XXX not implemented yet
+      //   if ($(event.target).is(this._element)) { // XXX not implemented yet
+      this._ignoreBackdropClick = true;
+      // }
+      // })
+    });
+
+    // this._showBackdrop(() => this._showElement(relatedTarget))
+    this._showElement();
+  }
+
+  hide(/*event*/):void {
+    // if (event) {
+    //   event.preventDefault()
+    // }
+
+    if (!this._isShown/* || this._isTransitioning*/) {
+      return;
+    }
+
+    const hideEvent = new CustomEvent(Event.HIDE);
+
+    this.dispatchEvent(hideEvent);
+
+    if (!this._isShown || hideEvent.defaultPrevented) {
+      return;
+    }
+
+    this._isShown = false;
+    // const transition = this.classList.contains(ClassName.FADE);
+
+    // if (transition) {
+    //   this._isTransitioning = true
+    // }
+
+    // this._setEscapeEvent();
+    // this._setResizeEvent();
+
+    $(document).off(Event.FOCUSIN);
+
+    this.classList.remove(ClassName.SHOW);
+
+    this.removeEventListener(Event.CLICK_DISMISS, this._clickDismiss);
+    $(this._dialog).off(Event.MOUSEDOWN_DISMISS);
+
+    // if (transition) {
+    //   const transitionDuration = this.getTransitionDuration();
+    //
+    //   this.addEventListener(Popup.TRANSITION_END, (event:Event) => this._hideModal(event));
+    //   this.emulateTransitionEnd(transitionDuration)
+    // } else {
+       this._hideModal();
+    // }
+  }
+
+  // dispose() {
+  //   [window, this._element, this._dialog]
+  //       .forEach((htmlElement) => $(htmlElement).off(`.bs.modal`));
+
+    /**
+     * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API`
+     * Do not move `document` in `htmlElements` array
+     * It will remove `Event.CLICK_DATA_API` event that should remain
+     */
+    // $(document).off(Event.FOCUSIN);
+    //
+    // $.removeData(this._element, 'bs.modal');
+    //
+    // this._dialog = null;
+    // this._backdrop = null;
+    // this._isShown = null;
+    // this._isBodyOverflowing = null;
+    // this._ignoreBackdropClick = null;
+    // this._isTransitioning = null;
+    // this._scrollbarWidth = null;
+  // }
+
+  // handleUpdate() {
+  //   this._adjustDialog();
+  // }
+
+  // Private
+/*
+  _triggerBackdropTransition() {
+    if (this._config.backdrop === 'static') {
+      const hideEventPrevented = $.Event(Event.HIDE_PREVENTED);
+
+      $(this._element).trigger(hideEventPrevented);
+      if (hideEventPrevented.defaultPrevented) {
+        return;
+      }
+
+      this._element.classList.add(ClassName.STATIC);
+
+      const modalTransitionDuration = Util.getTransitionDurationFromElement(this._element)
+
+      $(this._element).one(Util.TRANSITION_END, () => {
+        this._element.classList.remove(ClassName.STATIC)
+      })
+          .emulateTransitionEnd(modalTransitionDuration);
+      this._element.focus();
+    } else {
+      this.hide();
+    }
+  }
+*/
+  _showElement(/*relatedTarget*/):void {
+    // const transition = $(this._element).hasClass(ClassName.FADE)
+    const modalBody = this._dialog ? this._dialog.querySelector(Selector.MODAL_BODY) : null;
+
+    if (!this.parentNode ||
+        this.parentNode.nodeType !== Node.ELEMENT_NODE) {
+      // Don't move modal's DOM position
+      document.body.appendChild(this);
+    }
+
+    this.style.display = "block";
+    this.removeAttribute("aria-hidden");
+    this.setAttribute("aria-modal", "true");
+
+    if ($(this._dialog).hasClass(ClassName.SCROLLABLE) && modalBody) {
+      modalBody.scrollTop = 0;
+    } else {
+      this.scrollTop = 0;
+    }
+
+    // if (transition) {
+    //   Util.reflow(this._element)
+    // }
+
+    this.classList.add(ClassName.SHOW);
+
+    // if (this._config.focus) {
+    //   this._enforceFocus()
+    // }
+
+    // const shownEvent = $.Event(Event.SHOWN, {
+    //   relatedTarget
+    // });
+
+    // const transitionComplete = () => {
+      // if (this._config.focus) {
+      //   this._element.focus()
+      // }
+      // this._isTransitioning = false
+      // $(this._element).trigger(shownEvent)
+    // };
+
+    // if (transition) {
+    //   const transitionDuration = Util.getTransitionDurationFromElement(this._dialog)
+    //
+    //   $(this._dialog)
+    //       .one(Util.TRANSITION_END, transitionComplete)
+    //       .emulateTransitionEnd(transitionDuration)
+    // } else {
+    //   transitionComplete()
+    // }
+  }
+/*
+  _enforceFocus() {
+    $(document)
+        .off(Event.FOCUSIN) // Guard against infinite focus loop
+        .on(Event.FOCUSIN, (event) => {
+          if (document !== event.target &&
+              this._element !== event.target &&
+              $(this._element).has(event.target).length === 0) {
+            this._element.focus()
+          }
+        })
+  }
+
+  _setEscapeEvent() {
+    if (this._isShown && this._config.keyboard) {
+      $(this._element).on(Event.KEYDOWN_DISMISS, (event) => {
+        if (event.which === ESCAPE_KEYCODE) {
+          this._triggerBackdropTransition()
+        }
+      })
+    } else if (!this._isShown) {
+      $(this._element).off(Event.KEYDOWN_DISMISS)
+    }
+  }
+
+  _setResizeEvent() {
+    if (this._isShown) {
+      $(window).on(Event.RESIZE, (event) => this.handleUpdate(event))
+    } else {
+      $(window).off(Event.RESIZE)
+    }
+  }
+*/
+  _hideModal():void {
+    this.style.display = "none";
+    this.setAttribute("aria-hidden", "true");
+    this.removeAttribute("aria-modal");
+    // this._isTransitioning = false;
+/*
+    this._showBackdrop(() => {
+      $(document.body).removeClass(ClassName.OPEN);
+      this._resetAdjustments();
+      this._resetScrollbar();
+      $(this._element).trigger(Event.HIDDEN)
+    })
+*/
+  }
+/*
+  _removeBackdrop() {
+    if (this._backdrop) {
+      $(this._backdrop).remove();
+      this._backdrop = null;
+    }
+  }
+
+  _showBackdrop(callback) {
+    const animate = $(this._element).hasClass(ClassName.FADE)
+        ? ClassName.FADE : ''
+
+    if (this._isShown && this._config.backdrop) {
+      this._backdrop = document.createElement('div')
+      this._backdrop.className = ClassName.BACKDROP
+
+      if (animate) {
+        this._backdrop.classList.add(animate)
+      }
+
+      $(this._backdrop).appendTo(document.body)
+
+      $(this._element).on(Event.CLICK_DISMISS, (event) => {
+        if (this._ignoreBackdropClick) {
+          this._ignoreBackdropClick = false
+          return
+        }
+        if (event.target !== event.currentTarget) {
+          return
+        }
+
+        this._triggerBackdropTransition()
+      })
+
+      if (animate) {
+        Util.reflow(this._backdrop)
+      }
+
+      $(this._backdrop).addClass(ClassName.SHOW)
+
+      if (!callback) {
+        return
+      }
+
+      if (!animate) {
+        callback()
+        return
+      }
+
+      const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)
+
+      $(this._backdrop)
+          .one(Util.TRANSITION_END, callback)
+          .emulateTransitionEnd(backdropTransitionDuration)
+    } else if (!this._isShown && this._backdrop) {
+      $(this._backdrop).removeClass(ClassName.SHOW)
+
+      const callbackRemove = () => {
+        this._removeBackdrop()
+        if (callback) {
+          callback()
+        }
+      }
+
+      if ($(this._element).hasClass(ClassName.FADE)) {
+        const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)
+
+        $(this._backdrop)
+            .one(Util.TRANSITION_END, callbackRemove)
+            .emulateTransitionEnd(backdropTransitionDuration)
+      } else {
+        callbackRemove()
+      }
+    } else if (callback) {
+      callback()
+    }
+  }
+
+  // ----------------------------------------------------------------------
+  // the following methods are used to handle overflowing modals
+  // todo (fat): these should probably be refactored out of modal.js
+  // ----------------------------------------------------------------------
+
+  _adjustDialog() {
+    const isModalOverflowing =
+        this._element.scrollHeight > document.documentElement.clientHeight
+
+    if (!this._isBodyOverflowing && isModalOverflowing) {
+      this._element.style.paddingLeft = `${this._scrollbarWidth}px`
+    }
+
+    if (this._isBodyOverflowing && !isModalOverflowing) {
+      this._element.style.paddingRight = `${this._scrollbarWidth}px`
+    }
+  }
+
+  _resetAdjustments() {
+    this._element.style.paddingLeft = ''
+    this._element.style.paddingRight = ''
+  }
+
+  _checkScrollbar() {
+    const rect = document.body.getBoundingClientRect()
+    this._isBodyOverflowing = rect.left + rect.right < window.innerWidth
+    this._scrollbarWidth = this._getScrollbarWidth()
+  }
+
+  _setScrollbar() {
+    if (this._isBodyOverflowing) {
+      // Note: DOMNode.style.paddingRight returns the actual value or '' if not set
+      //   while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set
+      const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))
+      const stickyContent = [].slice.call(document.querySelectorAll(Selector.STICKY_CONTENT))
+
+      // Adjust fixed content padding
+      $(fixedContent).each((index, element) => {
+        const actualPadding = element.style.paddingRight
+        const calculatedPadding = $(element).css('padding-right')
+        $(element)
+            .data('padding-right', actualPadding)
+            .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
+      })
+
+      // Adjust sticky content margin
+      $(stickyContent).each((index, element) => {
+        const actualMargin = element.style.marginRight
+        const calculatedMargin = $(element).css('margin-right')
+        $(element)
+            .data('margin-right', actualMargin)
+            .css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)
+      })
+
+      // Adjust body padding
+      const actualPadding = document.body.style.paddingRight
+      const calculatedPadding = $(document.body).css('padding-right')
+      $(document.body)
+          .data('padding-right', actualPadding)
+          .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
+    }
+
+    $(document.body).addClass(ClassName.OPEN)
+  }
+
+  _resetScrollbar() {
+    // Restore fixed content padding
+    const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))
+    $(fixedContent).each((index, element) => {
+      const padding = $(element).data('padding-right')
+      $(element).removeData('padding-right')
+      element.style.paddingRight = padding ? padding : ''
+    })
+
+    // Restore sticky content
+    const elements = [].slice.call(document.querySelectorAll(`${Selector.STICKY_CONTENT}`))
+    $(elements).each((index, element) => {
+      const margin = $(element).data('margin-right')
+      if (typeof margin !== 'undefined') {
+        $(element).css('margin-right', margin).removeData('margin-right')
+      }
+    })
+
+    // Restore body padding
+    const padding = $(document.body).data('padding-right')
+    $(document.body).removeData('padding-right')
+    document.body.style.paddingRight = padding ? padding : ''
+  }
+
+  _getScrollbarWidth() { // thx d.walsh
+    const scrollDiv = document.createElement('div')
+    scrollDiv.className = ClassName.SCROLLBAR_MEASURER
+    document.body.appendChild(scrollDiv)
+    const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth
+    document.body.removeChild(scrollDiv)
+    return scrollbarWidth
+  }
+
+  // Static
+
+  static _jQueryInterface(config, relatedTarget) {
+    return this.each(function () {
+      let data = $(this).data('bs.modal')
+      const _config = {
+        ...Default,
+        ...$(this).data(),
+        ...typeof config === 'object' && config ? config : {}
+      }
+
+      if (!data) {
+        data = new Modal(this, _config)
+        $(this).data('bs.modal', data)
+      }
+
+      if (typeof config === 'string') {
+        if (typeof data[config] === 'undefined') {
+          throw new TypeError(`No method named "${config}"`)
+        }
+        data[config](relatedTarget)
+      } else if (_config.show) {
+        data.show(relatedTarget)
+      }
+    })
+  }
+
+  getTransitionDuration(): number {
+    // Get transition-duration of the element
+    let transitionDuration = this.style.transitionDuration;
+    let transitionDelay = this.style.transitionDelay;
+
+    const floatTransitionDuration = parseFloat(transitionDuration);
+    const floatTransitionDelay = parseFloat(transitionDelay);
+
+    // Return 0 if element or transition duration is not found
+    if (!floatTransitionDuration && !floatTransitionDelay) {
+      return 0;
+    }
+
+    // If multiple durations are defined, take the first
+    transitionDuration = transitionDuration.split(',')[0];
+    transitionDelay = transitionDelay.split(',')[0];
+
+    return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * 1000;
+  }
+*/
+
 }
 
 document.addEventListener("DOMContentLoaded", function (event: Event): void {
@@ -55,7 +590,6 @@
 
   static execute = function (action: string, target: HTMLElement): void {
     const hidden = Collapse.findHidden(target);
-    const isPopup = target.tagName === "TOBAGO-POPUP";
     let newCollapsed;
     switch (action) {
       case "hide":
@@ -68,14 +602,14 @@
         console.error("unknown action: '" + action + "'");
     }
     if (newCollapsed) {
-      if (isPopup) {
-        BootstrapUtils.modal(target, "hide");
+      if (target instanceof Popup) {
+        target.hide();
       } else {
         target.classList.add("tobago-collapsed");
       }
     } else {
-      if (isPopup) {
-        BootstrapUtils.modal(target, "show");
+      if (target instanceof Popup) {
+        target.show();
       } else {
         target.classList.remove("tobago-collapsed");
       }
diff --git a/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/tobago-config.xml b/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/tobago-config.xml
index 27eadcd..e289b77 100644
--- a/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/tobago-config.xml
+++ b/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/tobago-config.xml
@@ -42,7 +42,6 @@
           <script name="/webjars/jquery/3.4.1/jquery.min.js"/>
           <script name="/webjars/tether/1.4.0/js/tether.min.js"/>
           <script name="/webjars/popper.js/1.14.3/umd/popper.min.js"/>
-          <script name="/webjars/bootstrap/4.3.1/js/bootstrap.min.js"/>
           <script name="/webjars/momentjs/2.24.0/min/moment-with-locales.min.js"/>
 <!--          <script name="/tobago/standard/tobago-bootstrap/${project.version}/node_modules/@babel/polyfill/dist/polyfill.js"/>-->
           <script name="/tobago/standard/tobago-bootstrap/${project.version}/js/tobago-polyfill.js"/>
@@ -59,7 +58,6 @@
           <script name="/webjars/jquery/3.4.1/jquery.js"/>
           <script name="/webjars/tether/1.4.0/js/tether.js"/>
           <script name="/webjars/popper.js/1.14.3/umd/popper.js"/>
-          <script name="/webjars/bootstrap/4.3.1/js/bootstrap.js"/>
           <script name="/webjars/momentjs/2.24.0/min/moment-with-locales.js"/>
           <script name="/tobago/standard/tobago-bootstrap/${project.version}/js/tobago-myfaces.js"/>
           <script name="/tobago/standard/tobago-bootstrap/${project.version}/js/tobago-deltaspike.js"/>