diff --git a/packages/kanban/src/components/Kanban.svelte b/packages/kanban/src/components/Kanban.svelte index 8e04fb415a..159c308129 100644 --- a/packages/kanban/src/components/Kanban.svelte +++ b/packages/kanban/src/components/Kanban.svelte @@ -210,7 +210,7 @@ } export function select (offset: 1 | -1 | 0, of?: Doc, dir?: 'vertical' | 'horizontal'): void { - let pos = (of !== undefined ? objects.findIndex((it) => it._id === of._id) : selection) ?? -1 + let pos = (of != null ? objects.findIndex((it) => it._id === of._id) : selection) ?? -1 if (pos === -1) { for (const st of categories) { const stateObjs = getGroupByValues(groupByDocs, st) ?? [] @@ -237,7 +237,7 @@ if (objState === -1) { return } - const stateObjs = getGroupByValues(groupByDocs, categories.indexOf(objState)) ?? [] + const stateObjs = getGroupByValues(groupByDocs, categories[objState]) ?? [] const statePos = stateObjs.findIndex((it) => it._id === obj._id) if (statePos === undefined) { return diff --git a/packages/kanban/src/components/KanbanRow.svelte b/packages/kanban/src/components/KanbanRow.svelte index c01b59e8a6..d5fda1741a 100644 --- a/packages/kanban/src/components/KanbanRow.svelte +++ b/packages/kanban/src/components/KanbanRow.svelte @@ -14,7 +14,7 @@ --> <script lang="ts"> import { CategoryType, Doc, Ref } from '@hcengineering/core' - import ui, { Button, IconMoreH } from '@hcengineering/ui' + import ui, { Button, IconMoreH, mouseAttractor } from '@hcengineering/ui' import { createEventDispatcher } from 'svelte' import { slide } from 'svelte/transition' import { CardDragEvent, Item } from '../types' @@ -69,7 +69,7 @@ class="card-container" class:selection={selection !== undefined ? objects[selection]?._id === object._id : false} class:checked={checkedSet.has(object._id)} - on:mouseover={() => dispatch('obj-focus', object)} + on:mouseover={mouseAttractor(() => dispatch('obj-focus', object))} on:focus={() => {}} on:contextmenu={(evt) => showMenu(evt, object)} draggable={true} diff --git a/packages/ui/src/components/PanelInstance.svelte b/packages/ui/src/components/PanelInstance.svelte index 319bca08a4..6c5e18e17a 100644 --- a/packages/ui/src/components/PanelInstance.svelte +++ b/packages/ui/src/components/PanelInstance.svelte @@ -50,14 +50,17 @@ closePanel() } - $: props = $panelstore.panel - - $: if (props !== undefined) { - component = undefined - - getResource(props.component).then((r) => { - component = r - }) + $: if ($panelstore.panel !== undefined) { + if ($panelstore.panel.component === undefined) { + props = $panelstore.panel + } else { + getResource($panelstore.panel.component).then((r) => { + component = r + props = $panelstore.panel + }) + } + } else { + props = undefined } function escapeClose () { diff --git a/packages/ui/src/utils.ts b/packages/ui/src/utils.ts index 04dbb1d827..f0b8b6c1a5 100644 --- a/packages/ui/src/utils.ts +++ b/packages/ui/src/utils.ts @@ -105,3 +105,21 @@ export function tableToCSV (tableId: string, separator = ','): string { * @public */ export const networkStatus = writable<number>(0) + +let attractorMx = 0 +let attractorMy = 0 +/** + * perform mouse movement checks and call method if they was + */ +export function mouseAttractor (op: () => void, diff = 5): (evt: MouseEvent) => void { + return (evt: MouseEvent) => { + const dx = evt.clientX - attractorMx + const dy = evt.clientY - attractorMy + attractorMx = evt.clientX + attractorMy = evt.clientY + + if (Math.sqrt(dx * dx + dy * dy) > diff) { + op() + } + } +} diff --git a/plugins/board-resources/src/components/KanbanView.svelte b/plugins/board-resources/src/components/KanbanView.svelte index 4d155fe37e..dbc2841e91 100644 --- a/plugins/board-resources/src/components/KanbanView.svelte +++ b/plugins/board-resources/src/components/KanbanView.svelte @@ -95,7 +95,7 @@ let kanbanUI: KanbanUI const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => { - kanbanUI.select(offset, of, dir) + kanbanUI?.select(offset, of, dir) }) onMount(() => { ;(document.activeElement as HTMLElement)?.blur() @@ -125,6 +125,7 @@ ...options } ) + $: listProvider.update(cards) $: groupByDocs = groupBy(cards, 'state') const getUpdateProps = (doc: Doc, category: CategoryType): DocumentUpdate<DocWithRank> | undefined => { @@ -151,11 +152,8 @@ getGroupByValues={(groupByDocs, category) => getGroupByValues(groupByDocs, category)} {setGroupByValues} categories={states.map((it) => it._id)} - on:content={(evt) => { - listProvider.update(evt.detail) - }} on:obj-focus={(evt) => { - listProvider.updateFocus(evt.detail) + listProvider.updateFocus(evt.detail.object) }} {groupByDocs} {getUpdateProps} diff --git a/plugins/task-resources/src/components/kanban/KanbanView.svelte b/plugins/task-resources/src/components/kanban/KanbanView.svelte index 4d6738aecd..c1228698a3 100644 --- a/plugins/task-resources/src/components/kanban/KanbanView.svelte +++ b/plugins/task-resources/src/components/kanban/KanbanView.svelte @@ -106,7 +106,7 @@ let kanbanUI: KanbanUI const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => { - kanbanUI.select(offset, of, dir) + kanbanUI?.select(offset, of, dir) }) onMount(() => { ;(document.activeElement as HTMLElement)?.blur() @@ -144,6 +144,7 @@ } } ) + $: listProvider.update(tasks) let categories: CategoryType[] = [] @@ -247,9 +248,6 @@ {setGroupByValues} {getUpdateProps} {groupByDocs} - on:content={(evt) => { - listProvider.update(evt.detail) - }} on:obj-focus={(evt) => { listProvider.updateFocus(evt.detail) }} diff --git a/plugins/tracker-resources/src/components/issues/KanbanView.svelte b/plugins/tracker-resources/src/components/issues/KanbanView.svelte index 4c6fa60ee9..886c21217b 100644 --- a/plugins/tracker-resources/src/components/issues/KanbanView.svelte +++ b/plugins/tracker-resources/src/components/issues/KanbanView.svelte @@ -143,7 +143,7 @@ let kanbanUI: Kanban const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => { - kanbanUI.select(offset, of, dir) + kanbanUI?.select(offset, of, dir) }) onMount(() => { ;(document.activeElement as HTMLElement)?.blur() @@ -172,6 +172,8 @@ } ) + $: listProvider.update(issues) + let categories: CategoryType[] = [] const queryId = generateId() @@ -265,9 +267,6 @@ {setGroupByValues} {getUpdateProps} {groupByDocs} - on:content={(evt) => { - listProvider.update(evt.detail) - }} on:obj-focus={(evt) => { listProvider.updateFocus(evt.detail) }} diff --git a/plugins/tracker-resources/src/components/issues/timereport/TimeSpendReportsList.svelte b/plugins/tracker-resources/src/components/issues/timereport/TimeSpendReportsList.svelte index dc812b1ca2..e2894eb7e0 100644 --- a/plugins/tracker-resources/src/components/issues/timereport/TimeSpendReportsList.svelte +++ b/plugins/tracker-resources/src/components/issues/timereport/TimeSpendReportsList.svelte @@ -72,7 +72,7 @@ <div class="{twoRows ? 'flex-col' : 'flex-between'} p-text-2" on:contextmenu|preventDefault={(ev) => showContextMenu(ev, report)} - on:mouseover={() => { + on:mouseenter={() => { listProvider.updateFocus(report) }} on:focus={() => { diff --git a/plugins/view-resources/src/components/Table.svelte b/plugins/view-resources/src/components/Table.svelte index 3f0652dc88..af8d351d7f 100644 --- a/plugins/view-resources/src/components/Table.svelte +++ b/plugins/view-resources/src/components/Table.svelte @@ -282,7 +282,7 @@ class:checking={checkedSet.has(object._id)} class:fixed={row === selection} class:selected={row === selection} - on:mouseover={() => onRow(object)} + on:mouseenter={() => onRow(object)} on:focus={() => {}} bind:this={refs[row]} on:contextmenu|preventDefault={(ev) => { diff --git a/plugins/view-resources/src/components/list/List.svelte b/plugins/view-resources/src/components/list/List.svelte index 40756abafd..3abd22eccf 100644 --- a/plugins/view-resources/src/components/list/List.svelte +++ b/plugins/view-resources/src/components/list/List.svelte @@ -37,13 +37,10 @@ export let flatHeaders = false export let disableHeader = false export let props: Record<string, any> = {} + export let selection: number | undefined = undefined export let documents: Doc[] | undefined = undefined - const elementByIndex: Map<number, HTMLDivElement> = new Map() - const docByIndex: Map<number, Doc> = new Map() - const indexById: Map<Ref<Doc>, number> = new Map() - let docs: Doc[] = [] $: orderBy = viewOptions.orderBy @@ -63,16 +60,16 @@ resultQuery, (res) => { docs = res - dispatch('content', docs) }, resultOptions ) } else { docsQuery.unsubscribe() docs = documents - dispatch('content', docs) } + $: dispatch('content', docs) + const dispatch = createEventDispatcher() const client = getClient() @@ -100,26 +97,7 @@ } export function select (offset: 1 | -1 | 0, of?: Doc): void { - let pos = (of !== undefined ? indexById.get(of._id) : -1) ?? -1 - pos += offset - if (pos < 0) { - pos = 0 - } - if (pos >= docs.length) { - pos = docs.length - 1 - } - const target = docByIndex.get(pos) - if (target !== undefined) { - onRow(target) - } - const r = elementByIndex.get(pos) - if (r !== undefined) { - r.scrollIntoView({ behavior: 'auto', block: 'nearest' }) - } - } - - function onRow (object: Doc): void { - dispatch('row-focus', object) + listCategories?.select(offset, of) } const getLoadingElementsLength = (props: LoadingProps | undefined, options?: FindOptions<Doc>) => { @@ -137,23 +115,23 @@ } = {} let listDiv: HTMLDivElement + let listCategories: ListCategories </script> <div class="list-container" bind:this={listDiv}> <ListCategories + bind:this={listCategories} newObjectProps={() => (space ? { space } : {})} - {elementByIndex} - {indexById} {docs} {_class} {space} + {selection} query={resultQuery} {lookup} loadingPropsLength={getLoadingElementsLength(loadingProps, options)} {baseMenuClass} {config} {viewOptions} - {docByIndex} {viewOptionsConfig} {selectedObjectIds} level={0} @@ -167,6 +145,9 @@ {props} {listDiv} bind:dragItem + on:select={(evt) => { + select(0, evt.detail) + }} /> </div> diff --git a/plugins/view-resources/src/components/list/ListCategories.svelte b/plugins/view-resources/src/components/list/ListCategories.svelte index 713db288e2..11a4136fb3 100644 --- a/plugins/view-resources/src/components/list/ListCategories.svelte +++ b/plugins/view-resources/src/components/list/ListCategories.svelte @@ -18,7 +18,7 @@ import { getClient, statusStore } from '@hcengineering/presentation' import { AnyComponent } from '@hcengineering/ui' import { AttributeModel, BuildModelKey, CategoryOption, ViewOptionModel, ViewOptions } from '@hcengineering/view' - import { createEventDispatcher, onDestroy } from 'svelte' + import { createEventDispatcher, onDestroy, SvelteComponentTyped } from 'svelte' import { buildModel, concatCategories, @@ -31,8 +31,6 @@ import { CategoryQuery, noCategory } from '../../viewOptions' import ListCategory from './ListCategory.svelte' - export let elementByIndex: Map<number, HTMLDivElement> - export let indexById: Map<Ref<Doc>, number> export let docs: Doc[] export let _class: Ref<Class<Doc>> export let space: Ref<Space> | undefined @@ -51,13 +49,13 @@ export let level: number export let initIndex = 0 export let newObjectProps: (doc: Doc) => Record<string, any> | undefined - export let docByIndex: Map<number, Doc> export let viewOptionsConfig: ViewOptionModel[] | undefined export let dragItem: { doc?: Doc revert?: () => void } export let listDiv: HTMLDivElement + export let selection: number | undefined = undefined $: groupByKey = viewOptions.groupBy[level] ?? noCategory let categories: CategoryType[] = [] @@ -129,25 +127,118 @@ $: extraHeaders = getAdditionalHeader(client, _class) const dispatch = createEventDispatcher() + + function getState (doc: Doc): number { + let pos = 0 + for (const st of categories) { + const stateObjs = getGroupByValues(groupByDocs, st) ?? [] + if (stateObjs.findIndex((it) => it._id === doc._id) !== -1) { + return pos + } + pos++ + } + return -1 + } + + export function select (offset: 1 | -1 | 0, of?: Doc, dir?: 'vertical' | 'horizontal'): void { + let pos = (of != null ? docs.findIndex((it) => it._id === of._id) : selection) ?? -1 + if (pos === -1) { + for (const st of categories) { + const stateObjs = getGroupByValues(groupByDocs, st) ?? [] + if (stateObjs.length > 0) { + pos = docs.findIndex((it) => it._id === stateObjs[0]._id) + break + } + } + } + + if (pos < 0) { + pos = 0 + } + if (pos >= docs.length) { + pos = docs.length - 1 + } + + const obj = docs[pos] + if (obj === undefined) { + return + } + + // We found group + const objState = getState(obj) + if (objState === -1) { + return + } + + if (level + 1 >= viewOptions.groupBy.length) { + const stateObjs = getGroupByValues(groupByDocs, categories[objState]) ?? [] + + const statePos = stateObjs.findIndex((it) => it._id === obj._id) + if (statePos === undefined) { + return + } + + console.log(statePos, objState, offset) + if (offset === -1) { + if (dir === undefined || dir === 'vertical') { + if (statePos - 1 < 0 && objState > 0) { + const pstateObjs = getGroupByValues(groupByDocs, categories[objState - 1]) ?? [] + dispatch('select', pstateObjs[pstateObjs.length - 1]) + } else { + const obj = stateObjs[statePos - 1] ?? stateObjs[0] + scrollInto(objState, obj) + dispatch('row-focus', obj) + } + return + } + } + if (offset === 1) { + if (dir === undefined || dir === 'vertical') { + if (statePos + 1 >= stateObjs.length && objState < categories.length) { + const pstateObjs = getGroupByValues(groupByDocs, categories[objState + 1]) ?? [] + if (pstateObjs[0] !== undefined) { + dispatch('select', pstateObjs[0]) + } + } else { + const obj = stateObjs[statePos + 1] ?? stateObjs[stateObjs.length - 1] + scrollInto(objState, obj) + dispatch('row-focus', obj) + } + return + } + } + if (offset === 0) { + // scrollInto(objState, obj) + dispatch('row-focus', obj) + } + } else { + listCategory[objState]?.select(offset, of, dir) + } + } + function scrollInto (statePos: number, obj: Doc): void { + // listCategory[statePos]?.scrollIntoView({ behavior: 'auto', block: 'nearest' }) + listListCategory[statePos]?.scroll(obj) + listCategory[statePos]?.scroll(obj) + } + + const listCategory: SvelteComponentTyped[] = [] + const listListCategory: ListCategory[] = [] </script> {#each categories as category, i (typeof category === 'object' ? category.name : category)} {@const items = groupByKey === noCategory ? docs : getGroupByValues(groupByDocs, category)} <ListCategory - {elementByIndex} - {indexById} + bind:this={listListCategory[i]} {extraHeaders} {space} {selectedObjectIds} {headerComponent} - initIndex={getInitIndex(categories, i)} {baseMenuClass} {level} {viewOptions} {groupByKey} {lookup} {config} - {docByIndex} {itemModels} {_class} singleCat={level === 0 && categories.length === 1} @@ -175,8 +266,6 @@ > <svelte:fragment slot="category" - let:elementByIndex - let:indexById let:docs let:_class let:space @@ -192,16 +281,13 @@ let:flatHeaders let:props let:level - let:initIndex - let:docByIndex let:viewOptionsConfig let:listDiv let:dragstart > <svelte:self - {elementByIndex} - {indexById} {docs} + bind:this={listCategory[i]} {_class} {space} {lookup} @@ -217,7 +303,6 @@ {props} {level} {initIndex} - {docByIndex} {viewOptionsConfig} {listDiv} on:dragItem @@ -225,6 +310,9 @@ on:uncheckAll on:row-focus on:dragstart={dragstart} + on:select={(evt) => { + select(0, evt.detail) + }} /> </svelte:fragment> </ListCategory> diff --git a/plugins/view-resources/src/components/list/ListCategory.svelte b/plugins/view-resources/src/components/list/ListCategory.svelte index 1c6e87ada5..1723369209 100644 --- a/plugins/view-resources/src/components/list/ListCategory.svelte +++ b/plugins/view-resources/src/components/list/ListCategory.svelte @@ -16,17 +16,18 @@ import { Class, Doc, DocumentUpdate, Lookup, PrimitiveType, Ref, Space, StatusValue } from '@hcengineering/core' import { IntlString } from '@hcengineering/platform' import { getClient } from '@hcengineering/presentation' - import { calcRank, DocWithRank } from '@hcengineering/task' + import { DocWithRank, calcRank } from '@hcengineering/task' import { AnyComponent, CheckBox, ExpandCollapse, + Spinner, getEventPositionElement, - showPopup, - Spinner + mouseAttractor, + showPopup } from '@hcengineering/ui' import { AttributeModel, BuildModelKey, ViewOptionModel, ViewOptions } from '@hcengineering/view' - import { createEventDispatcher } from 'svelte' + import { createEventDispatcher, tick } from 'svelte' import { FocusSelection, focusStore } from '../../selection' import Menu from '../Menu.svelte' import ListHeader from './ListHeader.svelte' @@ -39,7 +40,6 @@ export let space: Ref<Space> | undefined export let baseMenuClass: Ref<Class<Doc>> | undefined export let items: Doc[] - export let initIndex: number export let createItemDialog: AnyComponent | undefined export let createItemLabel: IntlString | undefined export let loadingPropsLength: number | undefined @@ -50,14 +50,11 @@ export let disableHeader = false export let props: Record<string, any> = {} export let level: number - export let elementByIndex: Map<number, HTMLDivElement> - export let indexById: Map<Ref<Doc>, number> export let lookup: Lookup<Doc> export let _class: Ref<Class<Doc>> export let config: (string | BuildModelKey)[] export let viewOptions: ViewOptions export let newObjectProps: (doc: Doc) => Record<string, any> | undefined - export let docByIndex: Map<number, Doc> export let viewOptionsConfig: ViewOptionModel[] | undefined export let dragItem: { doc?: Doc @@ -92,7 +89,7 @@ dispatch('row-focus', object) } - const handleMenuOpened = async (event: MouseEvent, object: Doc, rowIndex: number) => { + const handleMenuOpened = async (event: MouseEvent, object: Doc) => { event.preventDefault() handleRowFocused(object) @@ -300,6 +297,24 @@ index: i }) } + export function scroll (item: Doc): void { + const pos = items.findIndex((it) => it._id === item._id) + if (pos >= 0) { + if (collapsed) { + collapsed = false + tick().then(() => scroll(item)) + return + } + if (pos >= limited.length) { + limit = (limit ?? 0) + 20 + + tick().then(() => scroll(item)) + } else { + listItems[pos]?.scroll() + } + } + } + const listItems: ListItem[] = [] </script> <div @@ -337,8 +352,6 @@ <div class="p-2"> <slot name="category" - {elementByIndex} - {indexById} docs={items} {_class} {space} @@ -354,8 +367,6 @@ {flatHeaders} {props} level={level + 1} - {initIndex} - {docByIndex} {viewOptionsConfig} {listDiv} dragItem @@ -366,12 +377,9 @@ {#if limited} {#each limited as docObject, i (docObject._id)} <ListItem + bind:this={listItems[i]} {docObject} - {elementByIndex} - {docByIndex} - {indexById} model={itemModels} - index={initIndex + i} {groupByKey} selected={isSelected(docObject, $focusStore)} checked={selectedObjectIdsSet.has(docObject._id)} @@ -386,9 +394,9 @@ on:dragover={(e) => dragover(e, i)} on:drop={dropItemHandle} on:check={(ev) => dispatch('check', { docs: ev.detail.docs, value: ev.detail.value })} - on:contextmenu={(event) => handleMenuOpened(event, docObject, initIndex + i)} + on:contextmenu={(event) => handleMenuOpened(event, docObject)} on:focus={() => {}} - on:mouseover={() => handleRowFocused(docObject)} + on:mouseover={mouseAttractor(() => handleRowFocused(docObject))} {props} /> {/each} diff --git a/plugins/view-resources/src/components/list/ListItem.svelte b/plugins/view-resources/src/components/list/ListItem.svelte index 60b5564fa5..d0f5533874 100644 --- a/plugins/view-resources/src/components/list/ListItem.svelte +++ b/plugins/view-resources/src/components/list/ListItem.svelte @@ -13,25 +13,25 @@ // limitations under the License. --> <script lang="ts"> - import core, { AnyAttribute, Doc, getObjectValue, Ref } from '@hcengineering/core' + import core, { AnyAttribute, Doc, getObjectValue } from '@hcengineering/core' import notification from '@hcengineering/notification' import { getClient, updateAttribute } from '@hcengineering/presentation' - import { CheckBox, Component, deviceOptionsStore as deviceInfo, tooltip, IconCircles } from '@hcengineering/ui' + import { CheckBox, Component, deviceOptionsStore as deviceInfo, IconCircles, tooltip } from '@hcengineering/ui' import { AttributeModel } from '@hcengineering/view' import { createEventDispatcher } from 'svelte' import { FixedColumn } from '../..' import view from '../../plugin' export let docObject: Doc - export let index: number export let model: AttributeModel[] export let groupByKey: string | undefined export let checked: boolean export let selected: boolean export let props: Record<string, any> = {} - export let elementByIndex: Map<number, HTMLDivElement> - export let indexById: Map<Ref<Doc>, number> - export let docByIndex: Map<number, Doc> + + export function scroll () { + elem?.scrollIntoView({ behavior: 'auto', block: 'nearest' }) + } let elem: HTMLDivElement @@ -47,10 +47,6 @@ $: compactMode = $deviceInfo.twoRows - $: elem && elementByIndex.set(index, elem) - $: indexById.set(docObject._id, index) - $: docByIndex.set(index, docObject) - const client = getClient() function onChange (value: any, doc: Doc, key: string, attribute: AnyAttribute) { @@ -94,6 +90,7 @@ draggable={true} on:contextmenu on:focus + on:mouseenter on:mouseover on:dragover on:dragenter diff --git a/plugins/view-resources/src/components/list/ListView.svelte b/plugins/view-resources/src/components/list/ListView.svelte index bbc72eb0b3..25c60d2e89 100644 --- a/plugins/view-resources/src/components/list/ListView.svelte +++ b/plugins/view-resources/src/components/list/ListView.svelte @@ -4,7 +4,14 @@ import { AnyComponent, issueSP, Scroller } from '@hcengineering/ui' import { BuildModelKey, Viewlet, ViewOptions } from '@hcengineering/view' import { onMount } from 'svelte' - import { ActionContext, ListSelectionProvider, LoadingProps, SelectDirection, selectionStore } from '../..' + import { + ActionContext, + ListSelectionProvider, + LoadingProps, + SelectDirection, + focusStore, + selectionStore + } from '../..' import List from './List.svelte' @@ -56,6 +63,7 @@ {props} viewOptionsConfig={viewlet.viewOptions?.other} selectedObjectIds={$selectionStore ?? []} + selection={listProvider.current($focusStore)} on:row-focus={(event) => { listProvider.updateFocus(event.detail ?? undefined) }}