wed
Version:
Wed is a schema-aware editor for XML documents.
453 lines • 20.6 kB
JavaScript
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", "../browsers", "../key", "../key-constants", "../transformation", "./context-menu", "./icon"], function (require, exports, jquery_1, browsers, keyMod, keyConstants, transformation_1, context_menu_1, icon) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
jquery_1 = __importDefault(jquery_1);
browsers = __importStar(browsers);
keyMod = __importStar(keyMod);
keyConstants = __importStar(keyConstants);
icon = __importStar(icon);
const KINDS = ["transform", "add", "delete", "wrap", "unwrap"];
// ``undefined`` is "other kinds".
const KIND_FILTERS = KINDS.concat(undefined);
// Sort order.
const KIND_ORDER = [undefined].concat(KINDS);
const TYPES = ["element", "attribute"];
const TYPE_FILTERS = TYPES.concat(undefined);
const ITEM_SELECTOR = "li:not(.divider):visible a";
const plus = keyMod.makeKey("+");
const minus = keyMod.makeKey("-");
const period = keyMod.makeKey(".");
const comma = keyMod.makeKey(",");
const question = keyMod.makeKey("?");
const less = keyMod.makeKey("<");
const at = keyMod.makeKey("@");
const exclamation = keyMod.makeKey("!");
const KEY_TO_FILTER = [
{ key: plus, filter: "add", which: "kind" },
{ key: minus, filter: "delete", which: "kind" },
{ key: comma, filter: "wrap", which: "kind" },
{ key: period, filter: "unwrap", which: "kind" },
{ key: question, filter: undefined, which: "kind" },
{ key: less, filter: "element", which: "type" },
{ key: at, filter: "attribute", which: "type" },
{ key: exclamation, filter: undefined, which: "type" },
];
function compareItems(a, b) {
const aKind = (a.action !== null && (a.action instanceof transformation_1.Transformation)) ?
a.action.kind : undefined;
const bKind = (b.action !== null && (b.action instanceof transformation_1.Transformation)) ?
b.action.kind : undefined;
if (aKind !== bKind) {
const aOrder = KIND_ORDER.indexOf(aKind);
const bOrder = KIND_ORDER.indexOf(bKind);
return aOrder - bOrder;
}
const aText = a.item.textContent;
const bText = b.item.textContent;
if (aText === bText) {
return 0;
}
if (aText < bText) {
return -1;
}
return 1;
}
/**
* A context menu for displaying actions. This class is designed to know how to
* sort [["wed/action".Action]] objects and
* [["wed/transformation".Transformation]] objects and how to filter them. Even
* though the names used here suggest that ``Action`` objects are the focus of
* this class, the fact is that it is really performing its work on
* ``Transformation`` objects. It does accept ``Action`` as a kind of lame
* ``Transformation``. So the following description will focus on
* ``Transformation`` objects rather than ``Action`` objects.
*
* Sorting is performed first by the ``kind`` of the ``Transformation`` and then
* by the text associated with the ``Transformation``. The kinds, in order, are:
*
* - other kinds than those listed below
*
* - undefined ``kind``
*
* - ``"add"``
*
* - ``"delete"``
*
* - ``"wrap"``
*
* - ``"unwrap"``
*
* The text associated with the transformation is the text value of the DOM
* ``Element`` object stored in the ``item`` field of the object given in the
* ``items`` array passed to the constructor. ``Actions`` are considered to have
* an undefined ``kind``.
*
* Filtering is performed by ``kind`` and on the text of the **element name**
* associated with a transformation. This class presents to the user a row of
* buttons that represent graphically the possible filters. Clicking on a button
* will reduce the list of displayed items only to those elements that
* correspond to the ``kind`` to which the button corresponds.
*
* Typing text (e.g. "foo") will narrow the list of items to the text that the
* user typed. Let's suppose that ``item`` is successively taking the values in
* the ``items`` array. The filtering algorithm first checks whether there is an
* ``item.data.name`` field. If there is, the match is performed against this
* field. If not, the match is performed against the text of ``item.item``.
*
* If the text typed begins with a caret (^), the text will be interpreted as a
* regular expression.
*
* Typing ESCAPE will reset filtering.
*
* When no option is focused, typing ENTER will select the first option of the
* menu.
*/
class ActionContextMenu extends context_menu_1.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 An array of action information in the form of anonymous
* objects. It is valid to have some items in the array be of the form
* ``{action: null, item: some_element, data: null}`` to insert arbitrary menu
* items.
*
* @param dismissCallback Function to call when the menu is dismissed.
*/
constructor(document, x, y, items, dismissCallback) {
super(document, x, y, [], dismissCallback, false);
this.filters = {
kind: null,
type: null,
};
this.actionTextFilter = "";
// Sort the items once and for all.
items.sort(compareItems);
this.actionItems = items;
// Create the filtering GUI...
// <li><div><button>... allows us to have this button group inserted in the
// menu and yet be ignored by Bootstrap's Dropdown class.
const li = document.createElement("li");
li.className = "wed-menu-filter";
li.style.whiteSpace = "nowrap";
const groupGroup = document.createElement("div");
const kindGroup = this.makeKindGroup(document);
const typeGroup = this.makeTypeGroup(document);
// Prevent clicks in the group from closing the context menu.
jquery_1.default(li).on("click", false);
li.appendChild(groupGroup);
groupGroup.appendChild(kindGroup);
groupGroup.appendChild(document.createTextNode("\u00a0"));
groupGroup.appendChild(typeGroup);
const textInput = document.createElement("input");
textInput.className = "form-control input-sm";
textInput.setAttribute("placeholder", "Filter choices by text.");
const textDiv = document.createElement("div");
textDiv.appendChild(textInput);
li.appendChild(textDiv);
const $textInput = jquery_1.default(textInput);
$textInput.on("input", this.inputChangeHandler.bind(this));
$textInput.on("keydown", this.inputKeydownHandler.bind(this));
this.actionFilterItem = li;
this.actionFilterInput = textInput;
const $menu = this.$menu;
$menu.parent().on("hidden.bs.dropdown", () => {
// Manually destroy the tooltips so that they are not
// left behind.
jquery_1.default(textInput).tooltip("destroy");
jquery_1.default(kindGroup).children().tooltip("destroy");
});
$menu.on("keydown", this.actionKeydownHandler.bind(this));
$menu.on("keypress", this.actionKeypressHandler.bind(this));
this.display([]);
textInput.focus();
}
makeKindGroup(document) {
const kindGroup = document.createElement("div");
kindGroup.className = "btn-group btn-group-xs";
for (const kind of KIND_FILTERS) {
const child = document.createElement("button");
child.className = "btn btn-default";
let title;
if (kind !== undefined) {
// tslint:disable-next-line:no-inner-html
child.innerHTML = icon.makeHTML(kind);
title = `Show only ${kind} operations.`;
}
else {
// tslint:disable-next-line:no-inner-html
child.innerHTML = icon.makeHTML("other");
title = "Show operations not covered by other filter buttons.";
}
jquery_1.default(child).tooltip({
title: title,
// If we don't set it to be on the body, then the tooltip will be
// clipped by the dropdown. However, we then run into the problem that
// when the dropdown menu is removed, the tooltip may remain displayed.
container: "body",
placement: "auto top",
trigger: "hover",
});
jquery_1.default(child).on("click", this.makeKindHandler(kind));
kindGroup.appendChild(child);
}
return kindGroup;
}
makeTypeGroup(document) {
const typeGroup = document.createElement("div");
typeGroup.className = "btn-group btn-group-xs";
for (const actionType of TYPE_FILTERS) {
const child = document.createElement("button");
child.className = "btn btn-default";
let title;
if (actionType !== undefined) {
// tslint:disable-next-line:no-inner-html
child.innerHTML = icon.makeHTML(actionType);
title = `Show only ${actionType} operations.`;
}
else {
// tslint:disable-next-line:no-inner-html
child.innerHTML = icon.makeHTML("other");
title = "Show operations not covered by other filter buttons.";
}
jquery_1.default(child).tooltip({
title: title,
// If we don't set it to be on the body, then the tooltip will be
// clipped by the dropdown. However, we then run into the problem that
// when the dropdown menu is removed, the tooltip may remain displayed.
container: "body",
placement: "auto top",
trigger: "hover",
});
jquery_1.default(child).on("click", this.makeTypeHandler(actionType));
typeGroup.appendChild(child);
}
return typeGroup;
}
makeKindHandler(kind) {
return () => {
this.filters.kind = kind;
this.render();
};
}
makeTypeHandler(actionType) {
return () => {
this.filters.type = actionType;
this.render();
};
}
handleToggleFocus() {
this.actionFilterInput.focus();
}
actionKeydownHandler(ev) {
if (keyConstants.ESCAPE.matchesEvent(ev) &&
(this.filters.kind !== null ||
this.filters.type !== null ||
this.actionTextFilter !== "")) {
this.filters.kind = null;
this.filters.type = null;
this.actionTextFilter = "";
// For some reason, on FF 24, stopping propagation and
// preventing the default is not enough.
if (!browsers.FIREFOX_24) {
this.actionFilterInput.value = "";
this.render();
}
else {
setTimeout(() => {
this.actionFilterInput.value = "";
this.render();
}, 0);
}
ev.stopPropagation();
ev.preventDefault();
return false;
}
return true;
}
actionKeypressHandler(ev) {
// If the user has started filtering on text, we don't interpret
// the key as setting a kind or type filter.
if (this.actionTextFilter !== "") {
return true;
}
for (const spec of KEY_TO_FILTER) {
const key = spec.key;
if (key.matchesEvent(ev)) {
const whichFilter = spec.which;
// Don't treat the key specially if the filter is already set.
if (this.filters[whichFilter] !== null) {
continue;
}
this.filters[whichFilter] = spec.filter;
this.render();
ev.stopPropagation();
ev.preventDefault();
return false;
}
}
return true;
}
inputChangeHandler(ev) {
const previous = this.actionTextFilter;
const newval = ev.target.value;
// IE11 generates input events when focus is lost/gained. These
// events do not change anything to the contents of the field so
// we protect against unnecessary renders a bit of logic here.
if (previous !== newval) {
this.actionTextFilter = newval;
this.render();
}
}
inputKeydownHandler(ev) {
if (keyConstants.ENTER.matchesEvent(ev)) {
this.$menu.find(ITEM_SELECTOR).first().focus().click();
ev.stopPropagation();
ev.preventDefault();
return false;
}
// Bootstrap 3.3.2 (and probably some versions before this one) introduces a
// change that prevents these events from being processed by the dropdown
// menu. We have to manually forward them. See bug report:
//
// https://github.com/twbs/bootstrap/issues/15757
//
let matches;
for (const check of ["UP_ARROW", "DOWN_ARROW", "ESCAPE"]) {
// tslint:disable-next-line:no-any
const key = keyConstants[check];
if (key.matchesEvent(ev)) {
matches = key;
break;
}
}
if (matches !== undefined) {
const fakeEv = new jquery_1.default.Event("keydown");
matches.setEventToMatch(fakeEv);
// We have to pass the event to ``actionKeypressHandler`` so that it can
// act in the same way as if the event had been directly on the menu. If
// ``actionKeypressHandler`` does not handle it, then pass it on to the
// toggle. We forward to the toggle because that's how Bootstrap normally
// works.
if (this.actionKeydownHandler(fakeEv) !== false) {
this.$toggle.trigger(fakeEv);
}
// We have to return `false` to make sure it is not mishandled.
return false;
}
return true;
}
render() {
const menu = this.menu;
const actionFilterItem = this.actionFilterItem;
const actionKindFilter = this.filters.kind;
const actionTypeFilter = this.filters.type;
// On IE 10, we don't want to remove and then add back this.actionFilterItem
// on each render because that makes this.actionFilterInput lose the
// focus. Yes, even with the call at the end of _render, IE 10 inexplicably
// makes the field lose focus **later**.
while (menu.lastChild !== null && menu.lastChild !== actionFilterItem) {
menu.removeChild(menu.lastChild);
}
let child = actionFilterItem
.firstElementChild.firstElementChild.firstElementChild;
for (const kind of KIND_FILTERS) {
const cl = child.classList;
const method = (actionKindFilter === kind) ? cl.add : cl.remove;
method.call(cl, "active");
child = child.nextElementSibling;
}
child = actionFilterItem
.firstElementChild.lastElementChild.firstElementChild;
for (const actionType of TYPE_FILTERS) {
const cl = child.classList;
const method = (actionTypeFilter === actionType) ? cl.add : cl.remove;
method.call(cl, "active");
child = child.nextElementSibling;
}
if (actionFilterItem.parentNode === null) {
menu.appendChild(actionFilterItem);
}
const items = this.computeActionItemsToDisplay(this.actionItems);
super.render(items);
}
computeActionItemsToDisplay(items) {
const kindFilter = this.filters.kind;
const typeFilter = this.filters.type;
const textFilter = this.actionTextFilter;
let kindMatch;
switch (kindFilter) {
case null:
kindMatch = () => true;
break;
case undefined:
kindMatch = (item) => !(item.action instanceof transformation_1.Transformation) ||
KINDS.indexOf(item.action.kind) === -1;
break;
default:
kindMatch = (item) => (item.action instanceof transformation_1.Transformation) &&
item.action.kind === kindFilter;
}
let typeMatch;
switch (typeFilter) {
case null:
typeMatch = () => true;
break;
case undefined:
typeMatch = (item) => !(item.action instanceof transformation_1.Transformation) ||
TYPES.indexOf(item.action.nodeType) === -1;
break;
default:
typeMatch = (item) => (item.action instanceof transformation_1.Transformation) &&
item.action.nodeType === typeFilter;
}
let textMatch;
if (textFilter !== "") {
if (textFilter[0] === "^") {
const textFilterRe = RegExp(textFilter);
textMatch = (item) => {
const text = (item.data !== null && item.data.name !== undefined) ?
item.data.name : item.item.textContent;
return textFilterRe.test(text);
};
}
else {
textMatch = (item) => {
const text = (item.data !== null && item.data.name !== undefined) ?
item.data.name : item.item.textContent;
return text.indexOf(textFilter) !== -1;
};
}
}
else {
textMatch = () => true;
}
const ret = [];
for (const item of items) {
if (kindMatch(item) && typeMatch(item) && textMatch(item)) {
ret.push(item.item);
}
}
return ret;
}
}
exports.ActionContextMenu = ActionContextMenu;
});
// LocalWords: MPL li Dropdown nowrap sm keydown tooltips keypress btn xs
// LocalWords: tooltip dropdown actionType actionFilterItem actionFilterInput
// LocalWords: actionKeypressHandler
//# sourceMappingURL=action-context-menu.js.map