Click here to Skip to main content
15,886,806 members
Articles / Web Development / HTML

JavaScript UI Control Suite using TypeScript

Rate me:
Please Sign up or sign in to vote.
3.20/5 (4 votes)
7 Dec 2014CPOL12 min read 40.3K   387   32  
A usable suite of JavaScript UI controls written with TypeScript.
/******************************************************************************************
* Developed by Shawn Lawsure - shawnl@maine.rr.com - http://www.linkedin.com/in/shawnlawsure
NOTE:  see ZenithControlBase.ts for documentation on the public members that are inherited
by this control.
* Example of use for top menu:
var cMenu = new Zenith.Menu('baseElement')
cMenu.addZenithEventListener(Zenith.ZenithEvent.EventType.Selected, function (id, text, contextElement)
{
var span = document.getElementById('output1');
if (span)
span.textContent = id + ' : ' + text;
if (contextElement)
span.textContent += ' : ' + contextElement.tagName;
//alert(span.textContent);
//alert(id + ' : ' + text);
//var span2 = document.getElementById('output2');
//span2.textContent = '';
});
var editMenu = cMenu.AddMenuItem(1, "Edit");
editMenu.AddMenuItem(2, "Edit 1");
editMenu.AddMenuItem(3, "Edit 2");
var copyMenu = cMenu.AddMenuItem(4, "Copy");
copyMenu.AddMenuItem(5, "Copy Func 1");
copyMenu.AddMenuItem(6, "Copy Func 2", "Copy.png");
copyMenu.AddMenuItem(7, "Copy Func 3");
var copyMenu2 = copyMenu.AddMenuItem(8, "Copy Func 4");
copyMenu.AddMenuItem(9, "Copy Func 5");
copyMenu2.AddMenuItem(10, "Copy Func 1");
copyMenu2.AddMenuItem(11, "Copy Func 2");
var pasteMenu = cMenu.AddMenuItem(12, "Paste");
pasteMenu.AddMenuItem(13, "Paste 1");
pasteMenu.AddMenuItem(14, "Paste 2");
var deleteMenu = cMenu.AddMenuItem(15, "Delete");
deleteMenu.AddMenuItem(16, "Delete 1");
deleteMenu.AddMenuItem(17, "Delete 2");
cMenu.Build();
* Example of use for context menu:
var cMenu = new Zenith.Menu('baseElement', true)
cMenu.contextElementType = Zenith.Menu.ContextElementType.Button;
//var btn = document.getElementById('testPopup');
//cMenu.AddContextElement(btn)
//cMenu.AddContextElement(testGrid.ParentElement)
cMenu.addZenithEventListener(Zenith.ZenithEvent.EventType.Selected, function (id, text, contextElement)
{
var span = document.getElementById('output1');
if (span)
span.textContent = id + ' : ' + text;
if (contextElement)
span.textContent += ' : ' + contextElement.tagName;
//alert(id + ' : ' + text);
//var span2 = document.getElementById('output2');
//span2.textContent = '';
});
cMenu.AddMenuItem(1, "Edit");
var copyMenu = cMenu.AddMenuItem(2, "Copy...", "Copy.png");     // 3rd parameter is URL of image to display with menu item.
cMenu.AddMenuItem(3, "Paste");
cMenu.AddMenuItem(4, "Delete");
copyMenu.AddMenuItem(5, "Copy Func 1");
copyMenu.AddMenuItem(6, "Copy Func 2");
copyMenu.AddMenuItem(7, "Copy Func 3");
var copyMenu2 = copyMenu.AddMenuItem(8, "Copy Func 4...");
copyMenu.AddMenuItem(9, "Copy Func 5");
copyMenu2.AddMenuItem(10, "Copy Func 1");
copyMenu2.AddMenuItem(11, "Copy Func 2");
cMenu.Build();
* CSS Class Names (hard-coded) - set these in your own CSS file:
> ZenithContextMenuTable
> ZenithMenuRow
> ZenithMenuItemLabel_Unselected
> ZenithMenuItem_SubMenuIndicator
******************************************************************************************/
var __extends = this.__extends || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    __.prototype = b.prototype;
    d.prototype = new __();
};
///<reference path='ZenithControlBase.ts'/>
///<reference path='ZenithList.ts'/>
var Zenith;
(function (Zenith) {
    var zenithTopLevelMenu = null;
    var Menu = (function (_super) {
        __extends(Menu, _super);
        // ===============  Constructor  ====================================================
        // The id of a 'div' HTML element must be passed in when creating this object type.
        function Menu(baseDivElementId, isContextMenu, isTopLevel) {
            if (typeof isContextMenu === "undefined") { isContextMenu = false; }
            if (typeof isTopLevel === "undefined") { isTopLevel = true; }
            _super.call(this, baseDivElementId);
            this.hoverBackgroundColor = 'white';
            // ===============  Private Attributes  =================================================
            this.items = new Array();
            this.IsContextMenu = false;
            this.isTopLevel = false;
            this.menuPosition = Zenith.Menu.MenuPosition.Top;
            this.contextElementType = Zenith.Menu.ContextElementType.None;
            this.contextElements = new Array();
            this.IsMouseCaptured = false;

            this.IsContextMenu = isContextMenu;
            this.isTopLevel = isTopLevel;

            if (this.isTopLevel)
                this.popupClickType = this.IsContextMenu ? Zenith.ControlBase.ClickTypePopup.Right : Zenith.ControlBase.ClickTypePopup.Left;

            if (isTopLevel)
                zenithTopLevelMenu = this;
        }
        // ===============  Public Methods  ====================================================
        Menu.prototype.AddContextElement = function (contextElement) {
            if (!(contextElement instanceof HTMLElement))
                throw Error('An HTML element must be passed into the AddContextElement method on a ContextMenu.');
            this.contextElements.push(contextElement);
        };

        Menu.prototype.AddMenuItem = function (id, text, imageURL) {
            var menuItem = new MenuItem(id, text, imageURL);
            this.items.push(menuItem);
            return menuItem;
        };

        Menu.prototype.AddZenithEventListener = function (eventType, listener) {
            this.events.push(new Zenith.ZenithEvent(eventType, listener));
        };

        Menu.prototype.IsVisible = function () {
            return (this.BaseElement.style.visibility == 'visible');
        };

        Menu.prototype.Build = function (firstLevelSubMenu) {
            var _this = this;
            if (typeof firstLevelSubMenu === "undefined") { firstLevelSubMenu = false; }
            this.Clear();

            this.popupOffset = 0;

            this.mouseOverEventHandler();

            var table = document.createElement('table');
            this.BaseElement.appendChild(table);
            this.ParentElement = table;
            table.cellPadding = '0px';
            table.cellSpacing = '0px';
            table.className = 'ZenithContextMenuTable';

            var tbody = document.createElement('tbody');
            table.appendChild(tbody);

            var trow, tcell;

            for (var index = 0; index < this.items.length; index++) {
                if (this.IsContextMenu || index == 0 || !this.isTopLevel) {
                    trow = document.createElement('tr');
                    tbody.appendChild(trow);
                    trow.setAttribute('value', this.items[index].Id);

                    var css = document.createElement("style");
                    css.type = "text/css";
                    if (!this.IsContextMenu && this.isTopLevel)
                        css.innerHTML = "td:hover { border-style: none !important; background-color: " + this.hoverBackgroundColor + "; }";
                    else {
                        trow.className = "ZenithMenuRow";
                        css.innerHTML = ".ZenithMenuRow:hover { border-style: none !important; background-color: " + this.hoverBackgroundColor + "; }";
                    }
                    trow.appendChild(css);

                    this.addEventListener(trow, 'click', function (event) {
                        _this.selectedEventHandler(event);
                    });
                }

                if (this.IsContextMenu || !this.isTopLevel) {
                    tcell = document.createElement('td');
                    trow.appendChild(tcell);
                    tcell.setAttribute('ZenithMenuElement', 'true');
                    tcell.setAttribute('value', this.items[index].Id);
                    tcell.height = 25;
                    if (this.UseDefaultStyles) {
                        tcell.style.paddingLeft = '5px';
                        tcell.style.paddingRight = '5px';
                        tcell.style.paddingTop = '2px';
                        tcell.style.paddingBottom = '2px';
                    }

                    if (this.items[index].ImageUrl && this.items[index].ImageUrl.length > 0) {
                        var icon = document.createElement('img');
                        tcell.appendChild(icon);
                        icon.setAttribute('ZenithMenuElement', 'true');
                        icon.setAttribute('value', this.items[index].Id);
                        icon.width = 20;
                        icon.height = 20;
                        icon.src = this.items[index].ImageUrl;
                    }
                }

                tcell = document.createElement('td');
                trow.appendChild(tcell);
                tcell.setAttribute('ZenithMenuElement', 'true');
                tcell.setAttribute('value', this.items[index].Id);

                if (this.items[index].subMenu) {
                    if (this.IsContextMenu || !this.isTopLevel)
                        this.items[index].subMenu.popupElement = trow;
                    else
                        this.items[index].subMenu.popupElement = tcell;
                }

                var label = document.createElement('label');
                tcell.appendChild(label);
                label.setAttribute('ZenithMenuElement', 'true');
                if (this.UseDefaultStyles) {
                    label.style.marginLeft = '10px';
                    label.style.marginRight = '10px';
                }
                label.setAttribute('value', this.items[index].Id);
                label.textContent = this.items[index].Text;
                label.innerHTML = this.items[index].Text;
                label.className = 'ZenithMenuItemLabel_Unselected';

                if (!this.isTopLevel) {
                    tcell = document.createElement('td');
                    trow.appendChild(tcell);

                    // Right arrow for submenu.
                    if (this.items[index].subMenu) {
                        var arrowDiv = document.createElement('div');
                        tcell.appendChild(arrowDiv);
                        tcell.setAttribute('ZenithMenuElement', 'true');
                        tcell.setAttribute('value', this.items[index].Id);
                        arrowDiv.className = 'ZenithMenuItem_SubMenuIndicator';
                        arrowDiv.style.borderTopStyle = 'solid';
                        arrowDiv.style.borderTopWidth = '5px';
                        arrowDiv.style.borderTopColor = 'transparent';
                        arrowDiv.style.borderBottomStyle = 'solid';
                        arrowDiv.style.borderBottomWidth = '5px';
                        arrowDiv.style.borderBottomColor = 'transparent';
                        arrowDiv.style.borderLeftStyle = 'solid';
                        arrowDiv.style.borderLeftWidth = '5px';
                        arrowDiv.style.borderLeftColor = 'black';
                        arrowDiv.setAttribute('ZenithMenuElement', 'true');
                        arrowDiv.setAttribute('value', this.items[index].Id);
                    }
                }
            }

            if (this.isTopLevel) {
                if (!_super.prototype.IsPopup.call(this) && this.IsContextMenu)
                    this.popupElement = document.documentElement;

                this.mouseDownEventHandler();
                this.keyDownHandler();

                // Disable default context menu.
                if (this.IsContextMenu)
                    this.addEventListener(document, 'contextmenu', function (event) {
                        event.preventDefault();
                    });
            }

            if (!this.IsContextMenu && firstLevelSubMenu && this.menuPosition == Zenith.Menu.MenuPosition.Top) {
                this.PopUpPosition = 'belowright';
                this.PopUpDirection = 'down';
            }

            // We need to let all of the sub menus build before hiding them so we pass in false
            // to the base class Build method and then below, after building all of them, we
            // hide all of them.
            _super.prototype.Build.call(this, false, false);
            if (this.isTopLevel)
                _super.prototype.SetPopupEventHandlers.call(this, false, false, false);

            for (var index = 0; index < this.items.length; index++) {
                for (var eventIndex = 0; eventIndex < this.events.length; eventIndex++)
                    if (this.events[eventIndex].listener)
                        this.items[index].AddZenithEventListener(this.events[eventIndex].eventType, this.events[eventIndex].listener);

                if (this.items[index].subMenu)
                    this.items[index].Build(this.isTopLevel);
            }

            if (this.popupElement)
                this.BaseElement.style.visibility = 'collapse';
            for (var index = 0; index < this.items.length; index++)
                if (this.items[index].subMenu)
                    this.items[index].Hide();
        };

        Menu.prototype.Close = function (closeTopLevel) {
            if (typeof closeTopLevel === "undefined") { closeTopLevel = true; }
            if (closeTopLevel)
                _super.prototype.Close.call(this);
            for (var index = 0; index < this.items.length; index++)
                if (this.items[index].subMenu)
                    this.items[index].subMenu.Close();
            this.IsMouseCaptured = false;
        };

        // ===============  Event Handlers  ====================================================
        Menu.prototype.keyDownHandler = function () {
            var _this = this;
            // Escape key listener.
            this.addEventListener(document, 'keydown', function (event) {
                var keyEvent = event;
                if ((event && keyEvent.keyCode) && keyEvent.keyCode == 27 || (window.event && window.event.keyCode && window.event.keyCode == 27))
                    if (_this.BaseElement.style.visibility == 'visible' || _this.BaseElement.style.visibility == '')
                        _this.Close(_this.IsContextMenu);
            });
        };

        Menu.prototype.mouseDownEventHandler = function () {
            var _this = this;
            this.addEventListener(document, 'mousedown', function (event) {
                var mouseEvent = event;

                var targetElement = mouseEvent.target;

                var menuItemId;
                if (targetElement.getAttribute('ZenithMenuElement'))
                    menuItemId = targetElement.getAttribute('value');

                _this.recurseMenuItem = null;
                for (var index = 0; index < _this.items.length; index++)
                    _this.recurseMenu(_this.items[index], menuItemId);

                var selectedMenu;
                if (_this.recurseMenuItem && _this.recurseMenuItem.subMenu)
                    selectedMenu = _this.recurseMenuItem.subMenu;
                else
                    selectedMenu = _this;

                var selectedTarget = targetElement;
                while (targetElement && targetElement != selectedMenu.BaseElement && targetElement != selectedMenu.popupElement && targetElement != document.documentElement)
                    targetElement = targetElement.parentElement;

                if (targetElement && selectedMenu && selectedMenu.popupElement == targetElement && ((selectedMenu.popupClickType == Zenith.ControlBase.ClickTypePopup.Left && mouseEvent.button == Zenith.ControlBase.ClickTypePopup.Left) || (selectedMenu.popupClickType == Zenith.ControlBase.ClickTypePopup.Right && mouseEvent.button == Zenith.ControlBase.ClickTypePopup.Right))) {
                    if (!_this.IsContextMenu)
                        _this.IsMouseCaptured = true;
                    if (_this.IsContextMenu && _this.contextElementCheck(selectedTarget) && !_this.recurseMenuItem) {
                        selectedMenu.BaseElement.style.posLeft = mouseEvent.x;
                        selectedMenu.BaseElement.style.posTop = mouseEvent.y;
                        if (!selectedMenu.IsVisible()) {
                            _this.SetPopup(true);
                            _this.OnPopupElement();
                        }
                    }
                    //else if (!this.IsContextMenu)
                    //	this.Close(false);
                } else if (!_this.recurseMenuItem)
                    _this.Close(_this.IsContextMenu);
            });
        };

        Menu.prototype.mouseOverEventHandler = function () {
            var _this = this;
            this.addEventListener(this.BaseElement, 'mouseover', function (event) {
                var mouseEvent = event;

                var targetElement = mouseEvent.srcElement;
                if (!targetElement)
                    targetElement = mouseEvent.target;

                //targetElement = <HTMLElement>document.elementFromPoint(mouseEvent.clientX, mouseEvent.clientY);
                var menuItemId;
                if (targetElement.getAttribute('ZenithMenuElement'))
                    menuItemId = targetElement.getAttribute('value');

                for (var index = 0; index < _this.items.length; index++) {
                    if (_this.items[index].Id == menuItemId && _this.items[index].subMenu && !_this.items[index].subMenu.IsVisible() && (!_this.isTopLevel || _this.IsMouseCaptured || _this.IsContextMenu)) {
                        _this.items[index].subMenu.SetPopup();
                        _this.items[index].subMenu.OnPopupElement();
                    } else if (_this.items[index].Id != menuItemId && _this.items[index].subMenu && _this.items[index].subMenu.IsVisible() && (_this.IsContextMenu || _this.IsMouseCaptured))
                        _this.items[index].subMenu.Close();
                }

                if (!menuItemId)
                    _this.IsMouseCaptured = false;
            });
        };

        Menu.prototype.selectedEventHandler = function (event) {
            var targetElement = event.target;
            if (targetElement) {
                var menuItemId = targetElement.getAttribute('value');

                // Get the corresponding menu item.
                this.recurseMenuItem = null;
                for (var index = 0; index < this.items.length; index++)
                    this.recurseMenu(this.items[index], menuItemId);

                // Get text value of the menu item.
                var menuText;
                if (this.recurseMenuItem)
                    menuText = this.recurseMenuItem.Text;
                else {
                    var labels = targetElement.getElementsByTagName('label');
                    if (labels.length > 0) {
                        var label = labels[0];
                        if (label)
                            menuText = label.textContent;
                    }
                }

                if (!this.recurseMenuItem.subMenu)
                    _super.prototype.ExecuteEvent.call(this, Zenith.ZenithEvent.EventType.Selected, [menuItemId, menuText, this.currentContextElement]);

                if (this.recurseMenuItem && this.recurseMenuItem.subMenu) {
                    if (!this.recurseMenuItem.subMenu.IsVisible()) {
                        if (this.isTopLevel)
                            for (var index = 0; index < this.items.length; index++)
                                if (this.items[index].subMenu && this.items[index].subMenu != this.recurseMenuItem.subMenu)
                                    this.items[index].subMenu.Close();

                        this.recurseMenuItem.subMenu.SetPopup();
                        this.recurseMenuItem.subMenu.OnPopupElement();
                    }
                } else if (zenithTopLevelMenu)
                    zenithTopLevelMenu.Close(this.IsContextMenu);
            }
        };

        // ===============  Private Methods  ===================================================
        Menu.prototype.recurseMenu = function (menuItem, menuItemId) {
            if (menuItem.Id == menuItemId)
                this.recurseMenuItem = menuItem;
            else if (menuItem.subMenu)
                for (var index = 0; index < menuItem.subMenu.items.length; index++)
                    this.recurseMenu(menuItem.subMenu.items[index], menuItemId);
        };

        Menu.prototype.contextElementCheck = function (element) {
            this.currentContextElement = element;

            // If the ContextElementType is not 'None' then try to match it with the element type passed in.
            if (this.contextElementType == Menu.ContextElementType.Any)
                return true;
            if (this.contextElementType == Menu.ContextElementType.Document && element == document.documentElement)
                return true;
            if (this.contextElementType == Menu.ContextElementType.Image && element instanceof HTMLImageElement)
                return true;
            if (this.contextElementType == Menu.ContextElementType.TextInput && element instanceof HTMLInputElement && element.type == 'text')
                return true;
            if (this.contextElementType == Menu.ContextElementType.Button && element instanceof HTMLInputElement && element.type == 'button')
                return true;
            if (this.contextElementType == Menu.ContextElementType.TableRow) {
                var targetElement = element;
                while (targetElement && !(targetElement instanceof HTMLTableRowElement))
                    targetElement = targetElement.parentElement;

                this.currentContextElement = targetElement;

                if (targetElement)
                    return true;
            }

            // If we didn't match on ContextElementType then check whether the passed in element is in the list of context elements.
            if (this.contextElements) {
                var targetElement = element;
                while (targetElement) {
                    for (var index = 0; index < this.contextElements.length; index++) {
                        if (this.contextElements[index] === targetElement) {
                            this.currentContextElement = this.contextElements[index];
                            return true;
                        }
                    }
                    targetElement = targetElement.parentElement;
                }
            }

            return false;
        };
        Menu.MenuPosition = { Top: 1, Left: 2 };

        Menu.ContextElementType = { None: 0, Any: 1, Document: 2, TextInput: 3, Image: 4, Button: 5, TableRow: 6 };
        return Menu;
    })(Zenith.ControlBase);
    Zenith.Menu = Menu;

    var MenuItem = (function () {
        // ===============  Constructor  ==================================================
        function MenuItem(id, text, imageURL) {
            if (typeof imageURL === "undefined") { imageURL = ''; }
            this.subMenu = null;
            this.Id = id;
            this.Text = text;
            this.ImageUrl = imageURL;
        }
        // ===============  Public Methods  ====================================================
        MenuItem.prototype.AddMenuItem = function (id, text, imageURL) {
            if (!this.subMenu)
                this.CreateSubMenu();

            return this.subMenu.AddMenuItem(id, text, imageURL);
        };

        MenuItem.prototype.Build = function (firstLevelSubMenu) {
            if (typeof firstLevelSubMenu === "undefined") { firstLevelSubMenu = false; }
            if (this.subMenu)
                this.subMenu.Build(firstLevelSubMenu);
        };

        MenuItem.prototype.AddZenithEventListener = function (eventType, listener) {
            if (this.subMenu)
                this.subMenu.AddZenithEventListener(eventType, listener);
        };

        MenuItem.prototype.Hide = function () {
            if (this.subMenu && this.subMenu.BaseElement)
                this.subMenu.BaseElement.style.visibility = 'collapse';
        };

        MenuItem.prototype.CreateSubMenu = function () {
            var baseElement = document.createElement('div');
            document.documentElement.appendChild(baseElement);
            baseElement.id = 'ZenithSubMenu_' + this.Id;

            this.subMenu = new Menu(baseElement.id, zenithTopLevelMenu && zenithTopLevelMenu.IsContextMenu, false);
        };
        return MenuItem;
    })();
    Zenith.MenuItem = MenuItem;
})(Zenith || (Zenith = {}));

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
For over 25 years I have worked in the Information Systems field as both a full-time employee and an independent contractor for a variety of companies.

I have extensive professional experience with numerous programming languages and technologies including C#, JavaScript, SQL, VB.NET, and ASP.NET as well as a working knowledge of a great variety of others. I also have an advanced understanding of the concepts behind these technologies including Object-Oriented Programming, Relational Data, Functional Programming, MVC and MVVM.

Some of my more recent work has been designing and building web applications primarily with JavaScript in conjunction with many of the JavaScript libraries/frameworks including jQuery, KnockoutJS and Bootstrap and consuming both JSON and REST services.

In nearly all of the work I have been involved with in the past ten years I have played a lead role in the design as well as the development of the work. More recently I have managed a team of software developers at a local mid-size company.

Comments and Discussions