|
824 | 824 | opt = data.contextMenu, |
825 | 825 | root = data.contextMenuRoot; |
826 | 826 |
|
| 827 | + // prevent fast hover on mobile tap-through |
| 828 | + if (isInteractionTooFast($this)) { |
| 829 | + return; |
| 830 | + } |
| 831 | + |
827 | 832 | root.hovering = true; |
828 | 833 |
|
829 | 834 | // abort if we're re-entering |
|
877 | 882 | key = data.contextMenuKey, |
878 | 883 | callback; |
879 | 884 |
|
| 885 | + // prevent fast click-through on mobile taps |
| 886 | + if (isInteractionTooFast($this)) { |
| 887 | + e.preventDefault(); |
| 888 | + e.stopImmediatePropagation(); |
| 889 | + return; |
| 890 | + } |
| 891 | + |
880 | 892 | // abort if the key is unknown or disabled or is a menu |
881 | | - if (!opt.items[key] || $this.is('.' + root.classNames.disabled + ', .context-menu-separator, .' + root.classNames.notSelectable) || ($this.is('.context-menu-submenu') && root.selectableSubMenu === false )) { |
| 893 | + // explicitly handle non-selectable submenu clicks first to stop propagation |
| 894 | + if ($this.is('.context-menu-submenu') && root.selectableSubMenu === false) { |
| 895 | + e.preventDefault(); |
| 896 | + e.stopImmediatePropagation(); // Stop event here for non-selectable submenus |
882 | 897 | return; |
883 | 898 | } |
884 | 899 |
|
| 900 | + // original check for other non-clickable/disabled items |
| 901 | + if (!opt.items[key] || $this.is('.' + root.classNames.disabled + ', .context-menu-separator, .' + root.classNames.notSelectable)) { |
| 902 | + return; |
| 903 | + } |
| 904 | + |
| 905 | + // if it wasn't a non-selectable submenu or other disabled item, prevent default and stop propagation before callback |
885 | 906 | e.preventDefault(); |
886 | 907 | e.stopImmediatePropagation(); |
887 | 908 |
|
|
943 | 964 | // position sub-menu - do after show so dumb $.ui.position can keep up |
944 | 965 | if (opt.$node) { |
945 | 966 | root.positionSubmenu.call(opt.$node, opt.$menu); |
| 967 | + if (opt.$menu) { |
| 968 | + var focusShowTimestamp = Date.now(); |
| 969 | + opt.$menu.data('_showTimestamp', focusShowTimestamp); |
| 970 | + } |
946 | 971 | } |
947 | 972 | }, |
948 | 973 | // blur <command> |
|
1008 | 1033 | opt.$menu.css(css)[opt.animation.show](opt.animation.duration, function () { |
1009 | 1034 | $trigger.trigger('contextmenu:visible'); |
1010 | 1035 |
|
| 1036 | + var rootShowTimestamp = Date.now(); |
| 1037 | + opt.$menu.data('_showTimestamp', rootShowTimestamp); |
| 1038 | + |
1011 | 1039 | op.activated(opt); |
1012 | 1040 | opt.events.activated(opt); |
1013 | 1041 | }); |
|
1116 | 1144 | root = opt; |
1117 | 1145 | } |
1118 | 1146 |
|
| 1147 | + // define handler for fast input clicks |
| 1148 | + var handleFastInputClick = function(e) { |
| 1149 | + var $inputClicked = $(this); |
| 1150 | + if (isInteractionTooFast($inputClicked)) { |
| 1151 | + e.preventDefault(); |
| 1152 | + e.stopImmediatePropagation(); |
| 1153 | + return false; |
| 1154 | + } |
| 1155 | + }; |
| 1156 | + |
1119 | 1157 | // create contextMenu |
1120 | 1158 | opt.$menu = $('<ul class="context-menu-list"></ul>').addClass(opt.className || '').data({ |
1121 | 1159 | 'contextMenu': opt, |
|
1266 | 1304 | .val(item.value || '') |
1267 | 1305 | .prop('checked', !!item.selected) |
1268 | 1306 | .prependTo($label); |
| 1307 | + // prevent checkbox default action on fast click-through |
| 1308 | + $input.on('click', handleFastInputClick); |
1269 | 1309 | break; |
1270 | 1310 |
|
1271 | 1311 | case 'radio': |
|
1274 | 1314 | .val(item.value || '') |
1275 | 1315 | .prop('checked', !!item.selected) |
1276 | 1316 | .prependTo($label); |
| 1317 | + // prevent radio default action on fast click-through |
| 1318 | + $input.on('click', handleFastInputClick); |
1277 | 1319 | break; |
1278 | 1320 |
|
1279 | 1321 | case 'select': |
|
2131 | 2173 | $.contextMenu.handle = handle; |
2132 | 2174 | $.contextMenu.op = op; |
2133 | 2175 | $.contextMenu.menus = menus; |
| 2176 | + |
| 2177 | + // helper function to check for rapid interactions after menu display |
| 2178 | + var isInteractionTooFast = function($element) { |
| 2179 | + if (!('ontouchstart' in window |
| 2180 | + || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0)) { |
| 2181 | + return false; |
| 2182 | + } |
| 2183 | + var interactionTime = Date.now(); |
| 2184 | + var $liItem = $element.is('input, textarea, select') ? $element.closest('.context-menu-item') : $element; |
| 2185 | + if (!$liItem || !$liItem.length) { |
| 2186 | + return false; |
| 2187 | + } |
| 2188 | + var $parentMenu = $liItem.parent(); |
| 2189 | + if (!$parentMenu || !$parentMenu.length) { |
| 2190 | + return false; |
| 2191 | + } |
| 2192 | + |
| 2193 | + // only apply the check for items within submenus |
| 2194 | + if ($parentMenu.hasClass('context-menu-root')) { |
| 2195 | + return false; |
| 2196 | + } |
| 2197 | + |
| 2198 | + var showTimestamp = $parentMenu.data('_showTimestamp'); |
| 2199 | + var timeDifference = showTimestamp ? interactionTime - showTimestamp : Infinity; |
| 2200 | + |
| 2201 | + // threshold for fast interaction (e.g., mobile tap) |
| 2202 | + var threshold = 50; // ms |
| 2203 | + |
| 2204 | + return timeDifference < threshold; |
| 2205 | + }; |
2134 | 2206 | }); |
0 commit comments