Reputation: 26281
I am creating my first jQuery plugin and have a somewhat working example (see http://tapmeister.com/test/selector.html) and script below. I have been struggling with positioning an element for days.
The desired look and functionality starts with showing some text and an optional down arrow to the right of the text. Upon clicking, the text/arrow is replaced with a list of options with the currently selected option highlighted and at the same location of the original text. Clicking an option makes it the currently selected option, and initiates a user defined callback, and clicking any other space closes the dialog.
The plugin replaces the original select element, and I wish to preserve whether it is inline or block. When making them inline, however, the position:absolute <ul class="selectIt-list">
popup does not appear above it's associated text <span class="selectIt-text">
. Since both the span text and ul popup are both in position:relative <div class="selectIt">
, why is it located to the far left?
As a side note, a major reason why I am doing this is to learn a consistent pattern on how plugins should be built. I am strongly basing my approach on http://docs.jquery.com/Plugins/Authoring (any reason I shouldn't?). I am hoping someone can review it, confirm whether I am correctly interpreting how to build a plugin, and provide any suggestions. I am also not sure whether I really get the whole namespace thing. Maybe I should post this as a separate question?, but I don't know whether asking for code critique is appropriate.
Thanks!
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1" />
<title>Testing</title>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<style type="text/css">
/* CSS associated with plugin */
div.selectIt {
position:relative;
}
span.selectIt-text {
color: #2C46B7;
cursor: pointer;
font-family: Verdana;
font-size: 10px;
display:inline;
}
span.selectIt-text,
ul.selectIt-list {
padding: 7px 3px 5px;
}
ul.selectIt-list {
display:none;
margin:0px;
position: absolute;
background-color: #FFFFFF;
border: 1px solid #B3B4A7;
z-index: 50;
box-shadow: 3px 1px 6px #505050;
}
ul.selectIt-list li {
background-color: #FFFFFF;
color: #393926;
list-style-type: none;
font-family: Verdana;
font-size: 10px;
margin-bottom: 2px;
padding-left: 2px;
padding-right: 2px;
}
ul.selectIt-list li.selectIt-hover {
background-color: #DFDED8;
color: #393926;
}
ul.selectIt-list li.selectIt-selected {
background-color: #737060;
color: #FFFFFF;
cursor: default;
}
/* CSS Not associated with plugin */
ul.myList > li {
list-style-type: none;
display:inline;
}
#horizontalList select.mySelect {
display:inline;
}
#verticalList select.mySelect {
display:block;
}
span.label {
color: rgb(57, 57, 38);
font-family: Verdana;
font-size: 10px;
font-weight: bold;
}
span.selectIt-text {
background-image: url("dropdown_arrow_blue.gif");
background-position:right center;
background-repeat: no-repeat;
padding-right: 10px; /*Override padding-right to allow for image*/
}
</style>
<script>
/*
* jQuery SelectIT
* Copyright 2012 Michael Reed
* Dual licensed under the MIT and GPL licenses.
*
*/
(function( $ ){
var methods = {
init : function( options ) {
//console.log('init');
// Create some defaults, extending them with any options that were provided
var settings = $.extend({
'class' : null, //Applies custom class so user may override
'extraWidth' : 0, //Extra space to add to width to allow for user image css
'click' : function(){} //Some user defined code.
}, options || {}); //Just in case user doesn't provide options
//Apply events that should only happen once (unlike what is done by http://docs.jquery.com/Plugins/Authoring#Events. Why did they do it that way?)
$(document).on('click.selectIt',methods.close);
return this.each(function(){
var $t = $(this),
data = $t.data('selectIt');
//Replace element
var list = $('<ul/>', {'class': "selectIt-list" }),
length,
max=0;
$t.find('option').each(function(i){
var $this=$(this),
text=$this.text(),
length = parseInt($this.css('width'));
max = length > max ? length : max;
list.append( $('<li/>', {text: text}))
});
max=max+settings.extraWidth;
list.css('width',max+'px');
var selectIt = $('<div/>', {class:"selectIt"+((settings.class)?' '+settings.class:'')}) //Didn't work to display same as original element: display:$t.css('display')
.append($('<span/>', {class:'selectIt-text',text:$t.find(":selected").text()}))
.append(list)
.css({display:$t.css('display')}); //Set to same as original element
//Apply events
selectIt.on('click.selectIt','span.selectIt-text',methods.open);
selectIt.on('mouseenter.selectIt','ul.selectIt-list li',methods.mouseenter);
selectIt.on('mouseleave.selectIt','ul.selectIt-list li',methods.mouseleave);
selectIt.on('click.selectIt','ul.selectIt-list li',methods.click);
// If the plugin hasn't been initialized yet. Why?
if ( ! data ) {
// Do more setup stuff here
$t.data('selectIt', {
target : $t, //Not really sure where this will be used
selectIt : selectIt, //Will be used when removing in destroy method
settings: settings //Save here so other methods have access
});
}
$t.hide().after(selectIt);
});
},
destroy : function(e) {
//console.log('destroy');
return this.each(function(){
var $t = $(this);
$t.off('.selectIt'); //Removes any events in selectIt namespace
$t.data('selectIt').selectIt.remove(); //Removes element from page
$t.removeData('selectIt'); //Removes data in selectIt namespace
//Should the original element be made visible?
})
},
open : function(e) {
//console.log('open');
methods.close();
var $t=$(this),
$p=$t.parent(),
index=$p.prev().prop("selectedIndex"),
list=$p.find('ul.selectIt-list').show();
list.find('li.selectIt-selected').removeClass('selectIt-selected');
list.find('li').eq(index).addClass('selectIt-selected');
var top = 0 - list.find('li').eq(index).position().top;
list.css({top: top});
e.stopPropagation(); //Don't trigger documents click
},
//If this is all I am doing, can change to just using CSS psudo class.
mouseenter : function(e) {
$(this).addClass('selectIt-hover');
},
mouseleave : function(e) {
$(this).removeClass('selectIt-hover');
},
click : function(e) {
//console.log('click');
var $t=$(this);
if(!$t.hasClass('selectIt-selected'))
{
var $p=$t.parent(),
$pp=$p.parent(),
select=$pp.prev(),
index=$t.index(),
option=select.find('option').eq(index),
value=(value=option.val())?value:option.text();
select.val(value).prop('selectedIndex',index);
$pp.find('span.selectIt-text').text($t.text());
select.data('selectIt').settings.click.call(this,value);
}
},
close : function(e) {
//console.log('close');
$('div.selectIt').find('ul.selectIt-list').hide().parent();//.find('span.selectIt-text').show();
},
update : function(content) {alert('When will this type of method be used?');}
};
$.fn.selectIt = function(method) {
if ( methods[method] ) {
return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof method === 'object' || ! method ) {
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + method + ' does not exist on jQuery.selectIt' );
}
};
})( jQuery );
$(function(){
$('select.mySelect').selectIt({
//'class' : 'whatever',
'extraWidth' : 0,
'click':function(value){
switch(value)
{
case 'first':console.log('Do something first');break;
case 'second':console.log('Do something second');break;
case 'third':console.log('Do something third');break;
default: console.log('Do something default');
}
}
});
$('#horizontalList button.destroy').click(function(){
$('#horizontalList select.mySelect').selectIt('destroy');
});
$('#verticalList button.destroy').click(function(){
$('#verticalList select.mySelect').selectIt('destroy');
});
});
</script>
</head>
<body>
<div id="horizontalList">
<h1>Horizontal List</h1>
<ul class="myList">
<li>
<span class="label">Label</span>
<select class="mySelect">
<option value="first">First</option>
<option value="second" selected="selected">Second</option>
<option>Third</option>
</select>
</li>
<li>
<span class="label">Some Label</span>
<select class="mySelect">
<option value="first">First</option>
<option value="second" selected="selected">Second</option>
<option value="third">Third</option>
</select>
</li>
<li>
<span class="label">Some Label</span>
<select class="mySelect">
<option value="">First</option>
<option value="second">Second</option>
<option value="third">Third</option>
</select>
</li>
</ul>
<button class="destroy">Destroy</button>
</div>
<div id="verticalList">
<h1>Vertical List</h1>
<ul class="myList">
<li>
<span class="label">Label</span>
<select class="mySelect">
<option value="first">First</option>
<option value="second" selected="selected">Second</option>
<option>Third</option>
</select>
</li>
<li>
<span class="label">Some Label</span>
<select class="mySelect">
<option value="first">First</option>
<option value="second" selected="selected">Second</option>
<option value="third">Third</option>
</select>
</li>
<li>
<span class="label">Some Label</span>
<select class="mySelect">
<option value="">First</option>
<option value="second">Second</option>
<option value="third">Third</option>
</select>
</li>
</ul>
<button class="destroy">Destroy</button>
</div>
</body>
</html>
Upvotes: 1
Views: 449
Reputation: 696
There is a line in your JQuery [line 173]:
list.css({top: top});
Insert this line below it:
list.css({left: $(this).position().left});
Upvotes: 1
Reputation: 4883
For positioning what you described I would highly recommend using jQuery's "my at of" positioning. It's a huge headache saver!
http://docs.jquery.com/UI/API/1.8/Position
Applying to your example, it would look something like:
//PLACE INSIDE YOUR EVENT HANDLER
$(".selectIt-list").position({
my: "bottom center",
at: "top center",
of: ".selectIt-text",
});
Upvotes: 1