Skip to content

Instantly share code, notes, and snippets.

@Akkuma
Created March 17, 2011 18:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Akkuma/874807 to your computer and use it in GitHub Desktop.
Save Akkuma/874807 to your computer and use it in GitHub Desktop.
Fixes the improper width issue by showing and then hiding the element before it does the width calculation. It could be stored, but then if the select's size is altered then the element would be off.
/*
* jQuery UI selectmenu
*
* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT (MIT-LICENSE.txt)
* and GPL (GPL-LICENSE.txt) licenses.
*
* http://docs.jquery.com/UI
* https://github.com/fnagel/jquery-ui/wiki/Selectmenu
*/
(function ($) {
$.widget("ui.selectmenu", {
getter: "value",
version: "1.8",
eventPrefix: "selectmenu",
options: {
transferClasses: true,
typeAhead: "sequential",
style: 'dropdown',
positionOptions: {
my: "left top",
at: "left bottom",
offset: null
},
width: null,
menuWidth: null,
handleWidth: 26,
maxHeight: null,
icons: null,
format: null,
bgImage: function () { },
wrapperElement: ""
},
_create: function () {
var self = this, o = this.options;
// set a default id value, generate a new random one if not set by developer
var selectmenuId = this.element.attr('id') || 'ui-selectmenu-' + Math.random().toString(16).slice(2, 10);
// quick array of button and menu id's
this.ids = [selectmenuId + '-button', selectmenuId + '-menu'];
// define safe mouseup for future toggling
this._safemouseup = true;
// FIXME temp workaround for IE
if ($.browser.msie) o.typeAhead = "";
// create menu button wrapper
this.newelement = $('<a class="' + this.widgetBaseClass + ' ui-widget ui-state-default ui-corner-all" id="' + this.ids[0] + '" role="button" href="#" tabindex="0" aria-haspopup="true" aria-owns="' + this.ids[1] + '"></a>')
.insertAfter(this.element);
this.newelement.wrap(o.wrapperElement);
// transfer tabindex
var tabindex = this.element.attr('tabindex');
if (tabindex) {
this.newelement.attr('tabindex', tabindex);
}
// save reference to select in data for ease in calling methods
this.newelement.data('selectelement', this.element);
// menu icon
this.selectmenuIcon = $('<span class="' + this.widgetBaseClass + '-icon ui-icon"></span>')
.prependTo(this.newelement);
// append status span to button
this.newelement.prepend('<span class="' + self.widgetBaseClass + '-status" />');
// make associated form label trigger focus
$('label[for="' + this.element.attr('id') + '"]')
.attr('for', this.ids[0])
.bind('click.selectmenu', function () {
self.newelement[0].focus();
return false;
});
// click toggle for menu visibility
this.newelement
.bind('mousedown.selectmenu', function (event) {
self._toggle(event, true);
// make sure a click won't open/close instantly
if (o.style == "popup") {
self._safemouseup = false;
setTimeout(function () { self._safemouseup = true; }, 300);
}
return false;
})
.bind('click.selectmenu', function () {
return false;
})
.bind("keydown.selectmenu", function (event) {
var ret = false;
switch (event.keyCode) {
case $.ui.keyCode.ENTER:
ret = true;
break;
case $.ui.keyCode.SPACE:
self._toggle(event);
break;
case $.ui.keyCode.UP:
if (event.altKey) {
self.open(event);
} else {
self._moveSelection(-1);
}
break;
case $.ui.keyCode.DOWN:
if (event.altKey) {
self.open(event);
} else {
self._moveSelection(1);
}
break;
case $.ui.keyCode.LEFT:
self._moveSelection(-1);
break;
case $.ui.keyCode.RIGHT:
self._moveSelection(1);
break;
case $.ui.keyCode.TAB:
ret = true;
break;
default:
ret = true;
self._typeAhead(event.keyCode, 'mouseup');
break;
}
return ret;
})
.bind('mouseover.selectmenu focus.selectmenu', function () {
if (!o.disabled) {
$(this).addClass(self.widgetBaseClass + '-focus ui-state-hover');
}
})
.bind('mouseout.selectmenu blur.selectmenu', function () {
if (!o.disabled) {
$(this).removeClass(self.widgetBaseClass + '-focus ui-state-hover');
}
});
// document click closes menu
$(document).bind("mousedown.selectmenu", function (event) {
self.close(event);
});
// change event on original selectmenu
this.element
.bind("click.selectmenu", function () {
self._refreshValue();
})
// FIXME: newelement can be null under unclear circumstances in IE8
.bind("focus.selectmenu", function () {
if (this.newelement) {
this.newelement[0].focus();
}
});
// original selectmenu width
var selectWidth = this.element.width();
// set menu button width
this.newelement.width(o.width ? o.width : selectWidth);
// create menu portion, append to body
this.list = $('<ul class="' + self.widgetBaseClass + '-menu ui-widget ui-widget-content" aria-hidden="true" role="listbox" aria-labelledby="' + this.ids[0] + '" id="' + this.ids[1] + '"></ul>').appendTo('body');
this.list.wrap(o.wrapperElement);
// transfer menu click to menu button
this.list
.bind("keydown.selectmenu", function (event) {
var ret = false;
switch (event.keyCode) {
case $.ui.keyCode.UP:
if (event.altKey) {
self.close(event, true);
} else {
self._moveFocus(-1);
}
break;
case $.ui.keyCode.DOWN:
if (event.altKey) {
self.close(event, true);
} else {
self._moveFocus(1);
}
break;
case $.ui.keyCode.LEFT:
self._moveFocus(-1);
break;
case $.ui.keyCode.RIGHT:
self._moveFocus(1);
break;
case $.ui.keyCode.HOME:
self._moveFocus(':first');
break;
case $.ui.keyCode.PAGE_UP:
self._scrollPage('up');
break;
case $.ui.keyCode.PAGE_DOWN:
self._scrollPage('down');
break;
case $.ui.keyCode.END:
self._moveFocus(':last');
break;
case $.ui.keyCode.ENTER:
case $.ui.keyCode.SPACE:
self.close(event, true);
$(event.target).parents('li:eq(0)').trigger('mouseup');
break;
case $.ui.keyCode.TAB:
ret = true;
self.close(event, true);
break;
case $.ui.keyCode.ESCAPE:
self.close(event, true);
break;
default:
ret = true;
self._typeAhead(event.keyCode, 'focus');
break;
}
return ret;
});
// needed when window is resized
$(window).bind("resize.selectmenu", $.proxy(self._refreshPosition, this));
},
_init: function () {
var self = this, o = this.options;
// serialize selectmenu element options
var selectOptionData = [];
this.element
.find('option')
.each(function () {
selectOptionData.push({
value: $(this).attr('value'),
text: self._formatText($(this).text()),
selected: $(this).attr('selected'),
classes: $(this).attr('class'),
typeahead: $(this).attr('typeahead'),
parentOptGroup: $(this).parent('optgroup').attr('label'),
bgImage: o.bgImage.call($(this))
});
});
// active state class is only used in popup style
var activeClass = (self.options.style == "popup") ? " ui-state-active" : "";
// empty list so we can refresh the selectmenu via selectmenu()
this.list.html("");
// write li's
for (var i = 0;i < selectOptionData.length;i++) {
var thisLi = $('<li role="presentation"><a href="#" tabindex="-1" role="option" aria-selected="false"' + (selectOptionData[i].typeahead ? ' typeahead="' + selectOptionData[i].typeahead + '"' : '') + '>' + selectOptionData[i].text + '</a></li>')
.data('index', i)
.addClass(selectOptionData[i].classes)
.data('optionClasses', selectOptionData[i].classes || '')
.bind("mouseup.selectmenu", function (event) {
if (self._safemouseup) {
var changed = $(this).data('index') != self._selectedIndex();
self.index($(this).data('index'));
self.select(event);
if (changed) {
self.change(event);
}
self.close(event, true);
}
return false;
})
.bind("click.selectmenu", function () {
return false;
})
.bind('mouseover.selectmenu focus.selectmenu', function () {
self._selectedOptionLi().addClass(activeClass);
self._focusedOptionLi().removeClass(self.widgetBaseClass + '-item-focus ui-state-hover');
$(this).removeClass('ui-state-active').addClass(self.widgetBaseClass + '-item-focus ui-state-hover');
})
.bind('mouseout.selectmenu blur.selectmenu', function () {
if ($(this).is(self._selectedOptionLi().selector)) {
$(this).addClass(activeClass);
}
$(this).removeClass(self.widgetBaseClass + '-item-focus ui-state-hover');
});
// optgroup or not...
if (selectOptionData[i].parentOptGroup) {
// whitespace in the optgroupname must be replaced, otherwise the li of existing optgroups are never found
var optGroupName = self.widgetBaseClass + '-group-' + selectOptionData[i].parentOptGroup.replace(/[^a-zA-Z0-9]/g, "");
if (this.list.find('li.' + optGroupName).size()) {
this.list.find('li.' + optGroupName + ':last ul').append(thisLi);
} else {
$('<li role="presentation" class="' + self.widgetBaseClass + '-group ' + optGroupName + '"><span class="' + self.widgetBaseClass + '-group-label">' + selectOptionData[i].parentOptGroup + '</span><ul></ul></li>')
.appendTo(this.list)
.find('ul')
.append(thisLi);
}
} else {
thisLi.appendTo(this.list);
}
// this allows for using the scrollbar in an overflowed list
this.list.bind('mousedown.selectmenu mouseup.selectmenu', function () { return false; });
// append icon if option is specified
if (o.icons) {
for (var j in o.icons) {
if (thisLi.is(o.icons[j].find)) {
thisLi
.data('optionClasses', selectOptionData[i].classes + ' ' + self.widgetBaseClass + '-hasIcon')
.addClass(self.widgetBaseClass + '-hasIcon');
var iconClass = o.icons[j].icon || "";
thisLi
.find('a:eq(0)')
.prepend('<span class="' + self.widgetBaseClass + '-item-icon ui-icon ' + iconClass + '"></span>');
if (selectOptionData[i].bgImage) {
thisLi.find('span').css('background-image', selectOptionData[i].bgImage);
}
}
}
}
}
// we need to set and unset the CSS classes for dropdown and popup style
var isDropDown = (o.style == 'dropdown');
this.newelement
.toggleClass(self.widgetBaseClass + "-dropdown", isDropDown)
.toggleClass(self.widgetBaseClass + "-popup", !isDropDown);
this.list
.toggleClass(self.widgetBaseClass + "-menu-dropdown ui-corner-bottom", isDropDown)
.toggleClass(self.widgetBaseClass + "-menu-popup ui-corner-all", !isDropDown)
// add corners to top and bottom menu items
.find('li:first')
.toggleClass("ui-corner-top", !isDropDown)
.end().find('li:last')
.addClass("ui-corner-bottom");
this.selectmenuIcon
.toggleClass('ui-icon-triangle-1-s', isDropDown)
.toggleClass('ui-icon-triangle-2-n-s', !isDropDown);
// transfer classes to selectmenu and list
if (o.transferClasses) {
var transferClasses = this.element.attr('class') || '';
this.newelement.add(this.list).addClass(transferClasses);
}
// original selectmenu width
this.element.show();
var selectWidth = this.element.width();
this.element.hide();
// set menu width to either menuWidth option value, width option value, or select width
if (o.style == 'dropdown') {
this.list.width(o.menuWidth ? o.menuWidth : (o.width ? o.width : selectWidth));
} else {
this.list.width(o.menuWidth ? o.menuWidth : (o.width ? o.width - o.handleWidth : selectWidth - o.handleWidth));
}
// calculate default max height
if (o.maxHeight) {
// set max height from option
if (o.maxHeight < this.list.height()) {
this.list.height(o.maxHeight);
}
} else {
if (!o.format && ($(window).height() / 3) < this.list.height()) {
o.maxHeight = $(window).height() / 3;
this.list.height(o.maxHeight);
}
}
// save reference to actionable li's (not group label li's)
this._optionLis = this.list.find('li:not(.' + self.widgetBaseClass + '-group)');
// transfer disabled state
if (this.element.attr('disabled') === true) {
this.disable();
}
// update value
this.index(this._selectedIndex());
// needed when selectmenu is placed at the very bottom / top of the page
window.setTimeout(function () {
self._refreshPosition();
}, 200);
},
destroy: function () {
this.element.removeData(this.widgetName)
.removeClass(this.widgetBaseClass + '-disabled' + ' ' + this.namespace + '-state-disabled')
.removeAttr('aria-disabled')
.unbind(".selectmenu");
$(window).unbind(".selectmenu");
$(document).unbind(".selectmenu");
// unbind click on label, reset its for attr
$('label[for=' + this.newelement.attr('id') + ']')
.attr('for', this.element.attr('id'))
.unbind('.selectmenu');
if (this.options.wrapperElement) {
this.newelement.find(this.options.wrapperElement).remove();
this.list.find(this.options.wrapperElement).remove();
} else {
this.newelement.remove();
this.list.remove();
}
this.element.show();
// call widget destroy function
$.Widget.prototype.destroy.apply(this, arguments);
},
_typeAhead: function (code, eventType) {
var self = this, focusFound = false, C = String.fromCharCode(code);
c = C.toLowerCase();
if (self.options.typeAhead == 'sequential') {
// clear the timeout so we can use _prevChar
window.clearTimeout('ui.selectmenu-' + self.selectmenuId);
// define our find var
var find = typeof (self._prevChar) == 'undefined' ? '' : self._prevChar.join('');
function focusOptSeq(elem, ind, c) {
focusFound = true;
$(elem).trigger(eventType);
typeof (self._prevChar) == 'undefined' ? self._prevChar = [c] : self._prevChar[self._prevChar.length] = c;
}
this.list.find('li a').each(function (i) {
if (!focusFound) {
// allow the typeahead attribute on the option tag for a more specific lookup
var thisText = $(this).attr('typeahead') || $(this).text();
if (thisText.indexOf(find + C) == 0) {
focusOptSeq(this, i, C)
} else if (thisText.indexOf(find + c) == 0) {
focusOptSeq(this, i, c)
}
}
});
// if we didnt find it clear the prevChar
if (!focusFound) {
//self._prevChar = undefined
}
// set a 1 second timeout for sequenctial typeahead
// keep this set even if we have no matches so it doesnt typeahead somewhere else
window.setTimeout(function (el) {
el._prevChar = undefined;
}, 1000, self);
} else {
//define self._prevChar if needed
if (!self._prevChar) { self._prevChar = ['', 0]; }
var focusFound = false;
function focusOpt(elem, ind) {
focusFound = true;
$(elem).trigger(eventType);
self._prevChar[1] = ind;
}
this.list.find('li a').each(function (i) {
if (!focusFound) {
var thisText = $(this).text();
if (thisText.indexOf(C) == 0 || thisText.indexOf(c) == 0) {
if (self._prevChar[0] == C) {
if (self._prevChar[1] < i) { focusOpt(this, i); }
}
else { focusOpt(this, i); }
}
}
});
this._prevChar[0] = C;
}
},
// returns some usefull information, called by callbacks only
_uiHash: function () {
var index = this.index();
return {
index: index,
option: $("option", this.element).get(index),
value: this.element[0].value
};
},
open: function (event) {
var self = this;
if (this.newelement.attr("aria-disabled") != 'true') {
// TODO: seems to be useless
// this._refreshPosition();
this._closeOthers(event);
this.newelement
.addClass('ui-state-active');
if (self.options.wrapperElement) {
this.list.parent().appendTo('body');
} else {
this.list.appendTo('body');
}
this.list.addClass(self.widgetBaseClass + '-open')
.attr('aria-hidden', false)
.find('li:not(.' + self.widgetBaseClass + '-group):eq(' + this._selectedIndex() + ') a')[0].focus();
if (this.options.style == "dropdown") {
this.newelement.removeClass('ui-corner-all').addClass('ui-corner-top');
}
this._refreshPosition();
this._trigger("open", event, this._uiHash());
}
},
close: function (event, retainFocus) {
if (this.newelement.is('.ui-state-active')) {
this.newelement
.removeClass('ui-state-active');
this.list
.attr('aria-hidden', true)
.removeClass(this.widgetBaseClass + '-open');
if (this.options.style == "dropdown") {
this.newelement.removeClass('ui-corner-top').addClass('ui-corner-all');
}
if (retainFocus) {
this.newelement.focus();
}
this._trigger("close", event, this._uiHash());
}
},
change: function (event) {
this.element.trigger("change");
this._trigger("change", event, this._uiHash());
},
select: function (event) {
this._trigger("select", event, this._uiHash());
},
_closeOthers: function (event) {
$('.' + this.widgetBaseClass + '.ui-state-active').not(this.newelement).each(function () {
$(this).data('selectelement').selectmenu('close', event);
});
$('.' + this.widgetBaseClass + '.ui-state-hover').trigger('mouseout');
},
_toggle: function (event, retainFocus) {
if (this.list.is('.' + this.widgetBaseClass + '-open')) {
this.close(event, retainFocus);
} else {
this.open(event);
}
},
_formatText: function (text) {
return (this.options.format ? this.options.format(text) : text);
},
_selectedIndex: function () {
return this.element[0].selectedIndex;
},
_selectedOptionLi: function () {
return this._optionLis.eq(this._selectedIndex());
},
_focusedOptionLi: function () {
return this.list.find('.' + this.widgetBaseClass + '-item-focus');
},
_moveSelection: function (amt) {
var currIndex = parseInt(this._selectedOptionLi().data('index'), 10);
var newIndex = currIndex + amt;
return this._optionLis.eq(newIndex).trigger('mouseup');
},
_moveFocus: function (amt) {
if (!isNaN(amt)) {
var currIndex = parseInt(this._focusedOptionLi().data('index') || 0, 10);
var newIndex = currIndex + amt;
}
else {
var newIndex = parseInt(this._optionLis.filter(amt).data('index'), 10);
}
if (newIndex < 0) {
newIndex = 0;
}
if (newIndex > this._optionLis.size() - 1) {
newIndex = this._optionLis.size() - 1;
}
var activeID = this.widgetBaseClass + '-item-' + Math.round(Math.random() * 1000);
this._focusedOptionLi().find('a:eq(0)').attr('id', '');
this._optionLis.eq(newIndex).find('a:eq(0)').attr('id', activeID).focus();
this.list.attr('aria-activedescendant', activeID);
},
_scrollPage: function (direction) {
var numPerPage = Math.floor(this.list.outerHeight() / this.list.find('li:first').outerHeight());
numPerPage = (direction == 'up' ? -numPerPage : numPerPage);
this._moveFocus(numPerPage);
},
_setOption: function (key, value) {
this.options[key] = value;
if (key == 'disabled') {
this.close();
this.element
.add(this.newelement)
.add(this.list)[value ? 'addClass' : 'removeClass'](
this.widgetBaseClass + '-disabled' + ' ' +
this.namespace + '-state-disabled')
.attr("aria-disabled", value);
}
},
index: function (newValue) {
if (arguments.length) {
this.element[0].selectedIndex = newValue;
this._refreshValue();
} else {
return this._selectedIndex();
}
},
value: function (newValue) {
if (arguments.length) {
this.element[0].value = newValue;
this._refreshValue();
} else {
return this.element[0].value;
}
},
_refreshValue: function () {
var activeClass = (this.options.style == "popup") ? " ui-state-active" : "";
var activeID = this.widgetBaseClass + '-item-' + Math.round(Math.random() * 1000);
// deselect previous
this.list
.find('.' + this.widgetBaseClass + '-item-selected')
.removeClass(this.widgetBaseClass + "-item-selected" + activeClass)
.find('a')
.attr('aria-selected', 'false')
.attr('id', '');
// select new
this._selectedOptionLi()
.addClass(this.widgetBaseClass + "-item-selected" + activeClass)
.find('a')
.attr('aria-selected', 'true')
.attr('id', activeID);
// toggle any class brought in from option
var currentOptionClasses = (this.newelement.data('optionClasses') ? this.newelement.data('optionClasses') : "");
var newOptionClasses = (this._selectedOptionLi().data('optionClasses') ? this._selectedOptionLi().data('optionClasses') : "");
this.newelement
.removeClass(currentOptionClasses)
.data('optionClasses', newOptionClasses)
.addClass(newOptionClasses)
.find('.' + this.widgetBaseClass + '-status')
.html(
this._selectedOptionLi()
.find('a:eq(0)')
.html()
);
this.list.attr('aria-activedescendant', activeID);
},
_refreshPosition: function () {
var o = this.options;
// if its a native pop-up we need to calculate the position of the selected li
if (o.style == "popup" && !o.positionOptions.offset) {
var selected = this._selectedOptionLi();
var _offset = "0 -" + (selected.outerHeight() + selected.offset().top - this.list.offset().top);
}
this.list
.css({
zIndex: this.element.zIndex()
})
.position({
// set options for position plugin
of: o.positionOptions.of || this.newelement,
my: o.positionOptions.my,
at: o.positionOptions.at,
offset: o.positionOptions.offset || _offset
});
}
});
}(jQuery));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment