Sylvain59650/htmlelement-contextmenu

View on GitHub
src/htmlelement-contextmenu.min.js

Summary

Maintainability
A
3 hrs
Test Coverage
window.isDef = function(value) {
  return (value !== null && typeof value !== "undefined");
}
const classNames = {
  menuItem: "cmItem",
  hover: "cmHover",
  disabled: "cmDisabled",
  visible: "cmVisible",
  notSelectable: "cmNoSelect",
  selectable: "cmSelect",
  icon: "cmIcon",
  separator: "cmSeparator"
};

const shortcutPrefixes = {
  win_msie: "Alt+", // eslint-disable-line camelcase
  win_mozilla: "Alt+Shift+", // eslint-disable-line camelcase
  win_chrome: "Alt+", // eslint-disable-line camelcase
  win_safari: "", // eslint-disable-line camelcase
  win_opera: "" // eslint-disable-line camelcase
};

/* global webBrowserDetection */
/* exported ContextMenu */
class ContextMenu {



  constructor(menuInfo) {
    this.__menuInfo = menuInfo;
    this.targets = document.querySelectorAll(menuInfo.targetId);
    this.makeContextMenu();
    document.body.appendChild(this.contextmenu);
    this.registerListeners();
    this.browser = webBrowserDetection();
    this.browserKey = this.browser.platform + "_" + this.browser.name;
  }

  menuOn() {
    var self = this;
    this.contextmenu.style.display = "block";
    this.contextmenu.selectedIndex = 0;
    document.addEventListener("click", function() {
      self.menuOff();
    }, { once: true });
    this.contextmenu.focus();
  }

  menuOff() {
    this.contextmenu.style.display = "none";
  }

  menuInfo() {
    return this.__menuInfo;
  }

  stopEvent(e) {
    e.preventDefault();
    e.stopImmediatePropagation();
  }


  moveSelected(direction) {
    var contextmenu = this.contextmenu;
    if (contextmenu.style.display === "block") {
      var menuItems = contextmenu.getElementsByClassName(classNames.selectable);
      if (contextmenu.selectedIndex !== -1) {
        menuItems[contextmenu.selectedIndex].classList.remove(classNames.hover);
      }
      contextmenu.selectedIndex += direction;
      if (contextmenu.selectedIndex === -1) {
        contextmenu.selectedIndex = menuItems.length - 1;
      } else if (contextmenu.selectedIndex >= menuItems.length) {
        contextmenu.selectedIndex = 0;
      }
      this.__setClasses(menuItems[contextmenu.selectedIndex], classNames.hover);
    }
  }

  popup(x, y) {
    let self = this;
    self.onBeforeShow();
    self.menuOn();
    self.setMenuPosition({ x: x, y: y });

  }

  registerListeners() {
    var self = this;
    window.addEventListener("resize", function() {
      self.menuOff();
    });

    this.contextmenu.addEventListener("mousedown", function(e) {
      self.stopEvent(e);
      if (e.button === 0 || e.button === 1 || e.button === 2) {
        self.menuOff();
        e.target.click();
      }
    });


    for (var i = 0; i < this.targets.length; i++) {
      var target = this.targets[i];
      target.addEventListener("contextmenu", function(e) {
        self.stopEvent(e);
        self.onBeforeShow();
        self.menuOn();
        var pos = self.getMousePosition(e);
        self.setMenuPosition(pos);
      });
    }


    this.contextmenu.addEventListener("keydown", function(e) {
      self.__keydownManagement(e);
    });
  }

  __keydownManagement(e) {
    switch (e.keyCode) {
      case 27: // ESC Key
        self.menuOff();
        break;
      case 38: // Up arrow
        self.moveSelected(-1);
        break;
      case 40: // Down arrow
        self.moveSelected(1);
        break;
      case 46:
        e.cancelBubble = true;
        self.stopEvent(e);
        break;
      case 8:
        e.cancelBubble = true;
        self.stopEvent(e);
        break;
      case 13: // Enter
        var menuItems = self.contextmenu.getElementsByClassName(classNames.selectable);
        if (self.contextmenu.selectedIndex >= 0) {
          var hovered = menuItems[self.contextmenu.selectedIndex];
          self.menuOff();
          hovered.click();
        }
        break;
    }
  }

  getMousePosition(e) {
    var posx = 0;
    var posy = 0;

    if (!e) {
      e = window.event;
    }
    if (e.pageX || e.pageY) {
      posx = e.pageX;
      posy = e.pageY;
    } else if (e.clientX || e.clientY) {
      posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
      posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
    }

    return {
      x: posx,
      y: posy
    };
  }

  setMenuPosition(pos) {
    var menuWidth = this.contextmenu.offsetWidth + 3;
    var menuHeight = this.contextmenu.offsetHeight + 3;
    var windowWidth = window.innerWidth;
    var windowHeight = window.innerHeight;
    if ((windowWidth - pos.x) < menuWidth) {
      this.contextmenu.style.left = (windowWidth - menuWidth - 3) + "px";
    } else {
      this.contextmenu.style.left = pos.x + "px";
    }
    if ((windowHeight - pos.y) < menuHeight) {
      this.contextmenu.style.top = (windowHeight - menuHeight + 3) + "px";
    } else {
      this.contextmenu.style.top = (pos.y + 3) + "px";
    }
  }


  onBeforeShow() {
    if (window.isDef(this.__menuInfo.beforeShowing)) {
      this.__menuInfo.beforeShowing();
    }
    let i = 0;
    var menuItems = this.contextmenu.getElementsByClassName(classNames.menuItem);
    for (let item of this.__menuInfo.items) {
      let show = "";
      if (item.showing) {
        show = item.showing();
      }
      this.__setClasses(menuItems[i], show);
      i++;
    }
    this.redrawItems();
  }


  insertItem(menuItem, position) {
    if (!window.isDef(position) || position >= this.__menuInfo.items.length) {
      this.__menuInfo.items.push(menuItem);
    } else {
      this.__menuInfo.items.splice(position, 0, menuItem);
    }
    this.redrawItems();
  }

  removeItem(menuItem) {
    this.__menuInfo.items = this.__menuInfo.items.filter(x => x !== menuItem.id);
  }


  redrawItems() {
    this.contextmenu.innerHTML = "";
    this.contextmenu.contentEditable = true;
    this.contextmenu.setAttribute("readonly", "readonly");
    this.contextmenu.setAttribute("spellcheck", "false");
    if (this.__menuInfo.items) {
      for (let i = 0; i < this.__menuInfo.items.length; i++) {
        this.contextmenu.appendChild(this.makeMenuItem(this.__menuInfo.items[i], i));
      }
    }
  }

  makeContextMenu() {
    this.contextmenu = document.createElement("ul");
    this.contextmenu.id = this.__menuInfo.menuId;
    this.contextmenu.classList.add("contextmenu");
    this.contextmenu.style.display = "none";
    this.redrawItems();
  }

  __setClasses(element, classes) {
    if (classes) {
      var arr = classes.split(" ");
      for (var cl of arr) {
        if (cl.indexOf("-") === 0) {
          cl = cl.substring(1);
          element.classList.remove(cl);
        } else {
          element.classList.add(cl);
        }
      }
    }
  }

  makeMenuItem(option, nth) {
    var li = document.createElement("li");
    li.classList.add(classNames.menuItem);
    if (option.label === "sep") {
      this.__setClasses(li, classNames.separator + " " + classNames.notSelectable);
    } else {
      if (option.html) {
        li.innerHTML = option.html();
      } else {
        li.innerText = option.label;
      }
      li.id = this.contextmenu.id + "-" + String(nth);
      if (option.accesskey && !option.disabled) {
        li.setAttribute("accesskey", option.accesskey);
        if (this.__menuInfo.showAccessKey) {
          let sc = document.createElement("span");
          sc.classList.add("contextmenu-shortcut");
          let tmp = shortcutPrefixes[this.browserKey];
          if (!window.isDef(tmp)) {
            tmp = "";
          }
          sc.innerText = tmp + option.accesskey.toUpperCase();
          li.appendChild(sc);
        }
      }
      this.__setClasses(li, option.class);
      // if (option.icon) {
      //   debugger;
      //   var url = "url(" + option.icon + ")";
      //   window.getComputedStyle(li, ":before").setPropertyValue("content", url);
      // }
      if (window.isDef(option.disabled) && option.disabled) {
        this.__setClasses(classNames.disabled + " -" + classNames.selectable);
      } else {
        this.__setClasses(classNames.selectable + " -" + classNames.disabled);
        if (window.isDef(option.fn) && !window.isDef(option.html)) {
          li.onclick = option.fn;
        }
        if (option.items) {
          for (let i = 0; i < option.items.length; i++) {
            let opt = option.items[i];
            let m = this.makeMenuItem(opt, i);
            li.appendChild(m);
          }
        }
      }

    }
    return li;
  }
}