3
\$\begingroup\$

I am just getting into front-end web development and to practice, I made a dropdown selector that allows for more styling than the regular <select> element.

Please let me know what I can improve on (examples are appreciated, but not necessarily needed). I am mainly looking at the JavaScript, but if I am structuring things poorly in the html or there are problems in the CSS, please let me know.

I also have the project on Codepen if it is easier to see it in action.

HTML:

<!DOCTYPE html>
<html>
  <head>
    <title>Selector Styling</title>
    <link rel="stylesheet" href="css/hex-selector-0.9.9.css">
    <style>
      .container {
        width: 800px;
        margin: 0 auto;
      } 
    </style>
  </head>   
  <body>
    <div class="container">
      <div class="hex-selector">
        <div class="hex-selection" tabindex="1">
          <span value="null">Select a Place</span>
        </div>
        <div class="hex-content">
          <ul class="hex-list">
            <li value="#">Afghanistan</li>
            <li value="#">Albania</li>
            <li value="#">Algeria</li>
            ...
            <li value="#">Zimbabwe</li>
          </ul>
        </div>
      </div>
      <div class="hex-selector">
        <div class="hex-selection" tabindex="1">
          <span value="null">Select a Color</span>
        </div>
        <div class="hex-content">
          <ul class="hex-list">
            <li value="#">Red</li>
            <li value="#">Orange</li>
            <li value="#">Yellow</li>
            <li value="#">Green</li>
            <li value="#">Blue</li>
            <li value="#">Violet</li>
          </ul>
        </div>
      </div>
    </div>
    <script src="scripts/jquery-1.11.2.js"></script>
    <script src="scripts/hex-selector-0.9.9.js"></script>
  </body>
</html>

CSS:

*,
*:after,
*:before {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.hex-selector {
  position: relative;

  display: inline-block;
  margin: 10px;
  width: 350px;

  color: #fff;
  font-weight: bold;
  font-family: arial, sans-serif;

  background: #0099FF;
  border: 1px solid #3079ed;
  border-radius: 7px;
  box-shadow: 0 1px 1px rgba(50,50,50,0.1);

  cursor: pointer;
  outline: none;
  -webkit-touch-callout: none;
    -webkit-user-select: none;
     -khtml-user-select: none;
       -moz-user-select: none;
        -ms-user-select: none;
            user-select: none;
}

.hex-selection:after {
  position: absolute;
  right: 15px;
  top: 50%;

  margin-top: -3px;
  width: 0;
  height: 0;

  border-width: 6px 6px 0 6px;
  border-style: solid;
  border-color: #fff transparent;

  content: "";
}

.hex-selector:hover,
.hex-selector.active { background: #005CE6; }

.hex-selector.active > .hex-content {
  opacity: 1;
  pointer-events: auto;
}

.hex-selection { padding: 10px; }
.hex-selection:focus { box-shadow: 0 0 10px rgba(50,50,250,0.5); }

.hex-content {
  position: absolute;
  top: 110%;
  left: 0;
  right: 0;

  opacity: 0;
  pointer-events: none;
}

.hex-list {
  max-height: 250px;

  font-weight: normal;
  list-style: none;

  background: white;
  border: 1px solid rgba(0,0,0,0.17);
  border-radius: inherit;
  box-shadow: 0 0 5px rgba(0,0,0,0.1);

  overflow-y: scroll;
}

.hex-list > li {
  display: block;
  padding: 10px;

  color: #8aa8bd;
  border-bottom: 1px solid #e6e8ea;
  box-shadow: inset 0 1px 0 rgba(255,255,255,1);
}

.hex-list > li.current { background: #f3f8f8; }

Javascript:

/*
 * Hexlan Selector Replacer v0.9.0
 * 
 * Author: Reyer Swengel
 * Date: 2015-4-16
 */

(function () {
  var CURRENT       = "current",
      SELECTED      = "selected",
      ACTIVE        = "active",
      HEX_SELECTOR  = ".hex-selector",
      HEX_SELECTION = ".hex-selection",
      HEX_LIST      = ".hex-list";

  var clickTarget  = false,
      mouseIgnore  = false,
      typingReset  = 0,
      searchString = "";


  /**
     * Used as a callback for mouse events, allows mouse selection to be overridden by key navigation
     * @params {jQuery} target - element to highlight in dropdown
     */
    var mouseSelection = function(target) {
        if(!mouseIgnore) {
            $(HEX_LIST+" > li.current").removeClass(CURRENT);
            target.addClass(CURRENT);
        }
    }

    /**
     * Positions the item either at the top or bottom of the dropdown through scrolling
     * @params {jQuery} target - item to position in the dropdown
     * @params {jQuery} position - either 'top' or 'bot' referencing where it should be positioned in the dropdown
     * @params {jQuery} container - The .hex-selector containing the target
     */
    var scrollTo = function(target, position, container) {
        var temp_hex_list = $(HEX_LIST, container);
        if     (position === "top")    { temp_hex_list.scrollTop(temp_hex_list.scrollTop() + target.position().top); }
        else if(position === "bot") { temp_hex_list.scrollTop(temp_hex_list.scrollTop() - temp_hex_list.height() + (target.height() + 1)*2 + target.position().top); }
    }

    /**
     * Removes .selected from previous element and adds it to target
     * If the dropdown is open, it also updates .current
     * @params {jQuery} target - Item to change to .selected
     * @params {jQuery} container - The .hex-selector containing the target
     */
    var selectItem = function(target, container) {
        if($("."+SELECTED, container).length) { $("."+SELECTED, container).removeClass(SELECTED); }
        target.addClass(SELECTED);
        $("span", container).text(target.text());
        $("span", container).attr("value", target.attr("value"));
        if(container.hasClass(ACTIVE)) { updateCurrent(target, container); }
    }

    /**
     * Toggles the dropdown for the target. Jumps to the selected element if exists
     * @params {jQuery} target - .hex-selector to toggle
     */
    var toggleSelector = function(target) {
        if(target.hasClass(ACTIVE)) {
            target.removeClass(ACTIVE);
            $("."+CURRENT, target).removeClass(CURRENT); 
        } else {
            target.addClass(ACTIVE);

            if($("."+SELECTED, target).length) { $("."+SELECTED, target).addClass(CURRENT); } 
            else { $(HEX_LIST+" li:first-child", target).addClass(CURRENT); }

            scrollTo($("."+CURRENT, target), "top", target);
        }
    };

    /**
     * Removes .current from previous element and adds it to target
     * @params {jQuery} target - Item to change to .current
     * @params {jQuery} container - The .hex-selector containing the target
     */
    var updateCurrent = function(target, container) {
        $("."+CURRENT, container).removeClass(CURRENT);
        target.addClass(CURRENT);
    }

    /**
     * Jumps up the length of the dropdown to the next item
     * @params {jQuery} currentElement - Item to jump up from
     * @params {jQuery} container - The .hex-selector containing this dropdown
     * @returns {jQuery}: The element jumped to
     */
    var pageUp = function(currentElement, container) {
        var nextPos = currentElement.position().top + currentElement.height() - container.height();
        while(currentElement.position().top > nextPos) {
            if(currentElement.prev().length) { currentElement = currentElement.prev() }
            else { break; }
        }   
        return currentElement;
    }

    /**
     * Jumps down the length of the dropdown to the next item
     * @params {jQuery} currentElement - Item to jump down from
     * @params {jQuery} container - The .hex-selector containing this dropdown
     * @returns {jQuery}: The element jumped to
     */
     var pageDown = function(currentElement, container) {
        var nextPos = currentElement.position().top - currentElement.height() + container.height();
        while(currentElement.position().top < nextPos) {
            if(currentElement.next().length) { currentElement = currentElement.next() }
            else { break; }
        }   
        return currentElement;
    }


    $(document).on('mousedown', function(e){ click = $(e.target).closest(HEX_SELECTOR); });
    $(document).on('mousemove', function(){ mouseIgnore = false; });
    $(document).on('keydown', function(event) {
        var TAB_KEY    =  9,
          ENTER_KEY  = 13,
          ESC_KEY    = 27,
          SPACE_KEY  = 32,
          PGUP_KEY   = 33,
          PGDOWN_KEY = 34,
          END_KEY    = 35,
          HOME_KEY   = 36,
          UP_KEY     = 38,
          DOWN_KEY   = 40;


    // When in a dropdown, selects current item and closes dropdown
    if(event.which == TAB_KEY) {
        var temp_hex_selector = $(HEX_SELECTOR);
        for(var i = 0; i < temp_hex_selector.length; i++){
                var selector = $(temp_hex_selector[i]);
                if(selector.hasClass(ACTIVE)) {
                    event.preventDefault();
                    selectItem(selector.find("."+CURRENT), selector);
                    toggleSelector(selector);
                }
            }
    }

    // When in a dropdown, selects current item and closes dropdown
    // Otherwise, opens the dropdown
    if(event.which == ENTER_KEY) {
        var temp_hex_selection = $(HEX_SELECTION);
        for(var i = 0;  i < temp_hex_selection.length; i++) {
            if($(temp_hex_selection[i]).is(":focus")) {
                var selector = $(temp_hex_selection[i]).closest(HEX_SELECTOR);
            if(selector.hasClass(ACTIVE)){ selectItem(selector.find("."+CURRENT), selector); }
                    toggleSelector(selector);
        }
        }
    }

    // Closes an open dropdown without selecting an item
    if(event.which == ESC_KEY) {
        var temp_hex_selector = $(HEX_SELECTOR);
        for(var i = 0; i < temp_hex_selector.length; i++){
                var selector = $(temp_hex_selector[i]);
                if(selector.hasClass(ACTIVE)) { toggleSelector(selector); }
            }
    }

    // Selects current item without closing the dropdown
    if(event.which == SPACE_KEY) {
        var temp_hex_selection = $(HEX_SELECTION);
        for(var i = 0;  i < temp_hex_selection.length; i++) {
            if($(temp_hex_selection[i]).is(":focus")) {
                var selector = $(temp_hex_selection[i]).closest(HEX_SELECTOR);
            if(selector.hasClass(ACTIVE)){ selectItem(selector.find("."+CURRENT), selector); }
        }
        }
    }

    // Jumps the length of dropdown to next selection
    if(event.which == PGUP_KEY || event.which == PGDOWN_KEY) {
        mouseIgnore = true;
        var temp_hex_selection = $(HEX_SELECTION);
        for(var i = 0;  i < temp_hex_selection.length; i++) {
            if($(temp_hex_selection[i]).is(":focus")) {
                event.preventDefault();
                var selector = $(temp_hex_selection[i]).closest(HEX_SELECTOR);
                if(selector.hasClass(ACTIVE)){
                    var currentElement = selector.find("."+CURRENT);
                    if(event.which == PGUP_KEY) {
                    currentElement = pageUp(currentElement, currentElement.closest(HEX_LIST));
                    scrollTo($(currentElement, selector), "top", selector);
                } else {
                    currentElement = pageDown(currentElement, currentElement.closest(HEX_LIST));
                    scrollTo($(currentElement, selector), "bot", selector);
                }
                } else {
                    var currentElement = $(HEX_LIST+" li:first-child", selector);
                    if(selector.find("."+SELECTED).length) 
                    { 
                        currentElement = selector.find("."+SELECTED); 
                        if(event.which == PGUP_KEY) { currentElement = pageUp(currentElement, currentElement.closest(HEX_LIST)); }
                        else { currentElement = pageDown(currentElement, currentElement.closest(HEX_LIST)); }
                    }
                }
                selectItem(currentElement, selector);
            }
        }
    }

    // Jumps to first/last element of dropdown
    if(event.which == END_KEY || event.which == HOME_KEY) {
        mouseIgnore = true;
        var temp_hex_selection = $(HEX_SELECTION);
        for(var i = 0;  i < temp_hex_selection.length; i++) {
            if($(temp_hex_selection[i]).is(":focus")) {
                event.preventDefault();
                var selector = $(temp_hex_selection[i]).closest(HEX_SELECTOR);
                var currentElement = $(HEX_LIST+" li:last-child", selector);
                if(event.which == HOME_KEY) { currentElement = $(HEX_LIST+" li:first-child", selector); }
                selectItem(currentElement, selector);
                if(selector.hasClass(ACTIVE))
                {  
                    if(event.which == END_KEY) { scrollTo($(currentElement, selector), "bot", selector); }
                    else { scrollTo($(currentElement, selector), "top", selector); }
                } 
            }
        }
    }

    // Moves to next/previous item in dropdown
    if(event.which == UP_KEY || event.which == DOWN_KEY) {
        mouseIgnore = true;
        var temp_hex_selection = $(HEX_SELECTION);
        for(var i = 0;  i < temp_hex_selection.length; i++) {
            if($(temp_hex_selection[i]).is(":focus")) {
                event.preventDefault();
                var selector = $(temp_hex_selection[i]).closest(HEX_SELECTOR);
                if(selector.hasClass(ACTIVE)){
                    var currentElement = selector.find("."+CURRENT);
                    if(event.which == UP_KEY) {
                        if(currentElement.prev().length)      { currentElement = currentElement.prev(); }
                        if(currentElement.position().top < 0) { scrollTo($(currentElement, selector), "top", selector); }
                    } else {
                        if(currentElement.next().length)        { currentElement = currentElement.next(); }
                        if(currentElement.position().top > 211) { scrollTo($(currentElement, selector), "bot", selector); }
                    }
                } else {
                    var currentElement = $(HEX_LIST+" li:first-child", selector);
                    if(selector.find("."+SELECTED).length) { currentElement = selector.find("."+SELECTED); }
                    if(event.which == UP_KEY) {
                        if(currentElement.prev().length) { currentElement = currentElement.prev(); }
                    } else {
                        if(currentElement.next().length) { currentElement = currentElement.next(); }
                    }
                }
                selectItem(currentElement, selector);
            }
        }
    }

    // Tracks users input and attempts to jump to matching entry in dropdown
    // user input resets after 1s of no input
    if(event.which > 59 && event.which < 91) {
        mouseIgnore = true;
            var currentTime = new Date().getTime();
            if(currentTime - typingReset < 1000) { searchString = searchString + String.fromCharCode(event.keyCode); }
            else { searchString = String.fromCharCode(event.keyCode); }
            var temp_hex_selection = $(HEX_SELECTION);
        for(var i = 0;  i < temp_hex_selection.length; i++) {
            if($(temp_hex_selection[i]).is(":focus")) {
                event.preventDefault();
                var selector = $(temp_hex_selection[i]).closest(HEX_SELECTOR);
                var currentElement = $(HEX_LIST+" li:first-child", selector);
                while(currentElement.length){
                        if(currentElement.text().toLowerCase().match("^" + searchString.toLowerCase())) {
                            selectItem(currentElement, selector);
                            if($(HEX_LIST, selector).hasClass(ACTIVE)) {
                                if(currentElement.position().top < 0)        { scrollTo($(currentElement, selector), "top", selector); }
                                else if(currentElement.position().top > 211) { scrollTo($(currentElement, selector), "bot", selector); }
                            }
                            break;
                        }
                        currentElement = currentElement.next();
                    }
            }
        }
        typingReset = new Date().getTime();
    }
    });

    $(HEX_LIST+" > li").on("click", function(event) {
        var container = $(this).closest(HEX_SELECTOR)
        selectItem($(this), container);
        container.find(HEX_SELECTION).focus()
    }); 
    $(HEX_LIST+" > li").on("mouseenter", function() { mouseSelection($(this)); });
    $(HEX_LIST+" > li").on("mousemove", function()  { mouseSelection($(this)); });

    $(HEX_SELECTION).on("blur", function() {
        var container = $(this).closest(HEX_SELECTOR);
        if(container.hasClass(ACTIVE) && container[0] !== click[0]) {
            container.removeClass(ACTIVE);
        }
    });

    $(HEX_SELECTOR).on("click", function(){ toggleSelector($(this)); });

}());
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

To help ensure your site displays well across browsers, you should validate your HTML at the W3C validator.

It is actually very good, but there are two problems. <span value="null"> and <span value="#"> are invalid - you should remove the value attribute.

You can validate your CSS at a W3C validator too. Other than the unknown vender extensions, it appears to be valid.

Your JavaScript looks neat and clean for the most part, but your have irregular indentation in a few places:

for(var i = 0;  i < temp_hex_selection.length; i++) {
    if($(temp_hex_selection[i]).is(":focus")) {
        var selector = $(temp_hex_selection[i]).closest(HEX_SELECTOR);
    if(selector.hasClass(ACTIVE)){ selectItem(selector.find("."+CURRENT), selector); }
}
}

That should be indented to easily see what is part of what, like this:

for(var i = 0;  i < temp_hex_selection.length; i++) {
    if($(temp_hex_selection[i]).is(":focus")) {
        var selector = $(temp_hex_selection[i]).closest(HEX_SELECTOR);
        if(selector.hasClass(ACTIVE)) {
            selectItem(selector.find("."+CURRENT), selector);
        }
    }
}

I would also put a space around my operators to make it easy to see the separate parts of the equations:

scrollTo($("."+CURRENT, target), "top", target);

Becomes:

scrollTo($("." + CURRENT, target), "top", target);

Right here:

if(event.which == UP_KEY) {
    if(currentElement.prev().length)      { currentElement = currentElement.prev(); }
    if(currentElement.position().top < 0) { scrollTo($(currentElement, selector), "top", selector); }
}

I would not indent my if blocks like that. I would write them like this:

if(event.which == UP_KEY) {
    if(currentElement.prev().length) {
        currentElement = currentElement.prev();
    }
    if(currentElement.position().top < 0) {
        scrollTo($(currentElement, selector), "top", selector);
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.