Rework panel navigation (#928)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-02-04 16:03:24 +07:00 committed by GitHub
parent f2402c3208
commit db52006a9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 430 additions and 201 deletions

View File

@ -14,17 +14,15 @@
// limitations under the License.
-->
<script lang="ts">
import type { AttachedData, Doc, Ref } from '@anticrm/core'
import type { IntlString, Asset } from '@anticrm/platform'
import type { Channel, ChannelProvider } from '@anticrm/contact'
import { getClient } from '..'
import type { AttachedData, Doc, Ref } from '@anticrm/core'
import type { Asset, IntlString } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui'
import { Tooltip, CircleButton } from '@anticrm/ui'
import ChannelsPopup from './ChannelsPopup.svelte'
import contact from '@anticrm/contact'
import { CircleButton, Tooltip } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import { getClient } from '..'
import { getChannelProviders } from '../utils'
import ChannelsPopup from './ChannelsPopup.svelte'
export let value: AttachedData<Channel>[] | AttachedData<Channel> | null
export let size: 'small' | 'medium' | 'large' | 'x-large' = 'large'
@ -42,15 +40,6 @@
const client = getClient()
const dispatch = createEventDispatcher()
async function getProviders (): Promise<Map<Ref<ChannelProvider>, ChannelProvider>> {
const providers = await client.findAll(contact.class.ChannelProvider, {})
const map = new Map<Ref<ChannelProvider>, ChannelProvider>()
for (const provider of providers) {
map.set(provider._id, provider)
}
return map
}
function getProvider (item: AttachedData<Channel>, map: Map<Ref<ChannelProvider>, ChannelProvider>): any | undefined {
const provider = map.get(item.provider)
if (provider) {
@ -68,7 +57,7 @@
async function update (value: AttachedData<Channel>[] | AttachedData<Channel>) {
const result = []
const map = await getProviders()
const map = await getChannelProviders()
if (Array.isArray(value)) {
for (const item of value) {
const provider = getProvider(item, map)

View File

@ -22,9 +22,11 @@ import login from '@anticrm/login'
import { getMetadata } from '@anticrm/platform'
import { LiveQuery as LQ } from '@anticrm/query'
import { onDestroy } from 'svelte'
import contact, { ChannelProvider } from '@anticrm/contact'
let liveQuery: LQ
let client: TxOperations
let channelProviders: Promise<ChannelProvider[]> | undefined
class UIClient extends TxOperations implements Client {
constructor (client: Client, private readonly liveQuery: LQ) {
@ -47,6 +49,7 @@ export function setClient (_client: Client): void {
_client.notify = (tx: Tx) => {
liveQuery.tx(tx).catch((err) => console.log(err))
}
channelProviders = client.findAll(contact.class.ChannelProvider, {})
}
export class LiveQuery {
@ -110,3 +113,12 @@ export function getAttributePresenterClass (attribute: AnyAttribute): Ref<Class<
}
return attrClass
}
export async function getChannelProviders (): Promise<Map<Ref<ChannelProvider>, ChannelProvider>> {
const cp = (await channelProviders) ?? []
const map = new Map<Ref<ChannelProvider>, ChannelProvider>()
for (const provider of cp) {
map.set(provider._id, provider)
}
return map
}

View File

@ -0,0 +1,146 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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">
import { getResource } from '@anticrm/platform'
import { afterUpdate } from 'svelte'
import { Spinner } from '..'
import { closePanel, PanelProps, panelstore as modal } from '../panelup'
let modalHTML: HTMLElement
let componentInstance: any
let show: boolean = false
let props: PanelProps | undefined
function _close () {
closePanel()
}
$: props = $modal.panel
function escapeClose () {
if (componentInstance && componentInstance.canClose) {
if (!componentInstance.canClose()) return
}
_close()
}
const fitPopup = (props: PanelProps): void => {
if (modalHTML) {
if (props.element) {
show = false
modalHTML.style.left = modalHTML.style.right = modalHTML.style.top = modalHTML.style.bottom = ''
modalHTML.style.maxHeight = modalHTML.style.height = ''
if (typeof props.element !== 'string') {
const el = props.element as HTMLElement
const rect = el.getBoundingClientRect()
const rectPopup = modalHTML.getBoundingClientRect()
// Vertical
if (rect.bottom + rectPopup.height + 28 <= document.body.clientHeight) {
modalHTML.style.top = `calc(${rect.bottom}px + .75rem)`
} else if (rectPopup.height + 28 < rect.top) {
modalHTML.style.bottom = `calc(${document.body.clientHeight - rect.y}px + .75rem)`
} else {
modalHTML.style.top = modalHTML.style.bottom = '1rem'
}
// Horizontal
if (rect.left + rectPopup.width + 16 > document.body.clientWidth) {
modalHTML.style.right = document.body.clientWidth - rect.right + 'px'
} else {
modalHTML.style.left = rect.left + 'px'
}
} else if (props.element === 'right') {
modalHTML.style.top = '0'
modalHTML.style.bottom = '0'
modalHTML.style.right = '0'
} else if (props.element === 'float') {
modalHTML.style.top = '4rem'
modalHTML.style.bottom = '4rem'
modalHTML.style.right = '4rem'
} else if (props.element === 'account') {
modalHTML.style.bottom = '2.75rem'
modalHTML.style.left = '5rem'
} else if (props.element === 'full') {
modalHTML.style.top = '0'
modalHTML.style.bottom = '0'
modalHTML.style.left = '0'
modalHTML.style.right = '0'
}
} else {
modalHTML.style.top = '50%'
modalHTML.style.left = '50%'
modalHTML.style.transform = 'translate(-50%, -50%)'
show = true
}
}
}
function handleKeydown (ev: KeyboardEvent) {
if (ev.key === 'Escape') {
escapeClose()
}
}
afterUpdate(() => {
if (props) fitPopup(props)
})
</script>
<svelte:window
on:resize={() => {
if (props) fitPopup(props)
}}
on:keydown={(evt) => {
if (props) handleKeydown(evt)
}}
/>
{#if props}
{#await getResource(props.component)}
<Spinner />
{:then component}
<div class="popup" bind:this={modalHTML} style={'z-index: 401'}>
<svelte:component
this={component}
bind:this={componentInstance}
_id={props._id}
_class={props._class}
on:update={fitPopup}
on:close={_close}
/>
</div>
<div class="modal-overlay" class:show style={'z-index: 400'} on:click={() => escapeClose()} />
{/await}
{/if}
<style lang="scss">
.popup {
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
max-height: calc(100vh - 2rem);
background-color: transparent;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
&.show {
background: rgba(0, 0, 0, 0.5);
}
}
</style>

View File

@ -14,10 +14,9 @@
//
import { SvelteComponent } from 'svelte'
import type { AnySvelteComponent, AnyComponent, PopupAlignment, LabelAndProps, TooltipAligment } from './types'
import { getResource, IntlString, addStringsLoader } from '@anticrm/platform'
import { addStringsLoader } from '@anticrm/platform'
import { uiId } from './plugin'
import { writable, readable } from 'svelte/store'
import { readable } from 'svelte/store'
import Root from './components/internal/Root.svelte'
@ -90,99 +89,17 @@ export { default as IconInfo } from './components/icons/Info.svelte'
export { default as IconBlueCheck } from './components/icons/BlueCheck.svelte'
export { default as IconArrowLeft } from './components/icons/ArrowLeft.svelte'
export { default as PanelInstance } from './components/PanelInstance.svelte'
export * from './utils'
export * from './popups'
export * from './tooltips'
export * from './panelup'
export function createApp (target: HTMLElement): SvelteComponent {
return new Root({ target })
}
interface CompAndProps {
id: string
is: AnySvelteComponent
props: any
element?: PopupAlignment
onClose?: (result: any) => void
close: () => void
}
export const popupstore = writable<CompAndProps[]>([])
function addPopup (props: CompAndProps): void {
popupstore.update((popups) => {
popups.push(props)
return popups
})
}
let popupId: number = 0
export function showPopup (
component: AnySvelteComponent | AnyComponent,
props: any,
element?: PopupAlignment,
onClose?: (result: any) => void
): () => void {
const id = `${popupId++}`
const closePopupOp = (): void => {
popupstore.update((popups) => {
const pos = popups.findIndex(p => p.id === id)
if (pos !== -1) {
popups.splice(pos, 1)
}
return popups
})
}
if (typeof component === 'string') {
getResource(component).then((resolved) => addPopup({ id, is: resolved, props, element, onClose, close: closePopupOp })).catch((err) => console.log(err))
} else {
addPopup({ id, is: component, props, element, onClose, close: closePopupOp })
}
return closePopupOp
}
export function closePopup (): void {
popupstore.update((popups) => {
popups.pop()
return popups
})
}
export const tooltipstore = writable<LabelAndProps>({
label: undefined,
element: undefined,
direction: undefined,
component: undefined,
props: undefined,
anchor: undefined
})
export function showTooltip (
label: IntlString | undefined,
element: HTMLElement,
direction?: TooltipAligment,
component?: AnySvelteComponent | AnyComponent,
props?: any,
anchor?: HTMLElement
): void {
tooltipstore.set({
label: label,
element: element,
direction: direction,
component: component,
props: props,
anchor: anchor
})
}
export function closeTooltip (): void {
tooltipstore.set({
label: undefined,
element: undefined,
direction: undefined,
component: undefined,
props: undefined,
anchor: undefined
})
}
export const ticker = readable(Date.now(), (set) => {
setInterval(() => {
set(Date.now())

View File

@ -0,0 +1,51 @@
import { writable } from 'svelte/store'
import { getCurrentLocation, location, navigate } from './location'
import { AnyComponent, PopupAlignment } from './types'
export interface PanelProps {
component: AnyComponent
_id: string
_class: string
element?: PopupAlignment
}
export const panelstore = writable < {panel?: PanelProps|undefined}>({ panel: undefined })
let currentLocation: string | undefined
location.subscribe((loc) => {
if (loc.fragment !== currentLocation && loc.fragment !== undefined && loc.fragment.trim().length > 0) {
const props = decodeURIComponent(loc.fragment).split('|')
showPanel(props[0] as AnyComponent, props[1], props[2], 'full')
}
})
export function showPanel (
component: AnyComponent,
_id: string,
_class: string,
element?: PopupAlignment
): void {
const newLoc = encodeURIComponent([component, _id, _class].join('|'))
if (currentLocation === newLoc) {
return
}
currentLocation = newLoc
panelstore.update(() => {
return { panel: { component, _id, _class, element } }
})
const location = getCurrentLocation()
if (location.fragment !== currentLocation) {
location.fragment = currentLocation
navigate(location)
}
}
export function closePanel (): void {
panelstore.update(() => {
return { panel: undefined }
})
const location = getCurrentLocation()
location.fragment = undefined
currentLocation = undefined
navigate(location)
}

51
packages/ui/src/popups.ts Normal file
View File

@ -0,0 +1,51 @@
import { AnySvelteComponent, AnyComponent, PopupAlignment } from './types'
import { getResource } from '@anticrm/platform'
import { writable } from 'svelte/store'
interface CompAndProps {
id: string
is: AnySvelteComponent
props: any
element?: PopupAlignment
onClose?: (result: any) => void
close: () => void
}
export const popupstore = writable<CompAndProps[]>([])
function addPopup (props: CompAndProps): void {
popupstore.update((popups) => {
popups.push(props)
return popups
})
}
let popupId: number = 0
export function showPopup (
component: AnySvelteComponent | AnyComponent,
props: any,
element?: PopupAlignment,
onClose?: (result: any) => void
): () => void {
const id = `${popupId++}`
const closePopupOp = (): void => {
popupstore.update((popups) => {
const pos = popups.findIndex(p => p.id === id)
if (pos !== -1) {
popups.splice(pos, 1)
}
return popups
})
}
if (typeof component === 'string') {
getResource(component).then((resolved) => addPopup({ id, is: resolved, props, element, onClose, close: closePopupOp })).catch((err) => console.log(err))
} else {
addPopup({ id, is: component, props, element, onClose, close: closePopupOp })
}
return closePopupOp
}
export function closePopup (): void {
popupstore.update((popups) => {
popups.pop()
return popups
})
}

View File

@ -0,0 +1,41 @@
import { AnySvelteComponent, AnyComponent, LabelAndProps, TooltipAligment } from './types'
import { IntlString } from '@anticrm/platform'
import { writable } from 'svelte/store'
export const tooltipstore = writable<LabelAndProps>({
label: undefined,
element: undefined,
direction: undefined,
component: undefined,
props: undefined,
anchor: undefined
})
export function showTooltip (
label: IntlString | undefined,
element: HTMLElement,
direction?: TooltipAligment,
component?: AnySvelteComponent | AnyComponent,
props?: any,
anchor?: HTMLElement
): void {
tooltipstore.set({
label: label,
element: element,
direction: direction,
component: component,
props: props,
anchor: anchor
})
}
export function closeTooltip (): void {
tooltipstore.set({
label: undefined,
element: undefined,
direction: undefined,
component: undefined,
props: undefined,
anchor: undefined
})
}

View File

@ -15,15 +15,15 @@
-->
<script lang="ts">
import { Organization } from '@anticrm/contact'
import { showPopup } from '@anticrm/ui'
import { showPanel } from '@anticrm/ui'
import view from '@anticrm/view'
import Company from './icons/Company.svelte'
import { EditDoc } from '@anticrm/view-resources'
export let value: Organization
export let inline: boolean = false
async function onClick () {
showPopup(EditDoc, { _id: value._id, _class: value._class }, 'full')
showPanel(view.component.EditDoc, value._id, value._class, 'full')
}
</script>

View File

@ -16,14 +16,14 @@
<script lang="ts">
import { formatName, Person } from '@anticrm/contact'
import { Avatar } from '@anticrm/presentation'
import { showPopup } from '@anticrm/ui'
import { EditDoc } from '@anticrm/view-resources'
import { showPanel } from '@anticrm/ui'
import view from '@anticrm/view'
export let value: Person
export let inline: boolean = false
async function onClick () {
showPopup(EditDoc, { _id: value._id, _class: value._class }, 'full')
showPanel(view.component.EditDoc, value._id, value._class, 'full')
}
</script>

View File

@ -15,16 +15,16 @@
-->
<script lang="ts">
import { Product } from '@anticrm/inventory'
import { closeTooltip, Icon, showPopup } from '@anticrm/ui'
import { closeTooltip, Icon, showPanel } from '@anticrm/ui'
import view from '@anticrm/view'
import inventory from '../plugin'
import { EditDoc } from '@anticrm/view-resources'
export let value: Product
export let inline: boolean = false
function show () {
closeTooltip()
showPopup(EditDoc, { _id: value._id, _class: value._class }, 'full')
showPanel(view.component.EditDoc, value._id, value._class, 'full')
}
</script>

View File

@ -19,8 +19,9 @@
import { ContactPresenter } from '@anticrm/contact-resources'
import type { WithLookup } from '@anticrm/core'
import type { Lead } from '@anticrm/lead'
import { ActionIcon, IconMoreH, showPopup } from '@anticrm/ui'
import { ContextMenu, EditDoc } from '@anticrm/view-resources'
import { ActionIcon, IconMoreH, showPanel, showPopup } from '@anticrm/ui'
import view from '@anticrm/view'
import { ContextMenu } from '@anticrm/view-resources'
import lead from '../plugin'
export let object: WithLookup<Lead>
@ -31,7 +32,7 @@
}
function showLead () {
showPopup(EditDoc, { _id: object._id, _class: object._class }, 'full')
showPanel(view.component.EditDoc, object._id, object._class, 'full')
}
</script>

View File

@ -15,15 +15,15 @@
-->
<script lang="ts">
import type { Lead } from '@anticrm/lead'
import { closeTooltip, Icon, showPopup } from '@anticrm/ui'
import { closeTooltip, Icon, showPanel } from '@anticrm/ui'
import view from '@anticrm/view'
import lead from '../plugin'
import { EditDoc } from '@anticrm/view-resources'
export let value: Lead
function show () {
closeTooltip()
showPopup(EditDoc, { _id: value._id, _class: value._class }, 'full')
showPanel(view.component.EditDoc, value._id, value._class, 'full')
}
</script>

View File

@ -14,11 +14,12 @@
// limitations under the License.
-->
<script lang="ts">
import type { Applicant } from '@anticrm/recruit'
import { closeTooltip, Icon, showPopup } from '@anticrm/ui'
import { getClient } from '@anticrm/presentation'
import { EditDoc } from '@anticrm/view-resources'
import type { Applicant } from '@anticrm/recruit'
import recruit from '@anticrm/recruit'
import { closeTooltip, Icon } from '@anticrm/ui'
import { showPanel } from '@anticrm/ui/src/panelup'
import view from '@anticrm/view'
export let value: Applicant
@ -27,7 +28,7 @@
function show () {
closeTooltip()
showPopup(EditDoc, { _id: value._id, _class: value._class }, 'full')
showPanel(view.component.EditDoc, value._id, value._class, 'full')
}
</script>

View File

@ -13,22 +13,21 @@
// limitations under the License.
-->
<script lang="ts">
import { Avatar } from '@anticrm/presentation'
import { showPopup, ActionIcon, IconMoreH } from '@anticrm/ui'
import type { WithLookup } from '@anticrm/core'
import type { Applicant } from '@anticrm/recruit'
import { CommentsPresenter } from '@anticrm/chunter-resources'
import { AttachmentsPresenter } from '@anticrm/attachment-resources'
import { CommentsPresenter } from '@anticrm/chunter-resources'
import { formatName } from '@anticrm/contact'
import type { WithLookup } from '@anticrm/core'
import { Avatar } from '@anticrm/presentation'
import type { Applicant } from '@anticrm/recruit'
import { ActionIcon, IconMoreH, showPanel } from '@anticrm/ui'
import view from '@anticrm/view'
import ApplicationPresenter from './ApplicationPresenter.svelte'
import { EditDoc } from '@anticrm/view-resources'
export let object: WithLookup<Applicant>
export let draggable: boolean
function showCandidate () {
showPopup(EditDoc, { _id: object.attachedTo, _class: object.attachedToClass }, 'full')
showPanel(view.component.EditDoc, object.attachedTo, object.attachedToClass, 'full')
}
</script>

View File

@ -14,11 +14,11 @@
// limitations under the License.
-->
<script lang="ts">
import type { Issue } from '@anticrm/task'
import { closeTooltip, Icon, showPopup } from '@anticrm/ui'
import { getClient } from '@anticrm/presentation'
import type { Issue } from '@anticrm/task'
import { closeTooltip, Icon, showPanel } from '@anticrm/ui'
import view from '@anticrm/view'
import task from '../plugin'
import { EditDoc } from '@anticrm/view-resources'
export let value: Issue
@ -27,7 +27,7 @@
function show () {
closeTooltip()
showPopup(EditDoc, { _id: value._id, _class: value._class }, 'full')
showPanel(view.component.EditDoc, value._id, value._class, 'full')
}
</script>

View File

@ -71,6 +71,7 @@ export default async (): Promise<Resources> => ({
DateEditor,
DatePresenter,
RolePresenter,
ObjectPresenter
ObjectPresenter,
EditDoc
}
})

View File

@ -15,7 +15,6 @@
//
import { IntlString, mergeIds } from '@anticrm/platform'
import view, { viewId } from '@anticrm/view'
export default mergeIds(viewId, view, {

View File

@ -171,7 +171,8 @@ const view = plugin(viewId, {
Table: '' as Ref<ViewletDescriptor>
},
component: {
ObjectPresenter: '' as AnyComponent
ObjectPresenter: '' as AnyComponent,
EditDoc: '' as AnyComponent
},
icon: {
Table: '' as Asset,

View File

@ -15,15 +15,15 @@
<script lang="ts">
import type { Ref } from '@anticrm/core';
import { getCurrentLocation,navigate } from '@anticrm/ui';
import type { Application } from '@anticrm/workbench';
import AppItem from './AppItem.svelte';
import type { Ref } from '@anticrm/core'
import { getCurrentLocation, navigate } from '@anticrm/ui'
import type { Application } from '@anticrm/workbench'
import AppItem from './AppItem.svelte'
export let active: Ref<Application> | undefined
export let apps: Application[] = []
function navigateApp(app: Ref<Application>) {
function navigateApp (app: Ref<Application>) {
const loc = getCurrentLocation()
loc.path[1] = app
loc.path.length = 2
@ -34,6 +34,6 @@
<div class="flex-col">
{#each apps as app}
<AppItem selected={app._id === active} icon={app.icon} label={app.label} action={async () => {navigateApp(app._id)}}/>
<AppItem selected={app._id === active} icon={app.icon} label={app.label} action={async () => { navigateApp(app._id) }} notify={false}/>
{/each}
</div>

View File

@ -25,7 +25,8 @@
navigate,
Popup,
showPopup,
TooltipInstance
TooltipInstance,
PanelInstance
} from '@anticrm/ui'
import type { Application, NavigatorModel, ViewConfiguration } from '@anticrm/workbench'
import { onDestroy } from 'svelte'
@ -234,6 +235,7 @@
</div>
<!-- <div class="aside"><Chat thread/></div> -->
</div>
<PanelInstance/>
<Popup />
<TooltipInstance />
{:else}

View File

@ -15,14 +15,12 @@
-->
<script lang="ts">
import type { IntlString, Asset } from '@anticrm/platform'
import type { Ref, Doc, Class, Space } from '@anticrm/core'
import { IconClose, Label, EditBox, ToggleWithLabel, Grid, Icon } from '@anticrm/ui'
import { getClient, createQuery, AttributeBarEditor } from '@anticrm/presentation'
import { createEventDispatcher } from 'svelte'
import type { Class, Ref, Space } from '@anticrm/core'
import core from '@anticrm/core'
import type { IntlString } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import { EditBox, Grid, Icon, IconClose, Label, ToggleWithLabel } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
export let _id: Ref<Space>
export let spaceClass: Ref<Class<Space>>
@ -43,7 +41,7 @@
const tabs: IntlString[] = ['General' as IntlString, 'Members' as IntlString]
let selected = 0
function onNameChange(ev: Event) {
function onNameChange (ev: Event) {
client.updateDoc(spaceClass, space.space, space._id, { name: (ev.target as HTMLInputElement).value })
}

View File

@ -71,20 +71,18 @@
</script>
<div>
<TreeNode label={model.label} actions={[addSpace]}>
<TreeNode label={model.label} actions={async () => [addSpace]}>
{#each spaces as space}
{#await getActions(space) then actions}
<TreeItem
_id={space._id}
title={space.name}
icon={classIcon(client, space._class)}
selected={currentSpace === space._id}
{actions}
on:click={() => {
selectSpace(space._id)
}}
/>
{/await}
<TreeItem
_id={space._id}
title={space.name}
icon={classIcon(client, space._class)}
selected={currentSpace === space._id}
actions={() => getActions(space)}
on:click={() => {
selectSpace(space._id)
}}
/>
{/each}
</TreeNode>
</div>

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import Collapsed from '../icons/Collapsed.svelte'
import Expanded from '../icons/Expanded.svelte'
@ -31,18 +30,23 @@
export let node = false
export let collapsed = false
export let selected = false
export let actions: Action[] = []
export let actions: () => Promise<Action[]> = async () => []
const dispatch = createEventDispatcher()
let hovered = false
function onMenuClick(ev: MouseEvent) {
async function onMenuClick (ev: MouseEvent) {
showPopup(Menu, { actions: await actions(), ctx: _id }, ev.target as HTMLElement, () => {
hovered = false
})
hovered = true
showPopup(Menu, { actions, ctx: _id }, ev.target as HTMLElement, () => { hovered = false })
}
</script>
<div class="container" class:selected class:hovered
<div
class="container"
class:selected
class:hovered
on:click|stopPropagation={() => {
if (node && !icon) collapsed = !collapsed
dispatch('click')
@ -50,29 +54,42 @@
>
<div class="icon" class:sub={!node}>
{#if icon}
<Icon {icon} size={'small'}/>
{:else}
{#if collapsed}<Collapsed size={'small'} />{:else}<Expanded size={'small'} />{/if}
{/if}
<Icon {icon} size={'small'} />
{:else if collapsed}<Collapsed size={'small'} />{:else}<Expanded size={'small'} />{/if}
</div>
<span class="label" class:sub={node}>
{#if label}<Label {label}/>{:else}{title}{/if}
{#if label}<Label {label} />{:else}{title}{/if}
</span>
{#if actions.length === 1}
<div class="tool">
<ActionIcon label={actions[0].label} icon={actions[0].icon} size={'small'} action={(ev) => { actions[0].action(_id, ev) }} />
</div>
{:else if actions.length > 1}
{#if node === false}
<div class="tool" on:click|stopPropagation={onMenuClick}>
<IconMoreV size={'small'} />
</div>
{:else}
{#await actions() then actionItems}
{#if actionItems.length === 1}
<div class="tool">
<ActionIcon
label={actionItems[0].label}
icon={actionItems[0].icon}
size={'small'}
action={(ev) => {
actionItems[0].action(_id, ev)
}}
/>
</div>
{:else if actionItems.length > 1}
<div class="tool" on:click|stopPropagation={onMenuClick}>
<IconMoreV size={'small'} />
</div>
{/if}
{/await}
{/if}
{#if notifications > 0 && collapsed}
<div class="counter">{notifications}</div>
{/if}
</div>
{#if node && !icon && !collapsed}
<div class="dropbox"><slot/></div>
<div class="dropbox"><slot /></div>
{/if}
<style lang="scss">
@ -81,19 +98,21 @@
align-items: center;
margin: 0 1rem;
height: 2.25rem;
border-radius: .5rem;
border-radius: 0.5rem;
user-select: none;
cursor: pointer;
.icon {
min-width: 1rem;
color: var(--theme-content-trans-color);
margin: 0 1.125rem 0 .625rem;
&.sub { margin: 0 .5rem 0 2.75rem }
margin: 0 1.125rem 0 0.625rem;
&.sub {
margin: 0 0.5rem 0 2.75rem;
}
}
.label {
flex-grow: 1;
margin-right: .75rem;
margin-right: 0.75rem;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@ -106,31 +125,34 @@
}
}
.tool {
margin-right: .75rem;
margin-right: 0.75rem;
visibility: hidden;
}
.counter {
margin-right: .75rem;
margin-right: 0.75rem;
font-weight: 600;
font-size: .75rem;
font-size: 0.75rem;
color: var(--theme-caption-color);
}
&:hover, &.hovered {
&:hover,
&.hovered {
background-color: var(--theme-button-bg-enabled);
.tool {
visibility: visible;
}
}
&.selected {
background-color: var(--theme-menu-selection);
&:hover { background-color: var(--theme-button-bg-enabled); }
&:hover {
background-color: var(--theme-button-bg-enabled);
}
}
}
.dropbox {
height: auto;
margin-bottom: .5rem;
margin-bottom: 0.5rem;
}
</style>
</style>

View File

@ -24,11 +24,11 @@
export let icon: Asset
export let title: string
export let notifications = 0
export let actions: Action[] = []
export let actions: () => Promise<Action[]> = async () => []
export let selected: boolean = false
const dispatch = createEventDispatcher()
</script>
<TreeElement {_id} {icon} {title} {notifications} {selected} {actions} collapsed on:click={() => {dispatch('click')}}/>
<TreeElement {_id} {icon} {title} {notifications} {selected} {actions} collapsed on:click={() => { dispatch('click') }}/>

View File

@ -19,7 +19,7 @@
import TreeElement from './TreeElement.svelte'
export let label: IntlString
export let actions: Action[] = []
export let actions: () => Promise<Action[]> = async () => []
export let notifications = 0
export let collapsed = false