Skip to content

Commit

Permalink
Move scrolling logic to editor and add automatic canvas overflow support
Browse files Browse the repository at this point in the history
  • Loading branch information
matthiaslehnertum committed Sep 30, 2023
1 parent 89f1c6a commit 0279eb9
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 75 deletions.
61 changes: 5 additions & 56 deletions src/main/components/canvas/canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,81 +12,34 @@ import { UMLElementState } from '../../services/uml-element/uml-element-types';
import { UMLRelationship } from '../../services/uml-relationship/uml-relationship';
import { EditorRepository } from '../../services/editor/editor-repository';

const MIN_SCALE: number = 0.5;
const MAX_SCALE: number = 5.0;

type OwnProps = {};

type StateProps = {
diagram: IUMLDiagram;
isStatic: boolean;
elements: UMLElementState;
zoomFactor: number;
};

type DispatchProps = {
changeZoomFactor: typeof EditorRepository.changeZoomFactor;
};
type DispatchProps = {};

type Props = OwnProps & StateProps & DispatchProps;

const initialState = Object.freeze({
gestureStartZoomFactor: 1.0 as number,
});

type State = typeof initialState;

const enhance = connect<StateProps, DispatchProps, OwnProps, ModelState>(
(state) => ({
diagram: state.diagram,
isStatic: state.editor.readonly,
elements: state.elements,
zoomFactor: state.editor.zoomFactor,
elements: state.elements
}),
{
changeZoomFactor: EditorRepository.changeZoomFactor,
},
null,
null,
{ forwardRef: true },
);

export class CanvasComponent extends Component<Props, State> implements Omit<ILayer, 'layer'> {
state = initialState;
export class CanvasComponent extends Component<Props> implements Omit<ILayer, 'layer'> {

layer: RefObject<SVGSVGElement> = createRef();

componentDidMount() {
const { zoomFactor = 1 } = this.props;

window.addEventListener('wheel', (event) => {
event.preventDefault();

if (event.ctrlKey) {
this.props.changeZoomFactor(this.clamp(zoomFactor - event.deltaY * 0.01, MIN_SCALE, MAX_SCALE));
}
});

window.addEventListener('gesturestart', (event) => {
event.preventDefault();

this.setState({
...this.state,
gestureStartZoomFactor: zoomFactor,
});
});

window.addEventListener('gesturechange', (event) => {
event.preventDefault();
this.props.changeZoomFactor(
this.clamp(this.state.gestureStartZoomFactor * (event as any).scale, MIN_SCALE, MAX_SCALE),
);
});

window.addEventListener('gestureend', function (event) {
event.preventDefault();
});
}

origin = (): Point => {
if (!this.layer.current) {
return new Point();
Expand All @@ -102,12 +55,8 @@ export class CanvasComponent extends Component<Props, State> implements Omit<ILa
return point.subtract(origin).round().add(origin);
};

clamp = (value: number, min: number, max: number): number => {
return Math.max(min, Math.min(value, max));
};

render() {
const { elements, diagram, isStatic, zoomFactor } = this.props;
const { elements, diagram, isStatic } = this.props;

let minX = 0;
let minY = 0;
Expand Down
83 changes: 69 additions & 14 deletions src/main/components/canvas/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ import { ModelState } from '../store/model-state';
import isMobile from 'is-mobile';
import { UMLElementRepository } from '../../services/uml-element/uml-element-repository';
import { AsyncDispatch } from '../../utils/actions/actions';
import {EditorRepository} from '../../services/editor/editor-repository';

const grid = 10;
const subdivisions = 5;
const borderWidth = 1;
const minScale: number = 0.5;
const maxScale: number = 5.0;

const grid: number = 10;
const subdivisions: number = 5;
const borderWidth: number = 1;

const StyledEditor = styled.div<{ zoomFactor?: number }>`
display: block;
width: 100%;
width: ${100 / minScale}%;
height: ${100 / minScale}%;
position: relative;
min-height: inherit;
max-height: inherit;
Expand Down Expand Up @@ -43,7 +48,10 @@ type OwnProps = { children: ReactNode };

type StateProps = { moving: string[]; connecting: boolean; reconnecting: boolean; zoomFactor: number };

type DispatchProps = { move: AsyncDispatch<typeof UMLElementRepository.move> };
type DispatchProps = {
move: AsyncDispatch<typeof UMLElementRepository.move>;
changeZoomFactor: typeof EditorRepository.changeZoomFactor;
};

const enhance = connect<StateProps, DispatchProps, OwnProps, ModelState>(
(state) => ({
Expand All @@ -54,6 +62,7 @@ const enhance = connect<StateProps, DispatchProps, OwnProps, ModelState>(
}),
{
move: UMLElementRepository.move,
changeZoomFactor: EditorRepository.changeZoomFactor,
},
);

Expand All @@ -62,6 +71,7 @@ type Props = OwnProps & StateProps & DispatchProps;
const getInitialState = () => {
return {
scrollingDisabled: false,
gestureStartZoomFactor: 1.0 as number,
isMobile: isMobile({ tablet: true }),
};
};
Expand All @@ -75,6 +85,39 @@ class EditorComponent extends Component<Props, State> {
state = getInitialState();
editor = createRef<HTMLDivElement>();

componentDidMount() {

const {zoomFactor = 1} = this.props;

window.addEventListener('wheel', (event) => {
event.preventDefault();

if (event.ctrlKey) {
this.props.changeZoomFactor(this.clamp(zoomFactor - event.deltaY * 0.01, minScale, maxScale));
}
});

window.addEventListener('gesturestart', (event) => {
event.preventDefault();

this.setState({
...this.state,
gestureStartZoomFactor: zoomFactor,
});
});

window.addEventListener('gesturechange', (event) => {
event.preventDefault();
this.props.changeZoomFactor(
this.clamp(this.state.gestureStartZoomFactor * (event as any).scale, minScale, maxScale),
);
});

window.addEventListener('gestureend', function (event) {
event.preventDefault();
});
}

componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>, snapshot?: any) {
if (this.state.isMobile) {
if (this.editor.current) {
Expand All @@ -91,34 +134,46 @@ class EditorComponent extends Component<Props, State> {
}

render() {
const { moving, connecting, reconnecting, zoomFactor, ...props } = this.props;

console.log('editor', zoomFactor);
const { moving, connecting, reconnecting, zoomFactor = 1.0, ...props } = this.props;

if (this.state.isMobile) {
return <StyledEditor ref={this.editor} {...props} onTouchMove={this.customScrolling} zoomFactor={zoomFactor} />;

return (
<StyledEditor ref={this.editor} {...props} onTouchMove={this.customScrolling} zoomFactor={zoomFactor} />
);
} else {
return <StyledEditor {...props} zoomFactor={zoomFactor} />;
return (
<div style={{height: '100%', width: '100%', overflow: 'auto'}}>
<StyledEditor {...props} zoomFactor={zoomFactor} />
</div>
);
}
}

clamp = (value: number, min: number, max: number): number => {
return Math.max(min, Math.min(value, max));
};

customScrolling = (event: React.TouchEvent) => {

const {zoomFactor = 1} = this.props;

if (this.editor.current) {
const clientRect = this.editor.current.getBoundingClientRect();

const touch = event.touches[event.touches.length - 1];

// scroll when on the edge of the element
const scrollHorizontally =
touch.clientX < clientRect.x + SCROLL_BORDER
(touch.clientX * zoomFactor) < clientRect.x + SCROLL_BORDER
? -SCROLL_DISTANCE
: touch.clientX > clientRect.x + clientRect.width - SCROLL_BORDER
: (touch.clientX * zoomFactor) > clientRect.x + clientRect.width - SCROLL_BORDER
? SCROLL_DISTANCE
: 0;
const scrollVertically =
touch.clientY < clientRect.y + SCROLL_BORDER
(touch.clientY * zoomFactor) < clientRect.y + SCROLL_BORDER
? -SCROLL_DISTANCE
: touch.clientY > clientRect.y + clientRect.height - SCROLL_BORDER
: (touch.clientY * zoomFactor)> clientRect.y + clientRect.height - SCROLL_BORDER
? SCROLL_DISTANCE
: 0;
this.editor.current.scrollBy(scrollHorizontally, scrollVertically);
Expand Down
8 changes: 4 additions & 4 deletions src/main/components/connectable/connection-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ class Preview extends Component<Props, State> {

let position: Point;
if (event instanceof PointerEvent) {
position = new Point(event.clientX / zoomFactor - offset.x, event.clientY / zoomFactor - offset.y);
position = new Point(event.clientX - offset.x, event.clientY - offset.y).scale(1 / zoomFactor);
} else {
position = new Point(
event.targetTouches[0].clientX / zoomFactor - offset.x,
event.targetTouches[0].clientY / zoomFactor - offset.y,
);
event.targetTouches[0].clientX - offset.x,
event.targetTouches[0].clientY - offset.y,
).scale(1 / zoomFactor);
}

this.setState({ position });
Expand Down
5 changes: 4 additions & 1 deletion src/main/components/uml-element/movable/movable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,14 @@ export const movable = (
moveWindow: { x: number; y: number } = { x: 0, y: 0 };

move = (x: number, y: number) => {

const {zoomFactor = 1} = this.props;

x = Math.round(x / 10) * 10;
y = Math.round(y / 10) * 10;
if (x === 0 && y === 0) return;

this.setState((state) => ({ offset: state.offset.add(x * this.props.zoomFactor, y * this.props.zoomFactor) }));
this.setState((state) => ({ offset: state.offset.add(x * zoomFactor, y * zoomFactor) }));
this.moveWindow = { x: this.moveWindow.x + x, y: this.moveWindow.y + y };
this.debouncedMove(this.moveWindow);
};
Expand Down

0 comments on commit 0279eb9

Please sign in to comment.