mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-26 13:47:26 +03:00
Basic focus management support (#1719)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
fe109a6b30
commit
fee0f28b07
@ -389,7 +389,7 @@ export function createModel (builder: Builder): void {
|
||||
action: view.actionImpl.SelectItem,
|
||||
keyBinding: ['keyX'],
|
||||
category: view.category.General,
|
||||
input: 'focus',
|
||||
input: 'any',
|
||||
target: core.class.Doc,
|
||||
context: { mode: 'browser' }
|
||||
},
|
||||
@ -434,7 +434,7 @@ export function createModel (builder: Builder): void {
|
||||
input: 'none',
|
||||
target: core.class.Doc,
|
||||
context: {
|
||||
mode: ['workbench', 'browser', 'popup', 'panel', 'editor', 'input']
|
||||
mode: ['workbench', 'browser', 'panel', 'editor', 'input']
|
||||
}
|
||||
},
|
||||
view.action.ShowActions
|
||||
|
@ -45,7 +45,7 @@
|
||||
{#if $$slots.space}
|
||||
<slot name="space" />
|
||||
{:else if spaceClass && spaceLabel && spacePlaceholder}
|
||||
<SpaceSelect _class={spaceClass} {spaceQuery} label={spaceLabel} bind:value={space} />
|
||||
<SpaceSelect focus focusIndex={-10} _class={spaceClass} {spaceQuery} label={spaceLabel} bind:value={space} />
|
||||
{/if}
|
||||
<span class="antiCard-header__divider">›</span>
|
||||
{/if}
|
||||
@ -53,6 +53,7 @@
|
||||
</div>
|
||||
<div class="buttons-group small-gap">
|
||||
<Button
|
||||
focusIndex={10002}
|
||||
icon={IconClose}
|
||||
kind={'transparent'}
|
||||
on:click={() => {
|
||||
@ -73,6 +74,7 @@
|
||||
<MiniToggle label={presentation.string.CreateMore} bind:on={createMore} />
|
||||
{/if}
|
||||
<Button
|
||||
focusIndex={10001}
|
||||
disabled={!canSave}
|
||||
label={okLabel}
|
||||
kind={'primary'}
|
||||
|
@ -16,7 +16,7 @@
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { getClient } from '../utils'
|
||||
|
||||
import { Label, showPopup, IconFolder, Button, eventToHTMLElement } from '@anticrm/ui'
|
||||
import { Label, showPopup, IconFolder, Button, eventToHTMLElement, getFocusManager } from '@anticrm/ui'
|
||||
import SpacesPopup from './SpacesPopup.svelte'
|
||||
|
||||
import type { Ref, Class, Space, DocumentQuery } from '@anticrm/core'
|
||||
@ -25,11 +25,14 @@
|
||||
export let spaceQuery: DocumentQuery<Space> | undefined = { archived: false }
|
||||
export let label: IntlString
|
||||
export let value: Ref<Space> | undefined
|
||||
export let focusIndex = -1
|
||||
export let focus = false
|
||||
|
||||
let selected: Space | undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const mgr = getFocusManager()
|
||||
async function updateSelected (value: Ref<Space> | undefined) {
|
||||
selected = value !== undefined ? await client.findOne(_class, { ...(spaceQuery ?? {}), _id: value }) : undefined
|
||||
}
|
||||
@ -38,6 +41,8 @@
|
||||
</script>
|
||||
|
||||
<Button
|
||||
{focus}
|
||||
{focusIndex}
|
||||
icon={IconFolder}
|
||||
size={'small'}
|
||||
kind={'no-border'}
|
||||
@ -45,6 +50,7 @@
|
||||
showPopup(SpacesPopup, { _class, spaceQuery }, eventToHTMLElement(ev), (result) => {
|
||||
if (result) {
|
||||
value = result._id
|
||||
mgr?.setFocusPos(focusIndex)
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
@ -19,6 +19,7 @@
|
||||
import { createQuery } from '../utils'
|
||||
import SpaceInfo from './SpaceInfo.svelte'
|
||||
import presentation from '..'
|
||||
import { ListView } from '@anticrm/ui'
|
||||
|
||||
export let _class: Ref<Class<Space>>
|
||||
export let spaceQuery: DocumentQuery<Space> | undefined
|
||||
@ -42,24 +43,64 @@
|
||||
onMount(() => {
|
||||
if (input) input.focus()
|
||||
})
|
||||
|
||||
let selection = 0
|
||||
let list: ListView
|
||||
|
||||
async function handleSelection (evt: Event | undefined, selection: number): Promise<void> {
|
||||
const space = objects[selection]
|
||||
dispatch('close', space)
|
||||
}
|
||||
|
||||
function onKeydown (key: KeyboardEvent): void {
|
||||
if (key.code === 'ArrowUp') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(selection - 1)
|
||||
}
|
||||
if (key.code === 'ArrowDown') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(selection + 1)
|
||||
}
|
||||
if (key.code === 'Enter') {
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
handleSelection(key, selection)
|
||||
}
|
||||
if (key.code === 'Escape') {
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
dispatch('close')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="selectPopup">
|
||||
<div class="selectPopup" on:keydown={onKeydown}>
|
||||
<div class="header">
|
||||
<input bind:this={input} type="text" bind:value={search} placeholder={phTraslate} on:input={() => {}} on:change />
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#each objects as space}
|
||||
<button
|
||||
class="menu-item flex-between"
|
||||
on:click={() => {
|
||||
dispatch('close', space)
|
||||
}}
|
||||
>
|
||||
<SpaceInfo size={'large'} value={space} />
|
||||
</button>
|
||||
{/each}
|
||||
<ListView
|
||||
bind:this={list}
|
||||
count={objects.length}
|
||||
bind:selection
|
||||
on:click={(evt) => handleSelection(evt, evt.detail)}
|
||||
>
|
||||
<svelte:fragment slot="item" let:item>
|
||||
{@const space = objects[item]}
|
||||
|
||||
<button
|
||||
class="menu-item flex-between"
|
||||
on:click={() => {
|
||||
handleSelection(undefined, item)
|
||||
}}
|
||||
>
|
||||
<SpaceInfo size={'large'} value={space} />
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</ListView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,7 +17,7 @@
|
||||
import contact, { Contact, formatName } from '@anticrm/contact'
|
||||
import type { Class, Ref } from '@anticrm/core'
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import type { TooltipAlignment, ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import { TooltipAlignment, ButtonKind, ButtonSize, getFocusManager } from '@anticrm/ui'
|
||||
import { Button, Label, showPopup, Tooltip } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import presentation from '..'
|
||||
@ -38,6 +38,7 @@
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = undefined
|
||||
export let labelDirection: TooltipAlignment | undefined = undefined
|
||||
export let focusIndex = -1
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -56,11 +57,13 @@
|
||||
const isPerson = client.getHierarchy().isDerived(obj._class, contact.class.Person)
|
||||
return isPerson ? formatName(obj.name) : obj.name
|
||||
}
|
||||
const mgr = getFocusManager()
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class="min-w-0">
|
||||
<Tooltip {label} fill={width === '100%'} direction={labelDirection}>
|
||||
<Button
|
||||
{focusIndex}
|
||||
icon={size === 'x-large' && selected ? undefined : IconPerson}
|
||||
width={width ?? 'min-content'}
|
||||
{size}
|
||||
@ -81,6 +84,7 @@
|
||||
value = result._id
|
||||
dispatch('change', value)
|
||||
}
|
||||
mgr?.setFocusPos(focusIndex)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
import { translate } from '@anticrm/platform'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
|
||||
import { Tooltip, CheckBox } from '@anticrm/ui'
|
||||
import { Tooltip, CheckBox, ListView } from '@anticrm/ui'
|
||||
import UserInfo from './UserInfo.svelte'
|
||||
|
||||
import type { Ref, Class } from '@anticrm/core'
|
||||
@ -70,43 +70,85 @@
|
||||
onMount(() => {
|
||||
if (input) input.focus()
|
||||
})
|
||||
|
||||
let selection = 0
|
||||
let list: ListView
|
||||
|
||||
async function handleSelection (evt: Event | undefined, selection: number): Promise<void> {
|
||||
const person = objects[selection]
|
||||
|
||||
if (!multiSelect) {
|
||||
selected = person._id === selected ? undefined : person._id
|
||||
dispatch('close', selected !== undefined ? person : undefined)
|
||||
} else {
|
||||
checkSelected(person)
|
||||
}
|
||||
}
|
||||
|
||||
function onKeydown (key: KeyboardEvent): void {
|
||||
if (key.code === 'ArrowUp') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(selection - 1)
|
||||
}
|
||||
if (key.code === 'ArrowDown') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(selection + 1)
|
||||
}
|
||||
if (key.code === 'Enter') {
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
handleSelection(key, selection)
|
||||
}
|
||||
if (key.code === 'Escape') {
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
dispatch('close')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="selectPopup" class:plainContainer={!shadows}>
|
||||
<div class="selectPopup" class:plainContainer={!shadows} on:keydown={onKeydown}>
|
||||
<div class="header">
|
||||
<input bind:this={input} type="text" bind:value={search} placeholder={phTraslate} on:change />
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#each objects as person}
|
||||
<button
|
||||
class="menu-item"
|
||||
on:click={() => {
|
||||
if (!multiSelect) {
|
||||
selected = person._id === selected ? undefined : person._id
|
||||
dispatch('close', selected !== undefined ? person : undefined)
|
||||
} else checkSelected(person)
|
||||
}}
|
||||
>
|
||||
{#if multiSelect}
|
||||
<div class="check pointer-events-none">
|
||||
<CheckBox checked={isSelected(person)} primary />
|
||||
</div>
|
||||
{/if}
|
||||
<UserInfo size={'x-small'} value={person} />
|
||||
{#if allowDeselect && person._id === selected}
|
||||
<div class="check-right pointer-events-none">
|
||||
{#if titleDeselect}
|
||||
<Tooltip label={titleDeselect ?? presentation.string.Deselect}>
|
||||
<ListView
|
||||
bind:this={list}
|
||||
count={objects.length}
|
||||
bind:selection
|
||||
on:click={(evt) => handleSelection(evt, evt.detail)}
|
||||
>
|
||||
<svelte:fragment slot="item" let:item>
|
||||
{@const person = objects[item]}
|
||||
<button
|
||||
class="menu-item w-full"
|
||||
on:click={() => {
|
||||
handleSelection(undefined, item)
|
||||
}}
|
||||
>
|
||||
{#if multiSelect}
|
||||
<div class="check pointer-events-none">
|
||||
<CheckBox checked={isSelected(person)} primary />
|
||||
</div>
|
||||
{/if}
|
||||
<UserInfo size={'x-small'} value={person} />
|
||||
{#if allowDeselect && person._id === selected}
|
||||
<div class="check-right pointer-events-none">
|
||||
{#if titleDeselect}
|
||||
<Tooltip label={titleDeselect ?? presentation.string.Deselect}>
|
||||
<CheckBox checked circle primary />
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<CheckBox checked circle primary />
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<CheckBox checked circle primary />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</ListView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,12 +13,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { IntlString, Asset } from '@anticrm/platform'
|
||||
import type { AnySvelteComponent, ButtonKind, ButtonSize } from '../types'
|
||||
import Spinner from './Spinner.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import Icon from './Icon.svelte'
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
import { onMount } from 'svelte'
|
||||
import { registerFocus } from '../focus'
|
||||
import type { AnySvelteComponent, ButtonKind, ButtonSize } from '../types'
|
||||
import Icon from './Icon.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import Spinner from './Spinner.svelte'
|
||||
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let labelParams: Record<string, any> = {}
|
||||
@ -38,7 +39,6 @@
|
||||
export let title: string | undefined = undefined
|
||||
export let borderStyle: 'solid' | 'dashed' = 'solid'
|
||||
export let id: string | undefined = undefined
|
||||
|
||||
export let input: HTMLButtonElement | undefined = undefined
|
||||
|
||||
$: iconOnly = label === undefined && $$slots.content === undefined
|
||||
@ -53,8 +53,31 @@
|
||||
click = false
|
||||
}
|
||||
})
|
||||
|
||||
// Focusable control with index
|
||||
export let focusIndex = -1
|
||||
const { idx, focusManager } = registerFocus(focusIndex, {
|
||||
focus: () => {
|
||||
if (!disabled) {
|
||||
input?.focus()
|
||||
}
|
||||
return !disabled && input != null
|
||||
},
|
||||
isFocus: () => document.activeElement === input
|
||||
})
|
||||
|
||||
$: if (idx !== -1 && focusManager) {
|
||||
focusManager.updateFocus(idx, focusIndex)
|
||||
}
|
||||
|
||||
$: if (input != null) {
|
||||
input.addEventListener('focus', () => {
|
||||
focusManager?.setFocus(idx)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- {focusIndex} -->
|
||||
<button
|
||||
bind:this={input}
|
||||
class="button {kind} {size} jf-{justify}"
|
||||
@ -172,7 +195,7 @@
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
border-color: var(--primary-edit-border-color);
|
||||
border-color: var(--accent-color) !important;
|
||||
}
|
||||
&:disabled {
|
||||
color: rgb(var(--caption-color) / 40%);
|
||||
|
@ -15,8 +15,8 @@
|
||||
<script lang="ts">
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getPlatformColor } from '..'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { getPlatformColor, ListView } from '..'
|
||||
|
||||
export let placeholder: IntlString | undefined = undefined
|
||||
export let placeholderParam: any | undefined = undefined
|
||||
@ -33,27 +33,79 @@
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: objects = value.filter((el) => el.label.toLowerCase().includes(search.toLowerCase()))
|
||||
|
||||
let selection = 0
|
||||
let list: ListView
|
||||
|
||||
async function handleSelection (evt: Event | undefined, selection: number): Promise<void> {
|
||||
const space = objects[selection]
|
||||
dispatch('close', space)
|
||||
}
|
||||
|
||||
function onKeydown (key: KeyboardEvent): void {
|
||||
if (key.code === 'ArrowUp') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(selection - 1)
|
||||
}
|
||||
if (key.code === 'ArrowDown') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(selection + 1)
|
||||
}
|
||||
if (key.code === 'Enter') {
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
handleSelection(key, selection)
|
||||
}
|
||||
if (key.code === 'Escape') {
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
dispatch('close')
|
||||
}
|
||||
}
|
||||
let input: HTMLElement
|
||||
onMount(() => {
|
||||
if (input) input.focus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="selectPopup">
|
||||
<div class="selectPopup" on:keydown={onKeydown}>
|
||||
{#if searchable}
|
||||
<div class="header">
|
||||
<input type="text" bind:value={search} placeholder={phTraslate} on:input={(ev) => {}} on:change />
|
||||
<input
|
||||
bind:this={input}
|
||||
type="text"
|
||||
bind:value={search}
|
||||
placeholder={phTraslate}
|
||||
on:input={(ev) => {}}
|
||||
on:change
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#each value.filter((el) => el.label.toLowerCase().includes(search.toLowerCase())) as item}
|
||||
<button
|
||||
class="menu-item"
|
||||
on:click={() => {
|
||||
dispatch('close', item)
|
||||
}}
|
||||
>
|
||||
<div class="color" style="background-color: {getPlatformColor(item.color)}" />
|
||||
<span class="label">{item.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
<ListView
|
||||
bind:this={list}
|
||||
count={objects.length}
|
||||
bind:selection
|
||||
on:click={(evt) => handleSelection(evt, evt.detail)}
|
||||
>
|
||||
<svelte:fragment slot="item" let:item>
|
||||
{@const itemValue = objects[item]}
|
||||
<button
|
||||
class="menu-item"
|
||||
on:click={() => {
|
||||
dispatch('close', itemValue)
|
||||
}}
|
||||
>
|
||||
<div class="color" style="background-color: {getPlatformColor(itemValue.color)}" />
|
||||
<span class="label">{itemValue.label}</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</ListView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,13 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, afterUpdate } from 'svelte'
|
||||
import type { IntlString, Asset } from '@anticrm/platform'
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import type { AnySvelteComponent } from '../types'
|
||||
import Label from './Label.svelte'
|
||||
import Icon from './Icon.svelte'
|
||||
import { afterUpdate, createEventDispatcher, onMount } from 'svelte'
|
||||
import { registerFocus } from '../focus'
|
||||
import plugin from '../plugin'
|
||||
import type { AnySvelteComponent } from '../types'
|
||||
import Icon from './Icon.svelte'
|
||||
import Label from './Label.svelte'
|
||||
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
@ -66,6 +67,22 @@
|
||||
afterUpdate(() => {
|
||||
computeSize(input)
|
||||
})
|
||||
|
||||
// Focusable control with index
|
||||
export let focusIndex = -1
|
||||
const { idx, focusManager } = registerFocus(focusIndex, {
|
||||
focus: () => {
|
||||
input?.focus()
|
||||
return input != null
|
||||
},
|
||||
isFocus: () => document.activeElement === input
|
||||
})
|
||||
|
||||
$: if (input) {
|
||||
input.addEventListener('focus', () => {
|
||||
focusManager?.setFocus(idx)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@ -74,6 +91,7 @@
|
||||
input.focus()
|
||||
}}
|
||||
>
|
||||
<!-- {focusIndex} -->
|
||||
<div class="hidden-text {kind}" bind:this={text} />
|
||||
{#if label}<div class="label"><Label {label} /></div>{/if}
|
||||
<div class="{kind} flex-row-center clear-mins">
|
||||
|
16
packages/ui/src/components/FocusHandler.svelte
Normal file
16
packages/ui/src/components/FocusHandler.svelte
Normal file
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { FocusManager } from '../focus'
|
||||
|
||||
export let manager: FocusManager
|
||||
|
||||
function handleKey (evt: KeyboardEvent): void {
|
||||
if (evt.code === 'Tab' && manager.hasFocus()) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
manager.next(evt.shiftKey ? -1 : 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={(evt) => handleKey(evt)} />
|
||||
<slot />
|
@ -13,11 +13,11 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, afterUpdate, onMount } from 'svelte'
|
||||
import { afterUpdate, createEventDispatcher, onMount } from 'svelte'
|
||||
import ui from '../plugin'
|
||||
import { Action } from '../types'
|
||||
import Icon from './Icon.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import ui from '../plugin'
|
||||
|
||||
export let actions: Action[] = []
|
||||
export let ctx: any = undefined
|
||||
@ -26,6 +26,9 @@
|
||||
const btns: HTMLButtonElement[] = []
|
||||
|
||||
const keyDown = (ev: KeyboardEvent, n: number): void => {
|
||||
if (ev.key === 'Tab') {
|
||||
dispatch('close')
|
||||
}
|
||||
if (ev.key === 'ArrowDown') {
|
||||
if (n === btns.length - 1) btns[0].focus()
|
||||
else btns[n + 1].focus()
|
||||
|
@ -108,7 +108,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{#if props.element !== 'content'}
|
||||
<div class="modal-overlay" class:show on:click={() => escapeClose()} />
|
||||
<div class="modal-overlay" class:show on:click={() => escapeClose()} on:keydown={() => {}} on:keyup={() => {}} />
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -14,8 +14,8 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { afterUpdate, onDestroy } from 'svelte'
|
||||
import { tooltipstore as tooltip, closeTooltip, Component } from '..'
|
||||
import type { TooltipAlignment } from '..'
|
||||
import { closeTooltip, Component, tooltipstore as tooltip } from '..'
|
||||
import Label from './Label.svelte'
|
||||
|
||||
let tooltipHTML: HTMLElement
|
||||
@ -151,6 +151,13 @@
|
||||
on:mousemove={(ev) => {
|
||||
whileShow(ev)
|
||||
}}
|
||||
on:keydown={(evt) => {
|
||||
if (($tooltip.component || $tooltip.label) && evt.key === 'Escape') {
|
||||
evt.preventDefault()
|
||||
evt.stopImmediatePropagation()
|
||||
hideTooltip()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if $tooltip.component}
|
||||
<div class="popup-tooltip" class:doublePadding={$tooltip.label} bind:clientWidth={clWidth} bind:this={tooltipHTML}>
|
||||
|
115
packages/ui/src/focus.ts
Normal file
115
packages/ui/src/focus.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { getContext, setContext } from 'svelte'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface FocusManager {
|
||||
next: (inc?: 1 | -1) => void
|
||||
setFocus: (idx: number) => void
|
||||
setFocusPos: (order: number) => void
|
||||
updateFocus: (idx: number, order: number) => void
|
||||
|
||||
// Check if current manager has focus
|
||||
hasFocus: () => boolean
|
||||
}
|
||||
|
||||
class FocusManagerImpl implements FocusManager {
|
||||
counter = 0
|
||||
elements: Array<{
|
||||
id: number
|
||||
order: number
|
||||
focus: () => boolean
|
||||
isFocus: () => boolean
|
||||
}> = []
|
||||
|
||||
current = 0
|
||||
register (order: number, focus: () => boolean, isFocus: () => boolean): number {
|
||||
const el = { id: this.counter++, order, focus, isFocus }
|
||||
this.elements.push(el)
|
||||
this.sort()
|
||||
return el.id
|
||||
}
|
||||
|
||||
sort (): void {
|
||||
// this.needSort = 0
|
||||
this.elements.sort((a, b) => {
|
||||
return a.order - b.order
|
||||
})
|
||||
}
|
||||
|
||||
next (inc?: 1 | -1): void {
|
||||
while (true) {
|
||||
this.current = this.current + (inc ?? 1)
|
||||
if (this.elements[Math.abs(this.current) % this.elements.length].focus()) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setFocus (idx: number): void {
|
||||
this.current = this.elements.findIndex((it) => it.id === idx) ?? 0
|
||||
this.elements[Math.abs(this.current) % this.elements.length].focus()
|
||||
}
|
||||
|
||||
setFocusPos (order: number): void {
|
||||
const idx = this.elements.findIndex((it) => it.order === order)
|
||||
if (idx !== undefined) {
|
||||
this.current = idx
|
||||
this.elements[Math.abs(this.current) % this.elements.length].focus()
|
||||
}
|
||||
}
|
||||
|
||||
updateFocus (idx: number, order: number): void {
|
||||
const el = this.elements.find((it) => it.id === idx)
|
||||
if (el !== undefined) {
|
||||
if (el.order !== order) {
|
||||
el.order = order
|
||||
this.sort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasFocus (): boolean {
|
||||
for (const el of this.elements) {
|
||||
if (el.isFocus()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function createFocusManager (): FocusManager {
|
||||
const mgr = new FocusManagerImpl()
|
||||
setFocusManager(mgr)
|
||||
return mgr
|
||||
}
|
||||
|
||||
export function setFocusManager (manager: FocusManager): void {
|
||||
setContext('ui.focus.elements', manager)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getFocusManager (): FocusManager | undefined {
|
||||
return getContext('ui.focus.elements')
|
||||
}
|
||||
|
||||
/**
|
||||
* Register new focus reciever if order !== -1
|
||||
* @public
|
||||
*/
|
||||
export function registerFocus (
|
||||
order: number,
|
||||
item: { focus: () => boolean, isFocus: () => boolean }
|
||||
): { idx: number, focusManager?: FocusManager } {
|
||||
const focusManager = getFocusManager() as FocusManagerImpl
|
||||
if (order === -1) {
|
||||
return { idx: -1, focusManager }
|
||||
}
|
||||
return { idx: focusManager?.register(order, item.focus, item.isFocus) ?? -1, focusManager }
|
||||
}
|
@ -128,11 +128,22 @@ export { default as IconDetails } from './components/icons/Details.svelte'
|
||||
export { default as PanelInstance } from './components/PanelInstance.svelte'
|
||||
export { default as Panel } from './components/Panel.svelte'
|
||||
|
||||
export { default as MonthCalendar } from './components/calendar/MonthCalendar.svelte'
|
||||
export { default as YearCalendar } from './components/calendar/YearCalendar.svelte'
|
||||
export { default as WeekCalendar } from './components/calendar/WeekCalendar.svelte'
|
||||
|
||||
export { default as FocusHandler } from './components/FocusHandler.svelte'
|
||||
export { default as ListView } from './components/ListView.svelte'
|
||||
|
||||
export * from './types'
|
||||
export * from './location'
|
||||
export * from './utils'
|
||||
export * from './popups'
|
||||
export * from './tooltips'
|
||||
export * from './panelup'
|
||||
export * from './components/calendar/internal/DateUtils'
|
||||
export * from './colors'
|
||||
export * from './focus'
|
||||
|
||||
export function createApp (target: HTMLElement): SvelteComponent {
|
||||
return new Root({ target })
|
||||
@ -151,8 +162,3 @@ addStringsLoader(uiId, async (lang: string) => {
|
||||
addLocation(uiId, async () => ({ default: async () => ({}) }))
|
||||
|
||||
export { default } from './plugin'
|
||||
export * from './colors'
|
||||
|
||||
export { default as MonthCalendar } from './components/calendar/MonthCalendar.svelte'
|
||||
export { default as YearCalendar } from './components/calendar/YearCalendar.svelte'
|
||||
export { default as WeekCalendar } from './components/calendar/WeekCalendar.svelte'
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { AnySvelteComponent, AnyComponent, LabelAndProps, TooltipAlignment } from './types'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import { writable } from 'svelte/store'
|
||||
import { AnyComponent, AnySvelteComponent, LabelAndProps, TooltipAlignment } from './types'
|
||||
|
||||
export const tooltipstore = writable<LabelAndProps>({
|
||||
const emptyTooltip: LabelAndProps = {
|
||||
label: undefined,
|
||||
element: undefined,
|
||||
direction: undefined,
|
||||
@ -10,7 +10,8 @@ export const tooltipstore = writable<LabelAndProps>({
|
||||
props: undefined,
|
||||
anchor: undefined,
|
||||
onUpdate: undefined
|
||||
})
|
||||
}
|
||||
export const tooltipstore = writable<LabelAndProps>(emptyTooltip)
|
||||
|
||||
export function showTooltip (
|
||||
label: IntlString | undefined,
|
||||
@ -33,13 +34,5 @@ export function showTooltip (
|
||||
}
|
||||
|
||||
export function closeTooltip (): void {
|
||||
tooltipstore.set({
|
||||
label: undefined,
|
||||
element: undefined,
|
||||
direction: undefined,
|
||||
component: undefined,
|
||||
props: undefined,
|
||||
anchor: undefined,
|
||||
onUpdate: undefined
|
||||
})
|
||||
tooltipstore.set(emptyTooltip)
|
||||
}
|
||||
|
@ -13,9 +13,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
import { /* Metadata, Plugin, plugin, */ Resource /*, Service */ } from '@anticrm/platform'
|
||||
import { /* getContext, */ SvelteComponent } from 'svelte'
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
|
||||
/**
|
||||
* Describe a browser URI location parsed to path, query and fragment.
|
||||
|
@ -16,8 +16,9 @@
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import { Button, IconClose, closeTooltip, IconBlueCheck } from '@anticrm/ui'
|
||||
import { Button, IconClose, closeTooltip, IconBlueCheck, registerFocus, createFocusManager } from '@anticrm/ui'
|
||||
import IconCopy from './icons/Copy.svelte'
|
||||
import { FocusHandler } from '@anticrm/ui'
|
||||
|
||||
export let value: string = ''
|
||||
export let placeholder: IntlString
|
||||
@ -31,8 +32,25 @@
|
||||
onMount(() => {
|
||||
if (input) input.focus()
|
||||
})
|
||||
|
||||
const mgr = createFocusManager()
|
||||
|
||||
const { idx } = registerFocus(1, {
|
||||
focus: () => {
|
||||
input?.focus()
|
||||
return true
|
||||
},
|
||||
isFocus: () => document.activeElement === input
|
||||
})
|
||||
|
||||
$: if (input) {
|
||||
input.addEventListener('focus', () => {
|
||||
mgr.setFocus(idx)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<FocusHandler manager={mgr} />
|
||||
<div class="buttons-group xsmall-gap">
|
||||
{#if editable}
|
||||
<input
|
||||
@ -51,6 +69,7 @@
|
||||
on:change
|
||||
/>
|
||||
<Button
|
||||
focusIndex={2}
|
||||
kind={'transparent'}
|
||||
size={'small'}
|
||||
icon={IconClose}
|
||||
@ -66,6 +85,7 @@
|
||||
<span>{value}</span>
|
||||
{/if}
|
||||
<Button
|
||||
focusIndex={3}
|
||||
kind={'transparent'}
|
||||
size={'small'}
|
||||
icon={IconCopy}
|
||||
@ -75,6 +95,7 @@
|
||||
/>
|
||||
{#if editable}
|
||||
<Button
|
||||
focusIndex={4}
|
||||
kind={'transparent'}
|
||||
size={'small'}
|
||||
icon={IconBlueCheck}
|
||||
|
@ -14,17 +14,28 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { Channel, ChannelProvider } from '@anticrm/contact'
|
||||
import contact from '@anticrm/contact'
|
||||
import type { AttachedData, Doc, Ref, Timestamp } from '@anticrm/core'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
import { AnyComponent, showPopup, Button, Menu, showTooltip, closeTooltip, eventToHTMLElement } from '@anticrm/ui'
|
||||
import type { Action, ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import presentation from '@anticrm/presentation'
|
||||
import {
|
||||
Action,
|
||||
AnyComponent,
|
||||
Button,
|
||||
ButtonKind,
|
||||
ButtonSize,
|
||||
closeTooltip,
|
||||
eventToHTMLElement,
|
||||
getFocusManager,
|
||||
Menu,
|
||||
showPopup,
|
||||
showTooltip
|
||||
} from '@anticrm/ui'
|
||||
import { createEventDispatcher, tick } from 'svelte'
|
||||
import { getChannelProviders } from '../utils'
|
||||
import ChannelEditor from './ChannelEditor.svelte'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
|
||||
export let value: AttachedData<Channel>[] | Channel | null
|
||||
export let editable: boolean = false
|
||||
@ -33,6 +44,7 @@
|
||||
export let length: 'short' | 'full' = 'full'
|
||||
export let shape: 'circle' | undefined = undefined
|
||||
export let integrations: Set<Ref<Doc>> = new Set<Ref<Doc>>()
|
||||
export let focusIndex = -1
|
||||
|
||||
const notificationClient = NotificationClientImpl.getClient()
|
||||
const lastViews = notificationClient.getLastViews()
|
||||
@ -79,7 +91,7 @@
|
||||
}
|
||||
|
||||
async function update (value: AttachedData<Channel>[] | Channel | null, lastViews: Map<Ref<Doc>, Timestamp>) {
|
||||
if (value === null) {
|
||||
if (value == null) {
|
||||
displayItems = []
|
||||
return
|
||||
}
|
||||
@ -99,7 +111,7 @@
|
||||
}
|
||||
}
|
||||
displayItems = result
|
||||
updateMenu()
|
||||
updateMenu(displayItems)
|
||||
}
|
||||
|
||||
$: if (value) update(value, $lastViews)
|
||||
@ -112,23 +124,30 @@
|
||||
let anchor: HTMLElement
|
||||
|
||||
function filterUndefined (channels: AttachedData<Channel>[]): AttachedData<Channel>[] {
|
||||
return channels.filter((channel) => channel.value !== undefined && channel.value.length > 0)
|
||||
return channels.filter((channel) => channel.value !== undefined)
|
||||
}
|
||||
const focusManager = getFocusManager()
|
||||
|
||||
getChannelProviders().then((pr) => (providers = pr))
|
||||
|
||||
const updateMenu = (): void => {
|
||||
const updateMenu = (_displayItems: Item[]): void => {
|
||||
actions = []
|
||||
providers.forEach((pr) => {
|
||||
if (displayItems.filter((it) => it.provider === pr._id).length === 0) {
|
||||
if (_displayItems.filter((it) => it.provider === pr._id).length === 0) {
|
||||
actions.push({
|
||||
icon: pr.icon ?? contact.icon.SocialEdit,
|
||||
label: pr.label,
|
||||
action: async () => {
|
||||
const provider = getProvider({ provider: pr._id, value: '' }, providers, $lastViews)
|
||||
if (provider !== undefined) {
|
||||
if (displayItems.filter((it) => it.provider === pr._id).length === 0) {
|
||||
displayItems = [...displayItems, provider]
|
||||
if (_displayItems.filter((it) => it.provider === pr._id).length === 0) {
|
||||
displayItems = [..._displayItems, provider]
|
||||
if (focusIndex !== -1) {
|
||||
await tick()
|
||||
focusManager?.setFocusPos(focusIndex + displayItems.length)
|
||||
await tick()
|
||||
editChannel(btns[displayItems.length - 1], displayItems.length - 1, provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -136,7 +155,7 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
$: if (providers) updateMenu()
|
||||
$: if (providers) updateMenu(displayItems)
|
||||
|
||||
const dropItem = (n: number): Item[] => {
|
||||
return displayItems.filter((it, i) => i !== n)
|
||||
@ -144,11 +163,15 @@
|
||||
const saveItems = (): void => {
|
||||
value = filterUndefined(displayItems)
|
||||
dispatch('change', value)
|
||||
updateMenu()
|
||||
updateMenu(displayItems)
|
||||
}
|
||||
|
||||
const showMenu = (ev: MouseEvent): void => {
|
||||
showPopup(Menu, { actions }, ev.target as HTMLElement)
|
||||
showPopup(Menu, { actions }, ev.target as HTMLElement, (result) => {
|
||||
if (result == null) {
|
||||
focusManager?.setFocusPos(focusIndex + 2 + displayItems.length)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const editChannel = (el: HTMLElement, n: number, item: Item): void => {
|
||||
@ -157,13 +180,21 @@
|
||||
el,
|
||||
undefined,
|
||||
ChannelEditor,
|
||||
{ value: item.value, placeholder: item.placeholder, editable },
|
||||
{
|
||||
value: item.value,
|
||||
placeholder: item.placeholder,
|
||||
editable
|
||||
},
|
||||
anchor,
|
||||
(result) => {
|
||||
if (result.detail !== undefined) {
|
||||
if (result.detail === '') displayItems = dropItem(n)
|
||||
else displayItems[n].value = result.detail
|
||||
if (result.detail != null) {
|
||||
if (result.detail === '') {
|
||||
displayItems = dropItem(n)
|
||||
} else {
|
||||
displayItems[n].value = result.detail
|
||||
}
|
||||
saveItems()
|
||||
focusManager?.setFocusPos(focusIndex + 1 + n)
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -183,6 +214,7 @@
|
||||
>
|
||||
{#each displayItems as item, i}
|
||||
<Button
|
||||
focusIndex={focusIndex === -1 ? focusIndex : focusIndex + 1 + i}
|
||||
id={item.label}
|
||||
bind:input={btns[i]}
|
||||
icon={item.icon}
|
||||
@ -193,18 +225,19 @@
|
||||
on:mousemove={(ev) => {
|
||||
_focus(ev, i, item)
|
||||
}}
|
||||
on:focus={(ev) => {
|
||||
_focus(ev, i, item)
|
||||
}}
|
||||
on:click={(ev) => {
|
||||
if (editable) editChannel(eventToHTMLElement(ev), i, item)
|
||||
else closeTooltip()
|
||||
if (editable) {
|
||||
editChannel(eventToHTMLElement(ev), i, item)
|
||||
} else {
|
||||
closeTooltip()
|
||||
}
|
||||
dispatch('open', item)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{#if actions.length > 0 && editable}
|
||||
<Button
|
||||
focusIndex={focusIndex === -1 ? focusIndex : focusIndex + 2 + displayItems.length}
|
||||
id={presentation.string.AddSocialLinks}
|
||||
bind:input={addBtn}
|
||||
icon={contact.icon.SocialEdit}
|
||||
|
@ -12,7 +12,7 @@
|
||||
"VacancyCreateLabel": "Vacancy",
|
||||
"VacancyPlaceholder": "Software Engineer",
|
||||
"MakePrivateDescription": "Only members can see it",
|
||||
"CreateAnApplication": "Create an Application",
|
||||
"CreateAnApplication": "New Application",
|
||||
"NoApplicationsForCandidate": "There are no applications for this candidate.",
|
||||
"CreateApplication": "Create an Application",
|
||||
"ApplicationCreateLabel": "Application",
|
||||
@ -93,7 +93,8 @@
|
||||
"GotoSkills": "Go to Skills",
|
||||
"GotoAssigned": "Go to my Assigned",
|
||||
"GotoApplicants": "Go to Applications",
|
||||
"GotoRecruitApplication": "Switch to Recruit Application"
|
||||
"GotoRecruitApplication": "Switch to Recruit Application",
|
||||
"AddDropHere": "Add or drop resume"
|
||||
},
|
||||
"status": {
|
||||
"CandidateRequired": "Please select candidate",
|
||||
|
@ -95,7 +95,8 @@
|
||||
"GotoSkills": "Перейти к навыкам",
|
||||
"GotoAssigned": "Перейти к моим назначениям",
|
||||
"GotoApplicants": "Перейти к претендентам",
|
||||
"GotoRecruitApplication": "Перейти к Приложению Рекрутинг"
|
||||
"GotoRecruitApplication": "Перейти к Приложению Рекрутинг",
|
||||
"AddDropHere": "Добавить или перетянуть резюме"
|
||||
},
|
||||
"status": {
|
||||
"CandidateRequired": "Пожалуйста выберите кандидата",
|
||||
|
@ -23,7 +23,9 @@
|
||||
import ui, {
|
||||
Button,
|
||||
ColorPopup,
|
||||
createFocusManager,
|
||||
eventToHTMLElement,
|
||||
FocusHandler,
|
||||
getPlatformColor,
|
||||
showPopup,
|
||||
Status as StatusControl
|
||||
@ -163,8 +165,11 @@
|
||||
{ sort: { rank: SortingOrder.Ascending } }
|
||||
)
|
||||
}
|
||||
const manager = createFocusManager()
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
|
||||
<Card
|
||||
label={recruit.string.CreateApplication}
|
||||
okAction={createApplication}
|
||||
@ -183,6 +188,7 @@
|
||||
<svelte:fragment slot="pool">
|
||||
{#if !preserveCandidate}
|
||||
<UserBox
|
||||
focusIndex={1}
|
||||
_class={contact.class.Person}
|
||||
label={recruit.string.Candidate}
|
||||
placeholder={recruit.string.Candidates}
|
||||
@ -192,6 +198,7 @@
|
||||
/>
|
||||
{/if}
|
||||
<UserBox
|
||||
focusIndex={2}
|
||||
_class={contact.class.Employee}
|
||||
label={recruit.string.AssignRecruiter}
|
||||
placeholder={recruit.string.Recruiters}
|
||||
@ -203,6 +210,7 @@
|
||||
/>
|
||||
{#if states && doc.space}
|
||||
<Button
|
||||
focusIndex={3}
|
||||
width="min-content"
|
||||
size="small"
|
||||
kind="no-border"
|
||||
@ -217,6 +225,7 @@
|
||||
selectedState = result
|
||||
selectedState.title = result.label
|
||||
}
|
||||
manager.setFocusPos(3)
|
||||
}
|
||||
)
|
||||
}}
|
||||
|
@ -35,7 +35,9 @@
|
||||
import {
|
||||
Button,
|
||||
Component,
|
||||
createFocusManager,
|
||||
EditBox,
|
||||
FocusHandler,
|
||||
getColorNumberByText,
|
||||
IconFile as FileIcon,
|
||||
IconInfo,
|
||||
@ -351,6 +353,7 @@
|
||||
if (file !== undefined) {
|
||||
createAttachment(file)
|
||||
}
|
||||
manager.setFocusPos(102)
|
||||
}
|
||||
|
||||
function onAvatarDone (e: any) {
|
||||
@ -386,8 +389,12 @@
|
||||
function removeAvatar (): void {
|
||||
avatar = undefined
|
||||
}
|
||||
|
||||
const manager = createFocusManager()
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
|
||||
<Card
|
||||
label={recruit.string.CreateCandidate}
|
||||
okAction={createCandidate}
|
||||
@ -416,17 +423,31 @@
|
||||
kind={'large-style'}
|
||||
maxWidth={'32rem'}
|
||||
focus
|
||||
focusIndex={1}
|
||||
/>
|
||||
<EditBox
|
||||
placeholder={recruit.string.PersonLastNamePlaceholder}
|
||||
bind:value={lastName}
|
||||
kind={'large-style'}
|
||||
maxWidth={'32rem'}
|
||||
focusIndex={2}
|
||||
/>
|
||||
<div class="mt-1">
|
||||
<EditBox placeholder={recruit.string.Title} bind:value={object.title} kind={'small-style'} maxWidth={'32rem'} />
|
||||
<EditBox
|
||||
placeholder={recruit.string.Title}
|
||||
bind:value={object.title}
|
||||
kind={'small-style'}
|
||||
maxWidth={'32rem'}
|
||||
focusIndex={3}
|
||||
/>
|
||||
</div>
|
||||
<EditBox placeholder={recruit.string.Location} bind:value={object.city} kind={'small-style'} maxWidth={'32rem'} />
|
||||
<EditBox
|
||||
placeholder={recruit.string.Location}
|
||||
bind:value={object.city}
|
||||
kind={'small-style'}
|
||||
maxWidth={'32rem'}
|
||||
focusIndex={4}
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<EditableAvatar
|
||||
@ -439,12 +460,23 @@
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="pool">
|
||||
<ChannelsDropdown bind:value={channels} editable />
|
||||
<YesNo label={recruit.string.Onsite} tooltip={recruit.string.WorkLocationPreferences} bind:value={object.onsite} />
|
||||
<YesNo label={recruit.string.Remote} tooltip={recruit.string.WorkLocationPreferences} bind:value={object.remote} />
|
||||
<ChannelsDropdown focusIndex={10} bind:value={channels} editable />
|
||||
<YesNo
|
||||
focusIndex={100}
|
||||
label={recruit.string.Onsite}
|
||||
tooltip={recruit.string.WorkLocationPreferences}
|
||||
bind:value={object.onsite}
|
||||
/>
|
||||
<YesNo
|
||||
focusIndex={101}
|
||||
label={recruit.string.Remote}
|
||||
tooltip={recruit.string.WorkLocationPreferences}
|
||||
bind:value={object.remote}
|
||||
/>
|
||||
<Component
|
||||
is={tags.component.TagsDropdownEditor}
|
||||
props={{
|
||||
focusIndex: 102,
|
||||
items: skills,
|
||||
key,
|
||||
targetClass: recruit.mixin.Candidate,
|
||||
@ -474,20 +506,26 @@
|
||||
on:drop|preventDefault|stopPropagation={drop}
|
||||
>
|
||||
{#if resume.uuid}
|
||||
<Link
|
||||
label={resume.name}
|
||||
<Button
|
||||
kind={'transparent'}
|
||||
focusIndex={103}
|
||||
icon={FileIcon}
|
||||
maxLenght={16}
|
||||
on:click={() => {
|
||||
showPopup(PDFViewer, { file: resume.uuid, name: resume.name }, 'right')
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
{resume.name}
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
{:else}
|
||||
{#if loading}
|
||||
<Link label={'Uploading...'} icon={Spinner} disabled />
|
||||
{:else}
|
||||
<Link
|
||||
label={'Add or drop resume'}
|
||||
<Button
|
||||
kind={'transparent'}
|
||||
focusIndex={103}
|
||||
label={recruit.string.AddDropHere}
|
||||
icon={FileUpload}
|
||||
on:click={() => {
|
||||
inputFile.click()
|
||||
|
@ -14,8 +14,8 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { Label, Tooltip, Button } from '@anticrm/ui'
|
||||
import type { TooltipAlignment, ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import type { ButtonKind, ButtonSize, TooltipAlignment } from '@anticrm/ui'
|
||||
import { Button, Label, Tooltip } from '@anticrm/ui'
|
||||
|
||||
export let label: IntlString
|
||||
export let tooltip: IntlString
|
||||
@ -27,10 +27,13 @@
|
||||
export let size: ButtonSize = 'small'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = 'fit-content'
|
||||
|
||||
export let focusIndex = -1
|
||||
</script>
|
||||
|
||||
<Tooltip direction={labelDirection} label={tooltip}>
|
||||
<Button
|
||||
{focusIndex}
|
||||
{kind}
|
||||
{size}
|
||||
{justify}
|
||||
|
@ -103,7 +103,8 @@ export default mergeIds(recruitId, recruit, {
|
||||
DueDate: '' as IntlString,
|
||||
CandidateReviews: '' as IntlString,
|
||||
AddDescription: '' as IntlString,
|
||||
NumberSkills: '' as IntlString
|
||||
NumberSkills: '' as IntlString,
|
||||
AddDropHere: '' as IntlString
|
||||
},
|
||||
space: {
|
||||
CandidatesPublic: '' as Ref<Space>
|
||||
|
@ -23,7 +23,7 @@
|
||||
import view from '../plugin'
|
||||
import { focusStore, selectionStore } from '../selection'
|
||||
import ActionContext from './ActionContext.svelte'
|
||||
import ListView from './ListView.svelte'
|
||||
import { ListView } from '@anticrm/ui'
|
||||
import ObjectPresenter from './ObjectPresenter.svelte'
|
||||
|
||||
export let viewContext: ViewContext
|
||||
|
@ -438,6 +438,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div bind:this={cover} class="cover" />
|
||||
<TooltipInstance />
|
||||
<PanelInstance bind:this={panelInstance} {contentPanel}>
|
||||
<svelte:fragment slot="panel-header">
|
||||
<ActionContext context={{ mode: 'panel' }} />
|
||||
@ -448,7 +449,6 @@
|
||||
<ActionContext context={{ mode: 'popup' }} />
|
||||
</svelte:fragment>
|
||||
</Popup>
|
||||
<TooltipInstance />
|
||||
<DatePickerPopup />
|
||||
{:else}
|
||||
No client
|
||||
|
Loading…
Reference in New Issue
Block a user