UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in

663 lines (662 loc) • 41.4 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return privateMap.get(receiver); }; var _onClick; exports.__esModule = true; exports.NeedleMenuElement = exports.NeedleMenu = void 0; var engine_license_js_1 = require("../../engine_license.js"); var engine_networking_utils_js_1 = require("../../engine_networking_utils.js"); var engine_utils_js_1 = require("../../engine_utils.js"); var events_js_1 = require("../../xr/events.js"); var buttons_js_1 = require("../buttons.js"); var fonts_js_1 = require("../fonts.js"); var icons_js_1 = require("../icons.js"); var logo_element_js_1 = require("../logo-element.js"); var needle_menu_spatial_js_1 = require("./needle-menu-spatial.js"); var elementName = "needle-menu"; var debug = engine_utils_js_1.getParam("debugmenu"); var debugNonCommercial = engine_utils_js_1.getParam("debugnoncommercial"); /** * The NeedleMenu is a menu that can be displayed in the needle engine webcomponent or in VR/AR sessions. * The menu can be used to add buttons to the needle engine that can be used to interact with the application. * The menu can be positioned at the top or the bottom of the needle engine webcomponent * * @example Create a button using the NeedleMenu * ```typescript * onStart(ctx => { * ctx.menu.appendChild({ * label: "Open Google", * icon: "google", * onClick: () => { window.open("https://www.google.com", "_blank") } * }); * }) * ``` * * Buttons can be added to the menu using the {@link NeedleMenu#appendChild} method or by sending a postMessage event to the needle engine with the type "needle:menu". Use the {@link NeedleMenuPostMessageModel} model to create buttons with postMessage. * @example Create a button using a postmessage * ```javascript * window.postMessage({ * type: "needle:menu", * button: { * label: "Open Google", * icon: "google", * onclick: "https://www.google.com", * target: "_blank", * } * }, "*"); * ``` */ var NeedleMenu = /** @class */ (function () { function NeedleMenu(context) { var _this = this; this.onPostMessage = function (e) { // lets just allow the same origin for now if (e.origin !== globalThis.location.origin) return; if (typeof e.data === "object") { var data = e.data; var type = data.type; if (type === "needle:menu") { var buttoninfo_1 = data.button; if (buttoninfo_1) { if (!buttoninfo_1.label) return console.error("NeedleMenu: buttoninfo.label is required"); if (!buttoninfo_1.onclick) return console.error("NeedleMenu: buttoninfo.onclick is required"); var button = document.createElement("button"); button.textContent = buttoninfo_1.label; if (buttoninfo_1.icon) { var icon = icons_js_1.getIconElement(buttoninfo_1.icon); button.prepend(icon); } if (buttoninfo_1.priority) { button.setAttribute("priority", buttoninfo_1.priority.toString()); } button.onclick = function () { if (buttoninfo_1.onclick) { var isLink = buttoninfo_1.onclick.startsWith("http") || buttoninfo_1.onclick.startsWith("www."); var target = buttoninfo_1.target || "_blank"; if (isLink) { globalThis.open(buttoninfo_1.onclick, target); } else console.error("NeedleMenu: onclick is not a valid link", buttoninfo_1.onclick); } }; _this._menu.appendChild(button); } else if (debug) console.error("NeedleMenu: unknown postMessage event", data); } else if (debug) console.warn("NeedleMenu: unknown postMessage type", type, data); } }; this.onStartXR = function (args) { if (args.session.isScreenBasedAR) { _this._menu["previousParent"] = _this._menu.parentNode; _this._context.arOverlayElement.appendChild(_this._menu); args.session.session.addEventListener("end", _this.onExitXR); } }; this.onExitXR = function () { if (_this._menu["previousParent"]) { _this._menu["previousParent"].appendChild(_this._menu); delete _this._menu["previousParent"]; } }; this._menu = NeedleMenuElement.getOrCreate(context.domElement, context); this._context = context; this._spatialMenu = new needle_menu_spatial_js_1.NeedleSpatialMenu(context, this._menu); window.addEventListener("message", this.onPostMessage); events_js_1.onXRSessionStart(this.onStartXR); } /** @ignore internal method */ NeedleMenu.prototype.onDestroy = function () { window.removeEventListener("message", this.onPostMessage); this._menu.remove(); this._spatialMenu.onDestroy(); }; /** Experimental: Change the menu position to be at the top or the bottom of the needle engine webcomponent * @param position "top" or "bottom" */ NeedleMenu.prototype.setPosition = function (position) { this._menu.setPosition(position); }; /** * Call to show or hide the menu. * NOTE: Hiding the menu is a PRO feature and requires a needle engine license. Hiding the menu will not work in production without a license. */ NeedleMenu.prototype.setVisible = function (visible) { this._menu.setVisible(visible); }; /** When set to false, the Needle Engine logo will be hidden. Hiding the logo requires a needle engine license */ NeedleMenu.prototype.showNeedleLogo = function (visible) { var _a; this._menu.showNeedleLogo(visible); (_a = this._spatialMenu) === null || _a === void 0 ? void 0 : _a.showNeedleLogo(visible); // setTimeout(()=>this.showNeedleLogo(!visible), 1000); }; /** When enabled=true the menu will be visible in VR/AR sessions */ NeedleMenu.prototype.showSpatialMenu = function (enabled) { this._spatialMenu.setEnabled(enabled); }; /** * Call to add or remove a button to the menu to show a QR code for the current page * If enabled=true then a button will be added to the menu that will show a QR code for the current page when clicked. */ NeedleMenu.prototype.showQRCodeButton = function (enabled) { if (enabled === "desktop-only") { enabled = !engine_utils_js_1.isMobileDevice(); } if (!enabled) { var button = buttons_js_1.ButtonsFactory.getOrCreate().qrButton; if (button) button.style.display = "none"; return button !== null && button !== void 0 ? button : null; } else { var button = buttons_js_1.ButtonsFactory.getOrCreate().createQRCode(); button.style.display = ""; this._menu.appendChild(button); return button; } }; /** Call to add or remove a button to the menu to mute or unmute the application * Clicking the button will mute or unmute the application */ NeedleMenu.prototype.showAudioPlaybackOption = function (visible) { var _a; if (!visible) { (_a = this._muteButton) === null || _a === void 0 ? void 0 : _a.remove(); return; } this._muteButton = buttons_js_1.ButtonsFactory.getOrCreate().createMuteButton(this._context); this._muteButton.setAttribute("priority", "100"); this._menu.appendChild(this._muteButton); }; NeedleMenu.prototype.showFullscreenOption = function (visible) { var _a; if (!visible) { (_a = this._fullscreenButton) === null || _a === void 0 ? void 0 : _a.remove(); return; } this._fullscreenButton = buttons_js_1.ButtonsFactory.getOrCreate().createFullscreenButton(this._context); if (this._fullscreenButton) { this._fullscreenButton.setAttribute("priority", "150"); this._menu.appendChild(this._fullscreenButton); } }; NeedleMenu.prototype.appendChild = function (child) { return this._menu.appendChild(child); }; return NeedleMenu; }()); exports.NeedleMenu = NeedleMenu; var NeedleMenuElement = /** @class */ (function (_super) { __extends(NeedleMenuElement, _super); function NeedleMenuElement() { var _a, _b, _c, _d, _e, _f; var _this = _super.call(this) || this; _this._domElement = null; _this._context = null; _onClick.set(_this, function (e) { // detect a click outside the opened foldout to automatically close it if (!e.defaultPrevented && e.target == _this._domElement && (e instanceof PointerEvent && e.button === 0) && _this.root.classList.contains("open")) { // The menu is open, it's a click outside the foldout? var rect = _this.foldout.getBoundingClientRect(); var pointerEvent = e; if (!(pointerEvent.clientX > rect.left && pointerEvent.clientX < rect.right && pointerEvent.clientY > rect.top && pointerEvent.clientY < rect.bottom)) { _this.root.classList.toggle("open", false); } } }); _this._userRequestedLogoVisible = undefined; _this._userRequestedMenuVisible = undefined; _this._isHandlingChange = false; _this._didSort = new Map(); _this._lastAvailableWidthChange = 0; _this._timeoutHandle = 0; _this.handleSizeChange = function (_evt, forceOrEvent) { if (!_this._domElement) return; var width = _this._domElement.clientWidth; if (width < 500) { clearTimeout(_this._timeoutHandle); _this.root.classList.add("compact"); _this.foldout.classList.add("floating-panel-style"); return; } var padding = 20 * 2; var availableWidth = width - padding; // if the available width has not changed significantly then we can skip the rest if (!forceOrEvent && Math.abs(availableWidth - _this._lastAvailableWidthChange) < 1) return; _this._lastAvailableWidthChange = availableWidth; clearTimeout(_this._timeoutHandle); _this._timeoutHandle = setTimeout(function () { var spaceLeft = getSpaceLeft(); if (spaceLeft < 0) { _this.root.classList.add("compact"); _this.foldout.classList.add("floating-panel-style"); } else if (spaceLeft > 0) { _this.root.classList.remove("compact"); _this.foldout.classList.remove("floating-panel-style"); if (getSpaceLeft() < 0) { // ensure we still have enough space left _this.root.classList.add("compact"); _this.foldout.classList.add("floating-panel-style"); } } }, 5); var getCurrentWidth = function () { return _this.options.clientWidth + _this.logoContainer.clientWidth; }; var getSpaceLeft = function () { return availableWidth - getCurrentWidth(); }; }; var template = document.createElement('template'); // TODO: make host full size again and move the buttons to a wrapper so that we can later easily open e.g. foldouts/dropdowns / use the whole canvas space template.innerHTML = "<style>\n\n #root {\n position: absolute;\n width: auto;\n max-width: 95%;\n left: 50%;\n transform: translateX(-50%);\n top: min(20px, 10vh);\n padding: 0.3rem;\n display: flex;\n visibility: visible;\n flex-direction: row-reverse; /* if we overflow this should be right aligned so the logo is always visible */\n pointer-events: all;\n z-index: 1000;\n }\n\n /** hide the menu if it's empty **/\n #root.has-no-options.logo-hidden {\n display: none; \n }\n\n /** using a div here because then we can change the class for placement **/\n #root.bottom {\n top: auto;\n bottom: min(30px, 10vh);\n }\n \n .wrapper {\n position: relative;\n display: flex;\n flex-direction: row;\n justify-content: center;\n align-items: stretch;\n gap: 0px;\n padding: 0 .3rem;\n }\n\n .wrapper > *, .options > button, ::slotted(*) {\n position: relative;\n border: none;\n border-radius: 0;\n outline: 1px solid rgba(0,0,0,0);\n display: flex;\n justify-content: center;\n align-items: center;\n max-height: 2.3rem;\n\n /** basic font settings for all entries **/\n font-size: 1rem;\n font-family: 'Roboto Flex', sans-serif;\n font-optical-sizing: auto;\n font-weight: 500;\n font-weight: 200;\n font-variation-settings: \"wdth\" 100;\n color: rgb(20,20,20);\n }\n\n .floating-panel-style {\n background: rgba(255, 255, 255, .4);\n outline: rgb(0 0 0 / 5%) 1px solid;\n border: 1px solid rgba(255, 255, 255, .1);\n box-shadow: 0px 7px 0.5rem 0px rgb(0 0 0 / 6%), inset 0px 0px 1.3rem rgba(0,0,0,.05);\n border-radius: 1.1999999999999993rem;\n /** \n * to make nested background filter work \n * https://stackoverflow.com/questions/60997948/backdrop-filter-not-working-for-nested-elements-in-chrome \n **/\n &::before {\n content: '';\n position: absolute;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n z-index: -1;\n border-radius: 1.1999999999999993rem;\n -webkit-backdrop-filter: blur(8px);\n backdrop-filter: blur(8px);\n }\n }\n\n a {\n color: inherit;\n text-decoration: none;\n }\n\n .options {\n display: flex;\n flex-direction: row;\n }\n\n .options > button, ::slotted(button) {\n height: 2.25rem;\n padding: .4rem .5rem;\n }\n \n :host .options > button, ::slotted(*) {\n background: transparent;\n border: none;\n white-space: nowrap;\n transition: all 0.1s linear .02s;\n border-radius: 0.8rem;\n user-select: none;\n }\n :host .options > *:hover, ::slotted(*:hover) {\n cursor: pointer;\n color: black;\n background: rgba(245, 245, 245, .8);\n box-shadow: inset 0 0 1rem rgba(0,0,30,.2);\n outline: rgba(0,0,0,.1) 1px solid;\n }\n :host .options > *:active, ::slotted(*:active) {\n background: rgba(255, 255, 255, .8);\n box-shadow: inset 0px 1px 1px rgba(255,255,255,.5), inset 0 0 2rem rgba(0,0,30,.2), inset 0px 2px 4px rgba(0,0,20,.5);\n transition: all 0.05s linear;\n }\n :host .options > *:focus, ::slotted(*:focus) {\n outline: rgba(255,255,255,.5) 1px solid;\n }\n\n :host .options > *:disabled, ::slotted(*:disabled) {\n background: rgba(0,0,0,.05);\n color: rgba(60,60,60,.7);\n pointer-events: none;\n }\n\n button, ::slotted(button) {\n gap: 0.3rem;\n }\n\n /** XR button animation **/\n :host button.this-mode-is-requested {\n background: repeating-linear-gradient(to right, #fff 0%, #fff 40%, #aaffff 55%, #fff 80%);\n background-size: 200% auto;\n background-position: 0 100%;\n animation: AnimationName .7s ease infinite forwards;\n }\n :host button.other-mode-is-requested {\n opacity: .5;\n }\n \n @keyframes AnimationName {\n 0% { background-position: 0% 0 }\n 100% { background-position: -200% 0 }\n }\n\n\n\n\n .logo {\n cursor: pointer;\n padding-left: 0.6rem;\n }\n .logo-hidden {\n .logo {\n display: none;\n }\n }\n :host .has-options .logo {\n border-left: 1px solid rgba(40,40,40,.4);\n margin-left: 0.3rem;\n }\n\n .logo > span {\n white-space: nowrap;\n }\n\n\n\n /** COMPACT */\n\n /** Hide the menu button normally **/\n .compact-menu-button { display: none; }\n /** And show it when we're in compact mode **/\n .compact .compact-menu-button {\n display: block;\n background: none;\n border: none;\n border-radius: 2rem;\n\n margin: 0;\n padding: 0 .3rem;\n padding-top: .2rem;\n\n color: #000;\n\n &:hover {\n background: rgba(255,255,255,.2);\n cursor: pointer;\n }\n &:focus {\n outline: 1px solid rgba(255,255,255,.5);\n }\n } \n .has-no-options .compact-menu-button {\n display: none;\n }\n .open .compact-menu-button {\n background: rgba(255,255,255,.2);\n }\n .logo-visible .compact-menu-button { \n margin-left: .2rem;\n }\n \n /** Open and hide menu **/\n .compact .foldout { \n display: none;\n }\n .open .options, .open .foldout {\n display: flex;\n }\n .compact .wrapper {\n padding: 0;\n }\n .compact .wrapper, .compact .options {\n height: auto;\n max-height: initial;\n flex-direction: row;\n gap: .12rem;\n }\n .compact .options { \n flex-wrap: wrap;\n gap: .3rem;\n }\n .compact .top .options {\n height: auto;\n flex-direction: row;\n }\n .compact .bottom .wrapper {\n height: auto;\n flex-direction: column;\n }\n\n .compact .foldout {\n max-height: min(100ch, calc(100vh - 100px));\n overflow: auto;\n overflow-x: hidden;\n align-items: center;\n\n position: fixed;\n bottom: calc(100% + 5px);\n z-index: 100;\n width: auto;\n left: .2rem;\n right: .2rem;\n padding: .2rem;\n\n }\n .compact.logo-hidden .foldout {\n /** for when there's no logo we want to center the foldout **/\n min-width: 24ch;\n margin-left: 50%;\n transform: translateX(calc(-50% - .2rem));\n }\n \n .compact.top .foldout {\n top: calc(100% + 5px);\n bottom: auto;\n }\n\n ::-webkit-scrollbar {\n max-width: 7px;\n background: rgba(100,100,100,.2);\n border-radius: .2rem;\n }\n ::-webkit-scrollbar-thumb {\n background: rgba(255, 255, 255, .3);\n border-radius: .2rem;\n }\n ::-webkit-scrollbar-thumb:hover {\n background: rgb(150,150,150);\n }\n\n .compact .options > * {\n font-size: 1.2rem;\n padding: .6rem .5rem;\n }\n .compact.has-options .logo {\n border: none;\n padding-left: 0;\n margin-left: 1rem;\n margin-bottom: .02rem;\n }\n .compact .options > button {\n display: flex;\n flex-basis: 100%;\n min-height: 3rem;\n }\n .compact .options > button.row2 {\n //border: 1px solid red !important;\n display: flex;\n flex: 1;\n flex-basis: 30%;\n }\n\n /** If there's really not enough space then just hide all options **/\n @media (max-width: 100px) or (max-height: 100px){\n .foldout {\n display: none !important;\n }\n .compact-menu-button {\n display: none !important;\n }\n }\n \n /* dark mode */\n /*\n @media (prefers-color-scheme: dark) {\n :host {\n background: rgba(0,0,0, .6);\n }\n :host button {\n color: rgba(200,200,200);\n }\n :host button:hover {\n background: rgba(100,100,100, .8);\n }\n }\n */\n\n </style>\n \n <div id=\"root\" class=\"logo-visible floating-panel-style bottom\">\n <div class=\"wrapper\">\n <div class=\"foldout\">\n <div class=\"options\" part=\"options\">\n <slot></slot>\n </div>\n <div class=\"options\" part=\"options\">\n <slot name=\"end\"></slot>\n </div>\n </div>\n <div style=\"user-select:none\" class=\"logo\">\n <span class=\"madewith notranslate\">powered by</span>\n </div>\n </div>\n <button class=\"compact-menu-button\"></button>\n </div>\n "; // we dont need to expose the shadow root var shadow = _this.attachShadow({ mode: 'open' }); // we need to add the icons to both the shadow dom as well as the HEAD to work // https://github.com/google/material-design-icons/issues/1165 fonts_js_1.ensureFonts(); fonts_js_1.loadFont(fonts_js_1.iconFontUrl, { loadedCallback: function () { _this.handleSizeChange(); } }); fonts_js_1.loadFont(fonts_js_1.iconFontUrl, { element: shadow }); var content = template.content.cloneNode(true); shadow === null || shadow === void 0 ? void 0 : shadow.appendChild(content); _this.root = shadow.querySelector("#root"); _this.wrapper = (_a = _this.root) === null || _a === void 0 ? void 0 : _a.querySelector(".wrapper"); _this.options = (_b = _this.root) === null || _b === void 0 ? void 0 : _b.querySelector(".options"); _this.logoContainer = (_c = _this.root) === null || _c === void 0 ? void 0 : _c.querySelector(".logo"); _this.compactMenuButton = (_d = _this.root) === null || _d === void 0 ? void 0 : _d.querySelector(".compact-menu-button"); _this.compactMenuButton.append(icons_js_1.getIconElement("more_vert")); _this.foldout = (_e = _this.root) === null || _e === void 0 ? void 0 : _e.querySelector(".foldout"); (_f = _this.root) === null || _f === void 0 ? void 0 : _f.appendChild(_this.wrapper); _this.wrapper.classList.add("wrapper"); var logo = logo_element_js_1.NeedleLogoElement.create(); logo.style.minHeight = "1rem"; _this.logoContainer.append(logo); _this.logoContainer.addEventListener("click", function () { globalThis.open("https://needle.tools", "_blank"); }); // if the user has a license then we CAN hide the needle logo engine_license_js_1.onLicenseCheckResultChanged(function (res) { if (res == true && engine_license_js_1.hasCommercialLicense() && !debugNonCommercial) { var visible = _this._userRequestedLogoVisible; if (visible === undefined) visible = false; _this..call(_this, visible); } }); _this.compactMenuButton.addEventListener("click", function (evt) { evt.preventDefault(); _this.root.classList.toggle("open"); }); var context = _this._context; // we need to assign it in the timeout because the reference is set *after* the constructor did run setTimeout(function () { return context = _this._context; }); // watch changes var changeEventCounter = 0; var forceVisible = function (parent, visible) { var _a, _b, _c; if (debug) console.log("Set menu visible", visible); if ((context === null || context === void 0 ? void 0 : context.isInAR) && context.arOverlayElement) { if (parent != context.arOverlayElement) { context.arOverlayElement.appendChild(_this); } } else if (_this.parentNode != ((_a = _this._domElement) === null || _a === void 0 ? void 0 : _a.shadowRoot)) (_c = (_b = _this._domElement) === null || _b === void 0 ? void 0 : _b.shadowRoot) === null || _c === void 0 ? void 0 : _c.appendChild(_this); _this.style.display = visible ? "flex" : "none"; _this.style.visibility = "visible"; _this.style.opacity = "1"; }; var isHandlingMutation = false; var rootObserver = new MutationObserver(function (mutations) { var _a; if (isHandlingMutation) { return; } try { isHandlingMutation = true; _this.onChangeDetected(mutations); // ensure the menu is not hidden or removed var requiredParent_1 = _this === null || _this === void 0 ? void 0 : _this.parentNode; if (_this.style.display != "flex" || _this.style.visibility != "visible" || _this.style.opacity != "1" || requiredParent_1 != ((_a = _this._domElement) === null || _a === void 0 ? void 0 : _a.shadowRoot)) { if (!engine_license_js_1.hasCommercialLicense()) { var change = changeEventCounter++; // if a user doesn't have a local pro license *but* for development the menu is hidden then we show a warning if (engine_networking_utils_js_1.isLocalNetwork() && _this._userRequestedMenuVisible === false) { // set visible once so that the check above is not triggered again if (change === 0) { // if the user requested visible to false before this code is called for the first time we want to respect the choice just in this case forceVisible(requiredParent_1, _this._userRequestedMenuVisible); } // warn only once if (change === 1) { console.warn("Needle Menu Warning: You need a PRO license to hide the Needle Engine menu \u2192 The menu will be visible in your deployed website if you don't have a PRO license. See https://needle.tools/pricing for details."); } } else { if (change === 0) { forceVisible(requiredParent_1, true); } else { setTimeout(function () { return forceVisible(requiredParent_1, true); }, 5); } } } } } finally { isHandlingMutation = false; } }); rootObserver.observe(_this.root, { childList: true, subtree: true, attributes: true }); if (debug) { _this.___insertDebugOptions(); } return _this; } NeedleMenuElement.create = function () { // https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement#is return document.createElement(elementName, { is: elementName }); }; NeedleMenuElement.getOrCreate = function (domElement, context) { var element = domElement.querySelector(elementName); if (!element && domElement.shadowRoot) { element = domElement.shadowRoot.querySelector(elementName); } if (!element) { element = NeedleMenuElement.create(); if (domElement.shadowRoot) domElement.shadowRoot.appendChild(element); else domElement.appendChild(element); } element._domElement = domElement; element._context = context; return element; }; NeedleMenuElement.prototype.connectedCallback = function () { var _this = this; window.addEventListener("resize", this.handleSizeChange); this.handleMenuVisible(); this._sizeChangeInterval = setInterval(function () { return _this.handleSizeChange(undefined, true); }, 5000); // the dom element is set after the constructor runs setTimeout(function () { var _a, _b; (_a = _this._domElement) === null || _a === void 0 ? void 0 : _a.addEventListener("resize", _this.handleSizeChange); (_b = _this._domElement) === null || _b === void 0 ? void 0 : _b.addEventListener("click", __classPrivateFieldGet(_this, _onClick)); }, 1); }; NeedleMenuElement.prototype.disconnectedCallback = function () { var _a, _b; window.removeEventListener("resize", this.handleSizeChange); clearInterval(this._sizeChangeInterval); (_a = this._domElement) === null || _a === void 0 ? void 0 : _a.removeEventListener("resize", this.handleSizeChange); (_b = this._context) === null || _b === void 0 ? void 0 : _b.domElement.removeEventListener("click", __classPrivateFieldGet(this, _onClick)); }; NeedleMenuElement.prototype.showNeedleLogo = function (visible) { this._userRequestedLogoVisible = visible; if (!visible) { if (!engine_license_js_1.hasCommercialLicense() || debugNonCommercial) { console.warn("Needle Menu: You need a PRO license to hide the Needle Engine logo."); var localNetwork = engine_networking_utils_js_1.isLocalNetwork(); if (!localNetwork) return; } } this..call(this, visible); }; NeedleMenuElement.prototype. = function (visible) { this.logoContainer.style.display = ""; this.logoContainer.style.opacity = "1"; this.logoContainer.style.visibility = "visible"; if (visible) { this.root.classList.remove("logo-hidden"); this.root.classList.add("logo-visible"); } else { this.root.classList.remove("logo-visible"); this.root.classList.add("logo-hidden"); } }; NeedleMenuElement.prototype.setPosition = function (position) { // ensure the position is of a known type: if (position !== "top" && position !== "bottom") { return console.error("NeedleMenu.setPosition: invalid position", position); } this.root.classList.remove("top", "bottom"); this.root.classList.add(position); }; NeedleMenuElement.prototype.setVisible = function (visible) { this._userRequestedMenuVisible = visible; this.style.display = visible ? "flex" : "none"; }; NeedleMenuElement.prototype.append = function () { var nodes = []; for (var _i = 0; _i < arguments.length; _i++) { nodes[_i] = arguments[_i]; } for (var _a = 0, nodes_1 = nodes; _a < nodes_1.length; _a++) { var node = nodes_1[_a]; if (typeof node === "string") { var element = document.createTextNode(node); this.options.appendChild(element); } else { this.options.appendChild(node); } } }; NeedleMenuElement.prototype.appendChild = function (node) { var _a, _b; if (!(node instanceof Node)) { var button = document.createElement("button"); button.textContent = node.label; button.onclick = node.onClick; button.setAttribute("priority", (_b = (_a = node.priority) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "0"); if (node.icon) { var icon = icons_js_1.getIconElement(node.icon); if (node.iconSide === "right") { button.appendChild(icon); } else { button.prepend(icon); } } if (node["class"]) { button.classList.add(node["class"]); } node = button; } var res = this.options.appendChild(node); return res; }; NeedleMenuElement.prototype.prepend = function () { var nodes = []; for (var _i = 0; _i < arguments.length; _i++) { nodes[_i] = arguments[_i]; } for (var _a = 0, nodes_2 = nodes; _a < nodes_2.length; _a++) { var node = nodes_2[_a]; if (typeof node === "string") { var element = document.createTextNode(node); this.options.prepend(element); } else { this.options.prepend(node); } } }; /** Called when any change in the web component is detected (including in children and child attributes) */ NeedleMenuElement.prototype.onChangeDetected = function (_mut) { if (this._isHandlingChange) return; this._isHandlingChange = true; try { // if (debug) console.log("NeedleMenu.onChangeDetected", _mut); this.handleMenuVisible(); for (var _i = 0, _mut_1 = _mut; _i < _mut_1.length; _i++) { var mut = _mut_1[_i]; if (mut.target == this.options) { this.onOptionsChildrenChanged(mut); } } } finally { this._isHandlingChange = false; } }; NeedleMenuElement.prototype.onOptionsChildrenChanged = function (_mut) { this.root.classList.toggle("has-options", this.hasAnyVisibleOptions); this.root.classList.toggle("has-no-options", !this.hasAnyVisibleOptions); this.handleSizeChange(undefined, true); if (_mut.type === "childList") { var needsSorting = false; var now = Date.now(); // sort children by priority only when necessary for (var i = 0; i < _mut.addedNodes.length; i++) { var child = _mut.addedNodes[i]; var lastTime = this._didSort.get(child); if (typeof lastTime === "number" && now - lastTime < 100) continue; this._didSort.set(child, now); needsSorting = true; } if (needsSorting) { var children = Array.from(this.options.children); children.sort(function (a, b) { var p1 = parseInt(a.getAttribute("priority") || "0"); var p2 = parseInt(b.getAttribute("priority") || "0"); return p1 - p2; }); for (var _i = 0, children_1 = children; _i < children_1.length; _i++) { var child = children_1[_i]; this.options.appendChild(child); } } } }; /** checks if the menu has any content and should be rendered at all * if we dont have any content and logo then we hide the menu */ NeedleMenuElement.prototype.handleMenuVisible = function () { if (debug) console.log("Update VisibleState: Any Content?", this.hasAnyContent); if (this.hasAnyContent) { this.root.style.display = ""; } else { this.root.style.display = "none"; } this.root.classList.toggle("has-options", this.hasAnyVisibleOptions); this.root.classList.toggle("has-no-options", !this.hasAnyVisibleOptions); }; Object.defineProperty(NeedleMenuElement.prototype, "hasAnyContent", { /** @returns true if we have any content OR a logo */ get: function () { // is the logo visible? if (this.logoContainer.style.display != "none") return true; if (this.hasAnyVisibleOptions) return true; return false; }, enumerable: false, configurable: true }); Object.defineProperty(NeedleMenuElement.prototype, "hasAnyVisibleOptions", { get: function () { // do we have any visible buttons? for (var i = 0; i < this.options.children.length; i++) { var child = this.options.children[i]; // is slot? if (child.tagName === "SLOT") { var slotElement = child; var nodes = slotElement.assignedNodes(); for (var _i = 0, nodes_3 = nodes; _i < nodes_3.length; _i++) { var node = nodes_3[_i]; if (node instanceof HTMLElement) { if (node.style.display != "none") return true; } } } else if (child.style.display != "none") return true; } return false; }, enumerable: false, configurable: true }); NeedleMenuElement.prototype.___insertDebugOptions = function () { var _this = this; window.addEventListener("keydown", function (e) { if (e.key === "p") { _this.setPosition(_this.root.classList.contains("top") ? "bottom" : "top"); } }); var removeOptionsButton = document.createElement("button"); removeOptionsButton.textContent = "Hide Buttons"; removeOptionsButton.onclick = function () { var optionsChildren = new Array(_this.options.children.length); for (var i = 0; i < _this.options.children.length; i++) { optionsChildren[i] = _this.options.children[i]; } for (var _i = 0, optionsChildren_1 = optionsChildren; _i < optionsChildren_1.length; _i++) { var child = optionsChildren_1[_i]; _this.options.removeChild(child); } setTimeout(function () { for (var _i = 0, optionsChildren_2 = optionsChildren; _i < optionsChildren_2.length; _i++) { var child = optionsChildren_2[_i]; _this.options.appendChild(child); } }, 1000); }; this.appendChild(removeOptionsButton); var anotherButton = document.createElement("button"); anotherButton.textContent = "Toggle Logo"; anotherButton.addEventListener("click", function () { _this.logoContainer.style.display = _this.logoContainer.style.display === "none" ? "" : "none"; }); this.appendChild(anotherButton); }; return NeedleMenuElement; }(HTMLElement)); exports.NeedleMenuElement = NeedleMenuElement; _onClick = new WeakMap(); if (!customElements.get(elementName)) customElements.define(elementName, NeedleMenuElement);