mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-30 21:45:01 +02:00
Fix Popup for @ document referencing and Emoji popup (#2398)
Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
@@ -17,7 +17,16 @@
|
||||
import { Ref, RelatedDocument } from '@hcengineering/core'
|
||||
|
||||
import { getResource, IntlString } from '@hcengineering/platform'
|
||||
import ui, { Button, createFocusManager, EditBox, FocusHandler, IconSearch, Label, ListView } from '@hcengineering/ui'
|
||||
import ui, {
|
||||
Button,
|
||||
createFocusManager,
|
||||
EditBox,
|
||||
FocusHandler,
|
||||
IconSearch,
|
||||
Label,
|
||||
ListView,
|
||||
resizeObserver
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import presentation from '../plugin'
|
||||
import { ObjectSearchCategory, ObjectSearchResult } from '../types'
|
||||
@@ -135,48 +144,42 @@
|
||||
|
||||
<FocusHandler {manager} />
|
||||
|
||||
<form class="antiCard dialog completion" on:keydown={onKeyDown}>
|
||||
<div class="p-4 flex-col flex-grow">
|
||||
<form class="antiCard dialog completion" on:keydown={onKeyDown} use:resizeObserver={() => dispatch('changeSize')}>
|
||||
<div class="header-dialog">
|
||||
{#if label}
|
||||
<div class="fs-title flex-grow mb-4">
|
||||
<Label {label} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-col flex-grow">
|
||||
<div class="flex">
|
||||
{#each categories as c, i}
|
||||
{@const status = categoryStatus[c._id] ?? 0}
|
||||
<div class="flex">
|
||||
<div class="ap-categoryItem">
|
||||
<Button
|
||||
focusIndex={i + 1}
|
||||
kind={'transparent'}
|
||||
showTooltip={{ label: c.label }}
|
||||
selected={category?._id === c._id}
|
||||
icon={c.icon}
|
||||
size={'x-large'}
|
||||
disabled={status === 0}
|
||||
on:click={() => {
|
||||
category = c
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mt-2 mb-1 flex-row-center">
|
||||
<EditBox
|
||||
focus
|
||||
icon={IconSearch}
|
||||
kind={'search-style'}
|
||||
focusIndex={0}
|
||||
bind:value={query}
|
||||
on:input={() => updateItems(category, query, relatedDocuments)}
|
||||
placeholder={category?.label}
|
||||
/>
|
||||
</div>
|
||||
<Label label={ui.string.Suggested} />
|
||||
<div class="flex-row-center gap-1">
|
||||
{#each categories as c, i}
|
||||
{@const status = categoryStatus[c._id] ?? 0}
|
||||
<div class="ap-categoryItem">
|
||||
<Button
|
||||
focusIndex={i + 1}
|
||||
kind={'transparent'}
|
||||
showTooltip={{ label: c.label }}
|
||||
selected={category?._id === c._id}
|
||||
icon={c.icon}
|
||||
size={'x-large'}
|
||||
disabled={status === 0}
|
||||
on:click={() => {
|
||||
category = c
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<EditBox
|
||||
focus
|
||||
icon={IconSearch}
|
||||
kind={'search-style'}
|
||||
focusIndex={0}
|
||||
bind:value={query}
|
||||
on:input={() => updateItems(category, query, relatedDocuments)}
|
||||
placeholder={category?.label}
|
||||
/>
|
||||
<Label label={ui.string.Suggested} />
|
||||
</div>
|
||||
<div class="antiCard-content min-h-60 max-h-60">
|
||||
<ListView bind:this={list} bind:selection count={items.length}>
|
||||
@@ -197,6 +200,14 @@
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
.header-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
padding: 1.125rem 1.125rem 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
.completion {
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import emojiRegex from 'emoji-regex'
|
||||
import { getContext } from 'svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { AnySvelteComponent, Label, tooltip } from '@hcengineering/ui'
|
||||
import { Label, tooltip, Scroller, emojiSP } from '@hcengineering/ui'
|
||||
import type { AnySvelteComponent } from '@hcengineering/ui'
|
||||
import Emoji from './icons/Emoji.svelte'
|
||||
import Food from './icons/Food.svelte'
|
||||
import Nature from './icons/Nature.svelte'
|
||||
@@ -15,6 +17,8 @@
|
||||
let div: HTMLDivElement
|
||||
const regex = emojiRegex()
|
||||
|
||||
const { currentFontSize } = getContext('fontsize') as { currentFontSize: string }
|
||||
|
||||
function getEmojis (startCode: number, endCode: number, postfix?: number[]): (string | undefined)[] {
|
||||
return [...Array(endCode - startCode + 1).keys()].map((v) => {
|
||||
const str = postfix ? String.fromCodePoint(v + startCode, ...postfix) : String.fromCodePoint(v + startCode)
|
||||
@@ -101,44 +105,30 @@
|
||||
]
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const headerHeight = 55
|
||||
function handleScrollToCategory (categoryId: string) {
|
||||
const offset = document.getElementById(categoryId)?.offsetTop
|
||||
if (offset) div.scrollTo(0, offset - headerHeight)
|
||||
const el = document.getElementById(categoryId)
|
||||
if (el) {
|
||||
const next = el.nextElementSibling as HTMLElement
|
||||
div.scroll(0, next.offsetTop - (currentFontSize === 'small-font' ? 14 : 16) * 1.75)
|
||||
}
|
||||
}
|
||||
|
||||
let currentCategory = categories[0]
|
||||
const padding = 16
|
||||
function handleScroll () {
|
||||
const divTop = div?.getBoundingClientRect().top
|
||||
const categoryDivs = div.getElementsByClassName('categoryName')
|
||||
const i = Array.from(categoryDivs).findIndex((element) => {
|
||||
if (element?.nodeType === Node.ELEMENT_NODE) {
|
||||
const elementTop = element?.getBoundingClientRect().top
|
||||
if (elementTop >= divTop + padding) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
let firstVisibleCategory: Element | null
|
||||
if (i > 0) {
|
||||
firstVisibleCategory = categoryDivs.item(i - 1)
|
||||
} else {
|
||||
firstVisibleCategory = categoryDivs.item(categoryDivs.length - 1)
|
||||
}
|
||||
if (firstVisibleCategory !== null) {
|
||||
currentCategory = categories.find((c) => c.id === firstVisibleCategory!.id) ?? categories[0]
|
||||
function handleScrolled (catId?: CustomEvent) {
|
||||
if (catId) {
|
||||
const curCat = categories.find((it) => it.id === catId.detail[catId.detail.length - 1])
|
||||
if (curCat) currentCategory = curCat
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiPopup antiPopup-withHeader pb-3 popup">
|
||||
<div class="flex-between ml-4 pt-2 pb-2 mr-4 header">
|
||||
<div class="antiPopup antiPopup-withHeader popup">
|
||||
<div class="flex-row-center popup-header">
|
||||
{#each categories as category}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
use:tooltip={{ label: category.label }}
|
||||
class="flex-grow pt-2 pb-2 pl-2 pr-2 element"
|
||||
class="element m-0-5"
|
||||
class:selected={currentCategory === category}
|
||||
on:click={() => handleScrollToCategory(category.id)}
|
||||
>
|
||||
@@ -146,35 +136,69 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex-col vScroll" bind:this={div} on:scroll={handleScroll}>
|
||||
<div class="w-85 flex-col">
|
||||
<div class="scrolling">
|
||||
<Scroller bind:divScroll={div} on:scrolledCategories={handleScrolled} fade={emojiSP} noStretch>
|
||||
{#each categories as category}
|
||||
<div class="ap-header">
|
||||
<div id={category.id} class="ap-caption categoryName"><Label label={category.label} /></div>
|
||||
<div id={category.id} class="scroll-header categoryHeader">
|
||||
<Label label={category.label} />
|
||||
</div>
|
||||
<div class="palette ml-4">
|
||||
<div class="palette ml-3">
|
||||
{#each category.emojis as emoji}
|
||||
{#if emoji !== undefined}
|
||||
<div class="p-1 element" on:click={() => dispatch('close', emoji)}>{emoji}</div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="element m-0-5" on:click={() => dispatch('close', emoji)}>{emoji}</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Scroller>
|
||||
</div>
|
||||
<div class="ap-space x2" />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.popup {
|
||||
height: 25rem;
|
||||
height: 21.25rem;
|
||||
}
|
||||
.scrolling {
|
||||
min-height: 0;
|
||||
height: 16.5rem;
|
||||
max-height: 16.5rem;
|
||||
}
|
||||
.popup-header {
|
||||
margin: 0.75rem 0.75rem 0.5rem;
|
||||
}
|
||||
.scroll-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
margin: 0.75rem 0.75rem 0.25rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--dark-color);
|
||||
background-color: var(--board-bg-color);
|
||||
border-radius: 0.25rem;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.palette {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
font-size: x-large;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 19.25rem;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.element {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.25rem;
|
||||
color: var(--caption-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--popup-bg-hover);
|
||||
}
|
||||
@@ -183,9 +207,4 @@
|
||||
background-color: var(--popup-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
justify-content: start;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { ObjectSearchPopup, ObjectSearchResult } from '@hcengineering/presentation'
|
||||
import { showPopup } from '@hcengineering/ui'
|
||||
import { showPopup, resizeObserver, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import DummyPopup from './DummyPopup.svelte'
|
||||
|
||||
@@ -28,9 +28,14 @@
|
||||
let popupClose: () => void
|
||||
|
||||
onMount(() => {
|
||||
popupClose = showPopup(DummyPopup, {}, undefined, () => {
|
||||
close()
|
||||
})
|
||||
popupClose = showPopup(
|
||||
DummyPopup,
|
||||
{},
|
||||
undefined,
|
||||
() => close(),
|
||||
() => {},
|
||||
{ overlay: false }
|
||||
)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -55,38 +60,42 @@
|
||||
|
||||
function updateStyle (): void {
|
||||
const rect = clientRect()
|
||||
const docW = document.body.clientWidth
|
||||
const wDoc = $deviceInfo.docWidth
|
||||
const hDoc = $deviceInfo.docHeight
|
||||
let tempStyle = ''
|
||||
if (rect.top < 292) {
|
||||
if (rect.top < hDoc - rect.bottom) {
|
||||
// 20rem - 1.75rem
|
||||
tempStyle = `top: calc(${rect.bottom}px + .75rem); max-heigth: calc(115vh - ${rect.bottom}px - 1.75rem); `
|
||||
tempStyle = `top: calc(${rect.bottom}px + .75rem); max-height: calc(${hDoc - rect.bottom}px - 1.75rem); `
|
||||
} else {
|
||||
tempStyle = `bottom: calc(115vh - ${rect.top}px + .75rem); max-heigth: calc(${rect.top}px - 1.75rem); `
|
||||
tempStyle = `bottom: calc(${hDoc - rect.top}px + .75rem); max-height: calc(${rect.top}px - 1.75rem); `
|
||||
}
|
||||
if (docW - rect.left > 452) {
|
||||
if (rect.left + wPopup > wDoc - 16) {
|
||||
// 30rem - 1.75rem
|
||||
tempStyle += `left: ${rect.left}px;`
|
||||
tempStyle += 'right: 1rem;'
|
||||
} else {
|
||||
tempStyle += `right: calc(115vh - ${rect.right}px);`
|
||||
tempStyle += `left: ${rect.left}px;`
|
||||
}
|
||||
style = tempStyle
|
||||
}
|
||||
|
||||
let style = 'visibility: hidden'
|
||||
$: if (popup) updateStyle()
|
||||
let wPopup: number = 0
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={() => updateStyle()} />
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="overlay" on:click={() => close()} />
|
||||
<div
|
||||
class="overlay"
|
||||
on:click={() => {
|
||||
close()
|
||||
bind:this={popup}
|
||||
class="completion"
|
||||
{style}
|
||||
use:resizeObserver={(element) => {
|
||||
wPopup = element.clientWidth
|
||||
updateStyle()
|
||||
}}
|
||||
/>
|
||||
<div bind:this={popup} class="antiPopup antiPopup-withHeader antiPopup-withCategory completion" {style}>
|
||||
<div class="completion">
|
||||
<ObjectSearchPopup bind:this={searchPopup} {query} on:close={(evt) => dispatchItem(evt.detail)} />
|
||||
</div>
|
||||
>
|
||||
<ObjectSearchPopup bind:this={searchPopup} {query} on:close={(evt) => dispatchItem(evt.detail)} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -101,10 +110,10 @@
|
||||
|
||||
.completion {
|
||||
position: absolute;
|
||||
min-width: 20rem;
|
||||
max-width: 30rem;
|
||||
min-height: 0;
|
||||
height: 20rem;
|
||||
// min-width: 20rem;
|
||||
// max-width: 30rem;
|
||||
// min-height: 0;
|
||||
// max-height: 30rem;
|
||||
z-index: 2000;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -417,6 +417,8 @@ input.search {
|
||||
.my-2 { margin: .5rem 0; }
|
||||
.my-4 { margin: 1rem 0; }
|
||||
|
||||
.m-0-5 { margin: .125rem; }
|
||||
|
||||
.pl-1 { padding-left: .25rem; }
|
||||
.pl-2 { padding-left: .5rem; }
|
||||
.pl-3 { padding-left: .75rem; }
|
||||
@@ -494,6 +496,7 @@ input.search {
|
||||
.h-full { height: 100%; }
|
||||
.h-75p { height: 75%; }
|
||||
.h-2 { height: .5rem; }
|
||||
.h-3 { height: .75rem; }
|
||||
.h-4 { height: 1rem; }
|
||||
.h-6 { height: 1.5rem; }
|
||||
.h-7 { height: 1.75rem; }
|
||||
|
||||
@@ -134,9 +134,9 @@
|
||||
|
||||
&.dialog {
|
||||
width: 45rem;
|
||||
height: max-content;
|
||||
height: auto;
|
||||
max-width: 60rem;
|
||||
max-height: calc(100vh - 2rem);
|
||||
max-height: inherit;
|
||||
}
|
||||
&.mobile {
|
||||
width: 90vw;
|
||||
|
||||
@@ -208,6 +208,8 @@
|
||||
.ap-space {
|
||||
flex-shrink: 0;
|
||||
height: .25rem;
|
||||
&.x2 { height: .5rem; }
|
||||
&.x3 { height: .75rem; }
|
||||
}
|
||||
.ap-scroll {
|
||||
flex-grow: 1;
|
||||
@@ -312,8 +314,8 @@
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
color: var(--dark-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
{#each Array(count) as _, row}
|
||||
<slot name="category" item={row} />
|
||||
<div
|
||||
class="list-item"
|
||||
class:selection={row === selection}
|
||||
on:mouseover={() => onRow(row)}
|
||||
on:focus={() => {}}
|
||||
@@ -84,6 +85,9 @@
|
||||
// border-radius: 0.25rem;
|
||||
user-select: none;
|
||||
|
||||
.list-item {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.selection {
|
||||
background-color: var(--menu-bg-select);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { beforeUpdate, afterUpdate, onDestroy, onMount } from 'svelte'
|
||||
import { beforeUpdate, afterUpdate, onDestroy, onMount, createEventDispatcher } from 'svelte'
|
||||
import { resizeObserver } from '../resize'
|
||||
import { themeStore as themeOptions } from '@hcengineering/theme'
|
||||
import type { FadeOptions } from '../types'
|
||||
@@ -27,13 +27,16 @@
|
||||
export let invertScroll: boolean = false
|
||||
export let horizontal: boolean = false
|
||||
export let contentDirection: 'vertical' | 'vertical-reverse' | 'horizontal' = 'vertical'
|
||||
export let noStretch: boolean = false
|
||||
export let divScroll: HTMLElement | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let mask: 'top' | 'bottom' | 'both' | 'none' = 'none'
|
||||
let topCrop: 'top' | 'bottom' | 'full' | 'none' = 'none'
|
||||
let topCropValue: number = 0
|
||||
let maskH: 'left' | 'right' | 'both' | 'none' = 'none'
|
||||
|
||||
let divScroll: HTMLElement
|
||||
let divHScroll: HTMLElement
|
||||
let divBox: HTMLElement
|
||||
let divBar: HTMLElement
|
||||
@@ -222,21 +225,29 @@
|
||||
}
|
||||
|
||||
const scrollDown = (): void => {
|
||||
divScroll.scrollTop = divScroll.scrollHeight - divHeight
|
||||
if (divScroll) divScroll.scrollTop = divScroll.scrollHeight - divHeight
|
||||
}
|
||||
$: if (scrolling && belowContent && belowContent > 10) scrollDown()
|
||||
|
||||
const checkIntersection = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
|
||||
const interArr: Element[] = []
|
||||
entries.forEach((el) => {
|
||||
if (el.isIntersecting) inter.add(el.target)
|
||||
else inter.delete(el.target)
|
||||
if (el.isIntersecting) {
|
||||
inter.add(el.target)
|
||||
interArr.push(el.target)
|
||||
} else inter.delete(el.target)
|
||||
})
|
||||
if (interArr.length > 0) {
|
||||
dispatch('lastScrolledCategory', interArr[interArr.length - 1]?.getAttribute('id'))
|
||||
const interCats: string[] = interArr.map((it) => it.getAttribute('id') as string)
|
||||
dispatch('scrolledCategories', interCats)
|
||||
}
|
||||
}
|
||||
|
||||
const checkIntersectionFade = () => {
|
||||
topCrop = 'none'
|
||||
topCropValue = 0
|
||||
if (!fade.offset?.top) return
|
||||
if (!fade.offset?.top || !divScroll) return
|
||||
const offset = divScroll.getBoundingClientRect().top
|
||||
inter.forEach((el) => {
|
||||
const rect = el.getBoundingClientRect()
|
||||
@@ -322,13 +333,14 @@
|
||||
: contentDirection === 'vertical-reverse'
|
||||
? 'column-reverse'
|
||||
: 'row'}
|
||||
style:height={contentDirection === 'vertical-reverse' ? 'max-content' : '100%'}
|
||||
style:height={contentDirection === 'vertical-reverse' ? 'max-content' : noStretch ? 'auto' : '100%'}
|
||||
use:resizeObserver={(element) => {
|
||||
boxHeight = element.clientHeight
|
||||
boxWidth = element.clientWidth
|
||||
}}
|
||||
on:dragover
|
||||
on:drop
|
||||
on:scroll
|
||||
>
|
||||
{#if bottomStart}<div class="flex-grow flex-shrink" />{/if}
|
||||
<slot />
|
||||
|
||||
@@ -227,6 +227,7 @@ export const defaultSP: FadeOptions = { multipler: { top: 0, bottom: 0 } }
|
||||
export const tableSP: FadeOptions = { offset: { top: true }, multipler: { top: 2.5, bottom: 0 } }
|
||||
export const tableHRscheduleY: FadeOptions = { offset: { top: true }, multipler: { top: 5, bottom: 0 } }
|
||||
export const issueSP: FadeOptions = { offset: { top: true }, multipler: { top: 3, bottom: 0 } }
|
||||
export const emojiSP: FadeOptions = { offset: { top: true }, multipler: { top: 1.5, bottom: 0 } }
|
||||
|
||||
export interface DeviceOptions {
|
||||
docWidth: number
|
||||
|
||||
Reference in New Issue
Block a user