Skip to content

Commit

Permalink
Add multiselect initial implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Mikhail Aheichyk <[email protected]>
  • Loading branch information
Mikhail Aheichyk committed Jan 15, 2024
1 parent 2521106 commit d173a7a
Show file tree
Hide file tree
Showing 21 changed files with 617 additions and 94 deletions.
1 change: 1 addition & 0 deletions .env.local.default
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# For more options, see docs/configuration.md
REACT_APP_HOME_SERVER_URL=https://matrix-client.matrix.org
REACT_APP_MULTISELECT=false
3 changes: 3 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ REACT_APP_HOME_SERVER_URL=https://matrix-client.matrix.org

# External link to the documentation that will be shown in the help menu if defined.
REACT_APP_HELP_CENTER_URL="https://github.com/nordeck/matrix-neoboard"

# optional - enables multiselect
REACT_APP_MULTISELECT=true
```
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { LayoutStateProvider } from '../Layout';
import { SlidesProvider } from '../Layout/SlidesProvider';
import { ElementOverridesProvider } from './ElementOverridesProvider';
import { useElementOverride } from './useElementOverride';
import { useElementOverrides } from './useElementOverrides';
import { useSetElementOverride } from './useSetElementOverride';

let widgetApi: MockedWidgetApi;
Expand Down Expand Up @@ -82,6 +83,25 @@ describe('useElementCoordsState', () => {
});
});

it('should return the original elements', () => {
const elementIds = ['element-1'];
const { result } = renderHook(() => useElementOverrides(elementIds), {
wrapper: Wrapper,
});

expect(result.current).toEqual({
'element-1': {
type: 'shape',
kind: 'ellipse',
fillColor: '#ffffff',
text: 'Hello World',
position: { x: 0, y: 1 },
height: 100,
width: 50,
},
});
});

it('should replace the element position', () => {
const { result } = renderHook(
() => {
Expand Down Expand Up @@ -109,6 +129,36 @@ describe('useElementCoordsState', () => {
});
});

it('should replace the elements position', () => {
const elementIds = ['element-1'];
const { result } = renderHook(
() => {
const element = useElementOverrides(elementIds);
const setElementOverride = useSetElementOverride();
return { element, setElementOverride };
},
{ wrapper: Wrapper },
);

act(() => {
result.current.setElementOverride('element-1', {
position: { x: 50, y: 51 },
});
});

expect(result.current.element).toEqual({
'element-1': {
type: 'shape',
kind: 'ellipse',
fillColor: '#ffffff',
text: 'Hello World',
position: { x: 50, y: 51 },
height: 100,
width: 50,
},
});
});

it('should replace the element height and width', () => {
const { result } = renderHook(
() => {
Expand Down
1 change: 1 addition & 0 deletions src/components/ElementOverridesProvider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@

export { ElementOverridesProvider } from './ElementOverridesProvider';
export { useElementOverride } from './useElementOverride';
export { useElementOverrides } from './useElementOverrides';
export { useSetElementOverride } from './useSetElementOverride';
60 changes: 60 additions & 0 deletions src/components/ElementOverridesProvider/useElementOverrides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2023 Nordeck IT + Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { useContext, useMemo } from 'react';
import { useElements } from '../../state';
import { Elements } from '../../state/types';
import {
ElementOverride,
ElementOverrideGetterContext,
} from './ElementOverridesProvider';

export function useElementOverrides(elementIds: string[]): Elements {
const getElementOverride = useContext(ElementOverrideGetterContext);

if (!getElementOverride) {
throw new Error(
'useElementOverride can only be used inside of <ElementOverridesProvider>',
);
}

const elements: Elements = useElements(elementIds);

return useMemo(
() =>
Object.fromEntries(
Object.entries(elements).map(([elementId, element]) => {
const override: ElementOverride | undefined =
getElementOverride(elementId);
return [
elementId,
element.type === 'path'
? {
...element,
position: override?.position ?? element.position,
}
: {
...element,
height: override?.height ?? element.height,
width: override?.width ?? element.width,
position: override?.position ?? element.position,
},
];
}),
),
[elements, getElementOverride],
);
}
9 changes: 6 additions & 3 deletions src/components/Whiteboard/Element/ConnectedElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { useActiveElement } from '../../../state';
import { useActiveElements } from '../../../state';
import { useElementOverride } from '../../ElementOverridesProvider';
import EllipseDisplay from '../../elements/ellipse/Display';
import LineDisplay from '../../elements/line/Display';
Expand All @@ -29,9 +29,12 @@ export const ConnectedElement = ({
id: string;
readOnly?: boolean;
}) => {
const { activeElementId } = useActiveElement();
const { activeElementIds } = useActiveElements();
const element = useElementOverride(id);
const isActive = !readOnly && id ? activeElementId === id : false;
const isActive =
!readOnly && id
? activeElementIds.length === 1 && activeElementIds[0] === id
: false;
const otherProps = {
// TODO: Align names
active: isActive,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,41 +17,38 @@
import { Box } from '@mui/material';
import { clamp } from 'lodash';
import { PropsWithChildren } from 'react';
import {
calculateBoundingRectForPoints,
useSlideIsLocked,
} from '../../../../state';
import { useElementOverride } from '../../../ElementOverridesProvider';
import { useSlideIsLocked } from '../../../../state';
import { calculateBoundingRectForElements } from '../../../../state/crdt/documents/elements';
import { useElementOverrides } from '../../../ElementOverridesProvider';
import { useMeasure, useSvgCanvasContext } from '../../SvgCanvas';

export function ElementBarWrapper({
children,
elementId,
}: PropsWithChildren<{ elementId: string }>) {
elementIds,
}: PropsWithChildren<{ elementIds: string[] }>) {
const isLocked = useSlideIsLocked();
const element = useElementOverride(elementId);
const elements = Object.values(useElementOverrides(elementIds));
const [sizeRef, { width: elementBarWidth, height: elementBarHeight }] =
useMeasure<HTMLDivElement>();
const {
scale,
width: canvasWidth,
height: canvasHeight,
} = useSvgCanvasContext();
const width =
element?.type === 'path'
? calculateBoundingRectForPoints(element.points).width
: element?.width ?? 0;
const height =
element?.type === 'path'
? calculateBoundingRectForPoints(element.points).height
: element?.height ?? 0;
const {
offsetX: x,
offsetY: y,
width,
height,
} = calculateBoundingRectForElements(elements);

const offset = 10;

function calculateTopPosition() {
if (!element) {
if (elements.length === 0) {
return 0;
}
const position = element.position.y * scale;
const position = y * scale;
const positionAbove = position - elementBarHeight - offset;
const positionBelow = position + height * scale + offset;
const positionInElement = position + offset;
Expand All @@ -66,19 +63,18 @@ export function ElementBarWrapper({
}

function calculateLeftPosition() {
if (!element) {
if (elements.length === 0) {
return 0;
}

const position =
(element.position.x + width / 2) * scale - elementBarWidth / 2;
const position = (x + width / 2) * scale - elementBarWidth / 2;

return clamp(position, 0, canvasWidth - elementBarWidth);
}

return (
<>
{element && !isLocked && (
{elements.length !== 0 && !isLocked && (
<Box
ref={sizeRef}
position="absolute"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
*/

import { styled, useTheme } from '@mui/material';
import { calculateBoundingRectForPoints } from '../../../../state';
import { useElementOverride } from '../../../ElementOverridesProvider';
import { first } from 'lodash';
import { calculateBoundingRectForElements } from '../../../../state/crdt/documents/elements';
import { useElementOverrides } from '../../../ElementOverridesProvider';
import { useLayoutState } from '../../../Layout';
import { useSvgCanvasContext } from '../../SvgCanvas';

Expand Down Expand Up @@ -55,34 +56,32 @@ function SelectionAnchor({
}

export type ElementBorderProps = {
elementId: string;
elementIds: string[];
padding?: number;
};

export function ElementBorder({ elementId, padding = 1 }: ElementBorderProps) {
export function ElementBorder({ elementIds, padding = 1 }: ElementBorderProps) {
const theme = useTheme();
const { activeTool } = useLayoutState();
const isInSelectionMode = activeTool === 'select';
const { scale } = useSvgCanvasContext();
const element = useElementOverride(elementId);
const x = element?.position.x ?? 0;
const y = element?.position.y ?? 0;
const height =
element?.type === 'path'
? calculateBoundingRectForPoints(element.points).height
: element?.height ?? 0;
const width =
element?.type === 'path'
? calculateBoundingRectForPoints(element.points).width
: element?.width ?? 0;

const elements = Object.values(useElementOverrides(elementIds));
const {
offsetX: x,
offsetY: y,
width,
height,
} = calculateBoundingRectForElements(elements);

const scaledPadding = padding / scale;
const selectionBorderWidth = 2 / scale;
const selectionX = x - (selectionBorderWidth / 2 + scaledPadding);
const selectionY = y - (selectionBorderWidth / 2 + scaledPadding);
const selectionWidth = width + 2 * (selectionBorderWidth / 2 + scaledPadding);
const selectionHeight =
height + 2 * (selectionBorderWidth / 2 + scaledPadding);
const resizable = element?.type === 'shape';
const resizable = first(elements)?.type === 'shape'; //TODO: implement resize for multiple selected elements

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import { getEnvironment } from '@matrix-widget-toolkit/mui';
import { MouseEvent, PropsWithChildren } from 'react';
import { useWhiteboardSlideInstance } from '../../../../state';
import { useLayoutState } from '../../../Layout';
Expand All @@ -25,14 +26,30 @@ export function SelectableElement({
children,
elementId,
}: SelectableElementProps) {
const multiselect =
getEnvironment('REACT_APP_MULTISELECT', 'false') === 'true';

const slideInstance = useWhiteboardSlideInstance();
const { activeTool } = useLayoutState();
const isInSelectionMode = activeTool === 'select';

function handleMouseDown(event: MouseEvent) {
if (isInSelectionMode) {
event.stopPropagation();
slideInstance.setActiveElementId(elementId);

if (!multiselect) {
slideInstance.setActiveElementId(elementId);
} else {
if (!event.shiftKey) {
if (!slideInstance.getActiveElementIds().includes(elementId)) {
slideInstance.setActiveElementId(elementId);
}
} else if (slideInstance.getActiveElementIds().includes(elementId)) {
slideInstance.unselectActiveElementId(elementId);
} else {
slideInstance.addActiveElementId(elementId);
}
}
}
}

Expand Down
14 changes: 7 additions & 7 deletions src/components/Whiteboard/WhiteboardHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { Box } from '@mui/material';
import {
useActiveElement,
useActiveElements,
useIsWhiteboardLoading,
usePresentationMode,
useSlideElementIds,
Expand Down Expand Up @@ -54,7 +54,7 @@ const WhiteboardHost = ({
}) => {
const slideInstance = useWhiteboardSlideInstance();
const { isShowCollaboratorsCursors } = useLayoutState();
const { activeElementId } = useActiveElement();
const { activeElementIds } = useActiveElements();

return (
<Box
Expand All @@ -75,8 +75,8 @@ const WhiteboardHost = ({
}}
additionalChildren={
!readOnly &&
activeElementId && (
<ElementBarWrapper elementId={activeElementId}>
activeElementIds.length > 0 && (
<ElementBarWrapper elementIds={activeElementIds}>
<ElementBar />
</ElementBarWrapper>
)
Expand All @@ -93,10 +93,10 @@ const WhiteboardHost = ({

{!readOnly && <DraftPicker />}

{!readOnly && activeElementId && (
{!readOnly && activeElementIds.length > 0 && (
<>
<ElementBorder elementId={activeElementId} />
<ResizeElement elementId={activeElementId} />
<ElementBorder elementIds={activeElementIds} />
<ResizeElement elementId={activeElementIds[0]} />
</>
)}

Expand Down
Loading

0 comments on commit d173a7a

Please sign in to comment.