Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Always display validation state for an entities and relations in case when the target does not have any authoring changes.
- Display elliptical authoring state overlays for elliptically-shaped entity elements.
- Use provided `duration` in `CanvasApi.animateGraph()` for element transitions without the need to override the styles.
- Trigger `keydown`, `keyup`, `scroll` and `contextMenu` canvas events only from a non-widget layer.

#### ⏱ Performance
- Optimize diagram loading time by avoiding unnecessary updates and separating a measuring element sizes step from applying the sizes to the rendering state.
Expand All @@ -42,6 +43,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
* `PLACEHOLDER_LINK_TYPE` -> `PlaceholderRelationType`;
- Support the ability to expand up the `Dropdown`, `DropdownMenu` and `Toolbar` by setting `direction` to `"up"` e.g. for docking the toolbar to the bottom of the viewport.
- Allow to return `iconMonochrome: true` for a type style to automatically apply dark theme filter for the icon.
- Add keyboard shortcuts for `Selection` (`Ctrl+A`: select all), `ToolbarActionUndo` (`Ctrl+Z`), `ToolbarActionRedo` (`Ctrl+Shift+Z`), `SelectionActionRemove` (`Delete`, same as before), `SelectionActionGroup` (`G`).
- Support optional dependency list in `useEventStore()` to re-subscribe to store if needed.

#### 🔧 Maintenance
Expand Down
22 changes: 22 additions & 0 deletions src/diagram/paperArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -857,10 +857,16 @@ export class PaperArea extends React.Component<PaperAreaProps, State> implements
};

private onScroll = (e: Event) => {
if (!this.isEventFromCellLayer(e)) {
return;
}
this.source.trigger('scroll', {source: this, sourceEvent: e});
};

private onContextMenu = (e: React.MouseEvent, cell: Cell | undefined) => {
if (!this.isEventFromCellLayer(e)) {
return;
}
this.source.trigger('contextMenu', {
source: this,
sourceEvent: e,
Expand All @@ -869,13 +875,29 @@ export class PaperArea extends React.Component<PaperAreaProps, State> implements
};

private onKeyDown = (e: React.KeyboardEvent) => {
if (!this.isEventFromCellLayer(e)) {
return;
}
this.source.trigger('keydown', {source: this, sourceEvent: e});
};

private onKeyUp = (e: React.KeyboardEvent) => {
if (!this.isEventFromCellLayer(e)) {
return;
}
this.source.trigger('keyup', {source: this, sourceEvent: e});
};

private isEventFromCellLayer(e: Event | React.SyntheticEvent): boolean {
const target = e.target;
return target instanceof Node && Boolean(
this.rootRef.current === target ||
this.linkLayerRef.current?.contains(target) ||
this.labelLayerRef.current?.contains(target) ||
this.elementLayerRef.current?.contains(target)
);
}

private makeToSVGOptions(baseOptions: ExportSvgOptions): ToSVGOptions {
const {colorSchemeApi} = this.props;
const {
Expand Down
13 changes: 13 additions & 0 deletions src/widgets/selection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ const CLASS_NAME = 'reactodia-selection';
/**
* Canvas widget component for rectangular element selection on the diagram.
*
* When mounted, handles the following keyboard shortcuts:
* - `Ctrl+A` / `⌘+A`: select all canvas elements.
*
* @category Components
*/
export function Selection(props: SelectionProps) {
Expand Down Expand Up @@ -120,6 +123,16 @@ export function Selection(props: SelectionProps) {
}
origin = undefined;
});
listener.listen(canvas.events, 'keydown', e => {
if (
e.sourceEvent.key === 'a' &&
(e.sourceEvent.ctrlKey || e.sourceEvent.metaKey) &&
!e.sourceEvent.altKey
) {
e.sourceEvent.preventDefault();
model.setSelection([...model.elements]);
}
});
return () => {
listener.stopListening();
moveListener?.stopListening();
Expand Down
67 changes: 49 additions & 18 deletions src/widgets/selectionAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ export interface SelectionActionRemoveProps extends SelectionActionStyleProps {}
*
* Removing the elements adds a command to the command history.
*
* When mounted, handles the following keyboard shortcuts:
* - `Delete`: remove all currently selected elements from the canvas.
*
* @category Components
*/
export function SelectionActionRemove(props: SelectionActionRemoveProps) {
Expand All @@ -198,8 +201,9 @@ export function SelectionActionRemove(props: SelectionActionRemoveProps) {
listener.listen(canvas.events, 'keyup', e => {
if (
e.sourceEvent.key === 'Delete' &&
document.activeElement &&
document.activeElement.localName !== 'input'
!e.sourceEvent.ctrlKey &&
!e.sourceEvent.altKey &&
!e.sourceEvent.shiftKey
) {
editor.removeSelectedElements();
canvas.focus();
Expand All @@ -209,6 +213,7 @@ export function SelectionActionRemove(props: SelectionActionRemoveProps) {
}, []);

const singleNewEntity = newEntities === 1 && totalEntities === 1;
const shortcut = ' (Delete)';
return (
<SelectionAction {...otherProps}
className={cx(
Expand All @@ -217,9 +222,9 @@ export function SelectionActionRemove(props: SelectionActionRemoveProps) {
)}
title={
title ? title :
singleNewEntity ? t.text('selection_action.remove.title_new') :
elements.length === 1 ? t.text('selection_action.remove.title_single') :
t.text('selection_action.remove.title')
singleNewEntity ? t.text('selection_action.remove.title_new') + shortcut :
elements.length === 1 ? t.text('selection_action.remove.title_single') + shortcut :
t.text('selection_action.remove.title') + shortcut
}
onSelect={() => editor.removeSelectedElements()}
/>
Expand Down Expand Up @@ -564,6 +569,9 @@ export interface SelectionActionGroupProps extends SelectionActionStyleProps {}
*
* Grouping or ungrouping the elements adds a command to the command history.
*
* When mounted, handles the following keyboard shortcuts:
* - `G`: group or ungroup selected elements.
*
* @category Components
*/
export function SelectionActionGroup(props: SelectionActionGroupProps) {
Expand All @@ -577,10 +585,43 @@ export function SelectionActionGroup(props: SelectionActionGroupProps) {
const canGroup = elements.length > 0 && elements.every(element => element instanceof EntityElement);
const canUngroup = elements.length > 0 && elements.every(element => element instanceof EntityGroup);

const onSelect = async () => {
if (canGroup) {
const group = await groupEntitiesAnimated(elements, canvas, workspace);
model.setSelection([group]);
group.focus();
} else if (canUngroup) {
const ungrouped = await ungroupAllEntitiesAnimated(elements, canvas, workspace);
model.setSelection(ungrouped);
canvas.focus();
}
};

const latestOnSelect = React.useRef<typeof onSelect>();
React.useEffect(() => {
latestOnSelect.current = onSelect;
});
React.useEffect(() => {
const listener = new EventObserver();
listener.listen(canvas.events, 'keydown', e => {
if (
e.sourceEvent.key === 'g' &&
!e.sourceEvent.ctrlKey &&
!e.sourceEvent.metaKey &&
!e.sourceEvent.altKey
) {
e.sourceEvent.preventDefault();
latestOnSelect.current?.();
}
});
return () => listener.stopListening();
}, []);

if (elements.length === 0 || elements.length === 1 && canGroup) {
return null;
}

const shortcut = ' (G)';
return (
<SelectionAction {...otherProps}
className={cx(
Expand All @@ -590,20 +631,10 @@ export function SelectionActionGroup(props: SelectionActionGroupProps) {
disabled={!(canGroup || canUngroup)}
title={title ?? (
canUngroup
? t.text('selection_action.group.title_ungroup')
: t.text('selection_action.group.title')
? t.text('selection_action.group.title_ungroup') + shortcut
: t.text('selection_action.group.title') + shortcut
)}
onMouseDown={async () => {
if (canGroup) {
const group = await groupEntitiesAnimated(elements, canvas, workspace);
model.setSelection([group]);
group.focus();
} else if (canUngroup) {
const ungrouped = await ungroupAllEntitiesAnimated(elements, canvas, workspace);
model.setSelection(ungrouped);
canvas.focus();
}
}}
onSelect={onSelect}
/>
);
}
Expand Down
55 changes: 49 additions & 6 deletions src/widgets/toolbarAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AuthoringState } from '../editor/authoringState';
import { DropdownMenuItem, useInsideDropdown } from './utility/dropdown';

import { useWorkspace } from '../workspace/workspaceContext';
import { EventObserver } from '../workspace';

const CLASS_NAME = 'reactodia-toolbar-action';

Expand Down Expand Up @@ -342,11 +343,14 @@ export interface ToolbarActionUndoProps extends Omit<ToolbarActionStyleProps, 'd
/**
* Toolbar action component to undo a command from the command history.
*
* When mounted, handles the following keyboard shortcuts:
* - `Ctrl+Z` / `⌘+Z`: perform the undo action.
*
* @category Components
*/
export function ToolbarActionUndo(props: ToolbarActionUndoProps) {
const {className, title, ...otherProps} = props;
const {model: {history}} = useCanvas();
const {canvas, model: {history}} = useCanvas();
const t = useTranslation();
const insideDropdown = useInsideDropdown();
const undoCommand = useObservedProperty(
Expand All @@ -358,15 +362,33 @@ export function ToolbarActionUndo(props: ToolbarActionUndoProps) {
? undefined : undoStack[undoStack.length - 1];
}
);
React.useEffect(() => {
const listener = new EventObserver();
listener.listen(canvas.events, 'keydown', e => {
if (
e.sourceEvent.key === 'z' &&
(e.sourceEvent.ctrlKey || e.sourceEvent.metaKey) &&
!e.sourceEvent.altKey
) {
e.sourceEvent.preventDefault();
history.undo();
}
});
return () => listener.stopListening();
}, [history]);
const commandTitle = !title && undoCommand ? resolveCommandTitle(undoCommand, t) : undefined;
const shortcut = ' (Ctrl+Z / ⌘+Z)';
return (
<ToolbarAction {...otherProps}
className={cx(className, `${CLASS_NAME}__undo`)}
disabled={!undoCommand}
title={title ?? (
commandTitle === undefined
? t.text('toolbar_action.undo.title')
: t.text('toolbar_action.undo.title_named', {command: commandTitle})
? t.text('toolbar_action.undo.title') + shortcut
: t.text(
'toolbar_action.undo.title_named',
{command: commandTitle}
) + shortcut
)}
onSelect={() => history.undo()}>
{insideDropdown ? t.text('toolbar_action.undo.label') : null}
Expand All @@ -384,11 +406,14 @@ export interface ToolbarActionRedoProps extends Omit<ToolbarActionStyleProps, 'd
/**
* Toolbar action component to redo a command from the command history.
*
* When mounted, handles the following keyboard shortcuts:
* - `Ctrl+Shift+Z` / `⌘+Shift+Z`: perform the redo action.
*
* @category Components
*/
export function ToolbarActionRedo(props: ToolbarActionRedoProps) {
const {className, title, ...otherProps} = props;
const {model: {history}} = useCanvas();
const {canvas, model: {history}} = useCanvas();
const t = useTranslation();
const insideDropdown = useInsideDropdown();
const redoCommand = useObservedProperty(
Expand All @@ -400,15 +425,33 @@ export function ToolbarActionRedo(props: ToolbarActionRedoProps) {
? undefined : redoStack[redoStack.length - 1];
}
);
React.useEffect(() => {
const listener = new EventObserver();
listener.listen(canvas.events, 'keydown', e => {
if (
e.sourceEvent.key === 'Z' &&
(e.sourceEvent.ctrlKey || e.sourceEvent.metaKey) &&
!e.sourceEvent.altKey
) {
e.sourceEvent.preventDefault();
history.redo();
}
});
return () => listener.stopListening();
}, [history]);
const commandTitle = !title && redoCommand ? resolveCommandTitle(redoCommand, t) : undefined;
const shortcut = ' (Ctrl+Shift+Z / ⌘+Shift+Z)';
return (
<ToolbarAction {...otherProps}
className={cx(className, `${CLASS_NAME}__redo`)}
disabled={!redoCommand}
title={title ?? (
commandTitle === undefined
? t.text('toolbar_action.redo.title')
: t.text('toolbar_action.redo.title_named', {command: commandTitle})
? t.text('toolbar_action.redo.title') + shortcut
: t.text(
'toolbar_action.redo.title_named',
{command: commandTitle}
) + shortcut
)}
onSelect={() => history.redo()}>
{insideDropdown ? t.text('toolbar_action.redo.label') : null}
Expand Down