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
11 changes: 4 additions & 7 deletions client/src/components/tapestry-elements/multiselection/index.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
import { MultiselectMenu } from '../multiselect-menu'
import { useTapestryData } from '../../../pages/tapestry/tapestry-providers'
import { getSelectionItems, isSingleGroupSelected } from 'tapestry-core-client/src/view-model/utils'
import { getSelectionItems } from 'tapestry-core-client/src/view-model/utils'
import { useSingleGroupSelection } from 'tapestry-core-client/src/components/lib/hooks/use-single-group-selection'
import { Multiselection as BaseMultiselection } from 'tapestry-core-client/src/components/tapestry/multiselection'
import { getMultiselectRectangle } from '../../../pages/tapestry/view-model/utils'
import { ResizeHandles } from '../resize-handles'
import { EditableGroupViewModel } from '../../../pages/tapestry/view-model'

export function Multiselection() {
const { items, selection, interactionMode, selectionResizeState, interactiveElement, groups } =
const { items, selection, interactionMode, selectionResizeState, interactiveElement } =
useTapestryData([
'items',
'selection',
'interactionMode',
'selectionResizeState',
'interactiveElement',
'groups',
])
const selectionItems = getSelectionItems({ items, selection })
const selectionBounds = getMultiselectRectangle(selectionItems, selectionResizeState)
const isEditMode = interactionMode === 'edit'

let selectedGroup: EditableGroupViewModel | undefined
if (isSingleGroupSelected(selection)) {
selectedGroup = groups[[...selection.groupIds][0]]
}
const selectedGroup = useSingleGroupSelection<EditableGroupViewModel>()

return (
<BaseMultiselection
Expand Down
10 changes: 6 additions & 4 deletions client/src/pages/tapestry/tapestry-components.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { PropsWithStyle } from 'tapestry-core-client/src/components/lib'
import { usePresentationShortcuts } from 'tapestry-core-client/src/components/lib/hooks/use-presentation-shortcuts'
import { TapestryCanvas } from 'tapestry-core-client/src/components/tapestry/tapestry-canvas'
import { useTapestryData } from './tapestry-providers'
import { PropsWithStyle } from 'tapestry-core-client/src/components/lib'

export function TapestryEditorCanvas({ className }: PropsWithStyle) {
const interactionMode = useTapestryData('interactionMode')
const isView = interactionMode === 'view'

usePresentationShortcuts(isView)

return (
<TapestryCanvas classes={{ root: className }} orderByPosition={interactionMode === 'view'} />
)
return <TapestryCanvas classes={{ root: className }} orderByPosition={isView} />
}
62 changes: 62 additions & 0 deletions core-client/src/components/lib/hooks/use-presentation-shortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useKeyboardShortcuts } from './use-keyboard-shortcuts'
import { useTapestryConfig } from '../../tapestry'
import { getAdjacentPresentationSteps } from '../../../view-model/utils'
import { focusPresentationStep } from '../../../view-model/store-commands/viewport'
import { useSingleGroupSelection } from './use-single-group-selection'
import { getPresentationSequence } from 'tapestry-core/src/utils'
import { mapValues } from 'lodash'

export function usePresentationShortcuts(enable = true) {
const { useStoreData, useDispatch } = useTapestryConfig()

const dispatch = useDispatch()
const presentationSteps = useStoreData('presentationSteps')
const selectedGroupId = useSingleGroupSelection()?.dto.id
const interactiveId = useStoreData('interactiveElement.modelId') ?? selectedGroupId
const adjacentPresentationSteps = interactiveId
? getAdjacentPresentationSteps(interactiveId, presentationSteps)
: undefined

useKeyboardShortcuts(
enable
? {
'ArrowRight | PageDown | ArrowLeft | PageUp': (e) => {
if (!adjacentPresentationSteps) {
return
}
const isNext = e.code === 'ArrowRight' || e.code === 'PageDown'
const presentation =
isNext && adjacentPresentationSteps.next
? 'next'
: !isNext && adjacentPresentationSteps.prev
? 'prev'
: null

if (presentation) {
dispatch(
focusPresentationStep(adjacentPresentationSteps[presentation]!.dto, {
zoomEffect: 'bounce',
duration: 1,
}),
)
}
},

'Home | End': (e) => {
const sequence = getPresentationSequence(mapValues(presentationSteps, (vm) => vm?.dto))

const step =
e.code === 'Home' && adjacentPresentationSteps?.prev
? sequence[0]
: e.code === 'End' && adjacentPresentationSteps?.next
? sequence[sequence.length - 1]
: undefined

if (step) {
dispatch(focusPresentationStep(step))
}
},
}
: {},
)
}
14 changes: 14 additions & 0 deletions core-client/src/components/lib/hooks/use-single-group-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { GroupViewModel } from '../../../view-model'
import { isSingleGroupSelected } from '../../../view-model/utils'
import { useTapestryConfig } from '../../tapestry'

export function useSingleGroupSelection<T extends GroupViewModel = GroupViewModel>():
| T
| undefined {
const { useStoreData } = useTapestryConfig()
const { selection, groups } = useStoreData(['selection', 'groups'])

if (isSingleGroupSelected(selection)) {
return groups[[...selection.groupIds][0]] as T
}
}
11 changes: 4 additions & 7 deletions core-client/src/components/tapestry/hooks/use-item-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,10 @@ export function useItemMenu<const M extends string>(
const focusElement = useFocusElement()
const [displayInfo, setDisplayInfo] = useState(false)

useKeyboardShortcuts(
{
...(menu.includes('info') ? { 'meta + KeyI': showInfo } : {}),
Escape: () => dispatch(deselectAll()),
},
[],
)
useKeyboardShortcuts({
...(menu.includes('info') ? { 'meta + KeyI': showInfo } : {}),
Escape: () => dispatch(deselectAll()),
})

function showInfo() {
setDisplayInfo(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,30 +49,27 @@ export function useZoomToolbarItems<
const focusElement = useFocusElement()

const store = useStore()
useKeyboardShortcuts(
{
...(menu.includes('zoom-out') ? { Minus: (e) => store.dispatch(zoomOut(e.repeat)) } : {}),
...(menu.includes('zoom-in') ? { Equal: (e) => store.dispatch(zoomIn(e.repeat)) } : {}),
...(menu.includes('zoom-to-fit')
? {
KeyF: () => {
// Do not call this handler if there is an active element, since its focus button should be triggered.
// TODO: Find a better way to handle this (perhaps using the capture phase only in the item's toolbar)
if (
store.get('interactiveElement') ||
isMultiselection(store.get('selection')) ||
!hasItems
) {
return
}

focusElement('all')
},
}
: {}),
},
[],
)
useKeyboardShortcuts({
...(menu.includes('zoom-out') ? { Minus: (e) => store.dispatch(zoomOut(e.repeat)) } : {}),
...(menu.includes('zoom-in') ? { Equal: (e) => store.dispatch(zoomIn(e.repeat)) } : {}),
...(menu.includes('zoom-to-fit')
? {
KeyF: () => {
// Do not call this handler if there is an active element, since its focus button should be triggered.
// TODO: Find a better way to handle this (perhaps using the capture phase only in the item's toolbar)
if (
store.get('interactiveElement') ||
isMultiselection(store.get('selection')) ||
!hasItems
) {
return
}

focusElement('all')
},
}
: {}),
})

const [selectedSubmenu, selectSubmenu, closeSubmenu] = useSingleChoice<MoreSubmenu>()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { MaybeMenuItem } from '../../../lib/toolbar/index'
import { Rectangle } from 'tapestry-core/src/lib/geometry'
import { useItemMenu } from '../../hooks/use-item-menu'
import { ElementToolbar, ElementToolbarProps } from '../../element-toolbar'
import { useTapestryConfig } from '../..'
import { MaybeMenuItem } from '../../../lib/toolbar/index'
import { ElementToolbar, ElementToolbarProps } from '../../element-toolbar'
import { useItemMenu } from '../../hooks/use-item-menu'
import { ACTIVE_ITEM_BORDER_WIDTH } from '../tapestry-item'

export type ItemToolbarProps = Omit<ElementToolbarProps, 'isOpen' | 'items' | 'elementBounds'> & {
Expand Down
10 changes: 3 additions & 7 deletions core-client/src/components/tapestry/multiselection/default.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import { Multiselection } from '.'
import { useTapestryConfig } from '..'
import { GroupViewModel } from '../../../view-model'
import {
getBoundingRectangle,
getSelectionItems,
isSingleGroupSelected,
MULTISELECT_RECTANGLE_PADDING,
} from '../../../view-model/utils'
import { useMultiselectMenu } from '../../lib/hooks/use-multiselect-menu'
import { useSingleGroupSelection } from '../../lib/hooks/use-single-group-selection'
import { ElementToolbar } from '../element-toolbar'

export function DefaultMultiselection() {
const { useStoreData } = useTapestryConfig()
const { items, selection, groups } = useStoreData(['items', 'selection', 'groups'])
const { items, selection } = useStoreData(['items', 'selection'])
const selectionItems = getSelectionItems({ items, selection })
const selectionBounds = getBoundingRectangle(selectionItems).expand(MULTISELECT_RECTANGLE_PADDING)
const selectedGroup = useSingleGroupSelection()

let selectedGroup: GroupViewModel | undefined
if (isSingleGroupSelected(selection)) {
selectedGroup = groups[[...selection.groupIds][0]]
}
const toolbar = useMultiselectMenu(
selectedGroup ? ['focus', 'separator', 'presentation'] : ['focus'],
selectedGroup?.dto.id,
Expand Down
13 changes: 4 additions & 9 deletions core-client/src/components/tapestry/multiselection/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import styles from './styles.module.css'
import { computeRestrictedScale, isSingleGroupSelected } from '../../../view-model/utils'
import { computeRestrictedScale } from '../../../view-model/utils'
import clsx from 'clsx'
import { idMapToArray } from 'tapestry-core/src/utils'
import { DOM_CONTAINER_CLASS } from '../../../stage/utils'
import { Rectangle } from 'tapestry-core/src/lib/geometry'
import { PropsWithChildren, ReactNode } from 'react'
import { GroupViewModel } from '../../../view-model'
import { PropsWithStyle } from '../../lib'
import { useTapestryConfig } from '..'
import { useSingleGroupSelection } from '../../lib/hooks/use-single-group-selection'

export interface MultiselectionProps extends PropsWithStyle<PropsWithChildren> {
bounds: Rectangle
Expand All @@ -16,18 +16,13 @@ export interface MultiselectionProps extends PropsWithStyle<PropsWithChildren> {

export function Multiselection({ bounds, halo, style, className, children }: MultiselectionProps) {
const { useStoreData } = useTapestryConfig()
const { items, selection, interactiveElement, groups, viewport } = useStoreData([
const { items, interactiveElement, viewport } = useStoreData([
'items',
'selection',
'interactiveElement',
'groups',
'viewport',
])

let selectedGroup: GroupViewModel | undefined
if (isSingleGroupSelected(selection)) {
selectedGroup = groups[[...selection.groupIds][0]]
}
const selectedGroup = useSingleGroupSelection()

const { top, left, width, height } = bounds

Expand Down
2 changes: 2 additions & 0 deletions viewer/src/components/tapestry/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useRef } from 'react'

import { useFocusedElement } from 'tapestry-core-client/src/components/tapestry/hooks/use-focus-element'
import { usePresentationShortcuts } from 'tapestry-core-client/src/components/lib/hooks/use-presentation-shortcuts'
import { useStageInit } from 'tapestry-core-client/src/components/tapestry/hooks/use-stage-init'
import { TapestryCanvas } from 'tapestry-core-client/src/components/tapestry/tapestry-canvas'
import { ViewportScrollbars } from 'tapestry-core-client/src/components/tapestry/viewport-scrollbars'
Expand Down Expand Up @@ -78,6 +79,7 @@ export function Tapestry({ onBack }: TapestryProps) {
})

useFocusedElement()
usePresentationShortcuts()

return (
<div ref={sceneRef} className="scene-container">
Expand Down