Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sub-menu implementation in menu-panel #1047

Merged
merged 13 commits into from
Feb 20, 2024
1 change: 1 addition & 0 deletions src/fontra/client/tabler-icons/chevron-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
114 changes: 103 additions & 11 deletions src/fontra/client/web-components/menu-panel.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { themeColorCSS } from "./theme-support.js";
import * as html from "/core/html-utils.js";
import { SimpleElement } from "/core/html-utils.js";
import { capitalizeFirstLetter, reversed } from "/core/utils.js";
import { capitalizeFirstLetter, enumerate, reversed } from "/core/utils.js";
import { InlineSVG } from "/web-components/inline-svg.js";

export const MenuItemDivider = { title: "-" };

Expand Down Expand Up @@ -61,11 +62,20 @@ export class MenuPanel extends SimpleElement {
.context-menu-item {
display: grid;
grid-template-columns: 1em auto;
align-items: center;
gap: 0em;
padding: 0.1em 0.8em 0.1em 0.5em; /* top, right, bottom, left */
color: #8080a0;
}

.with-submenu {
grid-template-columns: 1em auto auto;
}

.has-open-submenu {
background-color: #dedede;
}

.context-menu-item.enabled {
color: inherit;
}
Expand All @@ -81,27 +91,71 @@ export class MenuPanel extends SimpleElement {
gap: 0.5em;
justify-content: space-between;
}

.submenu-icon {
width: 10px;
height: 14px;
}
`;

constructor(menuItems, position, positionContainer, onSelect) {
constructor(
menuItems,
position,
positionContainer,
onSelect,
visible = true,
childOf
) {
super();
this.style = "display: none;";
this.visible = visible;
this.position = position;
this.onSelect = onSelect;
this.positionContainer = positionContainer;
this.menuElement = html.div({ class: "menu-container", tabindex: 0 });
this.childOf = childOf;

// No context menu on our context menu please:
this.menuElement.oncontextmenu = (event) => event.preventDefault();

for (const item of menuItems) {
this.menuItems = menuItems;

for (const [index, item] of enumerate(menuItems)) {
const hasSubMenu = typeof item.getItems === "function";
let itemElement;
if (item === MenuItemDivider || item.title === "-") {
itemElement = html.hr({ class: "menu-item-divider" });
} else {
const classNames = ["context-menu-item"];
if (
(!hasSubMenu || item.getItems().length > 0) &&
typeof item.enabled === "function" &&
item.enabled()
) {
classNames.push("enabled");
}
if (hasSubMenu) {
classNames.push("with-submenu");
}
const itemElementContent = [
html.div({ class: "check-mark" }, [item.checked ? "✓" : ""]),
html.div({ class: "item-content" }, [
typeof item.title === "function" ? item.title() : item.title,
html.span({}, [buildShortCutString(item.shortCut)]),
]),
];
if (hasSubMenu) {
itemElementContent.push(
html.div({ class: "submenu-icon" }, [
new InlineSVG(`/tabler-icons/chevron-right.svg`, {
style: "margin-top: 2px",
}),
])
);
}
itemElement = html.div(
{
class: `context-menu-item ${item.enabled() ? "enabled" : ""}`,
class: classNames.join(" "),
onmouseenter: (event) => this.selectItem(itemElement),
onmousemove: (event) => {
if (!itemElement.classList.contains("selected")) {
Expand All @@ -123,14 +177,9 @@ export class MenuPanel extends SimpleElement {
}
},
},
[
html.div({ class: "check-mark" }, [item.checked ? "✓" : ""]),
html.div({ class: "item-content" }, [
typeof item.title === "function" ? item.title() : item.title,
html.span({}, [buildShortCutString(item.shortCut)]),
]),
]
itemElementContent
);
itemElement.dataset.index = index;
}
this.menuElement.appendChild(itemElement);
}
Expand All @@ -143,6 +192,16 @@ export class MenuPanel extends SimpleElement {
}

connectedCallback() {
if (this.visible) {
this.show();
}
}

hide() {
this.style.display = "none";
}

show() {
this._savedActiveElement = document.activeElement;
const position = { ...this.position };
this.style = `display: inherited; left: ${position.x}px; top: ${position.y}px;`;
Expand Down Expand Up @@ -170,11 +229,44 @@ export class MenuPanel extends SimpleElement {
}

selectItem(itemElement) {
for (const menuPanel of MenuPanel.openMenuPanels) {
if (menuPanel.childOf === this) {
menuPanel.dismiss();
break;
}
}

for (const item of this.menuElement.children) {
if (item.classList.contains("has-open-submenu")) {
item.classList.remove("has-open-submenu");
}
}

const selectedItem = this.findSelectedItem();
if (selectedItem && selectedItem !== itemElement) {
selectedItem.classList.remove("selected");
}
itemElement.classList.add("selected");

if (itemElement.classList.contains("with-submenu")) {
const { y: menuElementY } = this.getBoundingClientRect();
const { y, width } = itemElement.getBoundingClientRect();
const submenu = new MenuPanel(
this.menuItems[itemElement.dataset.index].getItems(),
{
x: 0,
y: 0,
},
undefined,
undefined,
false,
this
);
this.menuElement.appendChild(submenu);
submenu.position = { x: width, y: y - menuElementY - 4 };
submenu.show();
itemElement.classList.add("has-open-submenu");
}
}

handleKeyDown(event) {
Expand Down
97 changes: 97 additions & 0 deletions src/fontra/views/plugins/plugins.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,103 @@
},
shortCut: undefined,
},
{
title: "Recently opened",
enabled: () => true,
callback: () => {
console.log(1);
},
shortCut: undefined,
getItems() {
return [
{
title: "File 1",
enabled: () => true,
callback: () => {
console.log(1);
},
getItems() {
return [
{
title: "File 1",
enabled: () => true,
callback: () => {
console.log(1);
},
getItems() {
return [
{
title: "File 1",
enabled: () => true,
callback: () => {
console.log(1);
},
},
{
title: "File 2",
enabled: () => true,
callback: () => {
console.log(2);
},
},
{
title: "File 3",
enabled: () => true,
callback: () => {
console.log(3);
},
},
];
},
},
{
title: "File 2",
enabled: () => true,
callback: () => {
console.log(2);
},
},
{
title: "File 3",
enabled: () => true,
callback: () => {
console.log(3);
},
},
];
},
},
{
title: "File 2",
enabled: () => true,
callback: () => {
console.log(2);
},
},
{
title: "File 3",
enabled: () => true,
callback: () => {
console.log(3);
},
},
{
title: "File 4",
enabled: () => true,
callback: () => {
console.log(4);
},
},
{
title: "File 5",
enabled: () => true,
callback: () => {
console.log(5);
},
},
];
},
},
];
},
},
Expand Down