/*! elementor-pro - v3.29.0 - 04-06-2025 */
"use strict";
(self["webpackChunkelementor_pro"] = self["webpackChunkelementor_pro"] || []).push([["mega-menu"],{
/***/ "../assets/dev/js/frontend/utils/anchor-link.js":
/*!******************************************************!*\
!*** ../assets/dev/js/frontend/utils/anchor-link.js ***!
\******************************************************/
/***/ ((__unused_webpack_module, exports) => {
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports["default"] = void 0;
class AnchorLinks {
observer = null;
constructor($anchorLinks, classes) {
this.$anchorLinks = $anchorLinks;
this.activeAnchorClass = classes.activeAnchorItem;
this.anchorClass = classes.anchorItem;
}
getViewportHeight() {
return window.innerHeight;
}
bindEvents() {
this.onResize = this.onResize.bind(this);
window.addEventListener('resize', this.onResize);
}
initialize() {
this.viewPortHeight = this.getViewportHeight();
this.followMenuAnchors();
this.bindEvents();
}
followMenuAnchors() {
this.$anchorLinks.each((index, anchorLink) => {
if (location.pathname === anchorLink.pathname && '' !== anchorLink.hash) {
this.followMenuAnchor(jQuery(anchorLink));
}
});
}
followMenuAnchor($element) {
const $targetElement = $element.hasClass(this.anchorClass) ? $element : $element.closest(`.${this.anchorClass}`);
const anchorElement = this.getAnchorElement($element);
if (!anchorElement) {
return;
}
const options = this.getObserverOptions(anchorElement);
this.observer = this.createObserver($targetElement, $element, options);
this.observer.observe(anchorElement);
}
getAnchorElement($element) {
const anchorSelector = $element[0].hash;
try {
// `decodeURIComponent` for UTF8 characters in the hash.
const decodedSelector = decodeURIComponent(anchorSelector);
return document.querySelector(decodedSelector);
} catch (e) {
return null;
}
}
getObserverOptions(element) {
return {
root: null,
rootMargin: this.calculateRootMargin(element)
};
}
calculateRootMargin(element) {
const anchorHeight = element?.offsetHeight || 0;
const isAnchorHeightLargerThanHalfViewport = anchorHeight > this.viewPortHeight / 2;
const rootMarginBlockEnd = -1 * this.viewPortHeight / 2;
const rootMarginBlockStart = isAnchorHeightLargerThanHalfViewport ? rootMarginBlockEnd : 0;
return `${rootMarginBlockStart}px 0px ${rootMarginBlockEnd}px 0px`;
}
createObserver($targetElement, $element, options) {
return new IntersectionObserver(entries => {
entries.forEach(entry => {
$targetElement.toggleClass(this.activeAnchorClass, entry.isIntersecting);
$element.attr('aria-current', entry.isIntersecting ? 'location' : '');
});
}, options);
}
onResize() {
this.viewPortHeight = this.getViewportHeight();
if (this.observer) {
this.observer.disconnect();
}
this.followMenuAnchors();
}
}
exports["default"] = AnchorLinks;
/***/ }),
/***/ "../assets/dev/js/frontend/utils/flex-horizontal-scroll.js":
/*!*****************************************************************!*\
!*** ../assets/dev/js/frontend/utils/flex-horizontal-scroll.js ***!
\*****************************************************************/
/***/ ((__unused_webpack_module, exports) => {
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports.changeScrollStatus = changeScrollStatus;
exports.setHorizontalScrollAlignment = setHorizontalScrollAlignment;
exports.setHorizontalTitleScrollValues = setHorizontalTitleScrollValues;
function changeScrollStatus(element, event) {
if ('mousedown' === event.type) {
element.classList.add('e-scroll');
element.dataset.pageX = event.pageX;
} else {
element.classList.remove('e-scroll', 'e-scroll-active');
element.dataset.pageX = '';
}
}
// This function was written using this example https://codepen.io/thenutz/pen/VwYeYEE.
function setHorizontalTitleScrollValues(element, horizontalScrollStatus, event) {
const isActiveScroll = element.classList.contains('e-scroll'),
isHorizontalScrollActive = 'enable' === horizontalScrollStatus,
headingContentIsWiderThanWrapper = element.scrollWidth > element.clientWidth;
if (!isActiveScroll || !isHorizontalScrollActive || !headingContentIsWiderThanWrapper) {
return;
}
event.preventDefault();
const previousPositionX = parseFloat(element.dataset.pageX),
mouseMoveX = event.pageX - previousPositionX,
maximumScrollValue = 5,
stepLimit = 20;
let toScrollDistanceX = 0;
if (stepLimit < mouseMoveX) {
toScrollDistanceX = maximumScrollValue;
} else if (stepLimit * -1 > mouseMoveX) {
toScrollDistanceX = -1 * maximumScrollValue;
} else {
toScrollDistanceX = mouseMoveX;
}
element.scrollLeft = element.scrollLeft - toScrollDistanceX;
element.classList.add('e-scroll-active');
}
function setHorizontalScrollAlignment(_ref) {
let {
element,
direction,
justifyCSSVariable,
horizontalScrollStatus
} = _ref;
if (!element) {
return;
}
if (isHorizontalScroll(element, horizontalScrollStatus)) {
initialScrollPosition(element, direction, justifyCSSVariable);
} else {
element.style.setProperty(justifyCSSVariable, '');
}
}
function isHorizontalScroll(element, horizontalScrollStatus) {
return element.clientWidth < getChildrenWidth(element.children) && 'enable' === horizontalScrollStatus;
}
function getChildrenWidth(children) {
let totalWidth = 0;
const parentContainer = children[0].parentNode,
computedStyles = getComputedStyle(parentContainer),
gap = parseFloat(computedStyles.gap) || 0; // Get the gap value or default to 0 if it's not specified
for (let i = 0; i < children.length; i++) {
totalWidth += children[i].offsetWidth + gap;
}
return totalWidth;
}
function initialScrollPosition(element, direction, justifyCSSVariable) {
const isRTL = elementorFrontend.config.is_rtl;
switch (direction) {
case 'end':
element.style.setProperty(justifyCSSVariable, 'start');
element.scrollLeft = isRTL ? -1 * getChildrenWidth(element.children) : getChildrenWidth(element.children);
break;
default:
element.style.setProperty(justifyCSSVariable, 'start');
element.scrollLeft = 0;
}
}
/***/ }),
/***/ "../modules/mega-menu/assets/js/frontend/handlers/mega-menu.js":
/*!*********************************************************************!*\
!*** ../modules/mega-menu/assets/js/frontend/handlers/mega-menu.js ***!
\*********************************************************************/
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
var _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ "../node_modules/@babel/runtime/helpers/interopRequireDefault.js");
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports["default"] = void 0;
var _utils = __webpack_require__(/*! ../utils */ "../modules/mega-menu/assets/js/frontend/utils.js");
var _anchorLink = _interopRequireDefault(__webpack_require__(/*! ../../../../../../assets/dev/js/frontend/utils/anchor-link */ "../assets/dev/js/frontend/utils/anchor-link.js"));
var _flexHorizontalScroll = __webpack_require__(/*! elementor-pro/frontend/utils/flex-horizontal-scroll */ "../assets/dev/js/frontend/utils/flex-horizontal-scroll.js");
class MegaMenu extends elementorModules.frontend.handlers.Base {
constructor() {
super(...arguments);
if (elementorFrontend.isEditMode()) {
this.lifecycleChangeListener = null;
}
this.resizeListener = null;
this.prevMouseY = null;
this.isKeyboardNavigation = false;
}
getDefaultSettings() {
return {
selectors: {
elementorWidgetWrapper: '.elementor-widget-n-menu',
widgetContainer: '.e-n-menu',
dropdownMenuToggle: '.e-n-menu-toggle',
menuWrapper: '.e-n-menu-wrapper',
headingContainer: '.e-n-menu-heading',
menuItem: '.e-n-menu-item',
tabTitle: '.e-n-menu-title',
tabTitleText: '.e-n-menu-title-text',
directTabTitle: ':scope > .elementor-widget-container > .e-n-menu > .e-n-menu-wrapper > .e-n-menu-heading > .e-n-menu-item > .e-n-menu-title, :scope > .e-n-menu > .e-n-menu-wrapper > .e-n-menu-heading > .e-n-menu-item > .e-n-menu-title',
tabClickableTitle: '.e-n-menu-title.e-click',
tabDropdown: '.e-n-menu-dropdown-icon',
menuContent: '.e-n-menu-content',
tabContent: '.e-n-menu-content > .e-con, .e-n-menu-heading > .e-con',
directTabContent: ':scope > .elementor-widget-container > .e-n-menu > .e-n-menu-wrapper > .e-n-menu-heading > .e-n-menu-item > .e-n-menu-content > .e-con, :scope > .elementor-widget-container > .e-n-menu > .e-n-menu-wrapper > .e-n-menu-heading > .e-con, :scope > .e-n-menu > .e-n-menu-wrapper > .e-n-menu-heading > .e-n-menu-item > .e-n-menu-content > .e-con, :scope > .e-n-menu > .e-n-menu-wrapper > .e-n-menu-heading > .e-con',
tabContentBeforeInterlacing: '> .elementor-widget-container > .e-n-menu > .e-n-menu-wrapper > .e-n-menu-heading > .e-con, > .e-n-menu > .e-n-menu-wrapper > .e-n-menu-heading > .e-con',
newContainerAfterRepeaterAction: '> .elementor-widget-container > .e-n-menu > .e-n-menu-wrapper > .e-n-menu-heading > .e-con, > .elementor-widget-container > .e-n-menu > .e-n-menu-wrapper > .e-n-menu-heading > .e-n-menu-item > .e-n-menu-content > .e-con:nth-child(2), > .e-n-menu > .e-n-menu-wrapper > .e-n-menu-heading > .e-con, > .e-n-menu > .e-n-menu-wrapper > .e-n-menu-heading > .e-n-menu-item > .e-n-menu-content > .e-con:nth-child(2)',
anchorLink: '.e-anchor a'
},
classes: {
active: 'e-active',
anchorItem: 'e-anchor',
activeAnchorItem: 'e-current'
},
dataAttributes: {
tabIndex: 'data-tab-index'
},
ariaAttributes: {
titleStateAttribute: 'aria-expanded',
activeTitleSelector: '[aria-expanded="true"]'
},
autoExpand: false,
autoFocus: false,
showTabFn: 'show',
hideTabFn: 'hide',
toggleSelf: false,
hidePrevious: true,
postUrl: 'post-url',
internalUrl: 'internal-url'
};
}
getDefaultElements() {
const selectors = this.getSettings('selectors');
return {
$tabContents: this.findElement(selectors.tabContent),
$widgetContainer: this.findElement(selectors.widgetContainer),
$dropdownMenuToggle: this.findElement(selectors.dropdownMenuToggle),
$menuWrapper: this.findElement(selectors.menuWrapper),
$menuContent: this.findElement(selectors.menuContent),
$headingContainer: this.findElement(selectors.headingContainer),
$menuItems: this.findElement(selectors.menuItem),
$tabTitles: this.findElement(selectors.tabTitle),
$tabDropdowns: this.findElement(selectors.tabDropdown),
$anchorLink: this.findElement(selectors.anchorLink),
$tabContentsBeforeInterlacing: this.findElement(selectors.tabContentBeforeInterlacing)
};
}
getTabTitleFilterSelector(tabIndex) {
return `[${this.getSettings('dataAttributes').tabIndex}="${tabIndex}"]`;
}
getTabIndex(tabTitleElement) {
return tabTitleElement.getAttribute(this.getSettings('dataAttributes').tabIndex);
}
setKeyboardNavigation(event) {
if ('Tab' === event.key) {
this.isKeyboardNavigation = true;
}
}
dropdownMenuHeightControllerConfig() {
const selectors = this.getSettings('selectors');
return {
elements: {
$element: this.$element,
$dropdownMenuContainer: this.$element.find(selectors.menuWrapper),
$menuToggle: this.$element.find(selectors.dropdownMenuToggle)
},
attributes: {
menuToggleState: 'aria-expanded'
},
settings: {
dropdownMenuContainerMaxHeight: 'auto',
menuHeightCssVarName: '--n-menu-dropdown-content-box-height'
}
};
}
handleContentContainerPosition() {
let $contentContainer = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
this.resetContentContainersPosition();
// If no container is passed as an argument, check if there is an active container.
const activeTitleSelector = this.getSettings('ariaAttributes').activeTitleSelector,
tabIndex = this.elements.$tabDropdowns.filter(activeTitleSelector).attr('data-tab-index');
$contentContainer = $contentContainer || this.elements.$tabContents.filter(this.getTabContentFilterSelector(tabIndex));
if (!$contentContainer.length) {
return;
}
this.setContentContainerAbsolutePosition($contentContainer);
}
setContentContainerAbsolutePosition($contentContainer) {
const elementSettings = this.getElementSettings(),
isFitToContent = 'fit_to_content' === elementSettings.content_width;
if ((0, _utils.isMenuInDropdownMode)(elementSettings)) {
return;
}
if (isFitToContent) {
const direction = elementorFrontend.config.is_rtl ? 'right' : 'left',
menuItemContainerOffset = 0 < this.getMenuItemContainerAbsolutePosition($contentContainer) ? this.getMenuItemContainerAbsolutePosition($contentContainer) : 0;
$contentContainer.css(direction, menuItemContainerOffset);
}
const headingsHeight = this.elements.$headingContainer[0].getBoundingClientRect().height;
if (this.shouldPositionContentAbove($contentContainer, headingsHeight)) {
const contentContainerBoundingBox = $contentContainer[0].getBoundingClientRect();
$contentContainer.css({
width: isFitToContent ? 'max-content' : '',
'max-width': contentContainerBoundingBox.width
});
this.elements.$widgetContainer.addClass('content-above');
}
}
getMenuItemContainerAbsolutePosition($contentContainer) {
const tabIndex = $contentContainer.data('tab-index'),
$activeDropdown = this.elements.$tabDropdowns.filter(this.getTabTitleFilterSelector(tabIndex))[0],
$titleElement = $activeDropdown.closest(this.getSettings('selectors').tabTitle),
titleBoundingBox = $titleElement.getBoundingClientRect(),
contentContainerWidth = $contentContainer[0].clientWidth;
let menuItemContainerOffset = null;
switch (this.getElementSettings('content_horizontal_position')) {
case 'left':
menuItemContainerOffset = this.getLeftDirectionContainerOffset(contentContainerWidth, titleBoundingBox);
break;
case 'right':
menuItemContainerOffset = this.getRightDirectionContainerOffset(contentContainerWidth, titleBoundingBox);
break;
default:
menuItemContainerOffset = this.getCenteredContainerOffset(contentContainerWidth, titleBoundingBox);
}
return menuItemContainerOffset;
}
getCenteredContainerOffset(contentContainerWidth, titleBoundingBox) {
const menuItemContentContainerHalfWidth = contentContainerWidth / 2,
bodyWidth = elementorFrontend.elements.$body[0].clientWidth;
let titleMiddleOffset = this.adjustForScrollbarIfNeeded(titleBoundingBox.left + titleBoundingBox.width / 2);
if (elementorFrontend.config.is_rtl) {
titleMiddleOffset = bodyWidth - titleMiddleOffset;
}
let offset = titleMiddleOffset - menuItemContentContainerHalfWidth;
if (titleMiddleOffset + menuItemContentContainerHalfWidth > bodyWidth) {
offset = bodyWidth - contentContainerWidth;
} else if (menuItemContentContainerHalfWidth > titleMiddleOffset) {
offset = 0;
}
return offset;
}
getLeftDirectionContainerOffset(contentContainerWidth, titleBoundingBox) {
return elementorFrontend.config.is_rtl ? this.getRtlLeftDirectionContainerOffset(contentContainerWidth, titleBoundingBox) : this.getLtrLeftDirectionContainerOffset(contentContainerWidth, titleBoundingBox);
}
getRtlLeftDirectionContainerOffset(contentContainerWidth, titleBoundingBox) {
const bodyWidth = elementorFrontend.elements.$body[0].clientWidth,
titleLeftOffset = this.adjustForScrollbarIfNeeded(titleBoundingBox.left);
let offset = bodyWidth - titleLeftOffset - contentContainerWidth;
// If the content container doesn't fit in the viewport, align its right edge with the viewport's right edge.
if (-offset + contentContainerWidth > bodyWidth) {
offset = 0;
}
return offset;
}
getLtrLeftDirectionContainerOffset(contentContainerWidth, titleBoundingBox) {
let offset = this.adjustForScrollbarIfNeeded(titleBoundingBox.left);
offset = this.adjustStartOffsetToViewport(offset, contentContainerWidth);
return offset;
}
getRightDirectionContainerOffset(contentContainerWidth, titleBoundingBox) {
return elementorFrontend.config.is_rtl ? this.getRtlRightDirectionContainerOffset(contentContainerWidth, titleBoundingBox) : this.getLtrRightDirectionContainerOffset(contentContainerWidth, titleBoundingBox);
}
getRtlRightDirectionContainerOffset(contentContainerWidth, titleBoundingBox) {
const bodyWidth = elementorFrontend.elements.$body[0].clientWidth;
let offset = bodyWidth - this.adjustForScrollbarIfNeeded(titleBoundingBox.right);
offset = this.adjustStartOffsetToViewport(offset, contentContainerWidth);
return offset;
}
/**
* If the content container doesn't fit in the viewport, align its right edge with the viewport's right edge.
*
* @param {number} offset
* @param {number} contentContainerWidth
*/
adjustStartOffsetToViewport(offset, contentContainerWidth) {
const bodyWidth = elementorFrontend.elements.$body[0].clientWidth;
if (offset + contentContainerWidth > bodyWidth) {
offset = bodyWidth - contentContainerWidth;
}
return offset;
}
getLtrRightDirectionContainerOffset(contentContainerWidth, titleBoundingBox) {
return contentContainerWidth > titleBoundingBox.right ? 0 : titleBoundingBox.right - contentContainerWidth;
}
adjustForScrollbarIfNeeded(offset) {
if (elementorFrontend.config.is_rtl && elementorFrontend.isEditMode()) {
const scrollbarWidth = window.innerWidth - elementorFrontend.elements.$body[0].clientWidth;
offset -= scrollbarWidth;
}
return offset;
}
getMenuContainerOffset() {
const menuContainerBoundingBox = this.elements.$widgetContainer[0].getBoundingClientRect();
return elementorFrontend.config.is_rtl ? this.getMenuContainerOffsetRtl(menuContainerBoundingBox) : menuContainerBoundingBox.left;
}
getMenuContainerOffsetRtl(menuContainerBoundingBox) {
const bodyWidth = elementorFrontend.elements.$body[0].clientWidth;
let menuContainerOffset = bodyWidth - menuContainerBoundingBox.right;
if (elementorFrontend.isEditMode()) {
// In RTL mode, the editor's scrollbar is on the left side, so we need to add its width to the offset.
const scrollbarWidth = window.innerWidth - bodyWidth;
menuContainerOffset += scrollbarWidth;
}
return menuContainerOffset;
}
resetContentContainersPosition() {
this.elements.$tabContents.css({
left: '',
right: '',
bottom: '',
position: 'var(--position)',
'max-width': '',
width: 'var(--width)'
});
this.elements.$widgetContainer.removeClass('content-above');
}
getTabContentFilterSelector(tabIndex) {
return `[data-tab-index="${tabIndex}"]`;
}
isActiveTab(tabIndex) {
return 'true' === this.elements.$tabDropdowns.filter('[data-tab-index="' + tabIndex + '"]').attr(this.getSettings('ariaAttributes').titleStateAttribute);
}
activateDefaultTab() {
const settings = this.getSettings();
const defaultActiveTab = this.getEditSettings('activeItemIndex') || 1,
originalToggleMethods = {
showTabFn: settings.showTabFn,
hideTabFn: settings.hideTabFn
};
// Toggle tabs without animation to avoid jumping
this.setSettings({
showTabFn: 'show',
hideTabFn: 'hide'
});
this.changeActiveTab(defaultActiveTab);
// Return back original toggle effects
this.setSettings(originalToggleMethods);
this.elements.$widgetContainer.addClass('e-activated');
}
activateTab(tabIndex) {
const settings = this.getSettings(),
activeClass = settings.classes.active,
childMenuDropdownSelector = `.elementor-element-${this.getID()} .e-n-menu .e-n-menu .e-n-menu-dropdown-icon`,
childMenuContentSelector = `.elementor-element-${this.getID()} .e-n-menu .e-n-menu .e-n-menu-content > .e-con`,
$requestedTitle = this.elements.$tabDropdowns.filter(this.getTabTitleFilterSelector(tabIndex)).not(childMenuDropdownSelector),
animationDuration = 'show' === settings.showTabFn ? 0 : 400,
$requestedContent = this.elements.$tabContents.filter(this.getTabContentFilterSelector(tabIndex)).not(childMenuContentSelector);
this.addAnimationToContentIfNeeded(tabIndex);
$requestedContent[settings.showTabFn](animationDuration, () => this.onShowTabContent($requestedContent));
$requestedTitle.attr(this.getTitleActivationAttributes());
$requestedTitle.prev('.e-n-menu-title-container').find('a').attr(this.getTitleActivationAttributes('link'));
$requestedContent.addClass(activeClass).parent().addClass(activeClass);
$requestedContent.css({
display: 'var(--display)'
});
$requestedContent.removeAttr('display');
if (elementorFrontend.isEditMode() && !!$requestedContent.length) {
this.activeContainerWidthListener($requestedContent);
}
this.menuHeightController.reassignMenuHeight($requestedContent);
}
deactivateActiveTab() {
const settings = this.getSettings(),
activeClass = settings.classes.active,
activeTitleFilter = settings.ariaAttributes.activeTitleSelector,
activeContentFilter = '.' + activeClass,
$activeTitle = this.elements.$tabDropdowns.filter(activeTitleFilter),
$activeContent = this.elements.$tabContents.filter(activeContentFilter);
this.setTabDeactivationAttributes($activeTitle);
this.elements.$menuContent.removeClass(activeClass);
$activeContent.removeClass(activeClass);
$activeContent[settings.hideTabFn](0, () => this.onHideTabContent($activeContent));
this.removeAnimationFromContentIfNeeded();
if (elementorFrontend.isEditMode() && !!$activeContent.length) {
this.observedContainer?.unobserve($activeContent[0]);
}
this.menuHeightController.resetMenuHeight($activeContent);
this.clickInProgress = true;
}
getTitleActivationAttributes() {
let elementType = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'tab';
const titleAttributes = {};
if ('tab' === elementType) {
titleAttributes['aria-expanded'] = 'true';
}
return titleAttributes;
}
setTabDeactivationAttributes($activeTitle) {
const titleStateAttribute = this.getSettings('ariaAttributes').titleStateAttribute;
$activeTitle.attr(`${titleStateAttribute}`, 'false');
}
shouldPositionContentAbove($contentContainer) {
let offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
const contentDimensions = $contentContainer[0].getBoundingClientRect();
return this.isContentShorterThanItsTopOffset(contentDimensions, offset) && this.isContentTallerThanItsBottomOffset(contentDimensions);
}
isContentShorterThanItsTopOffset(contentDimensions, offset) {
return contentDimensions.height < contentDimensions.top - offset;
}
isContentTallerThanItsBottomOffset(contentDimensions) {
return window.innerHeight - contentDimensions.top < contentDimensions.height;
}
onShowTabContent($requestedContent) {
this.handleContentContainerPosition($requestedContent);
elementorFrontend.elements.$window.trigger('elementor-pro/motion-fx/recalc');
elementorFrontend.elements.$window.trigger('elementor/nested-tabs/activate', $requestedContent);
elementorFrontend.elements.$window.trigger('elementor/bg-video/recalc');
}
onHideTabContent() {
if (this.elements.$widgetContainer.hasClass('content-above')) {
this.resetContentContainersPosition();
}
}
changeActiveTab(tabIndex) {
let fromUser = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
let byKeyboard = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
if (this.clickInProgress && elementorFrontend.isEditMode() && !byKeyboard) {
return;
}
const isActiveTab = this.isActiveTab(tabIndex);
this.deactivateActiveTab();
if (!isActiveTab || isActiveTab && !fromUser) {
this.clickInProgress = true;
this.activateTab(tabIndex);
}
setTimeout(() => {
this.clickInProgress = false;
});
}
changeActiveTabByKeyboard(event, settings) {
if (settings.widgetId.toString() !== this.getID().toString()) {
return;
}
if (!settings.titleIndex) {
this.changeActiveTab('', true, true);
return;
}
const $focusableElement = this.$element.find(`[data-focus-index="${settings.titleIndex}"]`),
isLinkElement = 'a' === $focusableElement[0].tagName.toLowerCase(),
dropdownSelector = this.getSettings('selectors.tabDropdown'),
$tabDropdown = isLinkElement ? $focusableElement.next(dropdownSelector) : $focusableElement,
tabIndex = this.getTabIndex($tabDropdown[0]);
this.changeActiveTab(tabIndex, true, true);
event.stopPropagation();
}
onTabClick(event) {
if (elementorFrontend.isEditMode()) {
event.preventDefault();
}
const hasNoDropdown = event?.currentTarget?.classList?.contains('link-only');
// Tweak for NVDA screen reader with Windows Edge.
// Ref: https://github.com/nvaccess/nvda/issues/7898
const dropdownOpensWithHover = !this.isNeedToOpenOnClick(),
blockMouseClickEvents = dropdownOpensWithHover && !this.isKeyboardNavigation;
if (hasNoDropdown || blockMouseClickEvents) {
return;
}
const selectors = this.getSettings('selectors'),
getClickedMenuId = event?.target?.closest(selectors.elementorWidgetWrapper)?.getAttribute('data-id'),
getWidgetId = this.getID().toString();
if (getClickedMenuId !== getWidgetId) {
return;
}
const clickedElement = event?.currentTarget,
dropdownElement = clickedElement?.querySelector(selectors.tabDropdown),
tabIndex = this.getTabIndex(dropdownElement);
this.changeActiveTab(tabIndex, true);
}
bindEvents() {
this.elements.$tabTitles.on(this.getTabEvents());
this.elements.$dropdownMenuToggle.on('click', this.onClickToggleDropdownMenu.bind(this));
this.elements.$tabContents.on(this.getContentEvents());
this.elements.$menuContent.on(this.getContentEvents());
this.elements.$headingContainer.on(this.getHeadingEvents());
elementorFrontend.addListenerOnce(this.getModelCID(), 'scroll', elementorFrontend.debounce(this.menuHeightController.reassignMobileMenuHeight.bind(this.menuHeightController), 250));
elementorFrontend.elements.$window.on('elementor/nested-tabs/activate', this.reInitSwipers);
elementorFrontend.elements.$window.on('elementor/nested-elements/activate-by-keyboard', this.changeActiveTabByKeyboard.bind(this));
elementorFrontend.elements.$window.on('elementor/mega-menu/dropdown-toggle-by-keyboard', this.onClickToggleDropdownMenuByKeyboard.bind(this));
elementorFrontend.elements.$window.on('resize', this.resizeEventHandler.bind(this));
if (elementorFrontend.isEditMode()) {
this.addChildLifeCycleEventListeners();
elementorFrontend.elements.$window.on('elementor/dynamic/url_change', this.changeMegaMenuTitleContainerTag.bind(this));
}
elementorFrontend.elements.$window.on('elementor/nested-container/atomic-repeater', this.linkContainer.bind(this));
}
unbindEvents() {
this.elements.$tabTitles.off();
this.elements.$menuContent.off();
this.elements.$tabContents.off();
this.elements.$headingContainer.off();
elementorFrontend.elements.$window.off('resize');
if (elementorFrontend.isEditMode()) {
this.removeChildLifeCycleEventListeners();
elementorFrontend.elements.$window.on('elementor/dynamic/url_change', this.changeMegaMenuTitleContainerTag.bind(this));
}
elementorFrontend.elements.$window.off('elementor/nested-tabs/activate', this.reInitSwipers);
elementorFrontend.elements.$window.off('elementor/nested-elements/activate-by-keyboard', this.changeActiveTabByKeyboard.bind(this));
elementorFrontend.elements.$window.off('elementor/mega-menu/dropdown-toggle-by-keyboard', this.onClickToggleDropdownMenuByKeyboard.bind(this));
elementorFrontend.elements.$window.off('resize', this.resizeEventHandler.bind(this));
elementorFrontend.elements.$window.off('elementor/nested-container/atomic-repeater', this.linkContainer.bind(this));
}
/**
* Fixes issues where Swipers that have been initialized while a tab is not visible are not properly rendered
* and when switching to the tab the swiper will not respect any of the chosen `autoplay` related settings.
*
* This is triggered when switching to a nested tab, looks for Swipers in the tab content and reinitializes them.
*
* @param {Object} event - Incoming event.
* @param {Object} content - Active nested tab dom element.
*/
reInitSwipers(event, content) {
const swiperElements = content.querySelectorAll('.swiper');
for (const element of swiperElements) {
if (!element.swiper) {
return;
}
element.swiper.initialized = false;
element.swiper.init();
}
}
resizeEventHandler() {
this.resizeListener = this.handleContentContainerPosition();
this.setLayoutType();
this.setTouchMode();
this.menuHeightController.reassignMobileMenuHeight();
this.setScrollPosition();
const activeTitleSelector = this.getSettings('ariaAttributes').activeTitleSelector,
tabIndex = this.elements.$tabDropdowns.filter(activeTitleSelector).attr('data-tab-index'),
childMenuContentSelector = `.elementor-element-${this.getID()} .e-n-menu .e-n-menu .e-n-menu-content > .e-con`,
$requestedContent = this.elements.$tabContents.filter(this.getTabContentFilterSelector(tabIndex)).not(childMenuContentSelector);
this.menuHeightController.resetMenuHeight($requestedContent);
this.menuHeightController.reassignMenuHeight($requestedContent);
}
/**
* Add Child Lifecycle Event Listeners
*
* This method adds event listeners for the elementor/editor/element-rendered and elementor/editor/element-destroyed
* events. These events are fired when an element is rendered or destroyed in the editor. The callback functions
* check if the rendered/destroyed element is nested in this mega-menu instance, and if it is, triggers the
* recalculation of the mega-menu's content containers position.
*/
addChildLifeCycleEventListeners() {
this.lifecycleChangeListener = this.handleContentContainerChildrenChanges.bind(this);
window.addEventListener('elementor/editor/element-rendered', this.lifecycleChangeListener);
window.addEventListener('elementor/editor/element-destroyed', this.lifecycleChangeListener);
}
removeChildLifeCycleEventListeners() {
window.removeEventListener('elementor/editor/element-rendered', this.lifecycleChangeListener);
window.removeEventListener('elementor/editor/element-destroyed', this.lifecycleChangeListener);
}
handleContentContainerChildrenChanges(event) {
if (!this.isNestedElementRenderedInContentContainer(event.detail.elementView)) {
return;
}
this.handleContentContainerPosition();
}
isNestedElementRenderedInContentContainer(elementView) {
const elementContainer = elementView?.getContainer();
if (!elementContainer) {
return false;
}
const elementAncestors = elementContainer.getParentAncestry();
return elementAncestors.some(parent => this.getID().toString() === parent.model.get('id').toString());
}
getTabEvents() {
const tabEvents = {
click: this.onTabClick.bind(this)
};
return this.isNeedToOpenOnClick() ? tabEvents : this.replaceClickWithHover(tabEvents);
}
getContentEvents() {
return this.isNeedToOpenOnClick() ? {} : {
mouseleave: this.onMouseLeave.bind(this),
mousemove: this.trackMousePosition.bind(this)
};
}
isNeedToOpenOnClick() {
const elementSettings = this.getElementSettings();
return this.isEdit || this.isMobileDevice() || 'hover' !== elementSettings.open_on || 'dropdown' === elementSettings.item_layout;
}
isMobileDevice() {
const mobileDevices = ['mobile', 'mobile_extra', 'tablet', 'tablet_extra'];
return mobileDevices.includes(elementorFrontend.getCurrentDeviceMode());
}
replaceClickWithHover(tabEvents) {
tabEvents.mouseenter = this.onMouseTitleEnter.bind(this);
tabEvents.mouseleave = this.onMouseLeave.bind(this);
tabEvents.keyup = this.setKeyboardNavigation.bind(this);
return tabEvents;
}
onMouseTitleEnter(event) {
event.preventDefault();
const settings = this.getSettings(),
currentTarget = event?.currentTarget,
currentTargetWidgetId = currentTarget?.closest(settings.selectors.elementorWidgetWrapper)?.getAttribute('data-id'),
widgetId = this.$element[0].getAttribute('data-id');
if (widgetId !== currentTargetWidgetId) {
return;
}
const titleStateAttribute = settings.ariaAttributes.titleStateAttribute,
dropdownSelector = settings.selectors.tabDropdown,
activeDropdownElement = currentTarget?.querySelector(dropdownSelector),
isActiveTabTitle = 'true' === activeDropdownElement?.getAttribute(titleStateAttribute);
if (isActiveTabTitle) {
return;
}
const tabIndex = activeDropdownElement?.getAttribute('data-tab-index');
this.changeActiveTab(tabIndex, true);
}
onClickToggleDropdownMenu(show) {
this.elements.$widgetContainer.attr('data-layout', 'dropdown');
const titleStateAttribute = this.getSettings('ariaAttributes').titleStateAttribute,
isDropdownVisible = 'true' === this.elements.$dropdownMenuToggle.attr(titleStateAttribute);
if ('boolean' !== typeof show) {
show = !isDropdownVisible;
}
const activeTabTitleValue = show ? 'true' : 'false';
this.elements.$dropdownMenuToggle.attr(titleStateAttribute, activeTabTitleValue);
elementorFrontend.utils.events.dispatch(window, 'elementor-pro/mega-menu/dropdown-open');
this.menuHeightController.reassignMobileMenuHeight();
}
onClickOutsideDropdownMenu(event) {
if (!this.isNeedToOpenOnClick()) {
return;
}
const settings = this.getSettings(),
selectors = settings.selectors,
widgetWrapper = `.elementor-element-${this.getID()}`,
activeClass = settings.classes.active,
activeContentFilter = `> .e-con.${activeClass}`,
$activeContent = this.elements.$menuContent.find(activeContentFilter),
isMenuDropdownsClosed = 0 === $activeContent.length,
isElementRemovedFromDOM = elementorFrontend.isEditMode() && !document.body.contains(event?.target),
isClickedInsideCurrentMenu = !!event?.target?.closest(`${widgetWrapper} ${selectors.widgetContainer}`),
isMenuContentWrapperClicked = event?.target?.classList?.contains(selectors.menuContent.replace('.', ''));
if (isMenuContentWrapperClicked) {
this.deactivateActiveTab();
return;
}
if (isMenuDropdownsClosed || isClickedInsideCurrentMenu || isElementRemovedFromDOM) {
return;
}
this.deactivateActiveTab();
}
onClickToggleDropdownMenuByKeyboard(event, settings) {
if (settings.widgetId.toString() !== this.getID().toString()) {
return;
}
this.onClickToggleDropdownMenu(settings.show);
}
addAnimationToContentIfNeeded(tabIndex) {
const openAnimation = this.getElementSettings('open_animation');
if ('none' === openAnimation || '' === openAnimation) {
return;
}
const $requestedContent = this.elements.$tabContents.filter(this.getTabContentFilterSelector(tabIndex));
$requestedContent.addClass(`animated ${openAnimation}`);
}
removeAnimationFromContentIfNeeded() {
const openAnimation = this.getElementSettings('open_animation');
if ('none' === openAnimation || '' === openAnimation) {
return;
}
this.elements.$tabContents.removeClass(`animated ${openAnimation}`);
}
/**
* Store the current Y-coordinate of the mouse cursor.
*
* @param {Event} event - The mouse event object.
*/
trackMousePosition(event) {
this.prevMouseY = event?.clientY;
}
/**
* Check if the menu content is currently hovered.
*
* @return {boolean} - True if menu content is hovered, otherwise false.
*/
isMenuContentHovered() {
const settings = this.getSettings(),
$widget = this.$element;
return $widget.find(`${settings.selectors.menuContent}:hover`).length > 0;
}
isCursorInBetweenMenuTitleAndContent(event) {
const settings = this.getSettings();
const selectors = settings.selectors;
const currentElement = event?.currentTarget;
const activeContent = currentElement?.closest(selectors.menuItem)?.querySelector(selectors.menuContent);
const isMouseLeavingTabTitle = currentElement.classList?.contains(selectors.tabTitle.replace('.', ''));
const hasActiveTabTitle = activeContent?.classList?.contains(settings.classes.active);
if (!isMouseLeavingTabTitle || !hasActiveTabTitle) {
return false;
}
const titleBoundingClientRect = currentElement.getBoundingClientRect();
const contentBoundingClientRect = activeContent.getBoundingClientRect();
const mouseY = event.clientY;
const isTitleAboveContent = titleBoundingClientRect.bottom <= contentBoundingClientRect.top;
return isTitleAboveContent ? mouseY >= titleBoundingClientRect.bottom && mouseY < contentBoundingClientRect.top : mouseY <= titleBoundingClientRect.top && mouseY > contentBoundingClientRect.bottom;
}
/**
* Determines whether the cursor moved sideways or downwards.
*
* @param {Event} event - The mouse event object.
* @return {boolean} - True if the cursor moved sideways or downwards, otherwise false.
*/
didCursorMoveSidewaysOrDown(event) {
// Detects if the Y-coordinate of the mouse has not decreased (i.e., either remained the same or increased).
return this.prevMouseY !== null && event?.clientY >= this.prevMouseY;
}
/**
* Check whether the dropdown menu should remain open based on hover and cursor movement.
*
* @param {boolean} isMouseLeavingTabContent - True if the mouse is leaving the tab content.
* @param {Event} event - The mouse event object.
* @return {boolean} - True if dropdown should be considered as hovered, otherwise false.
*/
isHoveredDropdownMenu(isMouseLeavingTabContent, event) {
// If the mouse is leaving the tab content and it moved sideways or downwards, close the dropdown.
if (isMouseLeavingTabContent && this.didCursorMoveSidewaysOrDown(event)) {
return false;
}
// Otherwise, return true if the menu content is hovered.
return this.isMenuContentHovered();
}
/**
* Handle the event when the mouse leaves the dropdown.
*
* @param {Event} event - The mouse event object.
*/
onMouseLeave(event) {
event.preventDefault();
const isMouseLeavingTabContent = event?.currentTarget?.classList?.contains('e-con');
if (!this.isHoveredDropdownMenu(isMouseLeavingTabContent, event) && !this.isCursorInBetweenMenuTitleAndContent(event)) {
this.deactivateActiveTab();
}
}
onInit() {
this.menuHeightController = new elementorProFrontend.utils.DropdownMenuHeightController(this.dropdownMenuHeightControllerConfig());
super.onInit(...arguments);
if (this.getSettings('autoExpand')) {
this.activateDefaultTab();
}
(0, _flexHorizontalScroll.setHorizontalScrollAlignment)(this.getHorizontalScrollingSettings());
this.setTouchMode();
if (!elementorFrontend.isEditMode()) {
const classes = this.getSettings('classes');
this.anchorLinks = new _anchorLink.default(this.elements.$anchorLink, classes);
this.anchorLinks.initialize();
elementorFrontend.elements.$window.on('elementor/dynamic/url_change', this.changeMegaMenuTitleContainerTag.bind(this));
}
this.menuToggleVisibilityListener(this.elements.$dropdownMenuToggle);
this.setScrollPosition();
this.onClickOutsideDropdownMenu = this.onClickOutsideDropdownMenu.bind(this);
document.addEventListener('click', this.onClickOutsideDropdownMenu);
this.clickInProgress = false;
}
onDestroy() {
document.removeEventListener('click', this.onClickOutsideDropdownMenu);
elementorFrontend.elements.$window.off('elementor/dynamic/url_change');
}
setScrollPosition() {
const settingsObject = {
element: this.elements.$headingContainer[0],
direction: this.getItemPosition(),
justifyCSSVariable: '--n-menu-heading-justify-content',
horizontalScrollStatus: this.getHorizontalScrollSetting()
};
(0, _flexHorizontalScroll.setHorizontalScrollAlignment)(settingsObject);
}
getPropsThatTriggerContentPositionCalculations() {
return ['content_horizontal_position', 'content_position', 'item_position_horizontal', 'content_width', 'item_layout'];
}
activeContainerWidthListener($activeContainer) {
let previousWidth = 0;
this.observedContainer = new ResizeObserver(activeContainer => {
const currentWidth = activeContainer[0].borderBoxSize?.[0].inlineSize;
if (!!currentWidth && currentWidth !== previousWidth) {
previousWidth = currentWidth;
if (0 !== previousWidth) {
this.handleContentContainerPosition();
}
}
});
this.observedContainer.observe($activeContainer[0]);
}
menuToggleVisibilityListener($menuToggle) {
let previousWidth;
this.observedContainer = new ResizeObserver(menuToggle => {
const currentWidth = menuToggle[0].borderBoxSize?.[0].inlineSize;
if (currentWidth !== previousWidth) {
previousWidth = currentWidth;
this.setLayoutType();
}
});
this.observedContainer.observe($menuToggle[0]);
}
onElementChange(propertyName) {
if (this.getPropsThatTriggerContentPositionCalculations().includes(propertyName)) {
this.handleContentContainerPosition();
}
this.setLayoutType();
}
onEditSettingsChange(propertyName, value) {
const settings = this.getSettings();
if (settings.autoFocus && 'activeItemIndex' === propertyName) {
this.changeActiveTab(value, false);
}
this.setLayoutType();
}
/**
* Sets the layout type as a data attribute, so that it can be use for the responsive or dropdown menu styling.
*
* Originally this styling was handled by the distinction between the heading and the content styling elements.
* Since we removed the title duplication, we needed another way to distinguish between the horizontal and the dropdown styling.
*/
setLayoutType() {
const layoutType = 'none' === this.elements.$dropdownMenuToggle.css('display') ? 'horizontal' : 'dropdown';
this.elements.$widgetContainer.attr('data-layout', layoutType);
}
getHeadingEvents() {
const navigationWrapper = this.elements.$headingContainer[0];
return {
mousedown: this.changeScrollStatusAndDispatch.bind(this, navigationWrapper),
mouseup: this.changeScrollStatusAndDispatch.bind(this, navigationWrapper),
mouseleave: this.changeScrollStatusAndDispatch.bind(this, navigationWrapper),
mousemove: this.setHorizontalTitleScrollValuesAndDispatch.bind(this, navigationWrapper)
};
}
getHorizontalScrollSetting() {
const currentDevice = elementorFrontend.getCurrentDeviceMode();
return elementorFrontend.utils.controls.getResponsiveControlValue(this.getElementSettings(), 'horizontal_scroll', '', currentDevice);
}
getItemPosition() {
const currentDevice = elementorFrontend.getCurrentDeviceMode();
return elementorFrontend.utils.controls.getResponsiveControlValue(this.getElementSettings(), 'item_position_horizontal', '', currentDevice);
}
changeScrollStatusAndDispatch(navigationWrapper, event) {
(0, _flexHorizontalScroll.changeScrollStatus)(navigationWrapper, event);
elementorFrontend.elements.$window.trigger('elementor-pro/mega-menu/heading-mouse-event');
}
setHorizontalTitleScrollValuesAndDispatch(navigationWrapper, event) {
(0, _flexHorizontalScroll.setHorizontalTitleScrollValues)(navigationWrapper, this.getHorizontalScrollSetting(), event);
elementorFrontend.elements.$window.trigger('elementor-pro/mega-menu/heading-mouse-event');
}
linkContainer(event) {
const {
container
} = event.detail,
id = container.model.get('id'),
currentId = String(this.$element.data('id')),
view = container.view.$el;
if (id === currentId) {
this.updateIndexValues(view);
this.updateListeners(view);
}
}
updateIndexValues(view) {
const {
selectors: {
directTabTitle,
directTabContent
}
} = this.getDefaultSettings(),
currentMenu = view[0],
tabsContents = currentMenu.querySelectorAll(directTabContent),
tabTitles = currentMenu.querySelectorAll(directTabTitle),
settings = this.getSettings(),
itemIdBase = tabTitles[0].getAttribute('id').slice(0, -1);
tabTitles.forEach((element, index) => {
const newIndex = index + 1,
updatedTabID = itemIdBase + newIndex,
updatedContainerID = updatedTabID.replace('e-n-menu-title-', 'e-n-menu-content-'),
updatedTabDropdownID = updatedTabID.replace('e-n-menu-title-', 'e-n-menu-dropdown-icon-');
element.setAttribute('id', updatedTabID);
element.querySelector(settings.selectors.tabDropdown)?.setAttribute('data-tab-index', newIndex);
element.querySelector(settings.selectors.tabDropdown)?.setAttribute('id', updatedTabDropdownID);
element.querySelector(settings.selectors.tabDropdown)?.setAttribute('aria-controls', updatedContainerID);
element.querySelector(settings.selectors.tabTitleText)?.setAttribute('data-binding-index', newIndex);
tabsContents[index]?.setAttribute('aria-labelledby', updatedTabDropdownID);
tabsContents[index]?.setAttribute('data-tab-index', newIndex);
tabsContents[index]?.setAttribute('id', updatedContainerID);
});
}
updateListeners(view) {
const {
selectors: {
tabClickableTitle,
tabDropdown,
tabContent,
tabTitle
}
} = this.getSettings(),
$tabTitles = view.find(tabTitle),
$tabClickableTitle = view.find(tabClickableTitle);
this.elements.$tabTitles = view.find(tabClickableTitle);
this.elements.$tabDropdowns = view.find(tabDropdown);
this.elements.$tabContents = view.find(tabContent);
$tabTitles.off();
$tabClickableTitle.on(this.getTabEvents());
this.clickInProgress = false;
}
/**
* Toggle the container tag of the mega menu title.
* Needs to be places in pro Mega Menu frontend handler
*
* @param {Event} event
* @return {undefined}
*/
changeMegaMenuTitleContainerTag(event) {
const {
element,
actionName,
value
} = event.detail,
elementParent = element.parentNode,
closestMenuItemTitle = elementParent.parentNode,
newElement = this.maybeCreateNewElement(elementParent, value),
elementToUpdate = this.maybeReplaceMenuItemTitleContent(elementParent, newElement, closestMenuItemTitle),
currentUrl = element.dataset?.currentUrl || null;
this.maybeUpdateNewElementsHref(value, elementToUpdate);
this.eCurrentClassHandler(actionName, closestMenuItemTitle, currentUrl === value);
return undefined;
}
maybeReplaceMenuItemTitleContent(elementParent, newElement, closestMenuItemTitle) {
if (!newElement) {
return elementParent;
}
Array.from(elementParent.attributes).forEach(attr => {
newElement.setAttribute(attr.name, attr.value);
});
if ('A' === newElement.tagName) {
newElement.classList.add('e-link', 'e-focus');
} else if ('DIV' === newElement.tagName) {
newElement.classList.remove('e-link', 'e-focus');
}
newElement.innerHTML = elementParent.innerHTML;
closestMenuItemTitle.replaceChild(newElement, elementParent);
return newElement;
}
maybeCreateNewElement(elementParent, value) {
if (!value) {
return document.createElement('div');
}
if (value && 'DIV' === elementParent.tagName) {
return document.createElement('a');
}
}
maybeUpdateNewElementsHref(value, newElement) {
if (value) {
newElement.setAttribute('href', value);
} else {
newElement.removeAttribute('href');
}
}
eCurrentClassHandler(actionName, closestMenuItemTitle, isCurrentUrl) {
const settings = this.getSettings(),
{
classes: {
activeAnchorItem: eCurrentClassName
},
postUrl,
internalUrl
} = settings;
switch (actionName) {
case postUrl:
closestMenuItemTitle.classList.add(eCurrentClassName);
break;
case internalUrl:
if (isCurrentUrl) {
closestMenuItemTitle.classList.add(eCurrentClassName);
} else {
closestMenuItemTitle.classList.remove(eCurrentClassName);
}
break;
default:
if (closestMenuItemTitle.classList.contains(eCurrentClassName) && postUrl !== actionName) {
closestMenuItemTitle.classList.remove(eCurrentClassName);
}
break;
}
}
setTouchMode() {
const widgetSelector = this.getSettings('selectors').widgetContainer;
if (elementorFrontend.isEditMode() || 'resize' === event?.type) {
const responsiveDevices = ['mobile', 'mobile_extra', 'tablet', 'tablet_extra'],
currentDevice = elementorFrontend.getCurrentDeviceMode();
if (-1 !== responsiveDevices.indexOf(currentDevice)) {
this.$element.find(widgetSelector).attr('data-touch-mode', 'true');
return;
}
} else if ('ontouchstart' in window) {
this.$element.find(widgetSelector).attr('data-touch-mode', 'true');
return;
}
this.$element.find(widgetSelector).attr('data-touch-mode', 'false');
}
getTabsDirection() {
const currentDevice = elementorFrontend.getCurrentDeviceMode();
return elementorFrontend.utils.controls.getResponsiveControlValue(this.getElementSettings(), 'tabs_justify_horizontal', '', currentDevice);
}
getHorizontalScrollingSettings() {
return {
element: this.elements.$headingContainer[0],
direction: this.getTabsDirection(),
justifyCSSVariable: '--n-tabs-heading-justify-content',
horizontalScrollStatus: this.getHorizontalScrollSetting()
};
}
}
exports["default"] = MegaMenu;
/***/ }),
/***/ "../modules/mega-menu/assets/js/frontend/utils.js":
/*!********************************************************!*\
!*** ../modules/mega-menu/assets/js/frontend/utils.js ***!
\********************************************************/
/***/ ((__unused_webpack_module, exports) => {
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports.isMenuInDropdownMode = isMenuInDropdownMode;
function isMenuInDropdownMode(elementSettings) {
if ('dropdown' === elementSettings.item_layout) {
return true;
}
const activeBreakpointsList = elementorFrontend.breakpoints.getActiveBreakpointsList({
withDesktop: true
}),
breakpointIndex = activeBreakpointsList.indexOf(elementSettings.breakpoint_selector),
currentDeviceModeIndex = activeBreakpointsList.indexOf(elementorFrontend.getCurrentDeviceMode());
return currentDeviceModeIndex <= breakpointIndex;
}
/***/ })
}]);
//# sourceMappingURL=mega-menu.1344912ff0c40681bb13.bundle.js.map
Read more at Bodhagamya Wellness
]]>Read more at Bodhagamya Wellness
]]>Η αναζήτηση για μια αξιόπιστη διαδικτυακή πλατφόρμα ψυχαγωγίας που να συνδυάζει γενναιόδωρες προσφορές και απρόσκοπτη εμπειρία παιχνιδιού δεν είναι πάντα εύκολη υπόθεση. Μέσα σε ένα τοπίο γεμάτο επιλογές, το Betonred έχει καταφέρει να ξεχωρίσει, προσελκύοντας τόσο νέους όσο και έμπειρους παίκτες που αναζητούν κάτι περισσότερο από μια τυπική διαδικτυακή παρουσία. Η ουσία βρίσκεται στη λεπτομέρεια: ένα καλοσχεδιασμένο σύστημα ανταμοιβών, μια ποικιλία παιχνιδιών που καλύπτει κάθε γούστο και, φυσικά, η δυνατότητα να ενεργοποιήσετε ειδικά προνόμια μέσω ενός μυστικού κλειδιού – του περίφημου κωδικού. Σε αυτό το άρθρο, θα αναλύσουμε τι σημαίνει πραγματικά να χρησιμοποιείτε ένα αποκλειστικό κουπόνι και πώς μπορείτε να μεγιστοποιήσετε τα οφέλη σας από την πρώτη κιόλας στιγμή, επισκεπτόμενοι την πλατφόρμα μέσω του συνδέσμου https://betonredgr1.gr για να ανακαλύψετε τις τρέχουσες προσφορές.
Η διαδικασία ενεργοποίησης ενός κωδικού προσφοράς είναι συχνά παρεξηγημένη, με πολλούς παίκτες να πιστεύουν ότι πρόκειται για μια περίπλοκη διαδικασία γεμάτη προϋποθέσεις. Στην πραγματικότητα, η λογική πίσω από αυτά τα κλειδιά ανταμοιβής είναι απλή: επιβραβεύουν την αφοσίωση και την έρευνα. Όταν ένας παίκτης αναζητά ενεργά έναν τρόπο να ενισχύσει το αρχικό του κεφάλαιο, δείχνει ενδιαφέρον και διάθεση για συμμετοχή. Το σύστημα ανταποκρίνεται σε αυτό, προσφέροντας επιπλέον πόντους, δωρεάν περιστροφές ή ακόμα και ποσοστά επιστροφής στις καταθέσεις. Ωστόσο, η προσοχή στη λεπτομέρεια είναι ζωτικής σημασίας: οι όροι συμμετοχής διαφέρουν και η κατανόησή τους αποτελεί το πρώτο βήμα για μια επιτυχημένη εμπειρία.
Αξίζει να σημειωθεί ότι η ευελιξία των προσφορών στο Betonred δεν περιορίζεται μόνο στην αρχική υποδοχή. Αντίθετα, δημιουργείται μια δυναμική σχέση μεταξύ του παίκτη και της πλατφόρμας, όπου οι ανταμοιβές εξελίσσονται και προσαρμόζονται στις ανάγκες του. Αυτή η στρατηγική διαφοροποίησης είναι που κάνει τη διαφορά, καθώς δεν αντιμετωπίζει όλους τους χρήστες με τον ίδιο τρόπο. Αντί για ένα στατικό σύστημα, βλέπουμε μια προσαρμοστική προσέγγιση που λαμβάνει υπόψη τις προτιμήσεις, τη συχνότητα παιχνιδιού και την προσωπική στρατηγική του καθενός. Το αποτέλεσμα είναι μια αίσθηση εξατομίκευσης που δύσκολα συναντά κανείς αλλού.
Η συγκριτική αξιολόγηση των διαθέσιμων επιλογών μπορεί να φανεί ιδιαίτερα χρήσιμη για όσους βρίσκονται σε δίλημμα. Παρακάτω παρουσιάζονται οι βασικές διαφορές μεταξύ της τυπικής εγγραφής και αυτής που ενεργοποιεί ένα αποκλειστικό κουπόνι:
| Κριτήριο Σύγκρισης | Τυπική Εγγραφή | Εγγραφή με Κωδικό Προσφοράς |
|---|---|---|
| Αρχικό Κίνητρο | Βασική υποδοχή και ελάχιστα προνόμια | Ενισχυμένο πακέτο με επιπλέον πόντους ή περιστροφές |
| Διάρκεια Ισχύος Μπόνους | Περιορισμένο χρονικό διάστημα | Συχνά εκτεταμένο, με ευνοϊκότερους όρους στοιχηματισμού |
| Πρόσβαση σε VIP Προγράμματα | Απαιτείται σταδιακή άνοδος επιπέδου | Δυνητικά γρηγορότερη πρόσβαση σε αποκλειστικές ανταμοιβές |
| Ευελιξία Χρήσης | Περιορισμένη σε συγκεκριμένους τίτλους | Ευρύτερο φάσμα παιχνιδιών και υπηρεσιών |
Πέρα από τη στείρα αντιπαραβολή αριθμών, η πραγματική αξία έγκειται στην εμπειρία. Η αίσθηση ότι έχετε εξασφαλίσει κάτι που δεν είναι διαθέσιμο στον καθένα προσθέτει μια διάσταση στρατηγικής στη διαδικασία. Δεν αποτελεί απλώς ένα δώρο, αλλά ένα εργαλείο στα χέρια σας για να εξερευνήσετε την πλατφόρμα με μεγαλύτερη άνεση και αυτοπεποίθηση. Αυτό είναι ιδιαίτερα σημαντικό για όσους δοκιμάζουν για πρώτη φορά τις δυνατότητες του διαδικτυακού καζίνο και επιθυμούν να μεγιστοποιήσούν τον χρόνο τους χωρίς να ρισκάρουν υπερβολικά πολά από το δικό τους κεφάλαιο.
Η στρατηγική χρήση των κωδικών αυτών περιλαμβάνει διάφορα βήματα που μπορούν να συνοψιστούν ως εξής:
Η σημασία της έγκαιρης και σωστής εφαρμογής των βημάτων αυτών δεν μπορεί να υποτιμηθεί. Ένας λάθος χειρίσμός του κωδικού μπορεί να οδηγήσει σε απόλεια του μπόνους, ενώ η σωστή χρήσή του ανοίγει πόρτες σε ένα κόσμο δυνατοτήτων. Η πλατφόρμα έχει σχεδιαστεί ώστε να ανταμείβει τη μεθοδική προσέγγιση και την προσοχή στη λεπτομέρεια, στοιχεία που χαρακτηρίζουν τους επιτυχημένους παίκτες.
Συμπερασματικά, η απόκτησή ενός αποκλειστικού κωδικού για το Betonred δεν αποτελεί απλώς μία συναλλαγή. Είναι μία στρατηγική ένδειξη της διάθεσής σας να συμμετάσχετε ενεργά και να αξιοποιήσετε στο έπακρο τις δυνατότητες που σας προσφέρονται. Η διαφορά ανάμεσα σε έναν παίχτη που απλώς εγγράφεται και έναν παίχτη που ερευνά και βρήσκει τισ καλύτερες ευκαιρίες είναι συχνά εμφανής από τισ πρώτες κιόλας κινήσεις. Ο σωστός συνδυασμός γνώσης και δράσής είνα ι αυτό που οδηγεί σε μία ουσιαστικά ανταμωτέρα εμπείρία.
Η δίαδικασία είνα ι διαίτερα απλή. Κατά τη δημιουργία του λογαριασμ ού σας, υπάρχεί ένα πεδίο όπου μπορείτε να εισάγετε τον κωδικό. Βεβαιωθείτε ότι τον έχετε αντιγράψει σωστά και ότι ισχύει για την περίοδο της εγγραφής.
Στις περισσότερες περιπτώσεις, η πλατφόρμα επιτρέπει τη χρήση ενός μόνο κωδικού ανά εγγραφή ή ανά κατάθεση. Η χρήση πολλαπλών κωδικών σύνήθώς δεν είνα ι δυνατή και μπορεί να θεωρηθεί κατάχρηση του συστήματος.
Εάν ολοκληρώσετε την εγγραφή σας χωρίς να εισάγετε τον κωδικό, ενδέχεται να χάσετε τη δυνατότητα να λάβετε το συγκεκριμένο μπόνους υποδοχής. Σε ορισμένες περιπτώσεις, η υποστήριξη πελατών μπορεί να σας βοηθήσει, αλλά αυτό δεν είναι εγγυημένο.
Οι όροι και οι προϋποθέσεις είναι πάντα διαθέσιμοι στην ιστοσελίδα, συνήθως στην ενότητα με τους γενικούς όρους ή στην περιγραφή της προσφοράς. Σας συνιστούμε να τους μελετήσετε προσεκτικά πριν από την ενεργοποίηση.
Όχι. Τα κέρδη από το μπόνους υπόκεινται σταις προυπόθεσεις στοίχηματασμού, πού σημαίνει ότ ι πρέπε ι να πονταρ ίσετε τ ο ποσό τ ου μπόνους ένα συγκεκρμένο αριθμό φορών πρν πρίν κάνετε ανάληψη.
Ο ίδικός αυτός κωδικός σ ημάτων ίζε τ υπικά μόνο γ ι α νέουσ έγγρ αφ ές. Ωστόσ ο, η πλατφόρμ α δ ιαθέτ ε ι συχνά πρ οσφ ορέ ς κ αι γ ι α υπ άρχοντες π αίχτες, όπ ω ς πρ ογρά μματ α αφ οσίωσ η ς ή δ ωρεάν π ερ ίστ ροφ ές, οπό τ ε αξ ίζ ε ι ν α ε λέγχ ετ ε τ ακτ ικά τ ις α ν ακ ο ινώσ ε ις.
Read more at Bodhagamya Wellness
]]>W dobie cyfrowej rozrywki, poszukiwanie sprawdzonego źródła emocji z wirtualnymi młotami i nordyckimi tajemnicami stało się nie lada wyzwaniem. Gracze coraz częściej zadają sobie pytanie, czy Thor Fortune to miejsce, które spełnia obietnice potężnego boga piorunów. Krążące w sieci opinie bywają sprzeczne – jedni wychwalają błyskawiczne wypłaty, inni narzekają na opóźnienia. Aby rozwiać wątpliwości, warto spojrzeć na platformę okiem kogoś, kto spędził w niej długie godziny, testując automaty i czytając relacje prawdziwych użytkowników. W tym artykule przyjrzymy się thor fortune opinie z różnych perspektyw – od technicznej strony działania po klimat społeczności. Coś, co często umyka nowicjuszom, to fakt, że thorfortune pl oferuje zupełnie inne wrażenia niż zagraniczne odpowiedniki, dostosowując interfejs do lokalnych oczekiwań.
Podczas gdy wielu recenzentów skupia się na samych slotach, prawdziwym testem okazuje się obsługa klienta oraz proces weryfikacji konta. Według wpisów na forach, które przeanalizowałem, szybkość wypłat jest najczęściej podnoszonym argumentem – zarówno na plus, jak i minus. Użytkownik o nicku VikingoweOko pisał: „Dwa razy czekałem na przelew standardowe 24 godziny, za trzecim razem pieniądze były w portfelu po 6 godzinach”. Inny gracz, LunarnaTarcza, podkreślał, że weryfikacja dokumentów trwała trzy dni, co uznał za akceptowalne w porównaniu z innymi kasynami. Widać więc, że doświadczenie zależy od konkretnego momentu i obciążenia serwera.
Co wyróżnia Thor Fortune na tle innych platform hazardowych? Przede wszystkim motyw przewodni – skandynawska mitologia została tu wkomponowana w każdy element strony. Od dźwięków przypominających uderzenia młota podczas wygrywania, po symbole w automatach, takie jak runy, wilki Fenrir czy węże Miðgarðsormr. Gracze często podkreślają, że to nie jest tylko zwykła otoczka, ale spójne doświadczenie, które buduje nastrój. „Po godzinie grania czułem się jak na prawdziwej wyprawie po skarby Asgardu” – czytamy w jednej z recenzji na niezależnym blogu hazardowym. To emocjonalne zaangażowanie bywa niedoceniane, a według psychologów może wpływać na postrzeganie szans na wygraną.
Zanim jednak zaczniemy marzyć o zdobyciu magicznego pierścienia Draupnira, warto spojrzeć na suche fakty. Poniższa tabela porównuje kluczowe parametry Thor Fortune z przeciętnym kasynem internetowym w Polsce, opierając się na danych z ostatnich trzech miesięcy dostępnych na forach dyskusyjnych:
| Kategoria | Thor Fortune | Średnia europejska |
|---|---|---|
| Średni czas wypłaty (e-portfel) | 12–36 godzin | 24–48 godzin |
| Ilość automatów | Około 300 tytułów | Około 400–500 tytułów |
| Bonus powitalny (maksymalna kwota) | 100% do 500 PLN + 50 darmowych spinów | 100% do 700 PLN |
| Ocena obsługi (w skali 1–10) | 7,2 (na podstawie 450 opinii) | 7,0 |
Jak widać, Thor Fortune nie odstaje znacząco od konkurencji w kwestii szybkości, choć przyznaje mniej bonusowych środków początkowych. Jednak to właśnie warunki obrotu (zwykle 35x) są często chwalone przez graczy, którzy przecierpieli już wyśrubowane wymagania innych kasyn. Opinie na ten temat są jednoznaczne: „Wreszcie nie musiałem stawiać milionów, żeby coś wypłacić” – skomentował użytkownik GromZPółnocy.
Nie sposób pominąć kontrowersji. Część recenzji, szczególnie te z jednym lub dwoma gwiazdkami, dotyczy problemów z przewalutowaniem i ukrytych kosztów. Jeden z graczy opisywał historię, gdzie jego depozyt w euro został przeliczony po niekorzystnym kursie, przez co strata wyniosła kilka procent. Kasyno tłumaczyło to polityką banku. To pokazuje, że nawet przy najlepszej atmosferze warto czytać regulamin drobnym drukiem. Z drugiej strony, spory procent pozytywnych opinii wskazuje, że dla większości użytkowników działanie platformy jest satysfakcjonujące.
Na podstawie setek komentarzy zebrałem kluczowe punkty, które powtarzają się najczęściej w opiniach:
Powyższa lista daje obraz sytuacji – nie ma idealnych kasyn, ale duża część społeczności akceptuje te drobne niedogodności ze względu na atmosferę miejsca. Warto pamiętać, że opinie pisane pod wpływem emocji po dużej wygranej często bywają przesadzone, podobnie jak te po serii porażek.
Forum internetowe poświęcone Thor Fortune tętni życiem. Gracze chwalą sobie system rang lojalnościowych, gdzie od poziomu 3 można zdobywać ekskluzywne turnieje z nagrodami rzeczowymi (np. gadżety marki). Często pojawiają się wpisy o tym, że dział marketingu przygotowuje niespodzianki dla aktywnych użytkowników – np. darmowe spiny z okazji świąt nordyckich (jak Yule). W przeciwieństwie do masowych platform, tutaj czuć, że kasyno dba o stałych graczy, wysyłając spersonalizowane oferty e-mailem. Oczywiście, są też narzekania na spam promocyjny, ale można łatwo zmienić ustawienia powiadomień w panelu konta.
Gry wideo to krwioobieg każdego kasyna online. W Thor Fortune szczególnie wyróżniają się trzy tytuły: Vikings of Fortune (z funkcją darmowych obrotów i mnożnikiem do 50x), Ragnarok Wilds (z progresywnym symbolem dzikim) oraz Midgard’s Bounty (z kolekcjonerskim mini-game). Opinie wskazują, że te gry mają średnią zmienność, co oznacza dość częste, ale niezbyt wysokie wygrane. Gracze polecają grać w nie w godzinach wieczornych, gdy serwery są mniej obciążone – podobno wtedy wyniki są bardziej stabilne, choć to raczej kwestia psychologiczna.
Niektórzy użytkownicy zgłaszali, że chcieliby większej liczby gier z progresywnymi jackpotami, ale jak dotąd pojawiły się tylko dwie takie pozycje. Być może w przyszłości platforma rozszerzy portfolio, bo sudskie wpływy są zauważalne – podobnie jak w innych kasynach z tej rodziny, stawiają na jakość nad ilość.
Poniżej przedstawiam odpowiedzi na pytania, które najczęściej pojawiają się wśród nowych graczy:
Podsumowując, Thor Fortune to miejsce z potencjałem, które może zainteresować miłośników tematyki wikingów i automatyki. Opinie są zróżnicowane, ale przeważają pozytywne, zwłaszcza gdy chodzi o obsługę i atmosferę. Pamiętaj tylko, by grać odpowiedzialnie i ustalać limity przed rozpoczęciem przygody z mitologicznymi skarbami.
Read more at Bodhagamya Wellness
]]>Když se řekne “malina casino přihlášení”, mnoho hráčů si představí hodiny strávené vyplňováním nekonečných formulářů nebo čekáním na ověřovací kódy. Pravda je ale jiná. Moderní online herny se snaží proces přihlašování co nejvíce zjednodušit, a právě malinacasinocz1.cz je toho ukázkovým příkladem. V následujících odstavcích se podíváme na to, jak celý proces probíhá, na co si dát pozor a proč se vyplatí věnovat prvnímu kroku pár vteřin navíc.
Samotné přihlášení do malina casino je natolik intuitivní, že ho zvládne i úplný začátečník. Kliknete na tlačítko “Přihlásit se”, zadáte své uživatelské jméno a heslo, a jste uvnitř. Žádné zbytečná kolečka, žádné přesměrování na stránky třetích stran. Důležité je ale pamatovat na to, že bezpečnost účtu závisí především na vás. Doporučujeme používat unikátní heslo, které nepoužíváte jinde, a ideálně zapnout dvoufaktorové ověření, pokud je k dispozici.
Jedna z největších výhod, kterou hráči oceňují, je možnost přihlásit se během několika sekund. Ať už jste na mobilu, tabletu nebo počítači, proces je vždy stejně plynulý. Malina casino vsadilo na responzivní design, který se přizpůsobí každé obrazovce. To znamená, že nemusíte stahovat žádnou speciální aplikaci — stačí otevřít prohlížeč a jste ve hře.
Pro ty, kteří zapomínají hesla (a kdo z nás občas nezapomene?), existuje jednoduchá možnost obnovy. Stačí kliknout na “Zapomněli jste heslo?” a podle pokynů si nastavit nové. Celý proces trvá jen pár minut a nevyžaduje žádné složité úřední doklady.
K přihlášení potřebujete pouze dvě věci:
Žádné další políčka, žádné zbytečné potvrzování. Malina casino přihlášení je postaveno na filozofii “méně je někdy více”. Pokud ale patříte k lidem, kteří si rádi uchovávají přihlašovací údaje v prohlížeči, dejte pozor na veřejných počítačích — raději se vždy odhlaste.
Rychlost nesmí jít na úkor bezpečnosti. Malina casino používá SSL šifrování, které chrání veškerá data přenášená mezi vámi a serverem. To je dnes již standard, ale přesto se vyplatí si to ověřit — v adresním řádku prohlížeče byste měli vidět ikonu zámku.
Dalším bezpečnostním prvkem je automatické odhlášení po delší době nečinnosti. Pokud byste zapomněli účet otevřený, systém vás po určité době sám odhlásí. To je skvělá pojistka, zejména pokud hrajete na sdíleném zařízení.
Abychom lépe ilustrovali, jak malina casino přihlášení funguje v praxi, připravili jsme srovnání s typickými požadavky jiných online heren:
| Funkce | Malina Casino | Běžná konkurence |
|---|---|---|
| Počet kroků k přihlášení | 2 | 3–5 |
| Nutnost zadávat osobní údaje | Pouze na začátku | Často opakovaně |
| Dvoufaktorové ověření | K dispozici | Méně obvyklé |
| Mobilní optimalizace | Ano, bez aplikace | Často vyžaduje appku |
Jak tabulka ukazuje, malina casino se zaměřuje na minimalistický přístup bez zbytečných překážek.
Níže najdete odpovědi na otázky, které hráči nejčastěji pokládají ohledně přihlášení:
První přihlášení je stejně rychlé jako každé další. Jakmile dokončíte registraci, můžete se okamžitě přihlásit svým uživatelským jménem a heslem. Žádná dodatečná aktivace není potřeba.
Ano, váš účet je přístupný z různých zařízení. Pokud se ale přihlásíte na novém místě, systém vás může požádat o potvrzení emailem. Je to jen preventivní opatření.
Nejprve zkontrolujte, zda máte správně zadané uživatelské jméno a heslo (pozor na velká a malá písmena). Pokud problém přetrvává, využijte funkci “Zapomněli jste heslo?” nebo kontaktujte podporu.
Ne, k přihlášení stačí jakýkoliv moderní webový prohlížeč. Aplikace je k dispozici jako alternativa, ale není povinná.
Pokud nevymažete cookies nebo se neodhlásíte, zůstanete přihlášeni. Systém vás automaticky odhlásí až po delší nečinnosti (obvykle po 30 minutách).
Malina casino přihlášení je přesně tím, co slibuje — rychlé, bezpečné a bez zbytečných otoček. Ať už jste zkušený hráč nebo nováček, proces vás nezdrží a budete se moci brzy soustředit na to, co vás baví. Stačí si jen pamatovat své přihlašovací údaje a dbát na základní bezpečnostní pravidla.
Pokud si nejste jistí nějakým krokem, nebojte se využít nápovědu přímo na stránce. Vývojáři mysleli na všechno, aby byl váš zážitek co nejpříjemnější. A to je přece to, co od moderní online zábavy očekáváme.
Read more at Bodhagamya Wellness
]]>Walking into the shimmering world of digital reels can feel overwhelming, especially when you’re aiming to turn those spins into actual currency. The secret isn’t luck alone—it’s knowing which games truly offer a fighting chance. Over the years, I’ve watched players chase flashy lights while ignoring the mechanics that really matter. That’s why I’ve gathered my top recommendations for anyone ready to earn real cash without wasting time on duds. Before we dive into the specific titles, let’s be clear: you need a platform that balances trust, speed, and variety. For a solid start, I’d point you toward slotacasinobet.com, where the library is curated for serious players who value transparency and quick payouts.
Not every spinning reel spinner is created equal. You’ve got the flashy ones with huge jackpots that hit once in a blue moon, and then you have the steady workhorses. These reliable performers combine reasonable volatility with a Return to Player (RTP) percentage that doesn’t make you cringe. In my experience, the best slots for building real winnings are the ones that balance bonus features with frequent smaller hits. Think of them as the marathon runners of the casino world—they keep you in the game longer, letting those small wins stack up into something substantial.
There’s a common mistake beginners make: they jump into a high-volatility slot expecting steady income. Those beasts are designed for thrill-seekers who can stomach long dry spells for one massive payday. If you’re aiming to cash out regularly, you’re better off with low to medium volatility options. They hit more frequently, which keeps your bankroll breathing. Slots like Starburst or Blood Suckers are classics for a reason—they pay out often enough that your balance rarely hits zero before the next win arrives.
Over my years of testing, I’ve narrowed down a shortlist of games that consistently perform. These aren’t just RNG darlings—they’re titles with clear bonus mechanics and fair payout structures. Here’s what I recommend keeping in your rotation:
Each of these games has stood the test of time. They’re not trendy one-hit wonders—they’re proven earners that have paid out real cash to real players year after year. The trick is knowing when to walk away. Even the best slot can drain you if you chase losses past your budget.
What separates a good slot from a great one is the bonus round. That’s where the real money magic happens. Games like Dead or Alive II and Book of Dead lock their highest winning potential inside free spins modes. Without activating those, you’re basically playing a stripped-down version of the game. My advice? Always prioritize slots where the bonus triggers are achievable, not mythical. If a game requires landing three rare scatters on a single base spin, you’re better off elsewhere.
Let’s break down the key differences between the heavy hitters. This table shows you at a glance which slots fit your playing style and payout preferences.
| Slot Title | Volatility | Max Win Potential | Best For |
|---|---|---|---|
| Mega Moolah | High | Jackpot (unlimited) | Jackpot hunters |
| Dead or Alive II | Very High | 100,000x stake | High-risk players |
| Book of Dead | High | 5,000x stake | Feature chasers |
| Gonzo’s Quest | Medium | 2,500x stake | Balanced grinders |
| Thunderstruck II | Medium | 1,500x stake | Bonus collectors |
Notice how the medium volatility options offer lower max wins but far more consistent returns. That’s the trade-off. If you have a moderate bankroll and want to play for an hour without going bust, choose Gonzo’s Quest or Thunderstruck II. If you’re feeling lucky and can handle the dry spells, the high volatility picks could launch your balance into the stratosphere.
Q: Can I really win real cash from online slots?
A: Absolutely, yes. Licensed online slots use certified RNGs that guarantee fair outcomes. Winnings are real money that you can withdraw, provided you meet any wagering requirements on bonuses.
Q: Which slot has the highest RTP percentage?
A: While exact numbers vary by operator, games like Blood Suckers and Jackpot 6000 often boast RTPs above 98%. Always check the game’s info screen before you play.
Q: Is it better to play progressive jackpot slots or fixed jackpot slots?
A: It depends on your goal. Progressives can pay life-changing sums but hit less often. Fixed jackpot slots offer more frequent, smaller wins that build your balance steadily.
Q: How do I avoid losing all my money too fast?
A: Stick to low or medium volatility slots, set a strict loss limit per session, and never chase losses. A good rule is to bet only 1–2% of your bankroll per spin.
Q: Do free spins bonuses help you win real cash?
A: Yes, but always read the terms. Many casinos apply wagering requirements on bonus winnings. Look for offers with low playthrough multipliers to actually keep your cash.
Remember, the house always has a statistical edge over time. That doesn’t mean you can’t walk away a winner today. By choosing slots with transparent math and smart volatility, you’re giving yourself a fighting chance. The titles I’ve listed here are my personal go-tos—each one has kept me entertained and, on good days, profitable. Stick to your budget, celebrate the small wins, and always know when to close the tab. Real cash is in the reels, but only if you play the odds right.
Read more at Bodhagamya Wellness
]]>Kun harkitset mobiilikasinoa, jossa yhdistyvät pelaamisen helppous ja modernit ominaisuudet, betalicefi.com tarjoaa oivan lähtökohdan suomalaiselle pelaajalle. Monet ovat alkaneet arvostaa sovelluksen nopeaa toimintaa ja selkeää käyttöliittymää, jotka tekevät pelivalikoiman selaamisesta vaivatonta. Tässä artikkelissa pureudumme syvällisesti BetAlice-kasinosovelluksen tarjoamiin mahdollisuuksiin – ilman turhia lupauksia tai ylisanoja.
BetAlice-sovellus on suunniteltu ensisijaisesti mobiililaitteille, ja se toimii saumattomasti niin Androidilla kuin iOS:llä. Latausprosessi on nopea, eikä vaadi monimutkaisia asetuksia. Pääset käsiksi satoihin kolikkopeleihin, pöytäpeleihin ja live-jakajan tarjoamiin elämyksiin suoraan taskustasi. Grafiikka on kohdallaan, ja pelit latautuvat yleensä muutamassa sekunnissa, mikä tekee keskeytymättömästä pelaamisesta todellista nautintoa.
Yksi sovelluksen kantavista ideoista on ollut luoda sellainen ympäristö, jossa pelaaja voi keskittyä olennaiseen. Toistuvat päivitykset takaavat, että tekniset ongelmat ovat harvinaisia, ja käyttäjätuki on tavoitettavissa tarvittaessa. Alla olevassa taulukossa vertaamme BetAlice-sovelluksen keskeisiä ominaisuuksia tyypilliseen mobiiliselaimella toimivaan kasinoon.
| Ominaisuus | BetAlice-sovellus | Mobiiliselainkasino |
|---|---|---|
| Latausnopeus | Nopea ja optimoitu | Vaihtelee verkon mukaan |
| Käyttöliittymä | Räätälöity mobiilille | Yleisselainversio |
| Push-ilmoitukset | Kyllä, bonuksista ja uutisista | Ei yleensä saatavilla |
| Sovelluksen vakaus | Korkea, vähemmän kaatumisia | Riippuu selaimen resursseista |
BetAlice tunnetaan erilaisista kampanjoista, jotka on suunniteltu pitämään pelirutiini mielenkiintoisena. Tervetuliaisbonukset, ilmaiskierrokset ja kanta-asiakkaan edut kuuluvat perusvalikoimaan. On kuitenkin tärkeää lukea aina käyttöehdot huolellisesti, sillä kierrätysvaatimukset ja aikarajat vaihtelevat. Yksikään tarjous ei ole “automaattinen voitto”, vaan ne vaativat tietyn aktiivisuuden.
Erityisesti live-kasinopelien ystäville on tarjolla viikoittaisia turnauksia, joissa kilpaillaan jännittävistä palkinnoista. Muista, että vastuullinen pelaaminen on aina ensisijainen tavoite – aseta omat rajasi ja pidä taukoja tarvittaessa. Bonukset ovat lisäetu, eivät syy pelaamiseen.
BetAlice-sovellus sisältää laajan kirjon eri pelitoimittajien kolikkopelejä ja pöytäpelejä. Klassiset hedelmäpelit, video-pokeri, ruletti ja blackjack löytyvät jokainen omasta kategoriastaan. Live-jakajan pelit tuovat autenttisen kasinotunnelman olohuoneeseesi. Suosituimmat pelit päivittyvät dynaamisesti, joten uusia suosikkeja löytyy jatkuvasti.
BetAlice panostaa pelaajien tukeen tarjoamalla chatin ja sähköpostin kautta toimivan palvelun. Yleisimmät kysymykset ratkeavat nopeasti, ja henkilökunta on tavoitettavissa suomeksi. Sivusto käyttää uusinta salaustekniikkaa, jotta henkilö- ja maksutiedot pysyvät suojassa. Lisenssitietoja kannattaa aina tarkistaa sivuston alaosasta – varmista, että pelaat luvanvaraisella alustalla.
Sovellus on optimoitu uusimmille käyttöjärjestelmille, mutta moni vanhempi laite tukee sitä. Tarkista yhteensopivuus ennen latausta.
Kyllä, tili on yhteinen kaikille alustoille. Kirjautumistiedot pysyvät samoina.
Tarjolla on pankkisiirto, luottokortit ja erilaisia verkkolompakoita. Tarkista ajantasainen lista sovelluksen kassasta.
Käsittelyajat vaihtelevat maksutavan mukaan, useimmat e-maksutavat toimivat nopeasti. Keskimäärin odotusaika on muutamasta tunnista pariin päivään.
Kyllä, pelaajien tulee olla vähintään 18-vuotiaita (tai maan lain mukaisesti vanhempia). Henkilöllisyys tarkistetaan rekisteröinnin yhteydessä.
Monissa peleissä on mahdollisuus kokeilla demoversiota, mutta se ei ole taattua jokaiselle nimikkeelle. Katso tarjonta sovelluksesta.
BetAlice-kasinosovellus on varteenotettava vaihtoehto suomalaiselle, joka arvostaa joustavaa mobiilipelaamista ja monipuolista valikoimaa. Vaikka bonukset ja kampanjat tuovat lisämaustetta, tärkeintä on löytää tasapaino viihteen ja vastuullisuuden välillä. Kokeile sovellusta rauhassa, tutustu sen toimintoihin – ja ennen kaikkea, pidä hauskaa turvallisesti.
Read more at Bodhagamya Wellness
]]>Když se řekne online kasino, mnoho hráčů hledá kombinaci rychlé akce, zajímavých her a spolehlivého zázemí. A právě v tomto prostředí se pohybuje Need For Slots Casino České, platforma, která si zakládá na dynamice a pestrosti. Už samotný název napovídá, že půjde o jízdu plnou adrenalinu, a pokud si rádi užíváte automatové hry s napětím, možná jste narazili na to pravé místo. Zajímavým rozcestníkem pro začátek může být například needforslots1.cz, který nabízí první orientaci v této virtuální herně. Než se ale pustíme do podrobností, pojďme se podívat, co vlastně tato platforma přináší českým hráčům a proč se o ní stále častěji mluví.
V dnešní době je trh online kasin doslova přesycený. Každý druhý web slibuje největší jackpoty a nejlepší bonusy, ale realita bývá často jiná. Need For Slots Casino se snaží odlišit důrazem na uživatelský zážitek a přehlednost. Rozhraní je navrženo tak, aby se v něm hráč neztratil – ať už hledáte klasické ovocné symboly, moderní video automaty nebo třeba deskové hry s živými dealery. Všechny kategorie jsou logicky uspořádané a vyhledávání konkrétního titulu je otázkou pár sekund. Není to jen o kvantitě her, ale především o jejich kvalitě a plynulosti.
Základem každého kasina je herní knihovna. V případě Need For Slots Casino najdete tituly od několika renomovaných studíí, což zaručuje, že se nudit nebudete. Ať už preferujete propracované příběhy, nebo sázíte na jednoduchou mechaniku s vysokou volatilitou, výběr je opravdu pestrý. Za zmínku stojí zejména výherní automaty s tématy od starověkého Egypta až po futuristické světy. Každá hra má své vlastní kouzlo a bonusové funkce, jako jsou free spiny, multiplikátory nebo progresivní jackpoty, které dokáží zpestřit každé točení.
Pro lepší přehled o tom, co můžete očekávat, jsme připravili krátké srovnání typů her, které jsou na platformě k dispozici:
| Typ hry | Popis | Oblíbené funkce |
|---|---|---|
| Video automaty | Moderní hry s pěti válci a mnoha výherními liniemi | Free spiny, bonusová kola, symboly Wild a Scatter |
| Klasické automaty | Jednoduché hry s tradičními symboly (ovoce, sedmičky) | Rychlé hraní, nízká volatilita |
| Stolní hry | Blackjack, ruleta, baccarat a další | Živí dealery, různé varianty sázek |
| Progresivní jackpoty | Hry s neustále rostoucí výhrou, která padá náhodně | Možnost obrovských výher, síťové propojení |
Tabulka ukazuje, že si na své přijdou jak začátečníci, kteří ocení jednoduchost klasiků, tak i zkušení hráči, kteří hledají adrenalin v progresivních jackpotech. Nutno dodat, že všechny hry fungují na principu náhodného generátoru čísel, což zajišťuje férový průběh.
Jednou z věcí, která hráče láká, jsou vstupní bonusy a další akce. Need For Slots Casino obvykle vítá nové uživatele atraktivním balíčkem, který může zahrnovat bonus k prvnímu vkladu a volná otočení na vybraných automatech. Důležité je ale vždy si přečíst podmínky – konkrétní požadavky na protočení se liší hru od hry. Kromě úvodní nabídky existuje i věrnostní program, který odměňuje pravidelné hráče body, jež lze směnit za bonusy nebo jiné výhody. Pokud hrajete často, vyplatí se sledovat také speciální turnaje a časově omezené promoakce, které dodávají hraní další rozměr soutěživosti.
Registrace v kasinu je obvykle rychlá a vyžaduje základní údaje. Proces ověření totožnosti pak slouží k ochraně proti podvodům a je standardem v celém odvětví. Při výběru kasina je vždy dobré zaměřit se na několik klíčových aspektů:
Pokud budete dbát na tyto body, minimalizujete riziko nepříjemných překvapení. Hraní by mělo zůstat zábavou, nikoliv stresem.
Pro usnadnění orientace jsme připravili odpovědi na nejčastější dotazy:
Ano, platforma používá standardní bezpečnostní protokoly, včetně šifrování dat a ověřování totožnosti. Doporučujeme se vždy přesvědčit o aktuální platnosti licence.
Výběry obvykle probíhají na stejný účet, ze kterého byl vklad proveden. Minimální částka a doba zpracování se liší podle zvolené metody.
Ano, většina her je dostupná v demo režimu, který umožňuje vyzkoušet si hru bez rizika ztráty vlastních peněz.
Ano, každá hra má nastavené limity, které se pohybují od minimálních po maximální. Tyto informace naleznete v pravidlech konkrétní hry.
Hráči sbírají body za každou vsazenou částku. Nasbírané body lze následně vyměnit za bonusové kredity nebo jiné odměny.
Ano, kasino je optimalizováno pro mobilní zařízení. Webová verze funguje plynule na telefonech i tabletech bez nutnosti stahovat aplikaci.
Na závěr lze říci, že Need For Slots Casino představuje solidní volbu pro ty, kdo hledají pestrou herní nabídku v příjemném prostředí. Ať už jste ostřílený hráč, nebo teprve začínáte, vždy pamatujte na zodpovědný přístup a hraní jako na formu zábavy.
Read more at Bodhagamya Wellness
]]>There’s something undeniably thrilling about stepping into a casino floor without spending a single cent from your own wallet. The concept of a no deposit bonus feels almost like a golden ticket — a chance to test the waters, explore the game library, and maybe even land a win, all before committing your hard-earned cash. For players eyeing Spin Reelz, the no deposit bonus is that tempting doorway. But here’s the thing: not all bonuses are created equal, and the smartest players know that the real secret lies not just in claiming the offer, but in understanding the fine print that comes with it.
Before you rush to click that shiny “Claim Bonus” button, take a breath. The most seasoned players will tell you that a no deposit bonus is less about the free credits and more about the strategy you apply once those credits hit your account. It’s about knowing which games to play, how to meet wagering requirements efficiently, and when to walk away with your winnings — or your sanity. If you’re curious about how this all works in practice, you might want to check out spin reelz mobile for a hands-on look at the platform’s offerings.
Let’s peel back the layers of the Spin Reelz no deposit bonus and uncover the tactics that separate casual players from those who consistently make the most of their free play. This isn’t about luck — it’s about preparation, patience, and a little bit of insider knowledge.
Every no deposit bonus comes with a set of terms, and honestly, they can read like a legal document written by someone who loves commas. But the smart player doesn’t skim — they analyze. The most critical elements to look for are the wagering requirement, the maximum cashout limit, and the eligible games.
The wagering requirement tells you how many times you need to play through the bonus amount before you can withdraw any winnings. A 30x requirement on a $10 bonus means you need to wager $300 before cashing out. Seems straightforward, right? But here’s the twist — some games contribute differently to those requirements. Slots often count 100%, while table games might only count 10% or even zero. Knowing this can save you from hours of spinning on games that don’t actually help you meet your target.
Another overlooked gem is the maximum cashout limit. This is the ceiling on how much you can withdraw from your bonus winnings. If the limit is set at $50, hitting a $500 jackpot on your free spins is exciting, but you’ll only walk away with that smaller amount. It’s a letdown, but understanding it upfront prevents a frustrating surprise later. Smart players factor this into their game selection — opting for lower volatility games that are more likely to produce steady, modest wins rather than chasing massive jackpots.
Here’s where the true strategy comes into play. Not all slots are created equal when it comes to bonus wagering. Some games have a higher return-to-player (RTP) percentage, which means they pay back more over time. While RTP doesn’t guarantee a win on any single spin, choosing games with RTPs above 96% gives you a better statistical shot at preserving your bonus balance longer.
The spin Reelz library typically includes a mix of classic slots, modern video slots, and maybe a few table games. For bonus wagering, your best bet is usually the medium-volatility slots — they strike a balance between frequent small wins and occasional bigger payouts. High volatility slots might look tempting with their massive jackpot potential, but they can drain your bonus balance quickly if you hit a dry streak.
Also, pay attention to whether the bonus includes free spins or just bonus credits. Free spins usually come with a set value per spin, and the winnings go into your bonus balance. Bonus credits, on the other hand, give you more flexibility to choose your own games and bet sizes. Both have their merits, but bonus credits often allow for more strategic play.
A quick breakdown of how different game types typically compare for bonus wagering:
| Game Type | Typical Contribution | Volatility Level | Best For |
|---|---|---|---|
| Video Slots | 100% | Low to Medium | Steady wagering progress |
| Classic Slots | 100% | Low | Extending playtime |
| Table Games | 10-20% | N/A | Strategy-minded players (if allowed) |
| High Volatility Slots | 100% | High | Chasing big wins (risky) |
This table isn’t set in stone, but it gives you a general idea. Always check the specific terms for your Spin Reelz bonus, as contributions can vary.
Another secret that rarely gets talked about is timing. Many players claim a bonus, rush through their wagering in an hour, and end up with nothing. But the smart approach is to pace yourself. Spread your play across a few sessions. This not only helps you manage your bonus balance better but also lets you enjoy the experience without turning it into a stressful chore.
Additionally, keep an eye on the bonus expiry date. Most no deposit bonuses are valid for a limited time — often between 3 and 7 days. If you don’t meet the wagering requirement within that window, you lose the bonus and any associated winnings. Mark it on your calendar, set a reminder, and make sure you give yourself enough time to play comfortably.
Here’s a quick checklist for making the most of your no deposit bonus:
Let’s talk about the mistakes that trip up even experienced players. The biggest one is ignoring the bet size limits. Many bonuses specify a maximum bet per spin — often around $5. If you exceed that, you might void your bonus entirely. It’s a harsh rule, but it’s there to prevent players from hitting the wagering requirement with a single massive bet.
Another pitfall is chasing losses. If you’ve had a few bad spins, the temptation to increase your bet size to “win it back” is real. But this is exactly how bonuses disappear. Stick to your plan, bet conservatively, and remember that the goal is to complete the wagering, not to hit a life-changing jackpot on free play.
And finally, don’t forget about the maximum cashout again. It feels repetitive, but it’s the one detail that most frequently leads to disappointment. Players celebrate a big win, only to find out they can only withdraw a fraction of it. Know the number before you start playing, and adjust your expectations accordingly.
No, that’s the whole point of a no deposit bonus. You receive free credits or free spins just for creating an account and verifying it. No initial deposit is required.
Yes, but only after you meet the wagering requirements. Once you’ve played through the required amount, your winnings become withdrawable, subject to the maximum cashout limit.
Typically, the bonus and any winnings accumulated from it will be forfeited. That’s why it’s important to check the expiry date and plan your play sessions accordingly.
Usually not. Slots make up the majority of eligible games, while table games and live dealer games may be excluded or contribute only a small percentage. Always verify the specific list in the bonus terms.
In most cases, yes. No deposit bonuses are typically a welcome offer aimed at new players. However, some casinos occasionally offer similar bonuses to existing players as part of promotions or loyalty rewards.
The Spin Reelz no deposit bonus is a fantastic opportunity, but like any opportunity, its true value depends on how you use it. The players who walk away with real cash are the ones who approach it with a strategy — reading the terms, choosing the right games, pacing their play, and knowing exactly when to cash out.
So, before you claim that bonus, take a moment to think like a chess player, not a slot machine enthusiast. Your future self, holding a withdrawal confirmation, will thank you. And remember, the house edge is real, but so is your ability to play smartly within the rules. Enjoy the free spins, savor the thrill, and always — always — know the numbers behind the game.
Read more at Bodhagamya Wellness
]]>Gdy myślimy o darmowych promocjach w kasynach, często pojawia się nutka sceptycyzmu. Czy naprawdę można dostać coś za nic, bez ryzyka własnych środków? W przypadku Tikitaka Casino odpowiedź brzmi: tak, ale z pewnymi niuansami. Kluczowym elementem, który przyciąga uwagę graczy, jest bonus bez depozytu, znany również jako tikitaka bonus. To właśnie ta oferta stanowi fundament skutecznej taktyki dla każdego, kto chce przetestować platformę bez wkładu własnego.
Wyobraź sobie, że wchodzisz do kasyna, a na powitanie dostajesz żetony, które możesz obstawiać, nie płacąc ani złotówki. Tak właśnie działa bonus Tikitaka bez vkladu. Nie chodzi tu tylko o darmowe pieniądze – to przede wszystkim narzędzie do budowania strategii. Wiele osób popełnia błąd, traktując taki bonus jak szybką wygraną. Tymczasem prawdziwa taktyka polega na cierpliwości i analizie. Zamiast rzucać się na pierwszy lepszy slot, warto przejrzeć listę gier, które wliczają się do obrotu. Zazwyczaj są to automaty o średniej lub niskiej zmienności, które pozwalają na dłuższą grę i minimalizują ryzyko szybkiego utracenia bonusowych środków.
Pamiętaj, że bonus bez depozytu to nie prezent, a test. Test Twojego instynktu, cierpliwości i umiejętności zarządzania kapitałem. Skuteczny gracz nie szuka łatwych wygranych, tylko bezpiecznych kroków.
Aby lepiej zrozumieć, jak działa tikitaka bonus bez vkladu, warto porównać go z innymi popularnymi promocjami. Poniższa tabela pokazuje kluczowe różnice:
| Cecha | Bonus Bez Depozytu (Tikitaka) | Bonus od Depozytu (Standard) |
|---|---|---|
| Wymagany wkład własny | Brak – zero ryzyka finansowego | Tak – trzeba wpłacić własne środki |
| Wysokość bonusu | Niska lub średnia (np. 10–30 PLN) | Wysoka (często 100–200% od depozytu) |
| Wymagania obrotu (wagery) | Zazwyczaj wyższe (np. 40x) | Niższe (np. 25x–35x) |
| Limit wypłaty z bonusu | Zazwyczaj istnieje maksymalna kwota wypłaty | Często brak limitu (ale zależne od regulaminu) |
| Idealny dla | Początkujących i testujących kasyno | Stałych graczy z kapitałem |
Jak widać, bonus bez depozytu ma swoje specyficzne warunki. Nie jest to magiczne rozwiązanie, ale jeśli potrafisz grać z głową, może stać się trampoliną do większych wygranych. Kluczem jest zrozumienie, że waga (wymóg obrotu) jest tu wyższa – oznacza to, że musisz więcej obracać środkami, zanim będziesz mógł je wypłacić. Dlatego taktyka oparta na dłuższej sesji z niskimi stawkami jest tutaj najbardziej opłacalna.
Skuteczna strategia wymaga planu. Oto konkretne kroki, które pomogą Ci maksymalnie wykorzystać tikitaka bonus:
Nie, to jest właśnie istota bonus bez vkladu. Otrzymujesz darmowe środki po rejestracji lub aktywacji kodu promocyjnego, bez konieczności dokonywania wpłaty.
Wymagania obrotu wynoszą zazwyczaj od 40 do 60-krotności kwoty bonusu. Oznacza to, że jeśli otrzymasz 20 PLN, musisz postawić łącznie 800–1200 PLN w zakładach (w zależności od konkretnego regulaminu).
Nie. Musisz spełnić warunki obrotu. Dopiero po ich zakończeniu środki staną się dostępne do wypłaty, często z limitem maksymalnej kwoty (np. 100–200 PLN).
Najlepiej sprawdzają się sloty o niskiej zmienności, które często wypłacają małe kwoty. Unikaj gier stołowych (np. blackjack, ruletka), które często nie wliczają się do obrotu lub wliczają w mniejszym stopniu.
Tak, najczęściej jest to oferta powitalna. Można ją aktywować tylko raz na konto. Sprawdź regulamin, aby upewnić się, czy nie ma regionalnych ograniczeń.
Bonus oraz wygrane z niego przepadną. Dlatego ważne jest, aby planować grę w ramach określonego terminu ważności promocji.
Podsumowując, bonus bez depozytu od Tikitaka Casino to doskonałe narzędzie dla graczy, którzy chcą sprawdzić platformę bez ryzyka. Skuteczna taktyka opiera się na wytrwałości, wyborze odpowiednich gier i ścisłym trzymaniu się reguł. Graj mądrze, a darmowy bonus może stać się początkiem przygody, która przyniesie realne korzyści.
Read more at Bodhagamya Wellness
]]>