Menu by Floating Layer

I recently created a generic floating layer menu replacement function that can easily replace traditional SELECT single-choice menus with fashionable floating layer menus.

Source Code

DivMenu.css

a.divmenu, a.divmenu:hover  {
    padding: 1px 1px 1px 3px;
    border: 1px solid #9999CC;
    color:#000;
    text-decoration:none;
    cursor:default;
}
/* Text style on the hint bar. 'a' is an inline element, and needs display:inline-block to set its width in the program */
a.divmenu span.menuhint {
    display:inline-block;
}
/* Arrow on the hint bar */
a.divmenu span.menuarrow {
    color: #000;
}
/* Style on mouse hover
IE6 bug handling for CSS pseudo-class :hover. In IE 6, a:hover and a must have clearly different definitions to make a:hover span effective, so a new definition that doesn't affect the effect can be added according to the actual situation.
*/
a.divmenu:hover
{
    color:#fff;
    border: 1px solid #33f;
    background-color: #36f;
}
/* Doesn't work in IE 6, see a.divmenu:hover above for solution */
a.divmenu:hover span {
    color: #fff;
}
/* Style of the floating menu layer. Setting to display:inline-block; allows this layer's width to automatically adapt to the internal text instead of the full window width. But it doesn't work in IE, and display:table-cell; doesn't work either. */
.divmenu_panel {
    position: absolute;
    display: none;
    z-index: 9999;
    width: 100px;
    border: 1px solid #999999;
    background-color: #fff;
}
/* Style of links that replace the original options in the menu layer. Using cursor:default; is to restore the same mouse pointer as traditional menus, rather than the hand pointer used for links. display:block; makes 'a' form its own line without writing other tags. */
.divmenu_panel a {
    cursor:default;
    display:block;
    color:#000;
    text-decoration:none;
}
.divmenu_panel a:hover {
    color:#399;
    background-color: #f99;
    background-color: #BBDDFF;
    color: #3366FF;
}
/* Style of option group bands */
.divmenu_group {
    display:block;
    background-color: #9cf;
}

DivMenu.js

// Because the replacement links need to use global event handlers, it's not very convenient for this program to use completely anonymous functions, so let's define a global object first
// Let's encapsulate a simple event handler registration object here first
var DivMenu =
{
    // Event registration compatible with IE and FF. No need to write event unregistration in this object for now.
    add : function(element, eventType, handler)
    {
        if (document.addEventListener)
        {
            element.addEventListener(eventType, handler, false);
        }
        else if (document.attachEvent)
        {
            element.attachEvent("on" + eventType, handler);
        };

    },
    // Get the absolute position of page elements relative to the window
    getAbsPoint : function (e)
    {
        var x = e.offsetLeft;
        var y = e.offsetTop;
        while(e = e.offsetParent)
        {
            x += e.offsetLeft;
            y += e.offsetTop;
        }
        return {"x": x, "y": y};
    },
    // The parameter of show is the id of the original SELECT form item such as city, but to find the position, we need to find the replacement SELECT 'a' element such as city_a, and the menu layer it displays is city_div
    show : function (sourceId)
    {
        var sourceobj = document.getElementById(sourceId+'_a');
        var panelId = "divmenu_panel";
        var panel= document.getElementById(panelId);
        if (!panel)
        {
            panel= document.createElement("div");
            panel.className = "divmenu_panel";
            panel.id=panelId;
            document.body.appendChild(panel);
        }
        panel.innerHTML = document.getElementById(sourceId+'_div').innerHTML;
        // In FF, we still need to display this layer when it's on the layer, otherwise it will be hidden again when moving to the menu layer. Currently, there's no need to use the contains method in FF, so let's comment out the contains method defined for FF on the previous page.
        //panel.setAttribute('onmouseover',"DivMenu.show('" + sourceId +"');");

        // Align left with the initiating element
        var xy = this.getAbsPoint(sourceobj);
        panel.style.left = xy.x + "px";
        // Add the height of the initiating element vertically
        panel.style.top = (xy.y + sourceobj.offsetHeight) + "px";
        panel.style.display = "block";
        if (navigator.appName == "Microsoft Internet Explorer")
        {
            // Add an IFRAME layer to solve the problem of SELECT blocking DIV layer in IE6
            var iframeId = "divmenu_panel_iframe";
            var iframe_dom = document.getElementById(iframeId);
            if(!iframe_dom) // If it doesn't exist, automatically generate iframe
            {
                var tmpIframeDom    = document.createElement("IFRAME");
                tmpIframeDom.id     = iframeId;
                document.body.appendChild(tmpIframeDom);
                iframe_dom = document.getElementById(iframeId);
                iframe_dom.src  = "about:blank";    //javascript:void(0);  about:blank
                iframe_dom.style.position = "absolute";
                iframe_dom.style.scrolling = "no";
                iframe_dom.style.frameBorder = 0;
                //iframe_dom.style.backgroundColor = "#ff0000"; // Add a background color just for debugging
            }
            // The hide() method in the hidden layer way will set this IFRAME to display = "none", so each time the menu layer is displayed, the IFRAME needs to be set to display = "block" again.
            iframe_dom.style.display = "block";
            // Set the same width, height and coordinates as the menu layer
            iframe_dom.style.width = panel.offsetWidth;
            iframe_dom.style.height = panel.offsetHeight;
            iframe_dom.style.top = panel.style.top;
            iframe_dom.style.left = panel.style.left;
            // Originally panel.style.zIndex - 1, but I can't get the panel.style.zIndex value here, so I'll manually set it to 9998, which is 1 less than panel.style.zIndex anyway
            iframe_dom.style.zIndex = 9998;
        }
    },
    // Hide menu
    hide : function()
    {
        if (document.getElementById('divmenu_panel'))
        {
            document.getElementById('divmenu_panel').style.display = "none";
        }
        // Add an IFRAME layer to solve the problem of SELECT blocking DIV layer in IE6. When hiding the menu, this IFRAME also needs to be hidden
        if (navigator.appName == "Microsoft Internet Explorer")
        {
            var iframeId = "divmenu_panel_iframe";
            var iframe_dom = document.getElementById(iframeId);
            if(iframe_dom)  // Hide only if it really exists
            {
                iframe_dom.style.display = "none";
            }
        }
    },
    // When an item is selected, assign the value to the hidden item that replaces select, and then replace the text in 'a'
    setInput : function(itemid, hint, val)
    {
        var formitem = document.getElementById(itemid);
        formitem.value = val;
        var nodeHint = document.getElementById(itemid+'_a');
        // An 'a' element looks like <a id="purpose_a" class="divmenu" href='javascript:DivMenu.show("purpose")'><span style="width: 6em;" class="menuhint">Office Building</span><span>▼<input type="hidden" id="purpose" name="purpose" class="" value=""/></span></a>. Replacing the display text means replacing the text of the first child node of 'a', so:
        var nodeHint = nodeHint.firstChild; // Take the first child node, at this time nodeHint is <span style="width: 6em;" class="menuhint">Office Building</span>
        var newHint = document.createTextNode(hint);    // Create a text node with the new display text
        nodeHint.replaceChild(newHint, nodeHint.firstChild);    // Use nodeHint to replace its first child node, i.e. the "Office Building" text node
    },
    // Calculate the byte length of a string
    byteLength : function (s)
    {
        var len = 0;
        for (i = 0; i < s.length; i++)
        {// Decide whether to add 1 or 2 to the total number of bytes based on character encoding. The problem is that charCodeAt returns Unicode encoding. Which Unicode encoding range counts as only 1 byte? Basically \x00-\xff, i.e. characters 0 - 255 are 1 byte. There are many other considerations, such as听说 there are also 3-byte characters, but most users won't be able to type them.
            len += (s.charCodeAt(i) < 256) ? 1:2;
        }
        return len;
    },
    // Menu transformation function
    menuTransform : function ()
    {
        var debug = '';
        // All menus exist in an array
        var menuSelects = [];
        var arrMenu = document.getElementsByTagName('select');
        for (var i=0; i<arrMenu.length; i++)
        {
            // Only replace single-choice menus, so find those that are single-choice menus
            // http://alex.zybar.net/javascript/IE/IE indeed doesn't support the hasAttribute() and hasAttributes() methods of DOM Level 2. attributes.length > 0 can be used instead of hasAttributes(), and getAttribute(attrName) != null can be used instead of hasAttribute(attrName). But for attributes like multiple that have no value, when they don't exist, FF returns null while IE returns false (when they exist, FF returns an empty string, IE returns true). We'll have to make do again, and we still need to use strict equality ===, otherwise it won't work. Both multiple and multiple="multiple" are可行
            var getMultiple = arrMenu[i].getAttribute('multiple');
            if ((null === getMultiple) || (false === getMultiple))
            {
                var nodesOption = arrMenu[i].childNodes;
                var menuName = arrMenu[i].getAttribute('name');
                // The hint bar for the entire menu, used for the text in the 'a' element that initially replaces the menu
                var strHint='';
                var valSelected = null;
                var objOptions = [];
                // Store the current menu's information as an object
                var objSelect = {};
                for (var j=0; j<nodesOption.length; j++)
                {
                    // Options within option groups are usually not used as hint bars, so the loop within these option groups doesn't process hint bars
                    if ('OPTGROUP'== nodesOption[j].nodeName)
                    {
                        objOptions.push({'hint':nodesOption[j].getAttribute('label'), 'value':null, 'type':'OPTGROUP'});
                        var nodesGroup = nodesOption[j].childNodes;
                        for (var k = 0;k<nodesGroup.length;k++)
                        {
                            if ('OPTION'== nodesGroup[k].nodeName)
                            {
                                var hintThis = nodesGroup[k].firstChild.data;
                                var valueThis = nodesGroup[k].getAttribute('value');
                                var selectedThis = nodesOption[j].getAttribute('selected');
                                if (('selected' == selectedThis) || (true === selectedThis))
                                {
                                    strHint = hintThis;
                                    valSelected = valueThis;
                                }
                                objOptions.push({'hint':hintThis, 'value':valueThis, 'type':'OPTION'});
                            }
                        }
                    }
                    else if ('OPTION'== nodesOption[j].nodeName)
                    {
                        var hintThis = nodesOption[j].firstChild.data;
                        var disabledThis = nodesOption[j].getAttribute('disabled');
                        var selectedThis = nodesOption[j].getAttribute('selected');
                        var valueThis = nodesOption[j].getAttribute('value');
                        //alert(selectedThis);
                        // Empty string values should be counted as options, not hint bars, so the condition doesn't add || ('' == valueThis)
                        if (('disabled' == disabledThis) || (true === disabledThis) || (null == valueThis))
                        {
                            strHint = hintThis;
                        }

                        else
                        {
                            // If there's a default selected item, use its display text as the hint bar
                            if (('selected' == selectedThis) || (true === selectedThis) || ('' === selectedThis))
                            {
                                strHint = hintThis;
                                valSelected = valueThis;
                            }
                            objOptions.push({'hint':hintThis, 'value':valueThis, 'type':'OPTION'});
                        }
                    }
                }
                // If the menu doesn't define an option to be used as a hint, use the first option as the hint
                if ('' == strHint)
                {
                    strHint = objOptions[0].hint;
                }

                // The menu layer is directly generated as a node and added to the entire document
                var nodeDiv = document.createElement('div');
                nodeDiv.id=menuName+'_div';
                // Generate menu layer as backup, set to invisible style
                nodeDiv.style.display='none';
                // Temporary string for assembling innerHTML
                var str = '';
                // First note down the byte count of the hint bar, then compare the byte count of the option display text one by one when assembling options, take the maximum value, and finally determine the width of the hint bar 'a'
                var strLength = DivMenu.byteLength(strHint);
                for (n=0; n<objOptions.length;n++)
                {
                    if ('OPTGROUP' == objOptions[n].type)
                    {
                        str += '<div class="divmenu_group">'+objOptions[n].hint+'</div>';
                    }
                    else if('OPTION' == objOptions[n].type)
                    {
                        str += '<a href="javascript:DivMenu.setInput("'+menuName+'", "'+objOptions[n].hint+'", "'+objOptions[n].value+'")">'+objOptions[n].hint+'</a>';
                    }
                    // Find the longest string in the display text to determine the width of 'a'. When JavaScript counts characters, one Chinese character counts as 1 character, but Chinese width is much larger than Western width, so counting characters to determine width is not good. Try directly comparing the width of 'a' and the created menu layer, and take the larger value. But directly getting ComputedStyle doesn't work either. These menu divs are hidden and absolutely positioned. In IE, ComputedStyle returns 'auto' for width, while FF returns the width of the entire window. In fact, except for monospaced fonts, each Western character has a different width, so it's difficult to determine the width through character count. Traditional SELECT is parsed and rendered by the browser after getting all the option text, but now replacing it with 'a' and 'div' elements is indeed troublesome. The current approach is a safer one: calculate the maximum character count strLength, then set width:'+strLength*0.6+'em, where 0.6 is just an empirical coefficient. Directly width:'+strLength+'em is too wide, so it's reduced by 60%.
                    var thisLength = DivMenu.byteLength(objOptions[n].hint);
                    if (thisLength > strLength)
                    {
                        strLength = thisLength;
                    }
                }
                nodeDiv.innerHTML = str;
                document.body.appendChild(nodeDiv);

                // Create the 'a' node that replaces select, and store it in the replacement array. This 'a' including the hidden input will be used to replace the original select node
                // Create the 'a' node that replaces select, and store it in the replacement array. This 'a' including the hidden input will be used to replace the original select node
                var nodeHint = document.createElement('a');
                nodeHint.id=menuName+'_a';
                nodeHint.className='divmenu';
                nodeHint.setAttribute('href', 'javascript:DivMenu.show("'+menuName+'")');
                str = '<span class="menuhint" style="width:'+strLength*0.6+'em">'+strHint+'</span><span class="menuarrow">▼<input type="hidden" name="'+menuName+'" id="'+menuName+'"'+((null == valSelected)? '':' value="'+valSelected+'"')+'></span>';
                nodeHint.innerHTML = str;
                // Store the new and old nodes in the object array first for unified replacement later
                var menuthis = {'node':arrMenu[i],'nodeNew':nodeHint};
                menuSelects.push(menuthis);
            }
        }
        for (var i=0; i<menuSelects.length; i++)
        {
            menuSelects[i].node.parentNode.replaceChild(menuSelects[i].nodeNew, menuSelects[i].node);
        }
    }
};

// Execute the above menu transformation function after the document is loaded
DivMenu.add(window, 'load', DivMenu.menuTransform);
// When traditional menus are opened, they occupy focus, and clicking somewhere else in the document will close the menu. To completely reproduce the usage habits of traditional menus, click the document again to hide the menu
DivMenu.add(document, 'click', DivMenu.hide);

DivMenu.htm

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<title>Menu by Floating Layer</title>
<script type="text/javascript" src="DivMenu.js"></script>
<link href="DivMenu.css" rel="stylesheet" type="text/css" />
</head>

<body>
<h1>Menu by Floating Layer</h1>
<p>This is a generic floating layer menu replacement function that can easily replace traditional SELECT single-choice menus with fashionable floating layer menus.</p>
<h3>Instructions</h3>
<p>Simply reference the JS file and CSS file in the attachment to the web page, such as:</p>
<pre><script type="text/javascript" src="DivMenu.js"></script>
<link href="DivMenu.css" rel="stylesheet" type="text/css" /> </pre>
<h4><a href="DivMenu.zip">Download Source Code Package</a></h4>
<p>Among them, DivMenu.js is a compressed streamlined version by Javascript compressor for actual production use. DivMenu_develop.js is a development version with detailed comments for learning and research. When using it, you can refer to the comments in the DivMenu.css file and modify the style definition by yourself.</p>
<p>The JavaScript program has implemented unobtrusive functionality, meaning that HTML files referencing this JS file don't need any other adjustments. However, it hasn't fully implemented anonymous functionality, meaning that this program still creates global variables and several web page nodes. We've tried to minimize the addition of global variables and nodes, but please still pay attention to variable naming conflicts when using it. This program only adds one global variable "DivMenu", and adds quite a few web page nodes. For each SELECT node that is replaced, two nodes are added, with names being the SELECT node's name value plus "_a" suffix and "_div" suffix respectively. For example, if the name of an original SELECT node is "menu", the names of the two newly added nodes are "menu_a" and "menu_div".</p>
<h3>Function Description</h3>
<form id="classic" method="post" action="">
  <div> City
    <select name="city">
        <option disabled="disabled">Please select a city</option>
        <option value="beijing">Beijing</option>
        <option value="tianjin">Tianjin</option>
        <optgroup label="Hebei Province">
        <option value="shijiazhuang">Shijiazhuang</option>
        <option value="tangshan">Tangshan</option>
        </optgroup>
        <optgroup label="Zhejiang Province">
        <option value="suzhou">Suzhou</option>
        <option value="hangzhou">Hangzhou</option>
        <option value="ningbo">Ningbo</option>
        </optgroup>
    </select>
    Type
    <select name="purpose">
      <option>Please select type</option>
      <option value="house" selected="selected" >Residential</option>
      <option value="economic">Affordable Housing</option>
      <option value="villa">Villa</option>
      <option value="building">Office Building</option>
      <option value="shop">Shop</option>
    </select>
    Price
    <select name="price">
      <option selected="selected" disabled="disabled">Please select price</option>
      <option value="0-1000">0 to 1000</option>
      <option value="1000-2000">1000-2000</option>
      <option value="2000-3000">2000-3000</option>
      <option value="3000-4000">3000-4000</option>
      <option value="4000-5000">4000-5000</option>
      <option value="5000-6000">5000-6000</option>
      <option value="6000-7000">6000-7000</option>
      <option value="7000-8000">7000-8000</option>
      <option value="8000-9000">8000-9000</option>
      <option value="9000-10000">9000-10000</option>
      <option value="" selected="selected">Unlimited</option>
    </select>
    <select name="nohint">
      <option value="0">Zero</option>
      <option value="1000-2000">1000-2000</option>
      <option value="2000-3000">2000-3000</option>
      <option value="3000-4000">3000-4000</option>
      <option value="4000-5000">4000-5000</option>
      <option value="5000-6000">5000-6000</option>
      <option value="6000-7000">6000-7000</option>
      <option value="7000-8000">7000-8000</option>
      <option value="8000-9000">8000-9000</option>
      <option value="9000-10000">9000-10000</option>
      <option value="10000-11000">10000-11000</option>
    </select>
    Multiple selection menus are not converted. Hint text in multiple selection menus can only be implemented with disabled="disabled".
    <select name="multi" multiple="multiple">
      <option disabled="disabled">To select multiple items, please hold down the Ctrl key and then select</option>
      <option value="house">Multiple Selection 1</option>
      <option value="economic">Multiple Selection 2</option>
      <option value="villa">Multiple Selection 3</option>
      <option value="building">Multiple Selection 4</option>
      <option value="shop">Multiple Selection 5</option>
    </select>
    Put a multiple selection menu to demonstrate that multiple selection menus will not be replaced, and that floating layers can be displayed normally above other SELECT elements in IE6. </div>
</form>
This is a practical example with several classic SELECT selection menus. The first one is complex with grouping, and the hint text is implemented with the first option that has disabled="disabled". The second one is simple, with hint text using the option that has no value attribute. Practice has found that FF can normally handle options with disabled="disabled" or just disabled, putting them in an unselectable state, while IE ignores any disabled="disabled" and disabled, so it's estimated that menus in IE usually use options with no value attribute or value="" as hint text. You can see that its source code is the original SELECT, requiring no changes.
<p>The basic idea is to convert the existing SELECT menus in the document into hidden INPUT form items, use 'A' elements for the menu hint bar and option bars, use DIV layers for menus, and also use 'A' elements to replace each option. When clicked, the value is assigned to the hidden INPUT form item.</p>
<p>Main functions or limitations include:</p>
<ol>
  <li>All usage habits follow traditional SELECT menus.</li>
  <li>Only single-choice SELECT menus will be replaced, while multiple-choice menus remain unchanged, because multiple-choice menus usually don't need to be replaced with floating layer menus.</li>
  <li>Supports various SELECT element features, such as using disabled or valueless options as hint bars; options grouped with optgroup will also be grouped in the new menu. Special corrections have been made for the bug in IE 6 where SELECT blocks DIV layers.</li>
  <li>Supports default selected items.</li>
  <li>Almost supports all functions of ordinary menus, but doesn't support additional interactive functions, such as JavaScript real-time created Option options, linked options, etc. For example, functions where selecting a city district will correspondingly change are not supported.</li>
  <li>All styles are defined with CSS, using relative font sizes, which can adapt to most web pages without modification.</li>
  <li>Menu position and size are set flexibly to adapt to any web page layout. When making it, the consideration was to be able to display the widest option text in the replacement 'A' element, so the width of the original SELECT box was not directly taken. Therefore, the menu size may be wider than the original SELECT, please note this when using.</li>
</ol>
<p>Welcome to try it out and provide suggestions and feedback for joint discussion and improvement.</p>
</body>
</html>

Instructions

Simply reference the above JS file and CSS file to the web page, such as:

<script type="text/javascript" src="DivMenu.js"></script>
<link href="DivMenu.css" rel="stylesheet" type="text/css" />

Among them, DivMenu.js is a compressed streamlined version by Javascript compressor for actual production use. DivMenu_develop.js is a development version with detailed comments for learning and research. When using it, you can refer to the comments in the DivMenu.css file and modify the style definition by yourself.

The JavaScript program has implemented unobtrusive functionality, meaning that HTML files referencing this JS file don't need any other adjustments. However, it hasn't fully implemented anonymous functionality, meaning that this program still creates global variables and several web page nodes. We've tried to minimize the addition of global variables and nodes, but please still pay attention to variable naming conflicts when using it. This program only adds one global variable "DivMenu", and adds quite a few web page nodes. For each SELECT node that is replaced, two nodes are added, with names being the SELECT node's name value plus "_a" suffix and "_div" suffix respectively. For example, if the name of an original SELECT node is "menu", the names of the two newly added nodes are "menu_a" and "menu_div".

Function Description

The basic idea is to convert the existing SELECT menus in the document into hidden INPUT form items, use 'A' elements for the menu hint bar and option bars, use DIV layers for menus, and also use 'A' elements to replace each option. When clicked, the value is assigned to the hidden INPUT form item.

Main functions or limitations include:

  1. All usage habits follow traditional SELECT menus.

  2. Only single-choice SELECT menus will be replaced, while multiple-choice menus remain unchanged, because multiple-choice menus usually don't need to be replaced with floating layer menus.

  3. Supports various SELECT element features, such as using disabled or valueless options as hint bars; options grouped with optgroup will also be grouped in the new menu. Special corrections have been made for the bug in IE 6 where SELECT blocks DIV layers.

  4. Supports default selected items.

  5. Almost supports all functions of ordinary menus, but doesn't support additional interactive functions, such as JavaScript real-time created Option options, linked options, etc. For example, functions where selecting a city district will correspondingly change are not supported.

  6. All styles are defined with CSS, using relative font sizes, which can adapt to most web pages without modification.

  7. Menu position and size are set flexibly to adapt to any web page layout. When making it, the consideration was to be able to display the widest option text in the replacement 'A' element, so the width of the original SELECT box was not directly taken. Therefore, the menu size may be wider than the original SELECT, please note this when using.