From 78cee91a532adc10cddc183f782634dbe4ffee10 Mon Sep 17 00:00:00 2001 From: jlukic Date: Mon, 23 Mar 2015 18:36:32 -0400 Subject: [PATCH] Iteration on multiselect, adds label deletion, proper string delimiters --- RELEASE-NOTES.md | 1 + src/definitions/elements/label.less | 40 +- src/definitions/modules/dropdown.js | 455 +++++++++++++----- src/definitions/modules/dropdown.less | 11 +- src/themes/default/elements/label.variables | 17 +- src/themes/default/globals/site.variables | 3 + src/themes/default/modules/dropdown.variables | 1 + 7 files changed, 386 insertions(+), 142 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 323ed425f..5cf3bcacc 100644 --- a/RELEASE-NOTES.md +++ b/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`. diff --git a/src/definitions/elements/label.less b/src/definitions/elements/label.less index 9e61d443c..43c1f4694 100755 --- a/src/definitions/elements/label.less +++ b/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 --------------------*/ diff --git a/src/definitions/modules/dropdown.js b/src/definitions/modules/dropdown.js index c96d9d20d..c8d9b5496 100644 --- a/src/definitions/modules/dropdown.js +++ b/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 = $('') .addClass(className.label) .attr('data-value', value) - .html(text + '') + .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 + ''; + }, dropdown: function(select) { var placeholder = select.placeholder || false, diff --git a/src/definitions/modules/dropdown.less b/src/definitions/modules/dropdown.less index ae3aa1778..d4f77297b 100755 --- a/src/definitions/modules/dropdown.less +++ b/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; } diff --git a/src/themes/default/elements/label.variables b/src/themes/default/elements/label.variables index b1c4292e1..872a88401 100644 --- a/src/themes/default/elements/label.variables +++ b/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); diff --git a/src/themes/default/globals/site.variables b/src/themes/default/globals/site.variables index ff977dfb8..e0844852a 100644 --- a/src/themes/default/globals/site.variables +++ b/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; diff --git a/src/themes/default/modules/dropdown.variables b/src/themes/default/modules/dropdown.variables index d9ac5bc74..40b93a672 100644 --- a/src/themes/default/modules/dropdown.variables +++ b/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;