UBER-1080 improve selection (#3857)

Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2023-10-19 12:55:28 +07:00 committed by GitHub
parent 20fe438e7e
commit 7fdfaaeece
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 176 additions and 73 deletions

View File

@ -38,7 +38,6 @@
groupBy, groupBy,
ListSelectionProvider, ListSelectionProvider,
SelectDirection, SelectDirection,
selectionStore,
setGroupByValues setGroupByValues
} from '@hcengineering/view-resources' } from '@hcengineering/view-resources'
import { onMount } from 'svelte' import { onMount } from 'svelte'
@ -100,6 +99,8 @@
;(document.activeElement as HTMLElement)?.blur() ;(document.activeElement as HTMLElement)?.blur()
}) })
const selection = listProvider.selection
const showMenu = async (ev: MouseEvent, object: Doc): Promise<void> => { const showMenu = async (ev: MouseEvent, object: Doc): Promise<void> => {
ev.preventDefault() ev.preventDefault()
if (object._class !== board.class.Card) { if (object._class !== board.class.Card) {
@ -156,7 +157,7 @@
}} }}
{groupByDocs} {groupByDocs}
{getUpdateProps} {getUpdateProps}
checked={$selectionStore ?? []} checked={$selection ?? []}
on:check={(evt) => { on:check={(evt) => {
listProvider.updateSelection(evt.detail.docs, evt.detail.value) listProvider.updateSelection(evt.detail.docs, evt.detail.value)
}} }}

View File

@ -56,7 +56,6 @@
Menu, Menu,
noCategory, noCategory,
SelectDirection, SelectDirection,
selectionStore,
setGroupByValues setGroupByValues
} from '@hcengineering/view-resources' } from '@hcengineering/view-resources'
import view from '@hcengineering/view-resources/src/plugin' import view from '@hcengineering/view-resources/src/plugin'
@ -113,6 +112,8 @@
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => { const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
kanbanUI?.select(offset, of, dir) kanbanUI?.select(offset, of, dir)
}) })
const selection = listProvider.selection
onMount(() => { onMount(() => {
;(document.activeElement as HTMLElement)?.blur() ;(document.activeElement as HTMLElement)?.blur()
}) })
@ -248,7 +249,7 @@
listProvider.updateFocus(evt.detail) listProvider.updateFocus(evt.detail)
}} }}
selection={listProvider.current($focusStore)} selection={listProvider.current($focusStore)}
checked={$selectionStore ?? []} checked={$selection ?? []}
on:check={(evt) => { on:check={(evt) => {
listProvider.updateSelection(evt.detail.docs, evt.detail.value) listProvider.updateSelection(evt.detail.docs, evt.detail.value)
}} }}

View File

@ -66,7 +66,6 @@
noCategory, noCategory,
openDoc, openDoc,
SelectDirection, SelectDirection,
selectionStore,
setGroupByValues setGroupByValues
} from '@hcengineering/view-resources' } from '@hcengineering/view-resources'
import view from '@hcengineering/view-resources/src/plugin' import view from '@hcengineering/view-resources/src/plugin'
@ -149,6 +148,8 @@
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => { const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
kanbanUI?.select(offset, of, dir) kanbanUI?.select(offset, of, dir)
}) })
const selection = listProvider.selection
onMount(() => { onMount(() => {
;(document.activeElement as HTMLElement)?.blur() ;(document.activeElement as HTMLElement)?.blur()
}) })
@ -288,7 +289,7 @@
listProvider.updateFocus(evt.detail) listProvider.updateFocus(evt.detail)
}} }}
selection={listProvider.current($focusStore)} selection={listProvider.current($focusStore)}
checked={$selectionStore ?? []} checked={$selection ?? []}
on:check={(evt) => { on:check={(evt) => {
listProvider.updateSelection(evt.detail.docs, evt.detail.value) listProvider.updateSelection(evt.detail.docs, evt.detail.value)
}} }}

View File

@ -1,5 +1,5 @@
<!-- <!--
// Copyright © 2022 Hardcore Engineering Inc. // Copyright © 2022, 2023 Hardcore Engineering Inc.
// //
// Licensed under the Eclipse Public License, Version 2.0 (the "License"); // Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may // you may not use this file except in compliance with the License. You may
@ -19,7 +19,7 @@
import { Issue } from '@hcengineering/tracker' import { Issue } from '@hcengineering/tracker'
import { AnyComponent, AnySvelteComponent, registerFocus } from '@hcengineering/ui' import { AnyComponent, AnySvelteComponent, registerFocus } from '@hcengineering/ui'
import { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view' import { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
import { List, ListSelectionProvider, SelectDirection, selectionStore } from '@hcengineering/view-resources' import { List, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import tracker from '../../../plugin' import tracker from '../../../plugin'
@ -47,6 +47,7 @@
listProvider.updateFocus(docs[0]) listProvider.updateFocus(docs[0])
list?.select(0, undefined) list?.select(0, undefined)
} }
const selection = listProvider.selection
// Focusable control with index // Focusable control with index
let focused = false let focused = false
@ -88,7 +89,8 @@
{createItemDialog} {createItemDialog}
{createItemDialogProps} {createItemDialogProps}
{createItemLabel} {createItemLabel}
selectedObjectIds={$selectionStore ?? []} {listProvider}
selectedObjectIds={$selection ?? []}
{compactMode} {compactMode}
on:row-focus={(event) => { on:row-focus={(event) => {
listProvider.updateFocus(event.detail ?? undefined) listProvider.updateFocus(event.detail ?? undefined)

View File

@ -14,7 +14,14 @@ import {
} from '@hcengineering/ui' } from '@hcengineering/ui'
import MoveView from './components/Move.svelte' import MoveView from './components/Move.svelte'
import view from './plugin' import view from './plugin'
import { FocusSelection, SelectDirection, focusStore, previewDocument, selectionStore } from './selection' import {
FocusSelection,
SelectDirection,
SelectionStore,
focusStore,
previewDocument,
selectionStore
} from './selection'
import { deleteObjects, getObjectLinkFragment } from './utils' import { deleteObjects, getObjectLinkFragment } from './utils'
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
@ -107,6 +114,11 @@ contextStore.subscribe((it) => {
$contextStore = it $contextStore = it
}) })
let $selectionStore: SelectionStore
selectionStore.subscribe((it) => {
$selectionStore = it
})
export function select ( export function select (
evt: Event | undefined, evt: Event | undefined,
offset: 1 | -1 | 0, offset: 1 | -1 | 0,
@ -128,8 +140,9 @@ export function select (
function SelectItem (doc: Doc | Doc[] | undefined, evt: Event): void { function SelectItem (doc: Doc | Doc[] | undefined, evt: Event): void {
const focus = $focusStore.focus const focus = $focusStore.focus
const provider = $selectionStore.provider ?? $focusStore.provider
if (focus !== undefined) { if (focus !== undefined) {
selectionStore.update((selection) => { provider?.selection.update((selection) => {
const ind = selection.findIndex((it) => it._id === focus._id) const ind = selection.findIndex((it) => it._id === focus._id)
if (ind === -1) { if (ind === -1) {
selection.push(focus) selection.push(focus)
@ -142,15 +155,21 @@ function SelectItem (doc: Doc | Doc[] | undefined, evt: Event): void {
evt.preventDefault() evt.preventDefault()
} }
function SelectItemNone (doc: Doc | undefined, evt: Event): void { function SelectItemNone (doc: Doc | undefined, evt: Event): void {
selectionStore.set([]) const provider = $selectionStore.provider ?? $focusStore.provider
previewDocument.set(undefined) if (provider !== undefined) {
evt.preventDefault() provider.selection.set([])
previewDocument.set(undefined)
evt.preventDefault()
}
} }
function SelectItemAll (doc: Doc | undefined, evt: Event): void { function SelectItemAll (doc: Doc | undefined, evt: Event): void {
const docs = $focusStore.provider?.docs() ?? [] const provider = $selectionStore.provider ?? $focusStore.provider
selectionStore.set(docs) if (provider !== undefined) {
previewDocument.set(undefined) const docs = provider.docs() ?? []
evt.preventDefault() provider.selection.set(docs)
previewDocument.set(undefined)
evt.preventDefault()
}
} }
const MoveUp = (doc: Doc | undefined, evt: Event): void => select(evt, -1, $focusStore.focus, 'vertical') const MoveUp = (doc: Doc | undefined, evt: Event): void => select(evt, -1, $focusStore.focus, 'vertical')

View File

@ -27,17 +27,17 @@ import core, {
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { Action, ActionGroup, ViewAction, ViewActionInput, ViewContextType } from '@hcengineering/view' import { Action, ActionGroup, ViewAction, ViewActionInput, ViewContextType } from '@hcengineering/view'
import view from './plugin' import view from './plugin'
import { FocusSelection } from './selection' import { FocusSelection, SelectionStore } from './selection'
/** /**
* @public * @public
*/ */
export function getSelection (focusStore: FocusSelection, selectionStore: Doc[]): Doc[] { export function getSelection (focus: FocusSelection, selection: SelectionStore): Doc[] {
let docs: Doc[] = [] let docs: Doc[] = []
if (selectionStore.length > 0) { if (selection.docs.length > 0) {
docs = selectionStore docs = selection.docs
} else if (focusStore.focus !== undefined) { } else if (focus.focus !== undefined) {
docs = [focusStore.focus] docs = [focus.focus]
} }
return docs return docs
} }

View File

@ -20,7 +20,7 @@
import { Action, ViewContextType } from '@hcengineering/view' import { Action, ViewContextType } from '@hcengineering/view'
import { fly } from 'svelte/transition' import { fly } from 'svelte/transition'
import { getContextActions, getSelection } from '../actions' import { getContextActions, getSelection } from '../actions'
import { focusStore, previewDocument, selectionStore } from '../selection' import { ListSelectionProvider, SelectionStore, focusStore, previewDocument, selectionStore } from '../selection'
import { getObjectPreview } from '../utils' import { getObjectPreview } from '../utils'
const client = getClient() const client = getClient()
@ -28,8 +28,9 @@
addTxListener((tx) => { addTxListener((tx) => {
if (tx._class === core.class.TxRemoveDoc) { if (tx._class === core.class.TxRemoveDoc) {
const docId = (tx as TxRemoveDoc<Doc>).objectId const docId = (tx as TxRemoveDoc<Doc>).objectId
if ($selectionStore.find((it) => it._id === docId) !== undefined) { const provider = ListSelectionProvider.Find(docId)
selectionStore.update((old) => { if (provider !== undefined) {
provider.selection.update((old) => {
return old.filter((it) => it._id !== docId) return old.filter((it) => it._id !== docId)
}) })
} }
@ -47,13 +48,13 @@
application?: Ref<Doc> application?: Ref<Doc>
}, },
focus: Doc | undefined | null, focus: Doc | undefined | null,
selection: Doc[] selection: SelectionStore
): Promise<Action[]> { ): Promise<Action[]> {
let docs: Doc | Doc[] = [] let docs: Doc | Doc[] = []
if (selection.find((it) => it._id === focus?._id) === undefined && focus != null) { if (selection.docs.find((it) => it._id === focus?._id) === undefined && focus != null) {
docs = focus docs = focus
} else { } else {
docs = selection docs = selection.docs
} }
return await getContextActions(client, docs, context) return await getContextActions(client, docs, context)

View File

@ -193,11 +193,11 @@
on:keydown={onKeydown} on:keydown={onKeydown}
use:resizeObserver={() => dispatch('changeContent')} use:resizeObserver={() => dispatch('changeContent')}
> >
{#if $selectionStore.length > 0 || $focusStore.focus !== undefined || (activeAction && activeAction?.actionPopup !== undefined)} {#if $selectionStore.docs.length > 0 || $focusStore.focus !== undefined || (activeAction && activeAction?.actionPopup !== undefined)}
<div class="mt-2 ml-2 flex-between flex-no-shrink"> <div class="mt-2 ml-2 flex-between flex-no-shrink">
{#if $selectionStore.length > 0} {#if $selectionStore.docs.length > 0}
<div class="item-box"> <div class="item-box">
<Label label={view.string.NumberItems} params={{ count: $selectionStore.length }} /> <Label label={view.string.NumberItems} params={{ count: $selectionStore.docs.length }} />
</div> </div>
{:else if $focusStore.focus !== undefined} {:else if $focusStore.focus !== undefined}
<div class="item-box"> <div class="item-box">

View File

@ -18,7 +18,7 @@
import { Scroller, tableSP, FadeOptions } from '@hcengineering/ui' import { Scroller, tableSP, FadeOptions } from '@hcengineering/ui'
import { BuildModelKey } from '@hcengineering/view' import { BuildModelKey } from '@hcengineering/view'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '../selection' import { focusStore, ListSelectionProvider, SelectDirection } from '../selection'
import { LoadingProps } from '../utils' import { LoadingProps } from '../utils'
import SourcePresenter from './inference/SourcePresenter.svelte' import SourcePresenter from './inference/SourcePresenter.svelte'
import Table from './Table.svelte' import Table from './Table.svelte'
@ -48,6 +48,7 @@
} }
} }
) )
const selection = listProvider.selection
onMount(() => { onMount(() => {
;(document.activeElement as HTMLElement)?.blur() ;(document.activeElement as HTMLElement)?.blur()
@ -101,7 +102,7 @@
highlightRows={true} highlightRows={true}
{enableChecking} {enableChecking}
showFooter showFooter
checked={$selectionStore ?? []} checked={$selection ?? []}
{prefferedSorting} {prefferedSorting}
{tableId} {tableId}
selection={listProvider.current($focusStore)} selection={listProvider.current($focusStore)}

View File

@ -19,6 +19,7 @@
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui' import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
import { BuildModelKey, ViewOptionModel, ViewOptions, ViewQueryOption, Viewlet } from '@hcengineering/view' import { BuildModelKey, ViewOptionModel, ViewOptions, ViewQueryOption, Viewlet } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { SelectionFocusProvider } from '../../selection'
import { buildConfigLookup } from '../../utils' import { buildConfigLookup } from '../../utils'
import ListCategories from './ListCategories.svelte' import ListCategories from './ListCategories.svelte'
@ -40,6 +41,7 @@
export let props: Record<string, any> = {} export let props: Record<string, any> = {}
export let selection: number | undefined = undefined export let selection: number | undefined = undefined
export let compactMode: boolean = false export let compactMode: boolean = false
export let listProvider: SelectionFocusProvider
const limiter = new RateLimitter(() => ({ rate: 10 })) const limiter = new RateLimitter(() => ({ rate: 10 }))
@ -189,6 +191,7 @@
{viewOptionsConfig} {viewOptionsConfig}
{selectedObjectIds} {selectedObjectIds}
{limiter} {limiter}
{listProvider}
level={0} level={0}
groupPersistKey={''} groupPersistKey={''}
{createItemDialog} {createItemDialog}

View File

@ -37,6 +37,7 @@
ViewOptions ViewOptions
} from '@hcengineering/view' } from '@hcengineering/view'
import { createEventDispatcher, onDestroy, SvelteComponentTyped } from 'svelte' import { createEventDispatcher, onDestroy, SvelteComponentTyped } from 'svelte'
import { SelectionFocusProvider } from '../../selection'
import { import {
buildModel, buildModel,
concatCategories, concatCategories,
@ -83,6 +84,7 @@
export let resultQuery: DocumentQuery<Doc> export let resultQuery: DocumentQuery<Doc>
export let resultOptions: FindOptions<Doc> export let resultOptions: FindOptions<Doc>
export let limiter: RateLimitter export let limiter: RateLimitter
export let listProvider: SelectionFocusProvider
$: groupByKey = viewOptions.groupBy[level] ?? noCategory $: groupByKey = viewOptions.groupBy[level] ?? noCategory
let categories: CategoryType[] = [] let categories: CategoryType[] = []
@ -380,6 +382,7 @@
{resultQuery} {resultQuery}
{resultOptions} {resultOptions}
{limiter} {limiter}
{listProvider}
on:check on:check
on:uncheckAll on:uncheckAll
on:row-focus on:row-focus

View File

@ -42,7 +42,7 @@
import { AttributeModel, BuildModelKey, ViewOptionModel, ViewOptions, Viewlet } from '@hcengineering/view' import { AttributeModel, BuildModelKey, ViewOptionModel, ViewOptions, Viewlet } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { fade } from 'svelte/transition' import { fade } from 'svelte/transition'
import { FocusSelection, focusStore } from '../../selection' import { FocusSelection, SelectionFocusProvider, focusStore } from '../../selection'
import Menu from '../Menu.svelte' import Menu from '../Menu.svelte'
import ListHeader from './ListHeader.svelte' import ListHeader from './ListHeader.svelte'
import ListItem from './ListItem.svelte' import ListItem from './ListItem.svelte'
@ -87,6 +87,7 @@
export let resultOptions: FindOptions<Doc> export let resultOptions: FindOptions<Doc>
export let parentCategories: number = 0 export let parentCategories: number = 0
export let limiter: RateLimitter export let limiter: RateLimitter
export let listProvider: SelectionFocusProvider
$: lastLevel = level + 1 >= viewOptions.groupBy.length $: lastLevel = level + 1 >= viewOptions.groupBy.length
@ -432,6 +433,7 @@
limited={lastLevel ? limited.length : itemProj.length} limited={lastLevel ? limited.length : itemProj.length}
itemsProj={itemProj} itemsProj={itemProj}
items={limited} items={limited}
{listProvider}
{headerComponent} {headerComponent}
{createItemDialog} {createItemDialog}
{createItemDialogProps} {createItemDialogProps}

View File

@ -36,7 +36,7 @@
import { AttributeModel, ViewOptions } from '@hcengineering/view' import { AttributeModel, ViewOptions } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import view from '../../plugin' import view from '../../plugin'
import { selectionStore, selectionStoreMap } from '../../selection' import { SelectionFocusProvider } from '../../selection'
import { noCategory } from '../../viewOptions' import { noCategory } from '../../viewOptions'
export let groupByKey: string export let groupByKey: string
@ -50,6 +50,7 @@
export let collapsed = false export let collapsed = false
export let lastCat = false export let lastCat = false
export let level: number export let level: number
export let listProvider: SelectionFocusProvider
export let createItemDialog: AnyComponent | AnySvelteComponent | undefined export let createItemDialog: AnyComponent | AnySvelteComponent | undefined
export let createItemDialogProps: Record<string, any> | undefined export let createItemDialogProps: Record<string, any> | undefined
@ -82,7 +83,10 @@
} }
let mouseOver = false let mouseOver = false
$: selected = items.filter((it) => $selectionStoreMap.has(it._id)) const selection = listProvider.selection
$: selectionIds = new Set($selection.map((it) => it._id))
$: selected = items.filter((it) => selectionIds.has(it._id))
</script> </script>
{#if headerComponent || groupByKey === noCategory} {#if headerComponent || groupByKey === noCategory}
@ -177,18 +181,18 @@
kind={'ghost'} kind={'ghost'}
showTooltip={{ label: view.string.Select }} showTooltip={{ label: view.string.Select }}
on:click={() => { on:click={() => {
let newSelection = [...$selectionStore] let newSelection = [...$selection]
if (selected.length > 0) { if (selected.length > 0) {
const smap = new Map(selected.map((it) => [it._id, it])) const smap = new Map(selected.map((it) => [it._id, it]))
newSelection = newSelection.filter((it) => !smap.has(it._id)) newSelection = newSelection.filter((it) => !smap.has(it._id))
} else { } else {
for (const s of items) { for (const s of items) {
if (!$selectionStoreMap.has(s._id)) { if (!selectionIds.has(s._id)) {
newSelection.push(s) newSelection.push(s)
} }
} }
} }
selectionStore.set(newSelection) listProvider.selection.set(newSelection)
}} }}
/> />
</div> </div>

View File

@ -1,3 +1,17 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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.
-->
<script lang="ts"> <script lang="ts">
import { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core' import { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
@ -5,7 +19,7 @@
import { BuildModelKey, Viewlet, ViewOptions } from '@hcengineering/view' import { BuildModelKey, Viewlet, ViewOptions } from '@hcengineering/view'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { ActionContext } from '@hcengineering/presentation' import { ActionContext } from '@hcengineering/presentation'
import { ListSelectionProvider, SelectDirection, focusStore, selectionStore } from '../..' import { ListSelectionProvider, SelectDirection, focusStore } from '../..'
import List from './List.svelte' import List from './List.svelte'
@ -37,6 +51,7 @@
} }
} }
) )
const selection = listProvider.selection
onMount(() => { onMount(() => {
;(document.activeElement as HTMLElement)?.blur() ;(document.activeElement as HTMLElement)?.blur()
@ -70,9 +85,10 @@
{createItemLabel} {createItemLabel}
{viewOptions} {viewOptions}
{props} {props}
{listProvider}
compactMode={listWidth <= 800} compactMode={listWidth <= 800}
viewOptionsConfig={viewlet.viewOptions?.other} viewOptionsConfig={viewlet.viewOptions?.other}
selectedObjectIds={$selectionStore ?? []} selectedObjectIds={$selection ?? []}
selection={listProvider.current($focusStore)} selection={listProvider.current($focusStore)}
on:row-focus={(event) => { on:row-focus={(event) => {
listProvider.updateFocus(event.detail ?? undefined) listProvider.updateFocus(event.detail ?? undefined)

View File

@ -1,7 +1,7 @@
import { Doc, Ref } from '@hcengineering/core' import { Doc, Ref } from '@hcengineering/core'
import { panelstore } from '@hcengineering/ui' import { panelstore } from '@hcengineering/ui'
import { onDestroy } from 'svelte' import { onDestroy } from 'svelte'
import { Unsubscriber, derived, writable } from 'svelte/store' import { Unsubscriber, Writable, writable } from 'svelte/store'
/** /**
* @public * @public
@ -32,7 +32,11 @@ export interface SelectionFocusProvider {
// Return all selectable documents // Return all selectable documents
docs: () => Doc[] docs: () => Doc[]
// All selected documents
selection: Writable<Doc[]>
} }
/** /**
* @public * @public
* *
@ -46,6 +50,18 @@ export interface FocusSelection {
provider?: SelectionFocusProvider provider?: SelectionFocusProvider
} }
/**
* @public
*
* Define document selection inside platform.
*/
export interface SelectionStore {
// Selected documents
docs: Doc[]
// Provider where documents are selected
provider?: SelectionFocusProvider
}
/** /**
* @public * @public
*/ */
@ -54,18 +70,17 @@ export const focusStore = writable<FocusSelection>({})
/** /**
* @public * @public
*/ */
export const selectionStore = writable<Doc[]>([]) export const selectionStore = writable<SelectionStore>({ docs: [] })
/** /**
* @public * @public
*/ */
export const selectionStoreMap = derived(selectionStore, (it) => new Set(it.map((it) => it._id)))
export const previewDocument = writable<Doc | undefined>() export const previewDocument = writable<Doc | undefined>()
panelstore.subscribe((val) => { panelstore.subscribe((val) => {
previewDocument.set(undefined) previewDocument.set(undefined)
}) })
/** /**
* @public * @public
*/ */
@ -88,17 +103,23 @@ export function updateFocus (selection?: FocusSelection): void {
return cur return cur
}) })
// We need to clear selection items not belong to passed provider.
if (selection?.provider !== undefined) {
const docs = new Set(selection?.provider.docs().map((it) => it._id))
selectionStore.update((old) => {
return old.filter((it) => docs.has(it._id))
})
}
} }
const providers: ListSelectionProvider[] = [] interface ProviderSelection {
docs: Doc[]
provider: ListSelectionProvider
}
const providers: ProviderSelection[] = []
function updateSelection (selection: ProviderSelection): void {
const index = providers.findIndex((p) => p.provider === selection.provider)
if (index !== -1) {
providers[index] = selection
}
selectionStore.set(selection)
}
/** /**
* @public * @public
@ -108,15 +129,27 @@ const providers: ListSelectionProvider[] = []
export class ListSelectionProvider implements SelectionFocusProvider { export class ListSelectionProvider implements SelectionFocusProvider {
private _docs: Doc[] = [] private _docs: Doc[] = []
_current?: FocusSelection _current?: FocusSelection
private readonly unsubscribe: Unsubscriber selection: Writable<Doc[]> = writable([])
private readonly unsubscribe: Unsubscriber[]
constructor ( constructor (
private readonly delegate: (offset: 1 | -1 | 0, of?: Doc, direction?: SelectDirection, noScroll?: boolean) => void, private readonly delegate: (offset: 1 | -1 | 0, of?: Doc, direction?: SelectDirection, noScroll?: boolean) => void,
autoDestroy = true autoDestroy = true
) { ) {
this.unsubscribe = focusStore.subscribe((doc) => { this.unsubscribe = [
this._current = doc // keep track of current focus
}) focusStore.subscribe((focus) => {
providers.push(this) this._current = focus
}),
// update global selection when current changes
this.selection.subscribe((docs) => {
updateSelection({ docs, provider: this })
})
]
providers.push({ docs: [], provider: this })
selectionStore.set({ docs: [], provider: this })
if (autoDestroy) { if (autoDestroy) {
onDestroy(() => { onDestroy(() => {
this.destroy() this.destroy()
@ -125,9 +158,11 @@ export class ListSelectionProvider implements SelectionFocusProvider {
} }
static Find (_id: Ref<Doc>): ListSelectionProvider | undefined { static Find (_id: Ref<Doc>): ListSelectionProvider | undefined {
for (const provider of providers) { for (const { provider } of providers) {
if (provider.docs().findIndex((p) => p._id === _id) !== -1) { if (provider !== undefined) {
return provider if (provider.docs().findIndex((p) => p._id === _id) !== -1) {
return provider
}
} }
} }
} }
@ -135,23 +170,35 @@ export class ListSelectionProvider implements SelectionFocusProvider {
static Pop (): void { static Pop (): void {
if (providers.length === 0) return if (providers.length === 0) return
const last = providers[providers.length - 1] const last = providers[providers.length - 1]
last.destroy() last.provider.destroy()
} }
destroy (): void { destroy (): void {
const thisIndex = providers.findIndex((p) => p === this) const thisIndex = providers.findIndex((p) => p.provider === this)
providers.splice(thisIndex, 1) providers.splice(thisIndex, 1)
// switch selection to the last provider if lost selection
if (thisIndex === providers.length) { if (thisIndex === providers.length) {
if (providers.length > 0) { if (providers.length > 0) {
const current = providers[providers.length - 1] const next = providers[providers.length - 1].provider
const index = current.current() const index = next.current()
const target = index !== undefined ? current.docs()[index] : undefined const target = index !== undefined ? next.docs()[index] : undefined
updateFocus({ focus: target, provider: current }) updateFocus({ focus: target, provider: next })
} else { } else {
updateFocus() updateFocus()
} }
} }
this.unsubscribe()
// switch selection to the last provider if lost selection
selectionStore.update((selection) => {
if (selection.provider === this) {
const next = providers[providers.length - 1]
return next ?? { docs: selection.docs }
}
return selection
})
this.unsubscribe.forEach((p) => p())
} }
select (offset: 1 | -1 | 0, of?: Doc, direction?: SelectDirection, noScroll?: boolean): void { select (offset: 1 | -1 | 0, of?: Doc, direction?: SelectDirection, noScroll?: boolean): void {
@ -162,7 +209,8 @@ export class ListSelectionProvider implements SelectionFocusProvider {
update (docs: Doc[]): void { update (docs: Doc[]): void {
this._docs = docs this._docs = docs
selectionStore.update((docs) => { // remove missing documents from selection
this.selection.update((docs) => {
const ids = new Set(docs.map((it) => it._id)) const ids = new Set(docs.map((it) => it._id))
return this._docs.filter((it) => ids.has(it._id)) return this._docs.filter((it) => ids.has(it._id))
}) })
@ -174,6 +222,7 @@ export class ListSelectionProvider implements SelectionFocusProvider {
// Check if we don't have object, we need to select first one. // Check if we don't have object, we need to select first one.
this.delegate(0, this._current?.focus, 'vertical', true) this.delegate(0, this._current?.focus, 'vertical', true)
} }
// focus current provider if nothing focused
if (this._current?.focus === undefined) { if (this._current?.focus === undefined) {
updateFocus({ focus: this._current?.focus, provider: this }) updateFocus({ focus: this._current?.focus, provider: this })
} }
@ -189,7 +238,7 @@ export class ListSelectionProvider implements SelectionFocusProvider {
} }
updateSelection (docs: Doc[], value: boolean): void { updateSelection (docs: Doc[], value: boolean): void {
selectionStore.update((selection) => { this.selection.update((selection) => {
const docsSet = new Set(docs.map((it) => it._id)) const docsSet = new Set(docs.map((it) => it._id))
const noDocs = selection.filter((it) => !docsSet.has(it._id)) const noDocs = selection.filter((it) => !docsSet.has(it._id))
return value ? [...noDocs, ...docs] : noDocs return value ? [...noDocs, ...docs] : noDocs