Skip to content

Commit 0a340f3

Browse files
SteveTheTechiemlh758
authored andcommitted
Menu clipping (#749)
* Menu clipping 1. Add zIndex option to allow overriding menu z-index for edge use cases. 2. Add logic to assign a reasonable z-index if not appending to a jQuery UI Dialog. (e.g., appending to document.body instead) 3. Go back to setting the menu position on every open in case the button has been moved (e.g. container dialog moved) from the last open. 4. Close the menu if the mouse cursor is outside the menu and the mouse wheel is turned. This fixes a scrolling issue that made the menu look detached from the button when the mouse wheel was turned. 5. Add _allowInteraction support for jQuery modal dialogs to prevent interaction issues when appending to document body to prevent menu truncation. Note these changes do not explicitly fix the menu truncation issue. However, they do make it much easier to address menu truncation by just explicitly setting the appendTo option to document.body. * Simplify the check for clicks outside menu
1 parent 7142177 commit 0a340f3

File tree

2 files changed

+75
-60
lines changed

2 files changed

+75
-60
lines changed

css/jquery.multiselect.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.ui-multiselect {box-sizing: border-box; padding:2px 0 2px 4px; text-align:left;}
22
.ui-multiselect .ui-multiselect-open { float:right }
33

4-
.ui-multiselect-menu { display:none; box-sizing:border-box; position:absolute; text-align:left; z-index:1010; width:auto; padding:3px;}
4+
.ui-multiselect-menu { display:none; box-sizing:border-box; position:absolute; text-align:left; z-index:101; width:auto; padding:3px;}
55

66
.ui-multiselect-header { display:block; box-sizing:border-box; position:relative; width:auto; padding:3px 0 3px 4px; margin-bottom:3px;}
77
.ui-multiselect-header > ul { font-size:0.9em }

src/jquery.multiselect.js

Lines changed: 74 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,14 @@
4646
noneSelectedText: 'Select options', // (str | null) The text to show in the button where nothing is selected. Set to null to use the native select's placeholder text.
4747
selectedText: '# of # selected', // (str) A "template" that indicates how to show the count of selections in the button. The "#'s" are replaced by the selection count & option count.
4848
selectedList: 0, // (int) The actual list selections will be shown in the button when the count of selections is <= than this number.
49+
selectedListSeparator: ', ', // (str) This allows customization of the list separator. Use ',<br/>' to make the button grow vertically showing 1 selection per line.
4950
maxSelected: null, // (int | null) If selected count > maxSelected, then message is displayed, and new selection is undone.
5051
show: null, // (array) An array containing menu opening effects.
5152
hide: null, // (array) An array containing menu closing effects.
5253
autoOpen: false, // (true | false) If true, then the menu will be opening immediately after initialization.
5354
position: {}, // (object) A jQuery UI position object that constrains how the pop-up menu is positioned.
5455
appendTo: null, // (jQuery | DOM element | selector str) If provided, this specifies what element to append the widget to in the DOM.
55-
selectedListSeparator: ', ', // (str) This allows customization of the list separator. Use ',<br/>' to make the button grow vertically showing 1 selection per line.
56+
zIndex: null, // (int) Overrides the z-index set for the menu container.
5657
htmlButtonText: false, // (true | false) If true, then the text used for the button's label is treated as html rather than plain text.
5758
htmlOptionText: false, // (true | false) If true, then the text for option label is treated as html rather than plain text.
5859
addInputNames: true, // (true | false) If true, names are created for each option input in the multi-select.
@@ -176,7 +177,22 @@
176177
.append($header, $checkboxes);
177178

178179
$button.insertAfter($element);
179-
this._getAppendEl().append($menu);
180+
var appendEl = this._getAppendEl();
181+
appendEl.append($menu);
182+
183+
// Set z-index of menu appropriately when it is not appended to a dialog and no z-index specified.
184+
if ( !options.zIndex && !appendEl.hasClass('.ui-front') ) {
185+
var $uiFront = this.element.closest('.ui-front, dialog');
186+
options.zIndex = Math.max( $uiFront && parseInt($uiFront.css('z-index'), 10) + 1 || 0,
187+
appendEl && parseInt(appendEl.css('z-index'), 10) + 1 || 0);
188+
}
189+
190+
if (options.zIndex) {
191+
$menu.css('z-index', options.zIndex);
192+
}
193+
194+
// Use $.extend below since the "of" position property may not be able to be supplied via the option.
195+
options.position = $.extend({'my': 'left top', 'at': 'left bottom', 'of': $button}, options.position || {});
180196

181197
this._bindEvents();
182198

@@ -373,11 +389,10 @@
373389
* Updates cached values used elsewhere in the widget
374390
*/
375391
_updateCache: function() {
376-
// Invalidate cached dimensions and positioning state to force recalcs.
392+
// Invalidate cached dimensions to force recalcs.
377393
this._savedButtonWidth = 0;
378394
this._savedMenuWidth = 0;
379395
this._ulHeight = 0;
380-
this._positioned = false;
381396

382397
// Recreate important cached jQuery objects
383398
this.$header = this.$menu.children('.ui-multiselect-header');
@@ -429,7 +444,7 @@
429444

430445
// Check if the menu needs to be repositioned due to button height changing from adding/removing selections.
431446
if (self._isOpen && self._savedButtonHeight != self.$button.outerHeight(false)) {
432-
self._position(true);
447+
self.position();
433448
}
434449
},
435450

@@ -510,18 +525,18 @@
510525
var $inputs = $this.next('ul').find('input').filter(':visible:not(:disabled)');
511526
var nodes = $inputs.get();
512527
var label = this.textContent;
513-
514-
// if maxSelected is in use, cannot exceed it
515-
var maxSelected = self.options.maxSelected;
516-
if (maxSelected && (self.$inputs.filter(':checked').length + $inputs.length > maxSelected) ) {
517-
return;
518-
}
519528

520529
// trigger before callback and bail if the return is false
521530
if (self._trigger('beforeoptgrouptoggle', e, { inputs:nodes, label:label }) === false) {
522531
return;
523532
}
524533

534+
// if maxSelected is in use, cannot exceed it
535+
var maxSelected = self.options.maxSelected;
536+
if (maxSelected && (self.$inputs.filter(':checked').length + $inputs.length > maxSelected) ) {
537+
return;
538+
}
539+
525540
// toggle inputs
526541
self._toggleChecked(
527542
$inputs.filter(':checked').length !== $inputs.length,
@@ -575,12 +590,17 @@
575590
case 32: // space
576591
$(this).find('input')[0].click();
577592
break;
578-
case 65: // Ctrl-A
593+
case 65: // Alt-A
579594
if (e.altKey) {
580595
self.checkAll();
581596
}
582597
break;
583-
case 85: // Ctrl-U
598+
case 70: // Alt-F
599+
if (e.altKey) {
600+
self.flipAll();
601+
}
602+
break;
603+
case 85: // Alt-U
584604
if (e.altKey) {
585605
self.uncheckAll();
586606
}
@@ -703,13 +723,10 @@
703723
self._bindMenuEvents();
704724
self._bindHeaderEvents();
705725

706-
// close each widget when clicking on any other element/anywhere else on the page
707-
self.document.on('mousedown.' + self._namespaceID, function(event) {
708-
var target = event.target;
709-
var button = self.$button.get(0);
710-
var menu = self.$menu.get(0);
711-
712-
if ( self._isOpen && button !== target && !$.contains(button, target) && menu !== target && !$.contains(menu, target) ) {
726+
// Close each widget when clicking on any other element/anywhere else on the page
727+
// or scrolling w/ the mouse wheel outside the menu button.
728+
self.document.on('mousedown.' + self._namespaceID + ' wheel.' + self._namespaceID + ' mousewheel.' + self._namespaceID, function(event) {
729+
if ( self._isOpen && !$(event.target).closest('.ui-multiselect,.ui-multiselect-menu').length ) {
713730
self.close();
714731
}
715732
});
@@ -780,16 +797,13 @@
780797
/**
781798
* Sets and caches the width of the button
782799
* Can set a minimum value if less than calculated width of native select.
783-
* If the cache is cleared, the menu will be re-positioned on the next open
784800
* @param {boolean} recalc true if cached value needs to be re-calculated
785801
*/
786802
_setButtonWidth: function(recalc) {
787803
if (this._savedButtonWidth && !recalc) {
788804
return;
789805
}
790806

791-
this._positioned = false;
792-
793807
// this._selectWidth set in _create() for native select element before hiding it.
794808
var width = this._selectWidth || this._getBCRWidth( this.element );
795809
var buttonWidth = this.options.buttonWidth || '';
@@ -815,16 +829,13 @@
815829
/**
816830
* Sets and caches the width of the menu
817831
* Will use the width in options if provided, otherwise matches the button
818-
* If the cache is cleared, the menu will be re-positioned on the next open
819832
* @param {boolean} recalc true if cached value needs to be re-calculated
820833
*/
821834
_setMenuWidth: function(recalc) {
822835
if (this._savedMenuWidth && !recalc) {
823836
return;
824837
}
825838

826-
this._positioned = false;
827-
828839
// Note that it is assumed that the button width was set prior.
829840
var width = this._savedButtonWidth || this._getBCRWidth( this.$button );
830841

@@ -869,7 +880,6 @@
869880
* Will use the height provided in the options unless using the select size
870881
* option or the option exceeds the available height for the menu
871882
* Will set a scrollbar if the options can't all be visible at once
872-
* If the cache is cleared, the menu will be re-positioned on the next open
873883
* @param {boolean} recalc true if cached value needs to be re-calculated
874884
*/
875885
_setMenuHeight: function(recalc) {
@@ -878,15 +888,16 @@
878888
return;
879889
}
880890

881-
self._positioned = false;
882891
var $menu = self.$menu;
883892
var $header = self.$header.filter(':visible');
884893
var headerHeight = $header.outerHeight(true) + self._jqHeightFix($header);
894+
var headerBottomMargin = 3;
885895
var $checkboxes = self.$checkboxes;
886896

887897
// The maximum available height for the $checkboxes:
888898
var maxHeight = $(window).height()
889899
- headerHeight
900+
- headerBottomMargin
890901
- this._parse2px( $menu.css('padding-top'), this.element, true ).px
891902
- this._parse2px( $menu.css('padding-bottom'), this.element, true ).px;
892903

@@ -906,7 +917,7 @@
906917

907918
var overflowSetting = 'hidden';
908919
var itemCount = 0;
909-
var ulHeight = 0;
920+
var ulHeight = 0; // Adjustment for hover height included here.
910921

911922
// The following adds up item heights. If the height sum exceeds the option height or if the number
912923
// of item heights summed equal or exceed the native select size attribute, the loop is aborted.
@@ -923,7 +934,7 @@
923934
});
924935

925936
$checkboxes.css('overflow', overflowSetting).height(ulHeight);
926-
$menu.height(headerHeight + ulHeight);
937+
$menu.height(headerHeight + headerBottomMargin + ulHeight);
927938
self._ulHeight = ulHeight;
928939
},
929940

@@ -1211,7 +1222,7 @@
12111222
}
12121223

12131224
this._resizeMenu();
1214-
this._position();
1225+
this.position();
12151226

12161227
// focus the first not disabled option or filter input if available
12171228
var filter = $header.find(".ui-multiselect-filter");
@@ -1297,7 +1308,7 @@
12971308
this._trigger('beforeFlipAll');
12981309

12991310
var maxSelected = this.options.maxSelected;
1300-
if (maxSelected === null || maxSelected > (this.$inputs.length - this.$inputs.filter(':checked').length) ) {
1311+
if (maxSelected === null || maxSelected >= (this.$inputs.length - this.$inputs.filter(':checked').length) ) {
13011312
this._toggleChecked('!');
13021313
this._trigger('flipAll');
13031314
}
@@ -1440,36 +1451,20 @@
14401451
this._updateCache();
14411452
},
14421453

1443-
/**
1444-
* Public version of _position, always ignores the cache
1445-
*/
1446-
position: function(){ this._position.call(this, true) },
1447-
/**
1448-
* Positions the menu
1449-
* Will attempt to use the UI position utility before falling back to a manual
1450-
* process by offsetting from the button height
1451-
* Saves a flag to avoid repeating this logic until necessary
1452-
* @param {boolean} reposition forces the menu to reposition if true
1453-
*/
1454-
_position: function(reposition) {
1455-
if (!!this._positioned && !reposition) {
1456-
return;
1457-
}
1454+
position: function() {
14581455
var $button = this.$button;
1459-
// Save this so that we can determine when the button height has changed due adding/removing selections.
1460-
this._savedButtonHeight = this.$button.outerHeight(false);
14611456

1462-
var pos = $.extend({'my': 'left top', 'at': 'left bottom', 'of': $button}, this.options.position || {});
1457+
// Save this so that we can determine when the button height has changed due adding/removing selections.
1458+
this._savedButtonHeight = $button.outerHeight(false);
14631459

14641460
if ($.ui && $.ui.position) {
1465-
this.$menu.position(pos);
1461+
this.$menu.position(this.options.position);
14661462
}
14671463
else {
1468-
pos = $button.position();
1464+
var pos = $button.position();
14691465
pos.top += this._savedButtonHeight;
14701466
this.$menu.offset(pos);
14711467
}
1472-
this._positioned = true;
14731468
},
14741469

14751470
/**
@@ -1541,13 +1536,33 @@
15411536
this.refresh();
15421537
}
15431538
break;
1544-
case 'position':
1545-
this._position(true); // true ignores cached setting
1546-
break;
1547-
}
1548-
$.Widget.prototype._setOption.apply(this, arguments);
1549-
},
1539+
case 'position':
1540+
if (value !== null && !$.isEmptyObject(value) ) {
1541+
this.options.position = value;
1542+
}
1543+
this.position();
1544+
break;
1545+
case 'zIndex':
1546+
this.options.zIndex = value;
1547+
this.$menu.css('z-index', value);
1548+
break;
1549+
}
1550+
$.Widget.prototype._setOption.apply(this, arguments);
1551+
},
15501552

15511553
});
15521554

1555+
// Fix for jQuery UI modal dialogs
1556+
// https://api.jqueryui.com/dialog/#method-_allowInteraction
1557+
// https://learn.jquery.com/jquery-ui/widget-factory/extending-widgets/
1558+
if ($.ui && $.ui.dialog) {
1559+
$.widget( "ui.dialog", $.ui.dialog, {
1560+
_allowInteraction: function( event ) {
1561+
if ( this._super( event ) || $( event.target ).closest('.ui-multiselect-menu' ).length ) {
1562+
return true;
1563+
}
1564+
}
1565+
});
1566+
}
1567+
15531568
})(jQuery);

0 commit comments

Comments
 (0)