@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
JavaScript
"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);