wed
Version:
Wed is a schema-aware editor for XML documents.
195 lines • 8.83 kB
JavaScript
/**
* Context menus.
* @author Louis-Dominique Dubeau
* @license MPL 2.0
* @copyright Mangalam Research Center for Buddhist Languages
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
define(["require", "exports", "jquery", "../domutil", "bootstrap"], function (require, exports, jquery_1, domutil) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
jquery_1 = __importDefault(jquery_1);
domutil = __importStar(domutil);
/**
* A context menu GUI element.
*/
class ContextMenu {
/**
* @param document The DOM document for which to make this
* context menu.
*
* @param x Position of the menu. The context menu may ignore this position if
* the menu would appear off-screen.
*
* @param y Position of the menu.
*
* @param items The items to show in the menu. These should be list items
* containing links appropriately formatted for a menu.
*
* @param dismissCallback Function to call when the menu is dismissed.
*
* @param immediateDisplay If true, will call ``render`` from the constructor.
*/
constructor(document, x, y, items, dismissCallback, immediateDisplay = true) {
this.dismissCallback = dismissCallback;
this.dismissed = false;
const dropdown = this.dropdown = document.createElement("div");
dropdown.className = "dropdown wed-context-menu";
// tslint:disable-next-line:no-inner-html
dropdown.innerHTML =
// This fake toggle is required for bootstrap to do its work.
"<a href='#' data-toggle='dropdown'></a>" +
"<ul class='dropdown-menu' role='menu'></ul>";
// We move the top and left so that we appear under the mouse cursor.
// Hackish, but it works. If we don't do this, then the mousedown that
// brought the menu up also registers as a click on the body element and the
// menu disappears right away. (It would be nice to have a more general
// solution some day.)
x -= 5;
y -= 5;
dropdown.style.top = `${y}px`;
dropdown.style.left = `${x}px`;
this.x = x;
this.y = y;
const menu = this.menu = dropdown.lastElementChild;
const $menu = this.$menu = jquery_1.default(menu);
const toggle = this.toggle = dropdown.firstElementChild;
const $toggle = this.$toggle = jquery_1.default(toggle);
const backdrop = this.backdrop = document.createElement("div");
backdrop.className = "wed-context-menu-backdrop";
jquery_1.default(backdrop).click(this.backdropClickHandler.bind(this));
$menu.on("click", this.contentsClickHandler.bind(this));
// Bootstrap may dispatch clicks onto the toggle. We must catch them.
$toggle.on("click", this.contentsClickHandler.bind(this));
$menu.on("mousedown", (ev) => {
ev.stopPropagation();
});
jquery_1.default(dropdown).on("contextmenu mouseup", false);
const body = document.body;
body.insertBefore(dropdown, body.firstChild);
body.insertBefore(backdrop, body.firstChild);
if (immediateDisplay) {
this.display(items);
}
}
display(items) {
const dropdown = this.dropdown;
const $toggle = jquery_1.default(dropdown.firstElementChild);
this.render(items);
const $menu = this.$menu;
const menu = this.menu;
const document = dropdown.ownerDocument;
const x = this.x;
let y = this.y;
// Verify if we're going to run off screen. If so, then modify our position
// to be inside the screen.
const width = $menu.outerWidth();
const winWidth = jquery_1.default(document.defaultView).width();
// The x value that would put the menu just against the side of the window
// is width - winWidth. If x is less than it, then x is the value we want,
// but we don't want less than 0.
dropdown.style.left = `${Math.max(0, Math.min(x, winWidth - width))}px`;
menu.style.maxWidth = `${winWidth}px`;
// Adjust height so that we can see about 5 lines.
const fiveLines = Number($menu.css("line-height").replace("px", "")) * 5;
const winHeight = jquery_1.default(document.defaultView).height();
let maxHeight = winHeight - y;
if (maxHeight < fiveLines) {
y -= fiveLines - maxHeight;
maxHeight = fiveLines;
}
dropdown.style.top = `${y}px`;
menu.style.maxHeight = `${maxHeight}px`;
$toggle.focus(this.handleToggleFocus.bind(this));
$toggle.dropdown("toggle");
//
// What is going on here? When Bootstrap detects that touch events are
// supported, it assumes it is on a mobile device (which is a false
// assumption) and adds a backdrop to its dropdowns so as to be able to
// close it if the user "clicks" outside the dropdown. This messes up our
// own handling of the same scenario. To prevent this issue, we remove any
// backdrop added by Bootstrap. (It may be possible to keep both backdrops
// around but it would just complicate the code needlessly.)
//
// Note that we cannot rely on Bootstrap's backdrop, generally, because, as
// mentioned already, it won't be added for non-mobile platforms. However,
// we *always* need to detect clicks outside our menu, on all platforms.
//
const bootstrapBackdrop = domutil.childByClass(dropdown, "dropdown-backdrop");
if (bootstrapBackdrop !== null) {
dropdown.removeChild(bootstrapBackdrop);
}
}
/**
* Event handler for focus events on the toggle. Bootstrap focuses the toggle
* when the dropdown is shown. This can cause problems on some platforms if
* the dropdown is meant to have a descendant focused. (IE in particular
* grants focus asynchronously.) This method can be used to focus the proper
* element.
*/
handleToggleFocus() {
// Default does nothing.
}
/**
* Event handler for clicks on the contents. Dismissed the menu.
*/
contentsClickHandler(ev) {
this.dismiss();
ev.stopPropagation();
ev.preventDefault();
return false;
}
/**
* Event handler for clicks on the backdrop. Dismisses the menu.
* @private
*/
backdropClickHandler() {
this.dismiss();
return false;
}
/**
* Subclasses can override this to customize what is shown to the user. For
* instance, subclasses could accept a list of items which is more complex
* than DOM ``Element`` objects. Or could include in the list shown to the
* user some additional GUI elements.
*
* @param items The list of items that should make up the menu.
*/
render(items) {
this.$menu.append(items);
}
/**
* Dismisses the menu.
*/
dismiss() {
if (this.dismissed) {
return;
}
this.$menu.dropdown("toggle");
if (this.dropdown.parentNode !== null) {
this.dropdown.parentNode.removeChild(this.dropdown);
}
if (this.backdrop.parentNode !== null) {
this.backdrop.parentNode.removeChild(this.backdrop);
}
if (this.dismissCallback !== undefined) {
this.dismissCallback();
}
this.dismissed = true;
}
}
exports.ContextMenu = ContextMenu;
});
// LocalWords: contextmenu mousedown dropdown tabindex href gui MPL px
// LocalWords: Mangalam Dubeau ul jQuery Prepend util jquery mouseup winWidth
// LocalWords: dropdowns
//# sourceMappingURL=context-menu.js.map