diff --git a/index.js b/index.js index 5811c1a..8a6314f 100644 --- a/index.js +++ b/index.js @@ -51,15 +51,6 @@ function onHoverDebounced(target, action) { })); } -function getBounding(elem) { - var container = elem.cloneNode(true); - container.style.visibility = "hidden"; - document.body.appendChild(container); - var result = container.getBoundingClientRect(); - document.body.removeChild(container); - return result; -} - function isDisabled(item) { return getProp(item.disabled) || itemIsSubMenu(item) && 0 === item.subMenu.length; } @@ -111,14 +102,126 @@ function generateBaseItemContent(item, li) { } } -var styles = 'html{min-height:100%}.ctxmenu{position:fixed;max-height:100vh;border:1px solid #999;padding:2px 0;box-shadow:#aaa 3px 3px 3px;background:#fff;margin:0;z-index:9999;overflow-y:auto;font:15px Verdana, sans-serif}.ctxmenu li{margin:1px 0;display:block;position:relative;user-select:none}.ctxmenu li.heading{font-weight:bold;margin-left:-5px}.ctxmenu li span{display:block;padding:2px 20px;cursor:default}.ctxmenu li a{color:inherit;text-decoration:none}.ctxmenu li.icon{padding-left:15px}.ctxmenu img.icon{position:absolute;width:18px;left:10px;top:2px}.ctxmenu li.disabled{color:#ccc}.ctxmenu li.divider{border-bottom:1px solid #aaa;margin:5px 0}.ctxmenu li.interactive:hover{background:rgba(0, 0, 0, .1)}.ctxmenu li.submenu::after{content:"";position:absolute;display:block;top:0;bottom:0;right:.4em;margin:auto .1rem auto auto;border-right:1px solid #000;border-top:1px solid #000;transform:rotate(45deg);width:.3rem;height:.3rem}.ctxmenu li.submenu.disabled::after{border-color:#ccc}'; +var hdir = "r"; + +var vdir = "d"; + +function resetDirections() { + hdir = "r"; + vdir = "d"; +} + +function setPosition(container, parentOrEvent) { + var scale = getScale(); + var _a = window.visualViewport, width = _a.width, height = _a.height; + Object.assign(container.style, { + maxHeight: height / scale.y + "px", + maxWidth: width / scale.x + "px" + }); + var rect = getUnmountedBoundingRect(container); + rect.width = Math.trunc(rect.width) + 1; + rect.height = Math.trunc(rect.height) + 1; + var pos = { + x: 0, + y: 0 + }; + if (parentOrEvent instanceof Element) { + var _b = getBoundingRect(parentOrEvent), x = _b.x, width_1 = _b.width, y = _b.y; + pos = { + x: "r" === hdir ? x + width_1 : x - rect.width, + y: y + }; + if (parentOrEvent.className.includes("submenu")) pos.y += "d" === vdir ? 4 : -12; + var safePos = getPosition(rect, pos); + if (pos.x !== safePos.x) { + hdir = "r" === hdir ? "l" : "r"; + pos.x = "r" === hdir ? x + width_1 : x - rect.width; + } + if (pos.y !== safePos.y) { + vdir = "u" === vdir ? "d" : "u"; + pos.y = safePos.y; + } + pos = getPosition(rect, pos); + } else { + var hasTransform = "" !== document.body.style.transform; + var body = hasTransform ? document.body.getBoundingClientRect() : { + x: 0, + y: 0 + }; + pos = getPosition(rect, { + x: (parentOrEvent.clientX - body.x) / scale.x, + y: (parentOrEvent.clientY - body.y) / scale.y + }); + } + Object.assign(container.style, { + left: pos.x + "px", + top: pos.y + "px", + width: rect.width + "px", + height: rect.height + "px" + }); +} + +function getPosition(rect, pos) { + var _a = window.visualViewport, width = _a.width, height = _a.height; + var hasTransform = "" !== document.body.style.transform; + var _b = hasTransform ? document.body.getBoundingClientRect() : { + left: 0, + top: 0 + }, left = _b.left, top = _b.top; + var scale = getScale(); + var minX = -left / scale.x; + var minY = -top / scale.y; + var maxX = (width - left) / scale.x; + var maxY = (height - top) / scale.y; + return { + x: "r" === hdir ? pos.x + rect.width > maxX ? maxX - rect.width : pos.x : pos.x < minX ? minX : pos.x, + y: "d" === vdir ? pos.y + rect.height > maxY ? maxY - rect.height : pos.y : pos.y < minY ? minY : pos.y + }; +} + +function getUnmountedBoundingRect(elem) { + var container = elem.cloneNode(true); + container.style.visibility = "hidden"; + document.body.appendChild(container); + var result = getBoundingRect(container); + document.body.removeChild(container); + return result; +} + +function getBoundingRect(elem) { + var x = elem.offsetLeft, y = elem.offsetTop, height = elem.offsetHeight, width = elem.offsetWidth; + if (elem.offsetParent instanceof HTMLElement) { + var parent_1 = getBoundingRect(elem.offsetParent); + return { + x: x + parent_1.x, + y: y + parent_1.y, + width: width, + height: height + }; + } + return { + x: x, + y: y, + width: width, + height: height + }; +} + +function getScale() { + var body = document.body; + var rect = body.getBoundingClientRect(); + return { + x: rect.width / body.offsetWidth, + y: rect.height / body.offsetHeight + }; +} + +var styles = 'html{min-height:100%}.ctxmenu{position:fixed;border:1px solid #999;padding:2px 0;box-shadow:#aaa 3px 3px 3px;background:#fff;margin:0;z-index:9999;overflow-y:auto;font:15px Verdana, sans-serif;box-sizing:border-box}.ctxmenu li{margin:1px 0;display:block;position:relative;user-select:none}.ctxmenu li.heading{font-weight:bold;margin-left:-5px}.ctxmenu li span{display:block;padding:2px 20px;cursor:default}.ctxmenu li a{color:inherit;text-decoration:none}.ctxmenu li.icon{padding-left:15px}.ctxmenu img.icon{position:absolute;width:18px;left:10px;top:2px}.ctxmenu li.disabled{color:#ccc}.ctxmenu li.divider{border-bottom:1px solid #aaa;margin:5px 0}.ctxmenu li.interactive:hover{background:rgba(0, 0, 0, .1)}.ctxmenu li.submenu::after{content:"";position:absolute;display:block;top:0;bottom:0;right:.4em;margin:auto .1rem auto auto;border-right:1px solid #000;border-top:1px solid #000;transform:rotate(45deg);width:.3rem;height:.3rem}.ctxmenu li.submenu.disabled::after{border-color:#ccc}'; /*! ctxMenu v1.4.5 | (c) Nikolaj Kappler | https://github.com/nkappler/ctxmenu/blob/master/LICENSE !*/ var ContextMenu = function() { function ContextMenu() { var _this = this; this.cache = {}; - this.hdir = "r"; - this.vdir = "d"; this.preventCloseOnScroll = false; window.addEventListener("click", (function(ev) { var item = ev.target instanceof Element && ev.target.parentElement; @@ -219,8 +322,7 @@ var styles = 'html{min-height:100%}.ctxmenu{position:fixed;max-height:100vh;bord ContextMenu.prototype.hide = function(menu) { var _a; if (void 0 === menu) menu = this.menu; - this.hdir = "r"; - this.vdir = "d"; + resetDirections(); if (menu) { if (menu === this.menu) delete this.menu; null === (_a = menu.parentElement) || void 0 === _a ? void 0 : _a.removeChild(menu); @@ -246,34 +348,7 @@ var styles = 'html{min-height:100%}.ctxmenu{position:fixed;max-height:100vh;bord container.appendChild(li); })); container.className = "ctxmenu"; - var rect = getBounding(container); - var pos = { - x: 0, - y: 0 - }; - if (parentOrEvent instanceof Element) { - var _a = parentOrEvent.getBoundingClientRect(), left = _a.left, width = _a.width, top_1 = _a.top; - pos = { - x: "r" === this.hdir ? left + width : left - rect.width, - y: top_1 - }; - if (parentOrEvent.className.includes("submenu")) pos.y += "d" === this.vdir ? 4 : -12; - var savePos = this.getPosition(rect, pos); - if (pos.x !== savePos.x) { - this.hdir = "r" === this.hdir ? "l" : "r"; - pos.x = "r" === this.hdir ? left + width : left - rect.width; - } - if (pos.y !== savePos.y) { - this.vdir = "u" === this.vdir ? "d" : "u"; - pos.y = savePos.y; - } - pos = this.getPosition(rect, pos, false); - } else pos = this.getPosition(rect, { - x: parentOrEvent.clientX, - y: parentOrEvent.clientY - }); - container.style.left = pos.x + "px"; - container.style.top = pos.y + "px"; + setPosition(container, parentOrEvent); container.addEventListener("contextmenu", (function(ev) { ev.stopPropagation(); ev.preventDefault(); @@ -290,25 +365,6 @@ var styles = 'html{min-height:100%}.ctxmenu{position:fixed;max-height:100vh;bord if (subMenu && subMenu.parentElement !== listElement) this.hide(subMenu); listElement.appendChild(this.generateDOM(ctxMenu, listElement)); }; - ContextMenu.prototype.getPosition = function(rect, pos, addScrollOffset) { - if (void 0 === addScrollOffset) addScrollOffset = true; - var html = document.documentElement; - var width = html.clientWidth; - var height = html.clientHeight; - var hasTransform = "" !== document.body.style.transform; - var minX = hasTransform ? window.scrollX : 0; - var minY = hasTransform ? window.scrollY : 0; - var maxX = hasTransform ? width + window.scrollX : width; - var maxY = hasTransform ? height + window.scrollY : height; - if (hasTransform && addScrollOffset) { - pos.x += window.scrollX; - pos.y += window.scrollY; - } - return { - x: "r" === this.hdir ? pos.x + rect.width > maxX ? maxX - rect.width : pos.x : pos.x < minX ? minX : pos.x, - y: "d" === this.vdir ? pos.y + rect.height > maxY ? maxY - rect.height : pos.y : pos.y < minY ? minY : pos.y - }; - }; ContextMenu.addStylesToDom = function() { var append = function() { if ("loading" === document.readyState) return document.addEventListener("readystatechange", append); diff --git a/index.min.js b/index.min.js index b37ad63..e21446d 100644 --- a/index.min.js +++ b/index.min.js @@ -1,2 +1,2 @@ -"use strict";function e(){for(var e=0,t=0,n=arguments.length;t"+t(e.text)+"",o=t(e.element);o?n.append(o):n.innerHTML=i||r,n.title=t(e.tooltip)||"",e.style&&n.setAttribute("style",t(e.style));e.icon&&(n.classList.add("icon"),n.innerHTML+='')}(e,a),!n(e))return a.classList.add("heading"),a;if(d(e))return a.classList.add("disabled"),o(e)&&a.classList.add("submenu"),a;if(a.classList.add("interactive"),r(e)){var s=document.createElement("a");return s.append.apply(s,Array.from(a.childNodes)),s.href=t(e.href),e.hasOwnProperty("download")&&(s.download=t(e.download)),e.hasOwnProperty("target")&&(s.target=t(e.target)),a.append(s),a}return i(e)?(a.addEventListener("click",e.action),a):(a.classList.add("submenu"),a)}Object.defineProperty(exports,"__esModule",{value:!0});var c=function(){function i(){var e=this;this.cache={},this.hdir="r",this.vdir="d",this.preventCloseOnScroll=!1,window.addEventListener("click",(function(t){var n=t.target instanceof Element&&t.target.parentElement;n&&"interactive"===n.className||e.hide()})),window.addEventListener("resize",(function(){return e.hide()}));var t=0;window.addEventListener("wheel",(function(){clearTimeout(t),t=setTimeout((function(){e.preventCloseOnScroll?e.preventCloseOnScroll=!1:e.hide()}))}),{passive:!0}),window.addEventListener("keydown",(function(t){"Escape"===t.key&&e.hide()})),i.addStylesToDom()}return i.getInstance=function(){i.instance||(i.instance=new i);var e=i.instance;return{attach:e.attach.bind(e),delete:e.delete.bind(e),hide:e.hide.bind(e),show:e.show.bind(e),update:e.update.bind(e)}},i.prototype.attach=function(t,n,i){var r=this;void 0===i&&(i=function(e){return e});var o=document.querySelector(t);if(void 0===this.cache[t])if(o){var a=function(t){var o=i(e(n),t);r.show(o,t)};this.cache[t]={ctxMenu:n,handler:a,beforeRender:i},o.addEventListener("contextmenu",a)}else console.error("target element "+t+" not found");else console.error("target element "+t+" already has a context menu assigned. Use ContextMenu.update() intstead.")},i.prototype.update=function(e,t,n){var i=this.cache[e],r=document.querySelector(e);i&&(null==r||r.removeEventListener("contextmenu",i.handler)),delete this.cache[e],this.attach(e,t||(null==i?void 0:i.ctxMenu)||[],n||(null==i?void 0:i.beforeRender))},i.prototype.delete=function(e){var t=this.cache[e];if(t){var n=document.querySelector(e);n?(n.removeEventListener("contextmenu",t.handler),delete this.cache[e]):console.error("target element "+e+" does not exist (anymore)")}else console.error("no context menu for target element "+e+" found")},i.prototype.show=function(t,n){var i=this;n instanceof MouseEvent&&n.stopImmediatePropagation(),this.hide(),this.menu=this.generateDOM(e(t),n),document.body.appendChild(this.menu),this.menu.addEventListener("wheel",(function(){return i.preventCloseOnScroll=!0}),{passive:!0}),n instanceof MouseEvent&&n.preventDefault()},i.prototype.hide=function(e){var t;void 0===e&&(e=this.menu),this.hdir="r",this.vdir="d",e&&(e===this.menu&&delete this.menu,null===(t=e.parentElement)||void 0===t||t.removeChild(e))},i.prototype.generateDOM=function(e,i){var r=this,c=document.createElement("ul");0===e.length&&(c.style.display="none"),e.forEach((function(e){var i=s(e);a(i,(function(){var e,t=null===(e=i.parentElement)||void 0===e?void 0:e.querySelector("ul");t&&t.parentElement!==i&&r.hide(t)})),n(e)&&!d(e)&&(o(e)?a(i,(function(n){i.querySelector("ul")||r.openSubMenu(n,t(e.subMenu),i)})):i.addEventListener("click",(function(){return r.hide()}))),c.appendChild(i)})),c.className="ctxmenu";var l=function(e){var t=e.cloneNode(!0);t.style.visibility="hidden",document.body.appendChild(t);var n=t.getBoundingClientRect();return document.body.removeChild(t),n}(c),u={x:0,y:0};if(i instanceof Element){var h=i.getBoundingClientRect(),p=h.left,m=h.width,f=h.top;u={x:"r"===this.hdir?p+m:p-l.width,y:f},i.className.includes("submenu")&&(u.y+="d"===this.vdir?4:-12);var v=this.getPosition(l,u);u.x!==v.x&&(this.hdir="r"===this.hdir?"l":"r",u.x="r"===this.hdir?p+m:p-l.width),u.y!==v.y&&(this.vdir="u"===this.vdir?"d":"u",u.y=v.y),u=this.getPosition(l,u,!1)}else u=this.getPosition(l,{x:i.clientX,y:i.clientY});return c.style.left=u.x+"px",c.style.top=u.y+"px",c.addEventListener("contextmenu",(function(e){e.stopPropagation(),e.preventDefault()})),c.addEventListener("click",(function(e){var t=e.target instanceof Element&&e.target.parentElement;t&&"interactive"!==t.className&&e.stopPropagation()})),c},i.prototype.openSubMenu=function(e,t,n){var i,r=null===(i=n.parentElement)||void 0===i?void 0:i.querySelector("li > ul");r&&r.parentElement!==n&&this.hide(r),n.appendChild(this.generateDOM(t,n))},i.prototype.getPosition=function(e,t,n){void 0===n&&(n=!0);var i=document.documentElement,r=i.clientWidth,o=i.clientHeight,a=""!==document.body.style.transform,d=a?window.scrollX:0,s=a?window.scrollY:0,c=a?r+window.scrollX:r,l=a?o+window.scrollY:o;return a&&n&&(t.x+=window.scrollX,t.y+=window.scrollY),{x:"r"===this.hdir?t.x+e.width>c?c-e.width:t.x:t.xl?l-e.height:t.y:t.y"+t(e.text)+"",o=t(e.element);o?n.append(o):n.innerHTML=i||r,n.title=t(e.tooltip)||"",e.style&&n.setAttribute("style",t(e.style));e.icon&&(n.classList.add("icon"),n.innerHTML+='')}(e,a),!n(e))return a.classList.add("heading"),a;if(d(e))return a.classList.add("disabled"),o(e)&&a.classList.add("submenu"),a;if(a.classList.add("interactive"),r(e)){var c=document.createElement("a");return c.append.apply(c,Array.from(a.childNodes)),c.href=t(e.href),e.hasOwnProperty("download")&&(c.download=t(e.download)),e.hasOwnProperty("target")&&(c.target=t(e.target)),a.append(c),a}return i(e)?(a.addEventListener("click",e.action),a):(a.classList.add("submenu"),a)}Object.defineProperty(exports,"__esModule",{value:!0});var s="r",u="d";function l(e,t){var n=p(),i=window.visualViewport,r=i.width,o=i.height;Object.assign(e.style,{maxHeight:o/n.y+"px",maxWidth:r/n.x+"px"});var a=function(e){var t=e.cloneNode(!0);t.style.visibility="hidden",document.body.appendChild(t);var n=f(t);return document.body.removeChild(t),n}(e);a.width=Math.trunc(a.width)+1,a.height=Math.trunc(a.height)+1;var d={x:0,y:0};if(t instanceof Element){var c=f(t),l=c.x,m=c.width,v=c.y;d={x:"r"===s?l+m:l-a.width,y:v},t.className.includes("submenu")&&(d.y+="d"===u?4:-12);var y=h(a,d);d.x!==y.x&&(s="r"===s?"l":"r",d.x="r"===s?l+m:l-a.width),d.y!==y.y&&(u="u"===u?"d":"u",d.y=y.y),d=h(a,d)}else{var x=""!==document.body.style.transform?document.body.getBoundingClientRect():{x:0,y:0};d=h(a,{x:(t.clientX-x.x)/n.x,y:(t.clientY-x.y)/n.y})}Object.assign(e.style,{left:d.x+"px",top:d.y+"px",width:a.width+"px",height:a.height+"px"})}function h(e,t){var n=window.visualViewport,i=n.width,r=n.height,o=""!==document.body.style.transform?document.body.getBoundingClientRect():{left:0,top:0},a=o.left,d=o.top,c=p(),l=-a/c.x,h=-d/c.y,f=(i-a)/c.x,m=(r-d)/c.y;return{x:"r"===s?t.x+e.width>f?f-e.width:t.x:t.xm?m-e.height:t.y:t.y ul");r&&r.parentElement!==n&&this.hide(r),n.appendChild(this.generateDOM(t,n))},i.addStylesToDom=function(){var e=function(){if("loading"===document.readyState)return document.addEventListener("readystatechange",e);var t=document.createElement("style");t.innerHTML='html{min-height:100%}.ctxmenu{position:fixed;border:1px solid #999;padding:2px 0;box-shadow:#aaa 3px 3px 3px;background:#fff;margin:0;z-index:9999;overflow-y:auto;font:15px Verdana, sans-serif;box-sizing:border-box}.ctxmenu li{margin:1px 0;display:block;position:relative;user-select:none}.ctxmenu li.heading{font-weight:bold;margin-left:-5px}.ctxmenu li span{display:block;padding:2px 20px;cursor:default}.ctxmenu li a{color:inherit;text-decoration:none}.ctxmenu li.icon{padding-left:15px}.ctxmenu img.icon{position:absolute;width:18px;left:10px;top:2px}.ctxmenu li.disabled{color:#ccc}.ctxmenu li.divider{border-bottom:1px solid #aaa;margin:5px 0}.ctxmenu li.interactive:hover{background:rgba(0, 0, 0, .1)}.ctxmenu li.submenu::after{content:"";position:absolute;display:block;top:0;bottom:0;right:.4em;margin:auto .1rem auto auto;border-right:1px solid #000;border-top:1px solid #000;transform:rotate(45deg);width:.3rem;height:.3rem}.ctxmenu li.submenu.disabled::after{border-color:#ccc}',document.head.insertBefore(t,document.head.childNodes[0]),e=function(){}};e()},i}().getInstance(); +/*! ctxMenu v1.4.5 | (c) Nikolaj Kappler | https://github.com/nkappler/ctxmenu/blob/master/LICENSE !*/exports.ctxmenu=m; diff --git a/src/ctxmenu.ts b/src/ctxmenu.ts index ac5799d..7d389b4 100644 --- a/src/ctxmenu.ts +++ b/src/ctxmenu.ts @@ -1,7 +1,8 @@ /*! ctxMenu v1.4.5 | (c) Nikolaj Kappler | https://github.com/nkappler/ctxmenu/blob/master/LICENSE !*/ -import { generateMenuItem, getBounding, isDisabled, onHoverDebounced } from "./elementFactory"; +import { generateMenuItem, isDisabled, onHoverDebounced } from "./elementFactory"; import type { BeforeRenderFN, CTXMenu, CTXMenuSingleton } from "./interfaces"; +import { resetDirections, setPosition } from "./position"; //@ts-ignore file will only be present after first run of npm run build import { styles } from "./styles"; import { getProp, itemIsInteractive, itemIsSubMenu } from "./typeguards"; @@ -16,18 +17,11 @@ interface CTXCache { } | undefined; } -interface Pos { - x: number; - y: number; -} - class ContextMenu implements CTXMenuSingleton { private static instance: ContextMenu; private menu: HTMLUListElement | undefined; private cache: CTXCache = {}; - private hdir: "r" | "l" = "r"; - private vdir: "u" | "d" = "d"; - /** + /** * used to track if wheel events originated from the ctx menu. * in that case we don't want to close the menu. (#28) */ @@ -137,9 +131,7 @@ class ContextMenu implements CTXMenuSingleton { } public hide(menu: Element | undefined = this.menu) { - //reset directions - this.hdir = "r"; - this.vdir = "d"; + resetDirections(); if (menu) { if (menu === this.menu) { @@ -181,37 +173,8 @@ class ContextMenu implements CTXMenuSingleton { container.appendChild(li); }); container.className = "ctxmenu"; + setPosition(container, parentOrEvent); - const rect = getBounding(container); - let pos = { x: 0, y: 0 }; - if (parentOrEvent instanceof Element) { - const { left, width, top } = parentOrEvent.getBoundingClientRect(); - pos = { - x: this.hdir === "r" ? left + width : left - rect.width, - y: top - }; - if (/* is submenu */ parentOrEvent.className.includes("submenu")) { - pos.y += (this.vdir === "d" ? 4 : -12) // add 8px vertical submenu offset: -4px means no vertical movement with default styles - } - const savePos = this.getPosition(rect, pos); - // change direction when reaching edge of screen - if (pos.x !== savePos.x) { - this.hdir = this.hdir === "r" ? "l" : "r"; - pos.x = this.hdir === "r" ? left + width : left - rect.width; - } - if (pos.y !== savePos.y) { - this.vdir = this.vdir === "u" ? "d" : "u"; - pos.y = savePos.y - } - /* on very tiny screens, the submenu may overlap the parent menu, - * so we recalculate the position again, but without adding the offset again */ - pos = this.getPosition(rect, pos, false); - } else { - pos = this.getPosition(rect, { x: parentOrEvent.clientX, y: parentOrEvent.clientY }); - } - - container.style.left = pos.x + "px"; - container.style.top = pos.y + "px"; container.addEventListener("contextmenu", ev => { ev.stopPropagation(); ev.preventDefault(); @@ -234,35 +197,6 @@ class ContextMenu implements CTXMenuSingleton { listElement.appendChild(this.generateDOM(ctxMenu, listElement)); } - /** returns a save position inside the viewport, given the desired position */ - private getPosition(rect: DOMRect, pos: Pos, addScrollOffset: boolean = true): Pos { - /* https://github.com/nkappler/ctxmenu/issues/31 - * When body has a transform applied, `position: fixed` behaves differently. - * We can fix it by adding the scroll offset of the window to the viewport dimensions - * and to the desired position */ - const html = document.documentElement; - const width = html.clientWidth; - const height = html.clientHeight; - const hasTransform = document.body.style.transform !== ""; - const minX = hasTransform ? window.scrollX : 0; - const minY = hasTransform ? window.scrollY : 0; - const maxX = hasTransform ? width + window.scrollX : width; - const maxY = hasTransform ? height + window.scrollY : height; - if (hasTransform && addScrollOffset) { - pos.x += window.scrollX; - pos.y += window.scrollY; - } - - return { - x: this.hdir === "r" - ? pos.x + rect.width > maxX ? maxX - rect.width : pos.x - : pos.x < minX ? minX : pos.x, - y: this.vdir === "d" - ? pos.y + rect.height > maxY ? maxY - rect.height : pos.y - : pos.y < minY ? minY : pos.y - }; - } - private static addStylesToDom() { let append = () => { if (document.readyState === "loading") { @@ -281,4 +215,3 @@ class ContextMenu implements CTXMenuSingleton { export const ctxmenu: CTXMenuSingleton = ContextMenu.getInstance(); export * from "./interfaces"; - diff --git a/src/elementFactory.ts b/src/elementFactory.ts index f3af8f4..11b7927 100644 --- a/src/elementFactory.ts +++ b/src/elementFactory.ts @@ -16,15 +16,6 @@ export function onHoverDebounced(target: HTMLLIElement, action: (e: MouseEvent) target.addEventListener("mouseleave", () => clearTimeout(timeout)); } -export function getBounding(elem: HTMLElement): DOMRect { - const container = elem.cloneNode(true) as HTMLElement; - container.style.visibility = "hidden"; - document.body.appendChild(container); - const result = container.getBoundingClientRect(); - document.body.removeChild(container); - return result; -} - /** checks if an item is disabled * * will be true if disabled flag is set or it has an empty submenu diff --git a/src/position.ts b/src/position.ts new file mode 100644 index 0000000..c0273e1 --- /dev/null +++ b/src/position.ts @@ -0,0 +1,132 @@ +interface Point { + x: number; + y: number; +} + +interface Rect extends Point { + width: number, + height: number, +}; + +let hdir: "r" | "l" = "r"; +let vdir: "u" | "d" = "d"; + +export function resetDirections() { + hdir = "r"; + vdir = "d"; +}; + +export function setPosition(container: HTMLUListElement, parentOrEvent: HTMLElement | MouseEvent): void { + // restrict menu size to viewport size + const scale = getScale(); + const { width, height } = window.visualViewport; + Object.assign(container.style, { + maxHeight: (height / scale.y) + "px", + maxWidth: (width / scale.x) + "px", + }); + + const rect = getUnmountedBoundingRect(container); + + // round up to full integer width/height for pixel perfect rendering + rect.width = Math.trunc(rect.width) + 1; + rect.height = Math.trunc(rect.height) + 1; + + let pos = { x: 0, y: 0 }; + if (parentOrEvent instanceof Element) { + const { x, width, y } = getBoundingRect(parentOrEvent); + pos = { + x: hdir === "r" ? x + width : x - rect.width, + y + }; + if (/* is submenu */ parentOrEvent.className.includes("submenu")) { + pos.y += (vdir === "d" ? 4 : -12) // add 8px vertical submenu offset: -4px means no vertical movement with default styles + } + const safePos = getPosition(rect, pos); + // change direction when reaching edge of screen + if (pos.x !== safePos.x) { + hdir = hdir === "r" ? "l" : "r"; + pos.x = hdir === "r" ? x + width : x - rect.width; + } + if (pos.y !== safePos.y) { + vdir = vdir === "u" ? "d" : "u"; + pos.y = safePos.y + } + /* on very tiny screens, the submenu may need to overlap the parent menu, + * so we recalculate the position again*/ + pos = getPosition(rect, pos); + } else { + const hasTransform = document.body.style.transform !== ""; + const body: Point = hasTransform ? document.body.getBoundingClientRect() : { x: 0, y: 0 }; + pos = getPosition(rect, { + x: (parentOrEvent.clientX - body.x) / scale.x, + y: (parentOrEvent.clientY - body.y) / scale.y + }); + } + + Object.assign(container.style, { + left: pos.x + "px", + top: pos.y + "px", + width: rect.width + "px", + height: rect.height + "px" + }); + +} + +/** returns a safe position inside the viewport, given the desired position */ +function getPosition(rect: Rect, pos: Point): Point { + const { width, height } = window.visualViewport; + const hasTransform = document.body.style.transform !== ""; + const { left, top } = hasTransform ? document.body.getBoundingClientRect() : { left: 0, top: 0 }; + const scale = getScale(); + const minX = -left / scale.x; + const minY = -top / scale.y; + const maxX = (width - left) / scale.x; + const maxY = (height - top) / scale.y; + + return { + x: hdir === "r" + ? pos.x + rect.width > maxX ? maxX - rect.width : pos.x + : pos.x < minX ? minX : pos.x, + y: vdir === "d" + ? pos.y + rect.height > maxY ? maxY - rect.height : pos.y + : pos.y < minY ? minY : pos.y + }; +} + +function getUnmountedBoundingRect(elem: HTMLElement): Rect { + const container = elem.cloneNode(true) as HTMLElement; + container.style.visibility = "hidden"; + document.body.appendChild(container); + const result = getBoundingRect(container); + document.body.removeChild(container); + return result; +} + +function getBoundingRect(elem: HTMLElement): Rect { + const { offsetLeft: x, offsetTop: y, offsetHeight: height, offsetWidth: width } = elem; + if (elem.offsetParent instanceof HTMLElement) { + // This isn't too bad for performance, but it would be nice if we could get rid of the recursiveness + const parent = getBoundingRect(elem.offsetParent); + return { + x: x + parent.x, + y: y + parent.y, + width: width, + height: height + } + } + return { + x, + y, + width, + height + }; +} + +function getScale(): Point { + const body = document.body; + const rect = body.getBoundingClientRect(); + return { + x: rect.width / body.offsetWidth, + y: rect.height / body.offsetHeight + }; +} diff --git a/src/styles.css b/src/styles.css index fec813c..c09ed20 100644 --- a/src/styles.css +++ b/src/styles.css @@ -4,7 +4,6 @@ html { .ctxmenu { position: fixed; - max-height: 100vh; border: 1px solid #999; padding: 2px 0; box-shadow: #aaa 3px 3px 3px; @@ -13,6 +12,7 @@ html { z-index: 9999; overflow-y: auto; font: 15px Verdana, sans-serif; + box-sizing: border-box; } .ctxmenu li { diff --git a/standalone/ctxmenu.js b/standalone/ctxmenu.js index 7a133d4..5014ceb 100644 --- a/standalone/ctxmenu.js +++ b/standalone/ctxmenu.js @@ -38,14 +38,6 @@ return clearTimeout(timeout); })); } - function getBounding(elem) { - var container = elem.cloneNode(true); - container.style.visibility = "hidden"; - document.body.appendChild(container); - var result = container.getBoundingClientRect(); - document.body.removeChild(container); - return result; - } function isDisabled(item) { return getProp(item.disabled) || itemIsSubMenu(item) && 0 === item.subMenu.length; } @@ -94,13 +86,117 @@ li.innerHTML += ''; } } - var styles = 'html{min-height:100%}.ctxmenu{position:fixed;max-height:100vh;border:1px solid #999;padding:2px 0;box-shadow:#aaa 3px 3px 3px;background:#fff;margin:0;z-index:9999;overflow-y:auto;font:15px Verdana, sans-serif}.ctxmenu li{margin:1px 0;display:block;position:relative;user-select:none}.ctxmenu li.heading{font-weight:bold;margin-left:-5px}.ctxmenu li span{display:block;padding:2px 20px;cursor:default}.ctxmenu li a{color:inherit;text-decoration:none}.ctxmenu li.icon{padding-left:15px}.ctxmenu img.icon{position:absolute;width:18px;left:10px;top:2px}.ctxmenu li.disabled{color:#ccc}.ctxmenu li.divider{border-bottom:1px solid #aaa;margin:5px 0}.ctxmenu li.interactive:hover{background:rgba(0, 0, 0, .1)}.ctxmenu li.submenu::after{content:"";position:absolute;display:block;top:0;bottom:0;right:.4em;margin:auto .1rem auto auto;border-right:1px solid #000;border-top:1px solid #000;transform:rotate(45deg);width:.3rem;height:.3rem}.ctxmenu li.submenu.disabled::after{border-color:#ccc}'; + var hdir = "r"; + var vdir = "d"; + function resetDirections() { + hdir = "r"; + vdir = "d"; + } + function setPosition(container, parentOrEvent) { + var scale = getScale(); + var _a = window.visualViewport, width = _a.width, height = _a.height; + Object.assign(container.style, { + maxHeight: height / scale.y + "px", + maxWidth: width / scale.x + "px" + }); + var rect = getUnmountedBoundingRect(container); + rect.width = Math.trunc(rect.width) + 1; + rect.height = Math.trunc(rect.height) + 1; + var pos = { + x: 0, + y: 0 + }; + if (parentOrEvent instanceof Element) { + var _b = getBoundingRect(parentOrEvent), x = _b.x, width_1 = _b.width, y = _b.y; + pos = { + x: "r" === hdir ? x + width_1 : x - rect.width, + y: y + }; + if (parentOrEvent.className.includes("submenu")) pos.y += "d" === vdir ? 4 : -12; + var safePos = getPosition(rect, pos); + if (pos.x !== safePos.x) { + hdir = "r" === hdir ? "l" : "r"; + pos.x = "r" === hdir ? x + width_1 : x - rect.width; + } + if (pos.y !== safePos.y) { + vdir = "u" === vdir ? "d" : "u"; + pos.y = safePos.y; + } + pos = getPosition(rect, pos); + } else { + var hasTransform = "" !== document.body.style.transform; + var body = hasTransform ? document.body.getBoundingClientRect() : { + x: 0, + y: 0 + }; + pos = getPosition(rect, { + x: (parentOrEvent.clientX - body.x) / scale.x, + y: (parentOrEvent.clientY - body.y) / scale.y + }); + } + Object.assign(container.style, { + left: pos.x + "px", + top: pos.y + "px", + width: rect.width + "px", + height: rect.height + "px" + }); + } + function getPosition(rect, pos) { + var _a = window.visualViewport, width = _a.width, height = _a.height; + var hasTransform = "" !== document.body.style.transform; + var _b = hasTransform ? document.body.getBoundingClientRect() : { + left: 0, + top: 0 + }, left = _b.left, top = _b.top; + var scale = getScale(); + var minX = -left / scale.x; + var minY = -top / scale.y; + var maxX = (width - left) / scale.x; + var maxY = (height - top) / scale.y; + return { + x: "r" === hdir ? pos.x + rect.width > maxX ? maxX - rect.width : pos.x : pos.x < minX ? minX : pos.x, + y: "d" === vdir ? pos.y + rect.height > maxY ? maxY - rect.height : pos.y : pos.y < minY ? minY : pos.y + }; + } + function getUnmountedBoundingRect(elem) { + var container = elem.cloneNode(true); + container.style.visibility = "hidden"; + document.body.appendChild(container); + var result = getBoundingRect(container); + document.body.removeChild(container); + return result; + } + function getBoundingRect(elem) { + var x = elem.offsetLeft, y = elem.offsetTop, height = elem.offsetHeight, width = elem.offsetWidth; + if (elem.offsetParent instanceof HTMLElement) { + var parent_1 = getBoundingRect(elem.offsetParent); + return { + x: x + parent_1.x, + y: y + parent_1.y, + width: width, + height: height + }; + } + return { + x: x, + y: y, + width: width, + height: height + }; + } + function getScale() { + var body = document.body; + var rect = body.getBoundingClientRect(); + return { + x: rect.width / body.offsetWidth, + y: rect.height / body.offsetHeight + }; + } + var styles = 'html{min-height:100%}.ctxmenu{position:fixed;border:1px solid #999;padding:2px 0;box-shadow:#aaa 3px 3px 3px;background:#fff;margin:0;z-index:9999;overflow-y:auto;font:15px Verdana, sans-serif;box-sizing:border-box}.ctxmenu li{margin:1px 0;display:block;position:relative;user-select:none}.ctxmenu li.heading{font-weight:bold;margin-left:-5px}.ctxmenu li span{display:block;padding:2px 20px;cursor:default}.ctxmenu li a{color:inherit;text-decoration:none}.ctxmenu li.icon{padding-left:15px}.ctxmenu img.icon{position:absolute;width:18px;left:10px;top:2px}.ctxmenu li.disabled{color:#ccc}.ctxmenu li.divider{border-bottom:1px solid #aaa;margin:5px 0}.ctxmenu li.interactive:hover{background:rgba(0, 0, 0, .1)}.ctxmenu li.submenu::after{content:"";position:absolute;display:block;top:0;bottom:0;right:.4em;margin:auto .1rem auto auto;border-right:1px solid #000;border-top:1px solid #000;transform:rotate(45deg);width:.3rem;height:.3rem}.ctxmenu li.submenu.disabled::after{border-color:#ccc}'; /*! ctxMenu v1.4.5 | (c) Nikolaj Kappler | https://github.com/nkappler/ctxmenu/blob/master/LICENSE !*/ var ContextMenu = function() { function ContextMenu() { var _this = this; this.cache = {}; - this.hdir = "r"; - this.vdir = "d"; this.preventCloseOnScroll = false; window.addEventListener("click", (function(ev) { var item = ev.target instanceof Element && ev.target.parentElement; @@ -201,8 +297,7 @@ ContextMenu.prototype.hide = function(menu) { var _a; if (void 0 === menu) menu = this.menu; - this.hdir = "r"; - this.vdir = "d"; + resetDirections(); if (menu) { if (menu === this.menu) delete this.menu; null === (_a = menu.parentElement) || void 0 === _a ? void 0 : _a.removeChild(menu); @@ -228,34 +323,7 @@ container.appendChild(li); })); container.className = "ctxmenu"; - var rect = getBounding(container); - var pos = { - x: 0, - y: 0 - }; - if (parentOrEvent instanceof Element) { - var _a = parentOrEvent.getBoundingClientRect(), left = _a.left, width = _a.width, top_1 = _a.top; - pos = { - x: "r" === this.hdir ? left + width : left - rect.width, - y: top_1 - }; - if (parentOrEvent.className.includes("submenu")) pos.y += "d" === this.vdir ? 4 : -12; - var savePos = this.getPosition(rect, pos); - if (pos.x !== savePos.x) { - this.hdir = "r" === this.hdir ? "l" : "r"; - pos.x = "r" === this.hdir ? left + width : left - rect.width; - } - if (pos.y !== savePos.y) { - this.vdir = "u" === this.vdir ? "d" : "u"; - pos.y = savePos.y; - } - pos = this.getPosition(rect, pos, false); - } else pos = this.getPosition(rect, { - x: parentOrEvent.clientX, - y: parentOrEvent.clientY - }); - container.style.left = pos.x + "px"; - container.style.top = pos.y + "px"; + setPosition(container, parentOrEvent); container.addEventListener("contextmenu", (function(ev) { ev.stopPropagation(); ev.preventDefault(); @@ -272,25 +340,6 @@ if (subMenu && subMenu.parentElement !== listElement) this.hide(subMenu); listElement.appendChild(this.generateDOM(ctxMenu, listElement)); }; - ContextMenu.prototype.getPosition = function(rect, pos, addScrollOffset) { - if (void 0 === addScrollOffset) addScrollOffset = true; - var html = document.documentElement; - var width = html.clientWidth; - var height = html.clientHeight; - var hasTransform = "" !== document.body.style.transform; - var minX = hasTransform ? window.scrollX : 0; - var minY = hasTransform ? window.scrollY : 0; - var maxX = hasTransform ? width + window.scrollX : width; - var maxY = hasTransform ? height + window.scrollY : height; - if (hasTransform && addScrollOffset) { - pos.x += window.scrollX; - pos.y += window.scrollY; - } - return { - x: "r" === this.hdir ? pos.x + rect.width > maxX ? maxX - rect.width : pos.x : pos.x < minX ? minX : pos.x, - y: "d" === this.vdir ? pos.y + rect.height > maxY ? maxY - rect.height : pos.y : pos.y < minY ? minY : pos.y - }; - }; ContextMenu.addStylesToDom = function() { var append = function() { if ("loading" === document.readyState) return document.addEventListener("readystatechange", append); diff --git a/standalone/ctxmenu.min.js b/standalone/ctxmenu.min.js index a2ffa8d..d8223da 100644 --- a/standalone/ctxmenu.min.js +++ b/standalone/ctxmenu.min.js @@ -1,2 +1,2 @@ -!function(){"use strict";function e(){for(var e=0,t=0,n=arguments.length;t"+t(e.text)+"",o=t(e.element);o?n.append(o):n.innerHTML=i||r,n.title=t(e.tooltip)||"",e.style&&n.setAttribute("style",t(e.style));e.icon&&(n.classList.add("icon"),n.innerHTML+='')}(e,a),!n(e))return a.classList.add("heading"),a;if(d(e))return a.classList.add("disabled"),o(e)&&a.classList.add("submenu"),a;if(a.classList.add("interactive"),r(e)){var s=document.createElement("a");return s.append.apply(s,Array.from(a.childNodes)),s.href=t(e.href),e.hasOwnProperty("download")&&(s.download=t(e.download)),e.hasOwnProperty("target")&&(s.target=t(e.target)),a.append(s),a}return i(e)?(a.addEventListener("click",e.action),a):(a.classList.add("submenu"),a)}var c=function(){function i(){var e=this;this.cache={},this.hdir="r",this.vdir="d",this.preventCloseOnScroll=!1,window.addEventListener("click",(function(t){var n=t.target instanceof Element&&t.target.parentElement;n&&"interactive"===n.className||e.hide()})),window.addEventListener("resize",(function(){return e.hide()}));var t=0;window.addEventListener("wheel",(function(){clearTimeout(t),t=setTimeout((function(){e.preventCloseOnScroll?e.preventCloseOnScroll=!1:e.hide()}))}),{passive:!0}),window.addEventListener("keydown",(function(t){"Escape"===t.key&&e.hide()})),i.addStylesToDom()}return i.getInstance=function(){i.instance||(i.instance=new i);var e=i.instance;return{attach:e.attach.bind(e),delete:e.delete.bind(e),hide:e.hide.bind(e),show:e.show.bind(e),update:e.update.bind(e)}},i.prototype.attach=function(t,n,i){var r=this;void 0===i&&(i=function(e){return e});var o=document.querySelector(t);if(void 0===this.cache[t])if(o){var a=function(t){var o=i(e(n),t);r.show(o,t)};this.cache[t]={ctxMenu:n,handler:a,beforeRender:i},o.addEventListener("contextmenu",a)}else console.error("target element "+t+" not found");else console.error("target element "+t+" already has a context menu assigned. Use ContextMenu.update() intstead.")},i.prototype.update=function(e,t,n){var i=this.cache[e],r=document.querySelector(e);i&&(null==r||r.removeEventListener("contextmenu",i.handler)),delete this.cache[e],this.attach(e,t||(null==i?void 0:i.ctxMenu)||[],n||(null==i?void 0:i.beforeRender))},i.prototype.delete=function(e){var t=this.cache[e];if(t){var n=document.querySelector(e);n?(n.removeEventListener("contextmenu",t.handler),delete this.cache[e]):console.error("target element "+e+" does not exist (anymore)")}else console.error("no context menu for target element "+e+" found")},i.prototype.show=function(t,n){var i=this;n instanceof MouseEvent&&n.stopImmediatePropagation(),this.hide(),this.menu=this.generateDOM(e(t),n),document.body.appendChild(this.menu),this.menu.addEventListener("wheel",(function(){return i.preventCloseOnScroll=!0}),{passive:!0}),n instanceof MouseEvent&&n.preventDefault()},i.prototype.hide=function(e){var t;void 0===e&&(e=this.menu),this.hdir="r",this.vdir="d",e&&(e===this.menu&&delete this.menu,null===(t=e.parentElement)||void 0===t||t.removeChild(e))},i.prototype.generateDOM=function(e,i){var r=this,c=document.createElement("ul");0===e.length&&(c.style.display="none"),e.forEach((function(e){var i=s(e);a(i,(function(){var e,t=null===(e=i.parentElement)||void 0===e?void 0:e.querySelector("ul");t&&t.parentElement!==i&&r.hide(t)})),n(e)&&!d(e)&&(o(e)?a(i,(function(n){i.querySelector("ul")||r.openSubMenu(n,t(e.subMenu),i)})):i.addEventListener("click",(function(){return r.hide()}))),c.appendChild(i)})),c.className="ctxmenu";var l=function(e){var t=e.cloneNode(!0);t.style.visibility="hidden",document.body.appendChild(t);var n=t.getBoundingClientRect();return document.body.removeChild(t),n}(c),u={x:0,y:0};if(i instanceof Element){var h=i.getBoundingClientRect(),m=h.left,p=h.width,f=h.top;u={x:"r"===this.hdir?m+p:m-l.width,y:f},i.className.includes("submenu")&&(u.y+="d"===this.vdir?4:-12);var v=this.getPosition(l,u);u.x!==v.x&&(this.hdir="r"===this.hdir?"l":"r",u.x="r"===this.hdir?m+p:m-l.width),u.y!==v.y&&(this.vdir="u"===this.vdir?"d":"u",u.y=v.y),u=this.getPosition(l,u,!1)}else u=this.getPosition(l,{x:i.clientX,y:i.clientY});return c.style.left=u.x+"px",c.style.top=u.y+"px",c.addEventListener("contextmenu",(function(e){e.stopPropagation(),e.preventDefault()})),c.addEventListener("click",(function(e){var t=e.target instanceof Element&&e.target.parentElement;t&&"interactive"!==t.className&&e.stopPropagation()})),c},i.prototype.openSubMenu=function(e,t,n){var i,r=null===(i=n.parentElement)||void 0===i?void 0:i.querySelector("li > ul");r&&r.parentElement!==n&&this.hide(r),n.appendChild(this.generateDOM(t,n))},i.prototype.getPosition=function(e,t,n){void 0===n&&(n=!0);var i=document.documentElement,r=i.clientWidth,o=i.clientHeight,a=""!==document.body.style.transform,d=a?window.scrollX:0,s=a?window.scrollY:0,c=a?r+window.scrollX:r,l=a?o+window.scrollY:o;return a&&n&&(t.x+=window.scrollX,t.y+=window.scrollY),{x:"r"===this.hdir?t.x+e.width>c?c-e.width:t.x:t.xl?l-e.height:t.y:t.y"+t(e.text)+"",o=t(e.element);o?n.append(o):n.innerHTML=i||r,n.title=t(e.tooltip)||"",e.style&&n.setAttribute("style",t(e.style));e.icon&&(n.classList.add("icon"),n.innerHTML+='')}(e,a),!n(e))return a.classList.add("heading"),a;if(d(e))return a.classList.add("disabled"),o(e)&&a.classList.add("submenu"),a;if(a.classList.add("interactive"),r(e)){var c=document.createElement("a");return c.append.apply(c,Array.from(a.childNodes)),c.href=t(e.href),e.hasOwnProperty("download")&&(c.download=t(e.download)),e.hasOwnProperty("target")&&(c.target=t(e.target)),a.append(c),a}return i(e)?(a.addEventListener("click",e.action),a):(a.classList.add("submenu"),a)}var s="r",u="d";function l(e,t){var n=p(),i=window.visualViewport,r=i.width,o=i.height;Object.assign(e.style,{maxHeight:o/n.y+"px",maxWidth:r/n.x+"px"});var a=function(e){var t=e.cloneNode(!0);t.style.visibility="hidden",document.body.appendChild(t);var n=f(t);return document.body.removeChild(t),n}(e);a.width=Math.trunc(a.width)+1,a.height=Math.trunc(a.height)+1;var d={x:0,y:0};if(t instanceof Element){var c=f(t),l=c.x,m=c.width,v=c.y;d={x:"r"===s?l+m:l-a.width,y:v},t.className.includes("submenu")&&(d.y+="d"===u?4:-12);var y=h(a,d);d.x!==y.x&&(s="r"===s?"l":"r",d.x="r"===s?l+m:l-a.width),d.y!==y.y&&(u="u"===u?"d":"u",d.y=y.y),d=h(a,d)}else{var x=""!==document.body.style.transform?document.body.getBoundingClientRect():{x:0,y:0};d=h(a,{x:(t.clientX-x.x)/n.x,y:(t.clientY-x.y)/n.y})}Object.assign(e.style,{left:d.x+"px",top:d.y+"px",width:a.width+"px",height:a.height+"px"})}function h(e,t){var n=window.visualViewport,i=n.width,r=n.height,o=""!==document.body.style.transform?document.body.getBoundingClientRect():{left:0,top:0},a=o.left,d=o.top,c=p(),l=-a/c.x,h=-d/c.y,f=(i-a)/c.x,m=(r-d)/c.y;return{x:"r"===s?t.x+e.width>f?f-e.width:t.x:t.xm?m-e.height:t.y:t.y ul");r&&r.parentElement!==n&&this.hide(r),n.appendChild(this.generateDOM(t,n))},i.addStylesToDom=function(){var e=function(){if("loading"===document.readyState)return document.addEventListener("readystatechange",e);var t=document.createElement("style");t.innerHTML='html{min-height:100%}.ctxmenu{position:fixed;border:1px solid #999;padding:2px 0;box-shadow:#aaa 3px 3px 3px;background:#fff;margin:0;z-index:9999;overflow-y:auto;font:15px Verdana, sans-serif;box-sizing:border-box}.ctxmenu li{margin:1px 0;display:block;position:relative;user-select:none}.ctxmenu li.heading{font-weight:bold;margin-left:-5px}.ctxmenu li span{display:block;padding:2px 20px;cursor:default}.ctxmenu li a{color:inherit;text-decoration:none}.ctxmenu li.icon{padding-left:15px}.ctxmenu img.icon{position:absolute;width:18px;left:10px;top:2px}.ctxmenu li.disabled{color:#ccc}.ctxmenu li.divider{border-bottom:1px solid #aaa;margin:5px 0}.ctxmenu li.interactive:hover{background:rgba(0, 0, 0, .1)}.ctxmenu li.submenu::after{content:"";position:absolute;display:block;top:0;bottom:0;right:.4em;margin:auto .1rem auto auto;border-right:1px solid #000;border-top:1px solid #000;transform:rotate(45deg);width:.3rem;height:.3rem}.ctxmenu li.submenu.disabled::after{border-color:#ccc}',document.head.insertBefore(t,document.head.childNodes[0]),e=function(){}};e()},i}().getInstance(); +/*! ctxMenu v1.4.5 | (c) Nikolaj Kappler | https://github.com/nkappler/ctxmenu/blob/master/LICENSE !*/window.ctxmenu=m}();