diff --git a/dist/jquery.contextMenu.js b/dist/jquery.contextMenu.js index 7eb96233..878d3d06 100755 --- a/dist/jquery.contextMenu.js +++ b/dist/jquery.contextMenu.js @@ -824,6 +824,11 @@ opt = data.contextMenu, root = data.contextMenuRoot; + // prevent fast hover on mobile tap-through + if (isInteractionTooFast($this)) { + return; + } + root.hovering = true; // abort if we're re-entering @@ -877,11 +882,27 @@ key = data.contextMenuKey, callback; + // prevent fast click-through on mobile taps + if (isInteractionTooFast($this)) { + e.preventDefault(); + e.stopImmediatePropagation(); + return; + } + // abort if the key is unknown or disabled or is a menu - if (!opt.items[key] || $this.is('.' + root.classNames.disabled + ', .context-menu-separator, .' + root.classNames.notSelectable) || ($this.is('.context-menu-submenu') && root.selectableSubMenu === false )) { + // explicitly handle non-selectable submenu clicks first to stop propagation + if ($this.is('.context-menu-submenu') && root.selectableSubMenu === false) { + e.preventDefault(); + e.stopImmediatePropagation(); // Stop event here for non-selectable submenus return; } + // original check for other non-clickable/disabled items + if (!opt.items[key] || $this.is('.' + root.classNames.disabled + ', .context-menu-separator, .' + root.classNames.notSelectable)) { + return; + } + + // if it wasn't a non-selectable submenu or other disabled item, prevent default and stop propagation before callback e.preventDefault(); e.stopImmediatePropagation(); @@ -943,6 +964,10 @@ // position sub-menu - do after show so dumb $.ui.position can keep up if (opt.$node) { root.positionSubmenu.call(opt.$node, opt.$menu); + if (opt.$menu) { + var focusShowTimestamp = Date.now(); + opt.$menu.data('_showTimestamp', focusShowTimestamp); + } } }, // blur @@ -1008,6 +1033,9 @@ opt.$menu.css(css)[opt.animation.show](opt.animation.duration, function () { $trigger.trigger('contextmenu:visible'); + var rootShowTimestamp = Date.now(); + opt.$menu.data('_showTimestamp', rootShowTimestamp); + op.activated(opt); opt.events.activated(opt); }); @@ -1116,6 +1144,16 @@ root = opt; } + // define handler for fast input clicks + var handleFastInputClick = function(e) { + var $inputClicked = $(this); + if (isInteractionTooFast($inputClicked)) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } + }; + // create contextMenu opt.$menu = $('').addClass(opt.className || '').data({ 'contextMenu': opt, @@ -1266,6 +1304,8 @@ .val(item.value || '') .prop('checked', !!item.selected) .prependTo($label); + // prevent checkbox default action on fast click-through + $input.on('click', handleFastInputClick); break; case 'radio': @@ -1274,6 +1314,8 @@ .val(item.value || '') .prop('checked', !!item.selected) .prependTo($label); + // prevent radio default action on fast click-through + $input.on('click', handleFastInputClick); break; case 'select': @@ -2131,4 +2173,34 @@ $.contextMenu.handle = handle; $.contextMenu.op = op; $.contextMenu.menus = menus; + + // helper function to check for rapid interactions after menu display + var isInteractionTooFast = function($element) { + if (!('ontouchstart' in window + || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0)) { + return false; + } + var interactionTime = Date.now(); + var $liItem = $element.is('input, textarea, select') ? $element.closest('.context-menu-item') : $element; + if (!$liItem || !$liItem.length) { + return false; + } + var $parentMenu = $liItem.parent(); + if (!$parentMenu || !$parentMenu.length) { + return false; + } + + // only apply the check for items within submenus + if ($parentMenu.hasClass('context-menu-root')) { + return false; + } + + var showTimestamp = $parentMenu.data('_showTimestamp'); + var timeDifference = showTimestamp ? interactionTime - showTimestamp : Infinity; + + // threshold for fast interaction (e.g., mobile tap) + var threshold = 50; // ms + + return timeDifference < threshold; + }; });