|
6 | 6 | * Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF) |
7 | 7 | * Web: http://swisnl.github.io/jQuery-contextMenu/ |
8 | 8 | * |
9 | | - * Copyright (c) 2011-2020 SWIS BV and contributors |
| 9 | + * Copyright (c) 2011-2025 SWIS BV and contributors |
10 | 10 | * |
11 | 11 | * Licensed under |
12 | 12 | * MIT License http://www.opensource.org/licenses/mit-license |
13 | 13 | * |
14 | | - * Date: 2020-05-13T13:55:36.983Z |
| 14 | + * Date: 2025-11-04T11:10:13.179Z |
15 | 15 | */ |
16 | 16 |
|
17 | 17 | // jscs:disable |
|
31 | 31 |
|
32 | 32 | 'use strict'; |
33 | 33 |
|
| 34 | + // helper function to check for rapid interactions after menu display |
| 35 | + var isInteractionTooFast = function($element) { |
| 36 | + if (!('ontouchstart' in window |
| 37 | + || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0)) { |
| 38 | + return false; |
| 39 | + } |
| 40 | + var interactionTime = Date.now(); |
| 41 | + var $liItem = $element.is('input, textarea, select') ? $element.closest('.context-menu-item') : $element; |
| 42 | + if (!$liItem || !$liItem.length) { |
| 43 | + return false; |
| 44 | + } |
| 45 | + var $parentMenu = $liItem.parent(); |
| 46 | + if (!$parentMenu || !$parentMenu.length) { |
| 47 | + return false; |
| 48 | + } |
| 49 | + |
| 50 | + // only apply the check for items within submenus |
| 51 | + if ($parentMenu.hasClass('context-menu-root')) { |
| 52 | + return false; |
| 53 | + } |
| 54 | + |
| 55 | + var showTimestamp = $parentMenu.data('_showTimestamp'); |
| 56 | + var timeDifference = showTimestamp ? interactionTime - showTimestamp : Infinity; |
| 57 | + |
| 58 | + // threshold for fast interaction (e.g., mobile tap) |
| 59 | + var threshold = 50; // ms |
| 60 | + |
| 61 | + return timeDifference < threshold; |
| 62 | + }; |
| 63 | + |
34 | 64 | // TODO: - |
35 | 65 | // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio |
36 | 66 | // create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative |
|
824 | 854 | opt = data.contextMenu, |
825 | 855 | root = data.contextMenuRoot; |
826 | 856 |
|
| 857 | + // prevent fast hover on mobile tap-through |
| 858 | + if (isInteractionTooFast($this)) { |
| 859 | + return; |
| 860 | + } |
| 861 | + |
827 | 862 | root.hovering = true; |
828 | 863 |
|
829 | 864 | // abort if we're re-entering |
|
877 | 912 | key = data.contextMenuKey, |
878 | 913 | callback; |
879 | 914 |
|
| 915 | + // prevent fast click-through on mobile taps |
| 916 | + if (isInteractionTooFast($this)) { |
| 917 | + e.preventDefault(); |
| 918 | + e.stopImmediatePropagation(); |
| 919 | + return; |
| 920 | + } |
| 921 | + |
880 | 922 | // 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 )) { |
| 923 | + // explicitly handle non-selectable submenu clicks first to stop propagation |
| 924 | + if ($this.is('.context-menu-submenu') && root.selectableSubMenu === false) { |
| 925 | + e.preventDefault(); |
| 926 | + e.stopImmediatePropagation(); // Stop event here for non-selectable submenus |
882 | 927 | return; |
883 | 928 | } |
884 | 929 |
|
| 930 | + // original check for other non-clickable/disabled items |
| 931 | + if (!opt.items[key] || $this.is('.' + root.classNames.disabled + ', .context-menu-separator, .' + root.classNames.notSelectable)) { |
| 932 | + return; |
| 933 | + } |
| 934 | + |
| 935 | + // if it wasn't a non-selectable submenu or other disabled item, prevent default and stop propagation before callback |
885 | 936 | e.preventDefault(); |
886 | 937 | e.stopImmediatePropagation(); |
887 | 938 |
|
|
943 | 994 | // position sub-menu - do after show so dumb $.ui.position can keep up |
944 | 995 | if (opt.$node) { |
945 | 996 | root.positionSubmenu.call(opt.$node, opt.$menu); |
| 997 | + if (opt.$menu) { |
| 998 | + var focusShowTimestamp = Date.now(); |
| 999 | + opt.$menu.data('_showTimestamp', focusShowTimestamp); |
| 1000 | + } |
946 | 1001 | } |
947 | 1002 | }, |
948 | 1003 | // blur <command> |
|
1008 | 1063 | opt.$menu.css(css)[opt.animation.show](opt.animation.duration, function () { |
1009 | 1064 | $trigger.trigger('contextmenu:visible'); |
1010 | 1065 |
|
| 1066 | + var rootShowTimestamp = Date.now(); |
| 1067 | + opt.$menu.data('_showTimestamp', rootShowTimestamp); |
| 1068 | + |
1011 | 1069 | op.activated(opt); |
1012 | 1070 | opt.events.activated(opt); |
1013 | 1071 | }); |
|
1116 | 1174 | root = opt; |
1117 | 1175 | } |
1118 | 1176 |
|
| 1177 | + // define handler for fast input clicks |
| 1178 | + var handleFastInputClick = function(e) { |
| 1179 | + var $inputClicked = $(this); |
| 1180 | + if (isInteractionTooFast($inputClicked)) { |
| 1181 | + e.preventDefault(); |
| 1182 | + e.stopImmediatePropagation(); |
| 1183 | + return false; |
| 1184 | + } |
| 1185 | + }; |
| 1186 | + |
1119 | 1187 | // create contextMenu |
1120 | 1188 | opt.$menu = $('<ul class="context-menu-list"></ul>').addClass(opt.className || '').data({ |
1121 | 1189 | 'contextMenu': opt, |
|
1266 | 1334 | .val(item.value || '') |
1267 | 1335 | .prop('checked', !!item.selected) |
1268 | 1336 | .prependTo($label); |
| 1337 | + // prevent checkbox default action on fast click-through |
| 1338 | + $input.on('click', handleFastInputClick); |
1269 | 1339 | break; |
1270 | 1340 |
|
1271 | 1341 | case 'radio': |
|
1274 | 1344 | .val(item.value || '') |
1275 | 1345 | .prop('checked', !!item.selected) |
1276 | 1346 | .prependTo($label); |
| 1347 | + // prevent radio default action on fast click-through |
| 1348 | + $input.on('click', handleFastInputClick); |
1277 | 1349 | break; |
1278 | 1350 |
|
1279 | 1351 | case 'select': |
|
1579 | 1651 | var $menu = opt.$menu; |
1580 | 1652 | var $menuOffset = $menu.offset(); |
1581 | 1653 | var winHeight = $(window).height(); |
| 1654 | + var winWidth = $(window).width(); |
1582 | 1655 | var winScrollTop = $(window).scrollTop(); |
| 1656 | + var winScrollLeft = $(window).scrollLeft(); |
1583 | 1657 | var menuHeight = $menu.height(); |
| 1658 | + var outerHeight = $menu.outerHeight(); |
| 1659 | + var outerWidth = $menu.outerWidth(); |
| 1660 | + |
1584 | 1661 | if(menuHeight > winHeight){ |
1585 | 1662 | $menu.css({ |
1586 | 1663 | 'height' : winHeight + 'px', |
1587 | 1664 | 'overflow-x': 'hidden', |
1588 | 1665 | 'overflow-y': 'auto', |
1589 | 1666 | 'top': winScrollTop + 'px' |
1590 | 1667 | }); |
1591 | | - } else if(($menuOffset.top < winScrollTop) || ($menuOffset.top + menuHeight > winScrollTop + winHeight)){ |
| 1668 | + } else if($menuOffset.top < winScrollTop){ |
1592 | 1669 | $menu.css({ |
1593 | | - 'top': winScrollTop + 'px' |
| 1670 | + 'top': winScrollTop + 'px' |
| 1671 | + }); |
| 1672 | + } else if($menuOffset.top + outerHeight > winScrollTop + winHeight){ |
| 1673 | + $menu.css({ |
| 1674 | + 'top': $menuOffset.top - (($menuOffset.top + outerHeight) - (winScrollTop + winHeight)) + "px" |
| 1675 | + }); |
| 1676 | + } |
| 1677 | + if($menuOffset.left + outerWidth > winScrollLeft + winWidth){ |
| 1678 | + $menu.css({ |
| 1679 | + 'left': $menuOffset.left - (($menuOffset.left + outerWidth) - (winScrollLeft + winWidth)) + "px" |
1594 | 1680 | }); |
1595 | 1681 | } |
1596 | 1682 | } |
|
2131 | 2217 | $.contextMenu.handle = handle; |
2132 | 2218 | $.contextMenu.op = op; |
2133 | 2219 | $.contextMenu.menus = menus; |
| 2220 | + |
2134 | 2221 | }); |
0 commit comments