Browse Source

Iteration on multiselect, adds label deletion, proper string delimiters

pull/2034/head
jlukic 10 years ago
parent
commit
78cee91a53
7 changed files with 386 additions and 142 deletions
  1. 1
      RELEASE-NOTES.md
  2. 40
      src/definitions/elements/label.less
  3. 455
      src/definitions/modules/dropdown.js
  4. 11
      src/definitions/modules/dropdown.less
  5. 17
      src/themes/default/elements/label.variables
  6. 3
      src/themes/default/globals/site.variables
  7. 1
      src/themes/default/modules/dropdown.variables

1
RELEASE-NOTES.md

@ -15,6 +15,7 @@
- **Grid** - `equal height` and `equal width` now work without `row` wrappers
- **Grid** - `equal height` rows can now be `stretched` as well as `middle aligned`, `bottom aligned` and `top aligned`!
- **Headers** - Added new header type `sub header`, useful for displaying small headers alongside text content. See examples [in the header docs](http://www.semantic-ui.com/elements/header.html#sub-headers)
- **Label** - Labels now have `active` and `active hover` states
- **Menu** - Added `stackable` menu variation for simple responsive menus
- **Menu** - Added many new variables to menu
- **Menu** - Fixed several inheritance issues for `dropdown item` inside `menu` appearing as `menu item`.

40
src/definitions/elements/label.less

@ -93,7 +93,8 @@ a.ui.label {
cursor: pointer;
margin-right: 0em;
margin-left: @deleteMargin;
opacity: @linkOpacity;
font-size: @deleteSize;
opacity: @deleteOpacity;
transition: @deleteTransition;
}
.ui.label .delete.icon:hover {
@ -472,6 +473,43 @@ a.ui.label:hover:before {
color: @labelHoverTextColor;
}
/*-------------------
Active
--------------------*/
.ui.active.label {
background-color: @labelActiveBackgroundColor;
border-color: @labelActiveBackgroundColor;
background-image: @labelActiveBackgroundImage;
color: @labelActiveTextColor;
}
.ui.active.label:before {
background-color: @labelActiveBackgroundColor;
background-image: @labelActiveBackgroundImage;
color: @labelActiveTextColor;
}
/*-------------------
Active Hover
--------------------*/
a.ui.labels .active.label:hover,
a.ui.active.label:hover {
background-color: @labelActiveHoverBackgroundColor;
border-color: @labelActiveHoverBackgroundColor;
background-image: @labelActiveHoverBackgroundImage;
color: @labelActiveHoverTextColor;
}
.ui.labels a.active.label:ActiveHover:before,
a.ui.active.label:ActiveHover:before {
background-color: @labelActiveHoverBackgroundColor;
background-image: @labelActiveHoverBackgroundImage;
color: @labelActiveHoverTextColor;
}
/*-------------------
Visible
--------------------*/

455
src/definitions/modules/dropdown.js

@ -42,6 +42,7 @@ $.fn.dropdown = function(parameters) {
namespace = settings.namespace,
selector = settings.selector,
error = settings.error,
templates = settings.templates,
eventNamespace = '.' + namespace,
moduleNamespace = 'module-' + namespace,
@ -207,7 +208,7 @@ $.fn.dropdown = function(parameters) {
.appendTo($module)
;
}
$menu.html( settings.templates.menu( selectValues ));
$menu.html( templates.menu( selectValues ));
}
else {
module.debug('Creating entire dropdown from select');
@ -215,7 +216,7 @@ $.fn.dropdown = function(parameters) {
.attr('class', $input.attr('class') )
.addClass(className.selection)
.addClass(className.dropdown)
.html( settings.templates.dropdown(selectValues) )
.html( templates.dropdown(selectValues) )
.insertBefore($input)
;
$input
@ -331,7 +332,7 @@ $.fn.dropdown = function(parameters) {
;
if( module.has.search() ) {
$module
.on(module.get.inputEvent(), selector.search, module.event.input)
.on(module.get.inputEvent() + eventNamespace, selector.search, module.event.input)
;
}
},
@ -500,7 +501,8 @@ $.fn.dropdown = function(parameters) {
: $activeItem,
hasSelected = ($selectedItem.size() > 0)
;
if(hasSelected && !module.is.multiple()) {
if(hasSelected) {
module.debug('Forcing partial selection to selected item', $selectedItem);
module.event.item.click.call($selectedItem);
module.remove.filteredItem();
}
@ -545,7 +547,10 @@ $.fn.dropdown = function(parameters) {
pageLostFocus = (document.activeElement === this)
;
if(!itemActivated && !pageLostFocus) {
if(settings.forceSelection) {
if(module.is.multiple()) {
module.remove.activeLabel();
}
else if(settings.forceSelection) {
module.forceSelection();
}
else {
@ -566,137 +571,207 @@ $.fn.dropdown = function(parameters) {
},
keydown: function(event) {
var
$currentlySelected = $item.not(className.filtered).filter('.' + className.selected).eq(0),
$activeItem = $menu.children('.' + className.active).eq(0),
$selectedItem = ($currentlySelected.length > 0)
? $currentlySelected
: $activeItem,
$visibleItems = ($selectedItem.length > 0)
? $selectedItem.siblings(':not(.' + className.filtered +')').andSelf()
: $menu.children(':not(.' + className.filtered +')'),
$subMenu = $selectedItem.children(selector.menu),
$parentMenu = $selectedItem.closest(selector.menu),
isSubMenuItem = $parentMenu[0] !== $menu[0],
inVisibleMenu = $parentMenu.is(':visible'),
pressedKey = event.which,
keys = {
enter : 13,
escape : 27,
leftArrow : 37,
upArrow : 38,
rightArrow : 39,
downArrow : 40
},
hasSubMenu = ($subMenu.length> 0),
hasSelectedItem = ($selectedItem.length > 0),
lastVisibleIndex = ($visibleItems.size() - 1),
$nextItem,
newIndex
keys = module.get.shortcutKeys(),
isShortcutKey = module.is.inObject(pressedKey, keys)
;
// visible menu keyboard shortcuts
if(module.is.visible()) {
// enter (select or sub-menu)
if(pressedKey == keys.enter && hasSelectedItem) {
if(hasSubMenu && !settings.allowCategorySelection) {
module.verbose('Pressed enter on unselectable category, opening sub menu');
pressedKey = keys.rightArrow;
if(isShortcutKey) {
var
$currentlySelected = $item.not(className.filtered).filter('.' + className.selected).eq(0),
$activeItem = $menu.children('.' + className.active).eq(0),
$selectedItem = ($currentlySelected.length > 0)
? $currentlySelected
: $activeItem,
$visibleItems = ($selectedItem.length > 0)
? $selectedItem.siblings(':not(.' + className.filtered +')').andSelf()
: $menu.children(':not(.' + className.filtered +')'),
$subMenu = $selectedItem.children(selector.menu),
$parentMenu = $selectedItem.closest(selector.menu),
inVisibleMenu = ($parentMenu.hasClass(className.visible) || $parentMenu.hasClass(className.animating)),
hasSubMenu = ($subMenu.length> 0),
hasSelectedItem = ($selectedItem.length > 0),
$nextItem,
isSubMenuItem,
newIndex
;
// visible menu keyboard shortcuts
if( module.is.visible() ) {
// enter (select or open sub-menu)
if(pressedKey == keys.enter && hasSelectedItem) {
if(hasSubMenu && !settings.allowCategorySelection) {
module.verbose('Pressed enter on unselectable category, opening sub menu');
pressedKey = keys.rightArrow;
}
else {
module.verbose('Enter key pressed, choosing selected item');
module.event.item.click.call($selectedItem, event);
}
}
else {
module.verbose('Enter key pressed, choosing selected item');
module.event.item.click.call($selectedItem, event);
// left arrow (hide sub-menu)
if(pressedKey == keys.leftArrow) {
isSubMenuItem = ($parentMenu[0] !== $menu[0]);
if(isSubMenuItem) {
module.verbose('Left key pressed, closing sub-menu');
module.animate.hide(false, $parentMenu);
$selectedItem
.removeClass(className.selected)
;
$parentMenu
.closest(selector.item)
.addClass(className.selected)
;
event.preventDefault();
}
}
}
// left arrow (hide sub-menu)
if(pressedKey == keys.leftArrow) {
if(isSubMenuItem) {
module.verbose('Left key pressed, closing sub-menu');
module.animate.hide(false, $parentMenu);
$selectedItem
.removeClass(className.selected)
// right arrow (show sub-menu)
if(pressedKey == keys.rightArrow) {
if(hasSubMenu) {
module.verbose('Right key pressed, opening sub-menu');
module.animate.show(false, $subMenu);
$selectedItem
.removeClass(className.selected)
;
$subMenu
.find(selector.item).eq(0)
.addClass(className.selected)
;
event.preventDefault();
}
}
// up arrow (traverse menu up)
if(pressedKey == keys.upArrow) {
$nextItem = (hasSelectedItem && inVisibleMenu)
? $selectedItem.prevAll(selector.item + ':not(.' + className.filtered + ')').eq(0)
: $item.eq(0)
;
$parentMenu
.closest(selector.item)
if($visibleItems.index( $nextItem ) < 0) {
module.verbose('Up key pressed but reached top of current menu');
return;
}
else {
module.verbose('Up key pressed, changing active item');
$selectedItem
.removeClass(className.selected)
;
$nextItem
.addClass(className.selected)
;
;
module.set.scrollPosition($nextItem);
}
event.preventDefault();
}
event.preventDefault();
}
// right arrow (show sub-menu)
if(pressedKey == keys.rightArrow) {
if(hasSubMenu) {
module.verbose('Right key pressed, opening sub-menu');
module.animate.show(false, $subMenu);
$selectedItem
.removeClass(className.selected)
// down arrow (traverse menu down)
if(pressedKey == keys.downArrow) {
$nextItem = (hasSelectedItem && inVisibleMenu)
? $nextItem = $selectedItem.nextAll(selector.item + ':not(.' + className.filtered + ')').eq(0)
: $item.eq(0)
;
$subMenu
.find(selector.item).eq(0)
if($nextItem.length === 0) {
module.verbose('Down key pressed but reached bottom of current menu');
return;
}
else {
module.verbose('Down key pressed, changing active item');
$item
.removeClass(className.selected)
;
$nextItem
.addClass(className.selected)
;
;
module.set.scrollPosition($nextItem);
}
event.preventDefault();
}
event.preventDefault();
}
// up arrow (traverse menu up)
if(pressedKey == keys.upArrow) {
$nextItem = (hasSelectedItem && inVisibleMenu)
? $selectedItem.prevAll(selector.item + ':not(.' + className.filtered + ')').eq(0)
: $item.eq(0)
;
if($visibleItems.index( $nextItem ) < 0) {
module.verbose('Up key pressed but reached top of current menu');
return;
else {
// enter (open menu)
if(pressedKey == keys.enter) {
module.verbose('Enter key pressed, showing dropdown');
module.show();
}
else {
module.verbose('Up key pressed, changing active item');
$selectedItem
.removeClass(className.selected)
;
$nextItem
.addClass(className.selected)
;
module.set.scrollPosition($nextItem);
// escape (close menu)
if(pressedKey == keys.escape) {
module.verbose('Escape key pressed, closing dropdown');
module.hide();
}
// down arrow (open menu)
if(pressedKey == keys.downArrow) {
module.verbose('Down key pressed, showing dropdown');
module.show();
}
event.preventDefault();
}
// down arrow (traverse menu down)
if(pressedKey == keys.downArrow) {
$nextItem = (hasSelectedItem && inVisibleMenu)
? $nextItem = $selectedItem.nextAll(selector.item + ':not(.' + className.filtered + ')').eq(0)
: $item.eq(0)
// multiple selection shortcuts
if( module.is.multiple() ) {
var
$label = $module.find(selector.label),
$activeLabel = $label.filter('.' + className.active),
activeValue = $activeLabel.data('value'),
labelIndex = $label.index($activeLabel),
labelCount = $label.length,
hasActiveLabel = ($activeLabel.length > 0),
isFirstLabel = (labelIndex == 0),
isLastLabel = (labelIndex + 1 == labelCount),
caretAtStart = (module.get.caretPosition() == 0)
;
if($nextItem.length === 0) {
module.verbose('Down key pressed but reached bottom of current menu');
return;
if(pressedKey == keys.delimiter) {
// tokenize on comma
}
else if(pressedKey == keys.leftArrow) {
// activate previous label
if(caretAtStart && !hasActiveLabel) {
$label.last().addClass(className.active);
}
else if(hasActiveLabel && !isFirstLabel) {
$activeLabel
.removeClass(className.active)
.prev()
.addClass(className.active)
.end()
;
event.preventDefault();
}
}
else if(pressedKey == keys.rightArrow) {
// activate next label
if(hasActiveLabel) {
$activeLabel
.removeClass(className.active)
.next()
.addClass(className.active)
.end()
;
event.preventDefault();
}
}
else if(pressedKey == keys.deleteKey || pressedKey == keys.backspace) {
if(caretAtStart && !hasActiveLabel) {
$activeLabel = $label.last().addClass(className.active);
activeValue = $activeLabel.data('value');
module.remove.selected(activeValue);
}
else if(hasActiveLabel) {
$activeLabel.next().addClass(className.active);
module.remove.selected(activeValue);
}
// delete tag if empty search and selected tag
}
else {
module.verbose('Down key pressed, changing active item');
$item
.removeClass(className.selected)
;
$nextItem
.addClass(className.selected)
;
module.set.scrollPosition($nextItem);
$activeLabel.removeClass(className.active);
}
event.preventDefault();
}
}
else {
// enter (open menu)
if(pressedKey == keys.enter) {
module.verbose('Enter key pressed, showing dropdown');
module.show();
}
// escape (close menu)
if(pressedKey == keys.escape) {
module.verbose('Escape key pressed, closing dropdown');
module.hide();
}
// down arrow (open menu)
if(pressedKey == keys.downArrow) {
module.verbose('Down key pressed, showing dropdown');
module.show();
}
}
},
test: {
@ -889,6 +964,42 @@ $.fn.dropdown = function(parameters) {
return $.inArray(value, array) === index;
});
},
caretPosition: function() {
var
input = $search.get(0),
range,
rangeLength
;
if ('selectionStart' in input) {
return input.selectionStart;
}
else if (document.selection) {
input.focus();
range = document.selection.createRange();
rangeLength = range.text.length;
range.moveStart('character', -input.value.length);
return range.text.length - rangeLength;
}
},
keyCode: function(letter) {
return (typeof letter == 'string')
? letter.charCodeAt(0)
: false
;
},
shortcutKeys: function() {
return {
backspace : 8,
delimiter : module.get.keyCode(settings.delimiter),
deleteKey : 46,
enter : 13,
escape : 27,
leftArrow : 37,
upArrow : 38,
rightArrow : 39,
downArrow : 40
};
},
value: function() {
return ($input.length > 0)
? $input.val()
@ -899,9 +1010,12 @@ $.fn.dropdown = function(parameters) {
var
value = module.get.value()
;
return $.isArray(value)
if(value == '') {
return '';
}
return ($input.is('select') || !module.is.multiple())
? value
: [value]
: value.split(settings.delimiter)
;
},
choiceText: function($choice, preserveHTML) {
@ -1001,8 +1115,8 @@ $.fn.dropdown = function(parameters) {
;
value = (value !== undefined)
? value
: ( module.get.value() !== undefined)
? module.get.value()
: ( module.get.values() !== undefined)
? module.get.values()
: module.get.text()
;
isMultiple = (module.is.multiple() && $.isArray(value));
@ -1115,10 +1229,15 @@ $.fn.dropdown = function(parameters) {
filtered: function() {
var
searchValue = $search.val(),
searchWidth = ((searchValue.length) * settings.glyphWidth) + 'em',
hasSearchValue = (typeof searchValue === 'string' && searchValue.length > 0)
;
if(hasSearchValue) {
$text.addClass(className.filtered);
if(module.is.multiple()) {
module.verbose('Adjusting input width', searchWidth, settings.glyphWidth)
$search.css('width', searchWidth);
}
}
else {
$text.removeClass(className.filtered);
@ -1247,9 +1366,9 @@ $.fn.dropdown = function(parameters) {
module.debug('Setting mutiple select values', values, $input);
}
else {
values = values.join(',');
values = values.join(settings.delimiter);
$input.val(values);
module.debug('Setting hidden input to comma separatd values', values, $input);
module.debug('Setting hidden input to delimited values', values, $input);
}
},
active: function() {
@ -1306,12 +1425,12 @@ $.fn.dropdown = function(parameters) {
$label = $('<a />')
.addClass(className.label)
.attr('data-value', value)
.html(text + '<i class="delete icon"></i>')
.html(templates.label(value, text))
;
if(settings.label.variation) {
$label.addClass(settings.label.variation);
}
if(shouldAnimate) {
if(shouldAnimate == true) {
module.debug('Animating in label', $label);
$label
.addClass(className.hidden)
@ -1332,6 +1451,9 @@ $.fn.dropdown = function(parameters) {
active: function() {
$module.removeClass(className.active);
},
activeLabel: function() {
$module.find(selector.label).removeClass(className.active);
},
visible: function() {
$module.removeClass(className.visible);
},
@ -1342,19 +1464,67 @@ $.fn.dropdown = function(parameters) {
$item.removeClass(className.filtered);
},
searchTerm: function() {
module.verbose('Cleared search term');
$search.val('');
},
selected: function(value) {
var
$selectedItem = module.get.item(value),
$option,
values = $input.val(),
selectedValue = module.get.choiceValue($selectedItem)
;
if($selectedItem) {
if( $input.is('select') ) {
$input
.find('option[value="' + selectedValue + '"]')
.prop('selected', false)
;
}
else {
values = module.remove.delimitedValue(selectedValue, values);
$input.val(values);
}
if(module.is.multiple()) {
module.remove.label(selectedValue);
}
$selectedItem
.removeClass(className.active)
;
}
},
selectedItem: function() {
$item.removeClass(className.selected);
},
delimitedValue: function(removedValue, values) {
if(typeof values != 'string') {
return false;
}
values = values.split(settings.delimiter);
values = $.grep(values, function(value){
return (removedValue != value);
});
values = values.join(settings.delimiter);
module.verbose('Removed value from delimited string', removedValue, values);
return values;
},
label: function(value) {
$module
.find(selector.label)
.filter('[data-value="' + value +'"]')
var
$labels = $module.find(selector.label),
$removedLabel = $labels.filter('[data-value="' + value +'"]'),
labelCount = $labels.length,
shouldAnimate = (labelCount == 1 || $labels.index($removedLabel) + 1 == labelCount)
;
if(shouldAnimate) {
$removedLabel
.transition(settings.label.transition, settings.label.duration, function() {
$(this).remove();
$removedLabel.remove();
})
;
;
}
else {
$removedLabel.remove();
}
},
tabbable: function() {
if( module.has.search() ) {
@ -1412,6 +1582,18 @@ $.fn.dropdown = function(parameters) {
: $menu.is(':hidden')
;
},
inObject: function(needle, object) {
var
found = false
;
$.each(object, function(index, property) {
if(property == needle) {
found = true;
return true;
}
});
return found;
},
multiple: function() {
return $module.hasClass(className.multiple);
},
@ -1850,10 +2032,14 @@ $.fn.dropdown.settings = {
touch : 50
},
forceSelection : true,
forceSelection : true,
// widest glyph width in em (W is 1.0714 em)
glyphWidth : 1.0714,
transition : 'auto',
duration : 250,
transition : 'auto',
delimiter : ',',
duration : 250,
/* Callbacks */
onNoResults : function(searchTerm){},
@ -1926,6 +2112,9 @@ $.fn.dropdown.settings.templates = {
});
return html;
},
label: function(value, text) {
return text + '<i class="delete icon"></i>';
},
dropdown: function(select) {
var
placeholder = select.placeholder || false,

11
src/definitions/modules/dropdown.less

@ -599,20 +599,18 @@ select.ui.dropdown {
}
/* Multiple Search Selection */
.ui.multiple.search.dropdown {
.ui.multiple.search.dropdown,
.ui.multiple.search.dropdown > input.search {
cursor: text;
}
/* Selection Label */
.ui.multiple.dropdown > .label {
display: inline-block;
vertical-align: middle;
vertical-align: baseline;
padding: @labelPadding;
margin: (@labelVerticalSpacing / 2) @labelHorizontalSpacing (@labelVerticalSpacing / 2) 0em;
}
.ui.multiple.dropdown > .label .delete {
opacity: 0.4;
}
/* Prompt Text */
.ui.multiple.dropdown > .text {
@ -626,7 +624,8 @@ select.ui.dropdown {
.ui.multiple.selection.dropdown > input.search {
position: static;
padding: 0;
width: 5em;
width: @multipleSelectionSearchWidth;
max-width: 100%;
margin: @multipleSelectionSearchMargin;
}

17
src/themes/default/elements/label.variables

@ -35,7 +35,7 @@
Parts
--------------------*/
@linkOpacity: 0.8;
@linkOpacity: 0.5;
@linkTransition: @labelTransitionDuration opacity @labelTransitionEasing;
@iconDistance: 0.75em;
@ -45,6 +45,8 @@
@detailIconDistance: 0.25em;
@detailMargin: 1em;
@deleteOpacity: @linkOpacity;
@deleteSize: @relativeSmall;
@deleteMargin: 0.5em;
@deleteTransition: background @labelTransitionDuration @labelTransitionEasing;
@ -78,12 +80,23 @@
@labelHoverBackgroundImage: none;
@labelHoverTextColor: @hoveredTextColor;
/* Hover */
@labelActiveBackgroundColor: #D0D0D0;
@labelActiveBackgroundImage: none;
@labelActiveTextColor: @selectedTextColor;
/* Active Hover */
@labelActiveHoverBackgroundColor: #C8C8C8;
@labelActiveHoverBackgroundImage: none;
@labelActiveHoverTextColor: @selectedTextColor;
/*-------------------
Variations
--------------------*/
/* Tag */
@tagCircleColor: #FFFFFF;
@tagCircleColor: @white;
@tagCircleSize: 0.5em;
@tagHorizontalPadding: 1.5em;
@tagCircleBoxShadow: 0 -1px 1px 0 rgba(0, 0, 0, 0.3);

3
src/themes/default/globals/site.variables

@ -386,6 +386,9 @@
/* Rendered Scrollbar Width */
@scrollBarWidth: 15px;
/* Maximum Single Character Glyph Width, aka Capital "W" */
@glyphWidth: 1.1em;
/* Used to match floats with text */
@lineHeightOffset : ((@lineHeight - 1em) / 2);
@headerLineHeightOffset : (@headerLineHeight - 1em) / 2;

1
src/themes/default/modules/dropdown.variables

@ -220,6 +220,7 @@
@multipleSelectionLeftPadding: (@selectionHorizontalPadding - @labelHorizontalPadding);
@multipleSelectionSearchMargin: 0.4em 0em 0.4em 0.5em;
@multipleSelectionSearchWidth: (@glyphWidth * 2);
/* Inline */
@inlineIconMargin: 0em 0.5em 0em 0.25em;

Loading…
Cancel
Save