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

Menu bar #929

Merged
merged 42 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
07f54c9
Initial implementation of menu-bar
fatih-erikli Oct 26, 2023
7b71534
Delete commented out import line
fatih-erikli Oct 26, 2023
10887e1
Delete .items getter&setters
fatih-erikli Oct 26, 2023
754a222
Make font size 1rem, Highlight the top bar menu items when hover
fatih-erikli Nov 7, 2023
5cab529
Menu items should be available when getItems called
fatih-erikli Jan 24, 2024
28ccfc8
Initial implementation of sub-menu
fatih-erikli Jan 2, 2024
7fa6d9b
Submenu should be populated right before it is shown
fatih-erikli Jan 24, 2024
99b084c
Delete forgotten commented out lines
fatih-erikli Jan 24, 2024
cf62454
Rename recentSubMenu -> lastShownSubMenu
fatih-erikli Jan 24, 2024
5eeed41
Delete the submenu when it's no longer visible
fatih-erikli Jan 24, 2024
9694b8f
Replaced ascii arrow with chevron-right
fatih-erikli Jan 24, 2024
a89f5c2
Fix spacing in submenu icon
fatih-erikli Jan 24, 2024
6039143
Made keeping the track of sub menus by a property
fatih-erikli Jan 26, 2024
67b3972
Prevent blur event being fired when mouseup
fatih-erikli Jan 29, 2024
ac7dbc6
add onSelect event, prevent dismissing the menu when click on menu-ba…
fatih-erikli Jan 29, 2024
31aa779
Delete commented-out code
fatih-erikli Jan 29, 2024
867dba9
Hidemenu when hover on top-menu, delete individual callbacks on menu …
fatih-erikli Jan 31, 2024
88388b8
Delete childOf property
fatih-erikli Jan 31, 2024
3e5e2b8
Bring back childOf
fatih-erikli Jan 31, 2024
20ea75b
Delete not used import
fatih-erikli Jan 31, 2024
b77ad4f
Merge branch 'menu-bar' into sub-menu
fatih-erikli Jan 31, 2024
496bb79
Fix merge error
fatih-erikli Jan 31, 2024
2065f70
Dismiss the menu when click on already open menu
fatih-erikli Feb 12, 2024
b91ad4a
Delete forgotten console.log
fatih-erikli Feb 13, 2024
f30008a
Top-bar menu
fatih-erikli Feb 15, 2024
5015a38
Merge pull request #1047 from googlefonts/sub-menu
fatih-erikli Feb 20, 2024
f8addc2
Delete top-menu in plugins page
fatih-erikli Feb 20, 2024
e204394
Disable the zoom to fit if it is already actual
fatih-erikli Feb 21, 2024
1782e91
Help menu links
fatih-erikli Feb 23, 2024
e88aafb
Thinner top-bar
fatih-erikli Feb 23, 2024
b39c2df
Fix: The "Edit" menu doesn't work when a glyph is in edit mode"
fatih-erikli Feb 27, 2024
9ec1d4c
Disable the add source if there is no selected glyph
fatih-erikli Feb 27, 2024
1a3f71c
Merge branch 'main' into menu-bar
justvanrossum Feb 27, 2024
b254f42
Add delete source to Glyph menu
fatih-erikli Feb 27, 2024
2edee7f
Edit local axes
fatih-erikli Feb 27, 2024
f04663a
The labels of menu items that open a dialog should end with "..."
fatih-erikli Feb 27, 2024
c22e801
Add select previous-next source under glyph menu
fatih-erikli Feb 27, 2024
6a2fc72
Reset the menu state when menu panel dismissed itself
fatih-erikli Feb 27, 2024
42bbf70
Merge branch 'main' into menu-bar
fatih-erikli Feb 28, 2024
decdf71
Make MenuPanel constructor arguments an object
fatih-erikli Feb 28, 2024
82e5db0
Move selected glyph items under View
fatih-erikli Feb 28, 2024
493b044
Fix 'this' w arrow functions
fatih-erikli Feb 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/fontra/client/core/canvas-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,17 @@ export class CanvasController {
});
}

isActualViewBox(viewBox) {
const canvasCenter = this.canvasPoint(rectCenter(viewBox));
return (
this.magnification === this._getProposedViewBoxMagnification(viewBox) &&
Math.round(this.origin.x) ===
Math.round(this.canvasWidth / 2 + this.origin.x - canvasCenter.x) &&
Math.round(this.origin.y) ===
Math.round(this.canvasHeight / 2 + this.origin.y - canvasCenter.y)
);
}

setViewBox(viewBox) {
this.magnification = this._getProposedViewBoxMagnification(viewBox);
const canvasCenter = this.canvasPoint(rectCenter(viewBox));
Expand Down
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.
145 changes: 145 additions & 0 deletions src/fontra/client/web-components/menu-bar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as html from "../core/html-utils.js";
import { SimpleElement } from "../core/html-utils.js";
import { MenuPanel } from "./menu-panel.js";

export class MenuBar extends SimpleElement {
static styles = `
.menu-bar {
padding: 0.2rem 0.5rem;
align-items: center;
position: absolute;
font-size: 1rem;
width: 100%;
}

.menu-item {
padding: 0.4rem 0.6rem;
display: inline-block;
cursor: default;
user-select: none;
}

.menu-item:hover,
.menu-item.current {
background: var(--editor-top-bar-link-hover);
border-radius: 5px;
}
`;

constructor(items = []) {
super();
this.items = items;
this.contentElement = this.shadowRoot.appendChild(html.div());
this.contentElement.classList.add("menu-bar");
this.render();
window.addEventListener("mousedown", this.onBlur.bind(this));
window.addEventListener("blur", this.onBlur.bind(this));
this.contentElement.addEventListener("mouseover", this.onMouseover.bind(this));
this.contentElement.addEventListener("click", this.onClick.bind(this));
this.showMenuWhenHover = false;
}

onClick(event) {
if (event.target.classList.contains("menu-item")) {
const currentSelection = this.contentElement.querySelector(".current");
if (currentSelection === event.target) {
this.clearCurrentSelection();
this.showMenuWhenHover = false;
} else {
for (let i = 0; i < this.contentElement.childElementCount; i++) {
const node = this.contentElement.childNodes[i];
if (node === event.target) {
this.clearCurrentSelection();
this.showMenuWhenHover = true;
this.showMenu(this.items[i].getItems(), node);
break;
}
}
}
}
}

onMouseover(event) {
const currentSelection = this.contentElement.querySelector(".current");
if (!currentSelection && !this.showMenuWhenHover) {
return;
}
if (event.target === this.contentElement) {
this.clearCurrentSelection();
this.showMenuWhenHover = true;
return;
}
if (event.target.classList.contains("menu-item")) {
this.clearCurrentSelection();
for (let i = 0; i < this.contentElement.childElementCount; i++) {
const node = this.contentElement.childNodes[i];
if (node === event.target) {
if (this.showMenuWhenHover) {
this.showMenu(this.items[i].getItems(), node);
}
break;
}
}
}
}

onBlur(event) {
this.clearCurrentSelection();
this.showMenuWhenHover = false;
}

clearCurrentSelection(event) {
const currentSelection = this.contentElement.querySelector(".current");
if (currentSelection) {
currentSelection.classList.remove("current");
const menuPanel = this.contentElement.querySelector("menu-panel");
if (menuPanel) {
this.contentElement.removeChild(menuPanel);
}
}
}

showMenu(items, menuItemElement) {
menuItemElement.classList.add("current");
const clientRect = menuItemElement.getBoundingClientRect();
const position = {
x: clientRect.x,
y: clientRect.y + clientRect.height,
};
const menuPanel = new MenuPanel(items, {
position,
onSelect: () => {
this.showMenuWhenHover = false;
this.clearCurrentSelection();
},
onClose: () => {
this.showMenuWhenHover = false;
this.clearCurrentSelection();
},
});
this.contentElement.appendChild(menuPanel);
}

render() {
const fragment = document.createDocumentFragment();
for (const item of this.items) {
fragment.appendChild(
html.div(
{
class: "menu-item",
onmousedown: (event) => {
const currentSelection = this.contentElement.querySelector(".current");
if (currentSelection === event.target) {
event.stopImmediatePropagation();
}
},
},
[item.title]
)
);
}
this.contentElement.appendChild(fragment);
}
}

customElements.define("menu-bar", MenuBar);
128 changes: 108 additions & 20 deletions src/fontra/client/web-components/menu-panel.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
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: "-" };

export function showMenu(menuItems, position, positionContainer, container) {
if (!container) {
container = document.querySelector("#menu-panel-container");
}
const menu = new MenuPanel(menuItems, position, positionContainer);
const menu = new MenuPanel(menuItems, { position, positionContainer });
container.appendChild(menu);
}

class MenuPanel extends SimpleElement {
export class MenuPanel extends SimpleElement {
static openMenuPanels = [];

static closeAllMenus(event) {
if (event) {
if (event.target instanceof MenuPanel) {
return;
}
}
for (const element of MenuPanel.openMenuPanels) {
element.parentElement?.removeChild(element);
}
Expand Down Expand Up @@ -66,11 +62,20 @@ 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 @@ -86,51 +91,91 @@ class MenuPanel extends SimpleElement {
gap: 0.5em;
justify-content: space-between;
}

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

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

// 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")) {
this.selectItem(itemElement);
}
},
onmousedown: (event) => {
event.preventDefault();
event.stopImmediatePropagation();
},
onmouseleave: (event) => itemElement.classList.remove("selected"),
onmouseup: (event) => {
event.preventDefault();
event.stopImmediatePropagation();
if (item.enabled()) {
item.callback?.(event);
this.dismiss();
this.onSelect?.(itemElement);
}
},
},
[
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 +188,16 @@ 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 All @@ -167,14 +222,47 @@ class MenuPanel extends SimpleElement {
}
this.parentElement?.removeChild(this);
this._savedActiveElement?.focus();
this.onClose?.();
}

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(),
{
position: {
x: 0,
y: 0,
},
childOf: 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
Loading