Modules (188)

InlineMenu

Description

Dependencies

Classes

Constructor

InlineMenu

Displays a popup list of items for a given editor context

editor Editor
menuText string
    function InlineMenu(editor, menuText) {

Methods

Private

_buildListView

Rebuilds the list items for the menu.

    InlineMenu.prototype._buildListView = function (items) {
        var self            = this,
            view            = { items: [] },
            _addItem;

        this.items = items;

        _addItem = function (item) {
            view.items.push({ formattedItem: "<span>" + item.name + "</span>"});
        };

        // clear the list
        this.$menu.find("li.inlinemenu-item").remove();

        // if there are no items then close the list; otherwise add them and
        // set the selection
        if (this.items.length === 0) {
            if (this.handleClose) {
                this.handleClose();
            }
        } else {
            this.items.some(function (item, index) {
                _addItem(item);
            });

            // render the menu list
            var $ul = this.$menu.find("ul.dropdown-menu"),
                $parent = $ul.parent();

            // remove list temporarily to save rendering time
            $ul.remove().append(Mustache.render(MenuHTML, view));

            $ul.children("li.inlinemenu-item").each(function (index, element) {
                var item      = self.items[index],
                    $element    = $(element);

                // store id of item in the element
                $element.data("itemid", item.id);
            });

            // delegate list item events to the top-level ul list element
            $ul.on("click", "li.inlinemenu-item", function (e) {
                // Don't let the click propagate upward (otherwise it will
                // hit the close handler in bootstrap-dropdown).
                e.stopPropagation();
                if (self.handleSelect) {
                    self.handleSelect($(this).data("itemid"));
                }
            });

            $ul.on("mouseover", "li.inlinemenu-item", function (e) {
                e.stopPropagation();
                // _setSelectedIndex sets the selected index and call handle hover
                // callback funtion
                self._setSelectedIndex(self.items.findIndex(function(element) {
                    return element.id === $(e.currentTarget).data("itemid");
                }));
            });

            $parent.append($ul);

            this._setSelectedIndex(0);
        }
    };
Private

_calcMenuLocation

Computes top left location for menu so that the menu is not clipped by the window. Also computes the largest available width.

Returns: {left: number,top: number,width: number}
    InlineMenu.prototype._calcMenuLocation = function () {
        var cursor      = this.editor._codeMirror.cursorCoords(),
            posTop      = cursor.bottom,
            posLeft     = cursor.left,
            textHeight  = this.editor.getTextHeight(),
            $window     = $(window),
            $menuWindow = this.$menu.children("ul"),
            menuHeight  = $menuWindow.outerHeight();

        // TODO Ty: factor out menu repositioning logic so inline menu and Context menus share code
        // adjust positioning so menu is not clipped off bottom or right
        var bottomOverhang = posTop + menuHeight - $window.height();
        if (bottomOverhang > 0) {
            posTop -= (textHeight + 2 + menuHeight);
        }

        posTop -= 30;   // shift top for hidden parent element

        var menuWidth = $menuWindow.width();
        var availableWidth = menuWidth;
        var rightOverhang = posLeft + menuWidth - $window.width();
        if (rightOverhang > 0) {
            // Right overhang is negative
            posLeft = Math.max(0, posLeft - rightOverhang);
        }

        return {left: posLeft, top: posTop, width: availableWidth};
    };
Private

_keydownHook

Convert keydown events into hint list navigation actions.

keyEvent KeyBoardEvent
    InlineMenu.prototype._keydownHook = function (event) {
        var keyCode,
            self = this;

        // positive distance rotates down; negative distance rotates up
        function _rotateSelection(distance) {
            var len = self.items.length,
                pos;

            if (self.selectedIndex < 0) {
                // set the initial selection
                pos = (distance > 0) ? distance - 1 : len - 1;

            } else {
                // adjust current selection
                pos = self.selectedIndex;

                // Don't "rotate" until all items have been shown
                if (distance > 0) {
                    if (pos === (len - 1)) {
                        pos = 0;  // wrap
                    } else {
                        pos = Math.min(pos + distance, len - 1);
                    }
                } else {
                    if (pos === 0) {
                        pos = (len - 1);  // wrap
                    } else {
                        pos = Math.max(pos + distance, 0);
                    }
                }
            }

            self._setSelectedIndex(pos);
        }

        // Calculate the number of items per scroll page.
        function _itemsPerPage() {
            var itemsPerPage = 1,
                $items = self.$menu.find("li.inlinemenu-item"),
                $view = self.$menu.find("ul.dropdown-menu"),
                itemHeight;

            if ($items.length !== 0) {
                itemHeight = $($items[0]).height();
                if (itemHeight) {
                    // round down to integer value
                    itemsPerPage = Math.floor($view.height() / itemHeight);
                    itemsPerPage = Math.max(1, Math.min(itemsPerPage, $items.length));
                }
            }

            return itemsPerPage;
        }

        // If we're no longer visible, skip handling the key and end the session.
        if (!this.isOpen()) {
            this.handleClose();
            return false;
        }

        // (page) up, (page) down, enter are handled by the list
        if ((event.type === "keydown") && this.isHandlingKeyCode(event)) {
            keyCode = event.keyCode;

            if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) {
                event.stopImmediatePropagation();
                this.handleClose();

                return false;
            } else if (event.shiftKey &&
                    (event.keyCode === KeyEvent.DOM_VK_UP ||
                     event.keyCode === KeyEvent.DOM_VK_DOWN ||
                     event.keyCode === KeyEvent.DOM_VK_PAGE_UP ||
                     event.keyCode === KeyEvent.DOM_VK_PAGE_DOWN)) {
                this.handleClose();
                // Let the event bubble.
                return false;
            } else if (keyCode === KeyEvent.DOM_VK_UP) {
                _rotateSelection.call(this, -1);
            } else if (keyCode === KeyEvent.DOM_VK_DOWN) {
                _rotateSelection.call(this, 1);
            } else if (keyCode === KeyEvent.DOM_VK_PAGE_UP) {
                _rotateSelection.call(this, -_itemsPerPage());
            } else if (keyCode === KeyEvent.DOM_VK_PAGE_DOWN) {
                _rotateSelection.call(this, _itemsPerPage());
            } else if (this.selectedIndex !== -1 &&
                    (keyCode === KeyEvent.DOM_VK_RETURN)) {
                // Trigger a click handler to commmit the selected item
                $(this.$menu.find("li.inlinemenu-item")[this.selectedIndex]).trigger("click");
            } else {
                return false;
            }

            event.stopImmediatePropagation();
            event.preventDefault();
            return true;
        }

        return false;
    };
Private

_setSelectedIndex

Select the item in the menu at the specified index, or remove the selection if index < 0.

index number
    InlineMenu.prototype._setSelectedIndex = function (index) {
        var items = this.$menu.find("li.inlinemenu-item");

        // Range check
        index = Math.max(-1, Math.min(index, items.length - 1));

        // Clear old highlight
        if (this.selectedIndex !== -1) {
            $(items[this.selectedIndex]).find("a").removeClass("highlight");
        }

        this.selectedIndex = index;

        // Highlight the new selected item, if necessary
        if (this.selectedIndex !== -1) {
            var $item = $(items[this.selectedIndex]);
            var $view = this.$menu.find("ul.dropdown-menu");

            $item.find("a").addClass("highlight");
            ViewUtils.scrollElementIntoView($view, $item, false);
        }

        // Invoke handleHover callback if any
        if (this.handleHover) {
            this.handleHover(this.items[index].id);
        }
    };

close

Closes the menu

    InlineMenu.prototype.close = function () {
        this.opened = false;

        if (this.$menu) {
            this.$menu.removeClass("open");
            PopUpManager.removePopUp(this.$menu);
            this.$menu.remove();
        }

        KeyBindingManager.removeGlobalKeydownHook(this._keydownHook);
    };

isHandlingKeyCode

Check whether Event is one of the keys that we handle or not.

keyEvent KeyBoardEvent,keyBoardEvent.keyCode
    InlineMenu.prototype.isHandlingKeyCode = function (keyCodeOrEvent) {
        var keyCode = typeof keyCodeOrEvent === "object" ? keyCodeOrEvent.keyCode : keyCodeOrEvent;
        var ctrlKey = typeof keyCodeOrEvent === "object" ? keyCodeOrEvent.ctrlKey : false;


        return (keyCode === KeyEvent.DOM_VK_UP || keyCode === KeyEvent.DOM_VK_DOWN ||
            keyCode === KeyEvent.DOM_VK_PAGE_UP || keyCode === KeyEvent.DOM_VK_PAGE_DOWN ||
            keyCode === KeyEvent.DOM_VK_RETURN ||
            keyCode === KeyEvent.DOM_VK_ESCAPE
        );
    };

isOpen

Is the Inline menu open?

Returns: boolean
    InlineMenu.prototype.isOpen = function () {
        // We don't get a notification when the dropdown closes. The best
        // we can do is keep an "opened" flag and check to see if we
        // still have the "open" class applied.
        if (this.opened && !this.$menu.hasClass("open")) {
            this.opened = false;
        }

        return this.opened;
    };

onClose

Set the menu closure callback function

callback Function
    InlineMenu.prototype.onClose = function (callback) {
        this.handleClose = callback;
    };

    // Define public API
    exports.InlineMenu = InlineMenu;
});

onHover

Set the hover callback function

callback Function
    InlineMenu.prototype.onHover = function (callback) {
        this.handleHover = callback;
    };

onSelect

Set the menu selection callback function

callback Function
    InlineMenu.prototype.onSelect = function (callback) {
        this.handleSelect = callback;
    };

open

Displays the menu at the current cursor position

hints Array.<{id: number,name: string>
    InlineMenu.prototype.open = function (items) {
        Menus.closeAll();

        this._buildListView(items);

        if (this.items.length) {
            // Need to add the menu to the DOM before trying to calculate its ideal location.
            $("#inlinemenu-menu-bar > ul").append(this.$menu);

            var menuPos = this._calcMenuLocation();

            this.$menu.addClass("open")
                .css({"left": menuPos.left, "top": menuPos.top, "width": menuPos.width + "px"});
            this.opened = true;

            KeyBindingManager.addGlobalKeydownHook(this._keydownHook);
        }
    };