Displays a popup list of items for a given editor context
function InlineMenu(editor, menuText) {
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);
}
};
Computes top left location for menu so that the menu is not clipped by the window. Also computes the largest available width.
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};
};
Convert keydown events into hint list navigation actions.
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;
};
Select the item in the menu at the specified index, or remove the selection if index < 0.
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);
}
};
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);
};
Check whether Event is one of the keys that we handle or not.
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
);
};
Is the Inline menu open?
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;
};
Set the menu closure callback function
InlineMenu.prototype.onClose = function (callback) {
this.handleClose = callback;
};
// Define public API
exports.InlineMenu = InlineMenu;
});
Set the hover callback function
InlineMenu.prototype.onHover = function (callback) {
this.handleHover = callback;
};
Set the menu selection callback function
InlineMenu.prototype.onSelect = function (callback) {
this.handleSelect = callback;
};
Displays the menu at the current cursor position
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);
}
};