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

feat: Selection right-click/long-press menu #78

Merged
merged 32 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5834432
feat: Add a floating "duplicate" button for selection regions
personalizedrefrigerator Aug 25, 2024
995b625
prototype: Show options in a menu
personalizedrefrigerator Aug 26, 2024
261af73
WIP: Activate menu on long-press
personalizedrefrigerator Aug 26, 2024
2131bfe
prototype: Right-click and long-press context menu for the selection
personalizedrefrigerator Aug 26, 2024
4e1430e
Fix incorrect menu positioning
personalizedrefrigerator Aug 27, 2024
c7b17fa
Handle case where menu is near the screen edge
personalizedrefrigerator Aug 27, 2024
574f736
Attempt to fix touchscreen menu toggle button
personalizedrefrigerator Aug 27, 2024
cf04894
Improve popover menu dismiss logic
personalizedrefrigerator Aug 27, 2024
28714fe
Fix menu button double-creates a context menu in some browsers
personalizedrefrigerator Aug 27, 2024
bdf9eef
Use paste icon for paste menu item
personalizedrefrigerator Aug 27, 2024
2ca0367
Remove console.log
personalizedrefrigerator Aug 27, 2024
35d9ad2
Allow selection menu items to be localized
personalizedrefrigerator Aug 27, 2024
171e579
WIP: Menu accessibility
personalizedrefrigerator Aug 27, 2024
8a3d542
Fix menu items fail to be activated with touch
personalizedrefrigerator Aug 27, 2024
4895531
Menu: Support arrowup/arrowdown keyboard navigation
personalizedrefrigerator Aug 27, 2024
41caeb1
Fix paste/copy event handlers not fired in user event
personalizedrefrigerator Aug 27, 2024
868cbe2
Add basic test for overlay menu
personalizedrefrigerator Sep 3, 2024
d308c33
Add Home and End keyboard shortcuts to the menu overlay.
personalizedrefrigerator Sep 3, 2024
769d3e2
Rename SelectionTopMenu -> SelectionMenuShortcut
personalizedrefrigerator Sep 3, 2024
40f9435
Add ALT text to the show/hide menu button
personalizedrefrigerator Sep 3, 2024
2af6fe5
Fix showing a context menu reverts the last pan/zoom wheel transform
personalizedrefrigerator Sep 3, 2024
29f8e07
Tests for ContextMenuRecognizer
personalizedrefrigerator Sep 3, 2024
c37fd5f
Only allow long-press events from touch devices to show context menus
personalizedrefrigerator Sep 3, 2024
5493bd0
Minor refactor: Resolve TODO (clear selection preview when elements a…
personalizedrefrigerator Sep 3, 2024
68f7c2f
Reduce shadow around context menu
personalizedrefrigerator Sep 3, 2024
354843b
Include paste in the selection menu
personalizedrefrigerator Sep 3, 2024
2642564
Show dialog when copy/paste fails
personalizedrefrigerator Sep 3, 2024
ee71110
Update outdated comment
personalizedrefrigerator Sep 3, 2024
953ada1
Add margin to error details box
personalizedrefrigerator Sep 3, 2024
08013d0
Improve copy error message
personalizedrefrigerator Sep 3, 2024
fb68fc4
Attempting to fix paste on iOS
personalizedrefrigerator Sep 3, 2024
8184bd1
Rename variable
personalizedrefrigerator Sep 3, 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
17 changes: 14 additions & 3 deletions packages/js-draw/src/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import mitLicenseAttribution from './util/mitLicenseAttribution';
import { PenTypeRecord } from './toolbar/widgets/PenToolWidget';
import ClipboardHandler from './util/ClipboardHandler';
import { ShowCustomFilePickerCallback } from './toolbar/widgets/components/makeFileInput';
import ContextMenuRecognizer from './tools/InputFilter/ContextMenuRecognizer';

/**
* Provides settings to an instance of an editor. See the Editor {@link Editor.constructor}.
Expand Down Expand Up @@ -408,6 +409,7 @@ export class Editor {

// TODO: Make this pipeline configurable (e.g. allow users to add global input stabilization)
this.toolController.addInputMapper(StrokeKeyboardControl.fromEditor(this));
this.toolController.addInputMapper(new ContextMenuRecognizer());

parent.appendChild(this.container);

Expand Down Expand Up @@ -480,6 +482,15 @@ export class Editor {
return this.container;
}

/**
* @returns the bounding box of the main rendering region of the editor in the HTML viewport.
*
* @internal
*/
public getOutputBBoxInDOM(): Rect2 {
return Rect2.of(this.renderingRegion.getBoundingClientRect());
}

/**
* Shows a "Loading..." message.
* @param fractionLoaded - should be a number from 0 to 1, where 1 represents completely loaded.
Expand Down Expand Up @@ -635,8 +646,8 @@ export class Editor {
}

// Ensure that `pos` is relative to `this.renderingRegion`
const bbox = this.renderingRegion.getBoundingClientRect();
const pos = Vec2.of(event.clientX, event.clientY).minus(Vec2.of(bbox.left, bbox.top));
const bbox = this.getOutputBBoxInDOM();
const pos = Vec2.of(event.clientX, event.clientY).minus(bbox.topLeft);

if (this.toolController.dispatchInputEvent({
kind: InputEvtType.WheelEvt,
Expand Down Expand Up @@ -1276,7 +1287,7 @@ export class Editor {
* This is useful for displaying content on top of the rendered content
* (e.g. a selection box).
*/
public createHTMLOverlay(overlay: HTMLElement) {
public createHTMLOverlay(overlay: HTMLElement) { // TODO(v2): Fix conflict with toolbars that have been added to the editor.
overlay.classList.add('overlay', 'js-draw-editor-overlay');
this.container.appendChild(overlay);

Expand Down
30 changes: 9 additions & 21 deletions packages/js-draw/src/dialogs/dialogs.scss
Original file line number Diff line number Diff line change
@@ -1,27 +1,9 @@

@use './makeAboutDialog.scss';
@use './makeMessageDialog.scss';

// Repeat to increase specificity -- dialog containers are often overlays
.dialog-container.dialog-container {
background-color: var( --background-color-transparent);

backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);

// Show above other items
position: absolute;
z-index: 3;

// Fill the editor
width: var(--editor-current-width-px);
height: var(--editor-current-height-px);

display: flex;
flex-direction: column-reverse;
align-items: center;
justify-content: center;

dialog {
.dialog-container {
> dialog {
background-color: var(--background-color-1);
color: var(--foreground-color-1);
border: none;
Expand All @@ -33,5 +15,11 @@
max-height: 90vh;
width: min(100%, 500px);
box-sizing: border-box;

&::backdrop {
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
background-color: var( --background-color-transparent);
}
}
}
47 changes: 12 additions & 35 deletions packages/js-draw/src/dialogs/makeAboutDialog.scss
Original file line number Diff line number Diff line change
@@ -1,41 +1,18 @@

.about-dialog-container dialog {
display: flex;
flex-direction: column;
.about-dialog-container > dialog > .content {
white-space: pre-wrap;
font-family: monospace;

.close-button {
// Center the close button
display: block;
margin-left: auto;
margin-right: auto;
}

.about-entry-container {
flex-grow: 1;
flex-shrink: 1;
overflow-y: auto;

margin-left: 20px;
margin-right: 20px;

padding-bottom: 20px;
& > h2, & > details > summary {
cursor: pointer;
margin-top: 15px;

white-space: pre-wrap;
font-family: monospace;
font-size: 1.2em;
font-weight: bold;

& > h2, & > details > summary {
cursor: pointer;
margin-top: 15px;

font-size: 1.2em;
font-weight: bold;

a {
color: var(--foreground-color-1);
text-decoration: underline;
}
a {
color: var(--foreground-color-1);
text-decoration: underline;
}
}


}
}
40 changes: 7 additions & 33 deletions packages/js-draw/src/dialogs/makeAboutDialog.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type Editor from '../Editor';
import makeMessageDialog from './makeMessageDialog';

export interface AboutDialogLink {
kind: 'link',
Expand All @@ -13,32 +14,10 @@ export interface AboutDialogEntry {
}

const makeAboutDialog = (editor: Editor, entries: AboutDialogEntry[]) => {
const overlay = document.createElement('div');
const { remove: removeOverlay } = editor.createHTMLOverlay(overlay);

overlay.classList.add('dialog-container', 'about-dialog-container');
const dialog = document.createElement('dialog');

const heading = document.createElement('h1');
heading.innerText = editor.localization.about;
heading.setAttribute('autofocus', 'true');

const closeButton = document.createElement('button');
closeButton.innerText = editor.localization.closeDialog;
closeButton.classList.add('close-button');

closeButton.onclick = () => removeOverlay();
overlay.onclick = event => {
if (event.target === overlay) {
removeOverlay();
}
};

const licenseContainer = document.createElement('div');
licenseContainer.classList.add('about-entry-container');

// Allow scrolling in the license container -- don't forward wheel events.
licenseContainer.onwheel = evt => evt.stopPropagation();
const dialog = makeMessageDialog(editor, {
title: editor.localization.about,
classNames: [ 'about-dialog-container' ],
});

for (const entry of entries) {
const container = document.createElement(entry.minimized ? 'details' : 'div');
Expand All @@ -64,17 +43,12 @@ const makeAboutDialog = (editor: Editor, entries: AboutDialogEntry[]) => {
container.appendChild(bodyText);
}

licenseContainer.appendChild(container);
dialog.appendChild(container);
}

dialog.replaceChildren(heading, licenseContainer, closeButton);
overlay.replaceChildren(dialog);

dialog.show();

return {
close: () => {
removeOverlay();
return dialog.close();
},
};
};
Expand Down
41 changes: 41 additions & 0 deletions packages/js-draw/src/dialogs/makeMessageDialog.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}

.message-dialog-container dialog {
display: flex;
flex-direction: column;

> .close {
// Center the close button
display: block;
margin-left: auto;
margin-right: auto;
}

> .content {
flex-grow: 1;
flex-shrink: 1;
overflow-y: auto;

margin-left: 20px;
margin-right: 20px;

padding-bottom: 20px;
}

&.-closing {
opacity: 0;

&::backdrop {
opacity: 0;
}
}

&, &::backdrop {
transition: opacity 0.2s ease;
animation: fade-in 0.2s ease;
}
}
65 changes: 65 additions & 0 deletions packages/js-draw/src/dialogs/makeMessageDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type Editor from '../Editor';
import waitForTimeout from '../util/waitForTimeout';

export interface MessageDialogOptions {
title: string;
classNames?: string[];
}

const makeAboutDialog = (editor: Editor, options: MessageDialogOptions) => {
const overlay = document.createElement('div');
const { remove: removeOverlay } = editor.createHTMLOverlay(overlay);

overlay.classList.add('dialog-container', 'message-dialog-container', ...(options.classNames ?? []));
const dialog = document.createElement('dialog');

const heading = document.createElement('h1');
heading.textContent = options.title;
heading.setAttribute('autofocus', 'true');

const closeButton = document.createElement('button');
closeButton.innerText = editor.localization.closeDialog;
closeButton.classList.add('close');

const contentWrapper = document.createElement('div');
contentWrapper.classList.add('content');

// Allow scrolling in the scrollable container -- don't forward wheel events.
contentWrapper.onwheel = evt => evt.stopPropagation();

dialog.replaceChildren(heading, contentWrapper, closeButton);
overlay.replaceChildren(dialog);

const closeTimeout = 300;
dialog.style.setProperty('--close-delay', `${closeTimeout}ms`);
const closeDialog = async () => {
dialog.classList.add('-closing');
await waitForTimeout(closeTimeout);
dialog.close();
};
const addCloseListeners = () => {
dialog.addEventListener('pointerdown', event => {
if (event.target === dialog) {
void closeDialog();
}
});
dialog.onclose = () => {
removeOverlay();
};
closeButton.onclick = () => closeDialog();
};
addCloseListeners();

dialog.showModal();

return {
close: () => {
return closeDialog();
},
appendChild: (child: Node) => {
contentWrapper.appendChild(child);
},
};
};

export default makeAboutDialog;
10 changes: 9 additions & 1 deletion packages/js-draw/src/inputEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export enum InputEvtType {

CopyEvent,
PasteEvent,

ContextMenu,
}

// [delta.x] is horizontal scroll,
Expand Down Expand Up @@ -111,6 +113,12 @@ export interface PointerUpEvt extends PointerEvtBase {
readonly kind: InputEvtType.PointerUpEvt;
}

export interface ContextMenuEvt {
readonly kind: InputEvtType.ContextMenu;
readonly screenPos: Point2;
readonly canvasPos: Point2;
}

/**
* An internal `js-draw` pointer event type.
*
Expand All @@ -126,7 +134,7 @@ export type PointerEvtType = InputEvtType.PointerDownEvt|InputEvtType.PointerMov
*
* These are not DOM events.
*/
export type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt | CopyEvent | PasteEvent;
export type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt | CopyEvent | PasteEvent | ContextMenuEvt;

type KeyEventType = InputEvtType.KeyPressEvent|InputEvtType.KeyUpEvent;

Expand Down
3 changes: 3 additions & 0 deletions packages/js-draw/src/localizations/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ const localization: EditorLocalization = {
filledRectanglePen: 'Rectángulo sin borde',
lockRotation: 'Bloquea rotación',
paste: 'Pegar',
selectionMenu__paste: 'Pegar',
selectionMenu__delete: 'Eliminar',
selectionMenu__duplicate: 'Duplicar',
closeSidebar: (toolName) => `Close sidebar for ${toolName}`,
dropdownShown: (toolName) => `Menú por ${toolName} es visible`,
dropdownHidden: (toolName) => { return `Menú por ${toolName} fue ocultado`; },
Expand Down
2 changes: 1 addition & 1 deletion packages/js-draw/src/testing/firstElementAncestorOfNode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

/** Returns the first ancestor of the given node that is an HTMLElement */
/** Returns the first ancestor of the given node (or the node itself) that is an HTMLElement */
const firstElementAncestorOfNode = (node: Node|null): HTMLElement|null => {
if (node instanceof HTMLElement) {
return node;
Expand Down
8 changes: 7 additions & 1 deletion packages/js-draw/src/toolbar/IconProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,13 @@ export default class IconProvider {
`);
}

/** Unused. @deprecated */
public makeCopyIcon(): IconElemType {
return this.makeIconFromPath(`
M 45,10 45,55 90,55 90,10 45,10 z
M 10,25 10,90 70,90 70,60 40,60 40,25 10,25 z
`);
}

public makePasteIcon(): IconElemType {
const icon = this.makeIconFromPath(`
M 50 0 L 50 5 L 35 5 L 40 24.75 L 20 25 L 20 100 L 85 100 L 100 90 L 100 24 L 75.1 24.3 L 80 5 L 65 5 L 65 0 L 50 0 z
Expand Down
Loading