@ne1410s/menu
Version:
Lightweight ES context menu.
177 lines (170 loc) • 10.1 kB
JavaScript
var dom = require('@ne1410s/dom');
var custElems = require('@ne1410s/cust-elems');
var markupUrl = "data:text/html;base64,PHNsb3Q+PC9zbG90Pg0KPHVsIGNsYXNzPSJ0b3AiPjwvdWw+DQo=";
var stylesUrl = "data:text/css;base64,dWwsDQpsaSB7DQogIHBhZGRpbmc6IDA7DQogIG1hcmdpbjogMDsNCiAgZm9udDogaW5oZXJpdDsNCn0NCnNsb3QsDQpsaS5zcGxpdCAuaWNvbiB7DQogIGRpc3BsYXk6IG5vbmU7DQp9DQoudG9wOm5vdCgub3BlbiksDQp1bDpub3QoLnRvcCkgew0KICB2aXNpYmlsaXR5OiBoaWRkZW47DQp9DQoNCnVsIHsNCiAgYm9yZGVyOiB2YXIoLS1ib3JkZXIsIDFweCBzb2xpZCAjYmJiKTsNCiAgYmFja2dyb3VuZDogdmFyKC0tYmcsICNmZmYpOw0KICBib3gtc2hhZG93OiB2YXIoLS1ib3gtc2hhZG93LCAycHggMnB4IDNweCAjODg4KTsNCiAgdHJhbnNpdGlvbjogYmFja2dyb3VuZCAwLjZzOw0KICB6LWluZGV4OiA5OTk5OTk5Ow0KfQ0KDQoudG9wIHsNCiAgcG9zaXRpb246IGZpeGVkOw0KICBmb250LXNpemU6IHZhcigtLWZvbnQtc2l6ZSwgMC42NXJlbSk7DQogIHVzZXItc2VsZWN0OiBub25lOw0KfQ0KDQouaWNvbi5sZWZ0IHsNCiAgbGVmdDogMWVtOw0KICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKTsNCn0NCi5pY29uLnJpZ2h0IHsNCiAgcmlnaHQ6IDFlbTsNCiAgdHJhbnNmb3JtOiB0cmFuc2xhdGUoNTAlLCAtNTAlKTsNCn0NCi5pY29uIHsNCiAgbWF4LXdpZHRoOiAxZW07DQogIG1heC1oZWlnaHQ6IDFlbTsNCiAgcG9zaXRpb246IGFic29sdXRlOw0KICB0b3A6IDUwJTsNCiAgbGluZS1oZWlnaHQ6IDA7DQp9DQoNCmxpID4gcDpub3QoOmVtcHR5KSB7DQogIG1hcmdpbjogMCAxZW07DQogIHdoaXRlLXNwYWNlOiBub3dyYXA7DQp9DQpsaS5kaXNhYmxlZCB7DQogIGNvbG9yOiB2YXIoLS1kaXNhYmxlZC1mZywgI2JiYik7DQp9DQpsaS5zcGxpdCB7DQogIGJvcmRlci10b3A6IHZhcigtLXNwbGl0LWJvcmRlciwgdmFyKC0tYm9yZGVyLCAxcHggc29saWQgI2JiYikpOw0KICBwYWRkaW5nOiAwOw0KICBtYXJnaW46IDAuM2VtOw0KfQ0KbGkuc3BsaXQ6Zmlyc3QtY2hpbGQsDQpsaS5zcGxpdDpsYXN0LWNoaWxkLA0KbGkuc3BsaXQgKyBsaS5zcGxpdCB7DQogIGRpc3BsYXk6IG5vbmU7DQp9DQpsaS5ob3Zlcjpub3QoLmRpc2FiZWQpOm5vdCguc3BsaXQpIHsNCiAgYmFja2dyb3VuZDogdmFyKC0taG92ZXItaXRlbS1iZywgI2JiYik7DQp9DQoudG9wLm9wZW4gbGkuaG92ZXIgPiB1bCB7DQogIHZpc2liaWxpdHk6IHZpc2libGU7DQp9DQpsaSB7DQogIG1hcmdpbjogMC4yZW0gMDsNCiAgcGFkZGluZzogMC41ZW0gMWVtOw0KICBkaXNwbGF5OiBmbGV4Ow0KICBqdXN0aWZ5LWNvbnRlbnQ6IHNwYWNlLWJldHdlZW47DQogIHRleHQtYWxpZ246IGxlZnQ7DQogIHBvc2l0aW9uOiByZWxhdGl2ZTsNCiAgY3Vyc29yOiBkZWZhdWx0Ow0KICBjb2xvcjogdmFyKC0tZmcsICMwMDApOw0KfQ0KDQpsaS5ncm91cDo6YWZ0ZXIgew0KICBjb250ZW50OiAnXDI1YjYnOw0KICBwb3NpdGlvbjogYWJzb2x1dGU7DQogIGZvbnQtc2l6ZTogMC42NWVtOw0KICByaWdodDogMDsNCiAgdG9wOiA1MCU7DQogIHRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpOw0KfQ0KDQp1bDpub3QoLnRvcCkgew0KICBwb3NpdGlvbjogYWJzb2x1dGU7DQogIGxlZnQ6IDEwMCU7DQogIHRvcDogLTAuMjVlbTsNCn0NCg0KdWwubmVzdGxlIHsNCiAgbGVmdDogMC41ZW07DQogIHRvcDogMTAwJTsNCn0NCg==";
class NeMenu extends custElems.CustomElementBase {
constructor() {
super(NeMenu.Css, NeMenu.Html);
this.top = this.root.querySelector('ul');
}
connectedCallback() {
if (!this._connected) {
setTimeout(() => this.reload());
dom.q(this.parentNode).on('contextmenu', (e) => this.onParentContext(e));
dom.q(this, this.parentNode).on('contextmenu wheel', (e) => {
e.preventDefault();
e.stopPropagation();
});
dom.q(this).on('mousedown', (e) => e.stopPropagation());
dom.q(window).on('mousedown resize wheel', () => this.close());
this._connected = true;
}
}
/** Opens the menu. */
open() {
// close all menus
const doc = this.parentElement.getRootNode();
doc.querySelectorAll('ne14-menu').forEach((m) => m.close());
// style this one as open
this.top.classList.add('open');
dom.q(this).fire('menuopen');
}
/** Closes the menu. */
close() {
if (this.top.classList.contains('open')) {
this.top.classList.remove('open');
dom.q(this).fire('menuclose');
}
}
/** Reloads active contents based on client dom. */
reload() {
dom.q(this.top)
.empty()
.append(...this.walk(this, false));
}
onParentContext(event) {
if (this.isConnected) {
// update position (y)
const y = event.clientY;
const height = this.top.offsetHeight;
const posY = y + height + 2 > window.innerHeight ? y - height : y;
this.top.style.top = `${Math.max(0, posY)}px`;
// update position (x)
const x = event.clientX;
const width = this.top.offsetWidth;
const posX = x + width + 2 > window.innerWidth ? x - width : x;
this.top.style.left = `${Math.max(0, posX)}px`;
// open
this.open();
}
}
walk(ul, parentDisabled, ref = '') {
let levelItemNo = 0;
return Array.from(ul.children)
.filter((c) => c instanceof HTMLLIElement &&
!c.classList.contains('hidden') &&
(c.textContent || c.classList.contains('split')))
.reduce((acc, li) => {
var _a, _b, _c;
const children = Array.from(li.children).map((el) => el);
const a = children.find((n) => n instanceof HTMLAnchorElement);
const ul = children.find((n) => n instanceof HTMLUListElement);
const isSplit = li.classList.contains('split');
const isGrouper = !isSplit && ul && ul.querySelector('li');
const isDisabled = !isSplit && (parentDisabled || li.classList.contains('disabled'));
const aChildren = Array.from((a === null || a === void 0 ? void 0 : a.children) || []).map((el) => el);
const imgs = children
.concat(aChildren)
.filter((n) => n instanceof HTMLImageElement)
.map((n) => n);
if (!isSplit)
levelItemNo++;
const classes = [];
if (isSplit)
classes.push('split');
else {
if (isDisabled)
classes.push('disabled');
if (isGrouper)
classes.push('group');
if ((a === null || a === void 0 ? void 0 : a.target) === '_blank')
classes.push('click-out');
else if (a)
classes.push('click-in');
}
const bestTextNode = [...children, li].find((c) => c.innerText);
const bestText = isSplit ? null : ((_a = bestTextNode === null || bestTextNode === void 0 ? void 0 : bestTextNode.innerText) !== null && _a !== void 0 ? _a : `Item ${levelItemNo}`);
const shortcut = isSplit || isGrouper ? null : li.getAttribute('aria-keyshortcuts');
const liRef = `${ref}${levelItemNo}`;
const eventDetail = { ref: liRef, title: bestText, origin: a || li };
const handleClick = () => {
if (!isDisabled && !isGrouper && !isSplit) {
eventDetail.origin.click();
dom.q(this).fire('itemselect', eventDetail);
this.close();
}
};
const handleMouseEnter = (e) => {
if (!isDisabled && !isSplit) {
const domLi = e.target;
if (isGrouper) {
const domUl = Array.from(domLi.children).find((n) => n instanceof HTMLUListElement);
const liRect = domLi.getBoundingClientRect();
domUl.classList.toggle('nestle', liRect.right + domUl.clientWidth + 2 > window.innerWidth);
}
domLi.classList.add('hover');
dom.q(this).fire('itemhover', eventDetail);
}
};
const handleMouseLeave = (e) => {
if (!isDisabled && !isSplit) {
e.target.classList.remove('hover');
dom.q(this).fire('itemunhover', eventDetail);
}
};
const $domItem = dom.q({ tag: 'li' })
.attr('class', classes.length ? classes.join(' ') : null)
.attr('aria-keyshortcuts', shortcut)
.on('click contextmenu', handleClick)
.on('mouseenter', handleMouseEnter)
.on('mouseleave', handleMouseLeave);
const charLeft = li.dataset.charLeft;
const charRight = li.dataset.charRight;
const imgLeft = imgs.find((i) => !i.classList.contains('right'));
const imgRight = imgs.find((i) => i.classList.contains('right'));
if (!isSplit && !isGrouper && bestText) {
if (NeMenu.CHAR_REF_REGEX.test(charLeft))
$domItem.append(`<span class='icon left'>&#x${charLeft};</span>`);
else if (charLeft)
console.warn(`ne14-menu: Bad hex code '${charLeft}' to left of '${bestText}'.`);
else if (imgLeft)
$domItem.append(`<img class='icon left' src='${imgLeft.src}'/>`);
if (NeMenu.CHAR_REF_REGEX.test(charRight))
$domItem.append(`<span class='icon right'>&#x${charRight};</span>`);
else if (charRight)
console.warn(`ne14-menu: Bad hex code '${charRight}' to right of '${bestText}'.`);
else if (imgRight)
$domItem.append(`<img class='icon right' src='${imgRight.src}'/>`);
}
if (bestText)
$domItem.append({ tag: 'p', text: bestText });
if (shortcut)
$domItem.append({ tag: 'p', text: shortcut });
if (isGrouper)
$domItem.appendIn({ tag: 'ul' }).append(...this.walk(ul, isDisabled, `${liRef}-`));
// Do not push two consecutive splits
if (!isSplit || !((_c = (_b = acc[acc.length - 1]) === null || _b === void 0 ? void 0 : _b.classList) === null || _c === void 0 ? void 0 : _c.contains('split'))) {
acc.push($domItem.elements[0]);
}
return acc;
}, []);
}
}
NeMenu.Css = custElems.reduceCss(custElems.decode(stylesUrl));
NeMenu.Html = custElems.reduceHtml(custElems.decode(markupUrl));
NeMenu.CHAR_REF_REGEX = /^[0-9a-f]{4,5}$/i;
if ('customElements' in window) {
window.customElements.define('ne14-menu', NeMenu);
}
exports.ContextMenu = NeMenu;
;