mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-15 04:49:00 +00:00
Add TabList component. Update StatesBar layout. Fix Scroller. (#1980)
Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
parent
b3e516beb4
commit
06d45d6a1f
@ -78,6 +78,7 @@
|
||||
--caret-color: #6e5ed2;
|
||||
--warning-color: #f2994a;
|
||||
--error-color: #eb5757;
|
||||
--done-color: #34DB80;
|
||||
|
||||
--divider-color: #303236;
|
||||
--menu-bg-select: #2d2f36;
|
||||
@ -228,6 +229,7 @@
|
||||
--caret-color: #6e5ed2;
|
||||
--warning-color: #f2994a; // Dark
|
||||
--error-color: #eb5757; // Dark
|
||||
--done-color: #34DB80; // Dark
|
||||
|
||||
--divider-color: #e0e0e0;
|
||||
--menu-bg-select: #f0f3f9;
|
||||
|
@ -270,10 +270,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 0;
|
||||
padding: 0.125rem 0;
|
||||
min-width: 0;
|
||||
|
||||
&::-webkit-scrollbar:horizontal { height: 0.125rem; }
|
||||
&::-webkit-scrollbar:horizontal { height: 0; }
|
||||
&::-webkit-scrollbar-track { margin: 0.25rem; }
|
||||
&::-webkit-scrollbar-thumb { background-color: var(--theme-bg-accent-color); }
|
||||
|
||||
@ -299,15 +298,19 @@
|
||||
|
||||
&__back {
|
||||
width: auto;
|
||||
height: 2.25rem;
|
||||
padding: 1px 0.5px;
|
||||
height: calc(1.5rem + 2px);
|
||||
// height: 1.5rem;
|
||||
}
|
||||
&__element {
|
||||
fill: var(--theme-button-bg-enabled);
|
||||
stroke: var(--theme-bg-accent-color);
|
||||
fill: var(--accent-bg-color);
|
||||
stroke: var(--divider-color);
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
|
||||
&:hover { fill: var(--button-bg-color); }
|
||||
}
|
||||
&__selected { fill: red; }
|
||||
&__selected { fill: var(--button-bg-hover); }
|
||||
|
||||
.asb-label__container {
|
||||
position: absolute;
|
||||
@ -315,11 +318,19 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
left: .5rem;
|
||||
right: .5rem;
|
||||
min-width: 0;
|
||||
width: calc(100% - 2rem);
|
||||
width: calc(100% - 1rem);
|
||||
height: 100%;
|
||||
font-weight: 500;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--dark-color);
|
||||
pointer-events: none;
|
||||
|
||||
&.selected {
|
||||
color: var(--caption-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,7 +96,7 @@
|
||||
<svelte:fragment slot="item" let:item>
|
||||
{@const itemValue = objects[item]}
|
||||
<button
|
||||
class="menu-item"
|
||||
class="menu-item w-full"
|
||||
on:click={() => {
|
||||
dispatch('close', itemValue)
|
||||
}}
|
||||
|
@ -54,7 +54,7 @@
|
||||
divBar.style.opacity = '1'
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
divBar.style.opacity = '0'
|
||||
if (divBar) divBar.style.opacity = '0'
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
128
packages/ui/src/components/TabList.svelte
Normal file
128
packages/ui/src/components/TabList.svelte
Normal file
@ -0,0 +1,128 @@
|
||||
<!--
|
||||
// Copyright © 2022 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 { createEventDispatcher } from 'svelte'
|
||||
import type { TabItem } from '../types'
|
||||
import { Label, Icon } from '..'
|
||||
|
||||
export let selected: string | string[] = ''
|
||||
export let multiselect: boolean = false
|
||||
export let items: TabItem[]
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
if (multiselect && selected === '') selected = []
|
||||
if (selected === '') selected = items[0].id
|
||||
|
||||
const getSelected = (id: string): boolean => {
|
||||
let res: boolean = false
|
||||
if (multiselect && Array.isArray(selected)) res = selected.filter((it) => it === id).length > 0
|
||||
else if (selected === id) res = true
|
||||
return res
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if items.length > 0}
|
||||
<div class="tablist-container">
|
||||
{#each items as item}
|
||||
<div
|
||||
class="button"
|
||||
class:selected={getSelected(item.id)}
|
||||
on:click={() => {
|
||||
if (multiselect) {
|
||||
if (Array.isArray(selected)) {
|
||||
if (selected.includes(item.id)) selected = selected.filter((it) => it !== item.id)
|
||||
else selected.push(item.id)
|
||||
}
|
||||
} else selected = item.id
|
||||
dispatch('select', item)
|
||||
items = items
|
||||
}}
|
||||
>
|
||||
{#if item.icon}
|
||||
<div class="icon"><Icon icon={item.icon} size={'small'} /></div>
|
||||
{:else if item.color}
|
||||
<div class="color" style:background-color={item.color} />
|
||||
{/if}
|
||||
<span class="overflow-label">
|
||||
{#if item.label}
|
||||
{item.label}
|
||||
{:else if item.labelIntl}
|
||||
<Label label={item.labelIntl} />
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.tablist-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
background-color: var(--accent-bg-color);
|
||||
border-radius: 0.5rem;
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
width: fit-content;
|
||||
height: 1.5rem;
|
||||
min-height: 1.5rem;
|
||||
max-width: 12.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.8125rem;
|
||||
background-color: var(--accent-bg-color);
|
||||
border: 1px solid transparent;
|
||||
border-radius: calc(0.5rem - 1px);
|
||||
cursor: pointer;
|
||||
transition-property: background-color, color;
|
||||
transition-duration: 0.15s;
|
||||
|
||||
.icon {
|
||||
margin-right: 0.375rem;
|
||||
}
|
||||
.color {
|
||||
margin-right: 0.375rem;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--button-bg-color);
|
||||
}
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0.35rem;
|
||||
left: -1.5px;
|
||||
height: 0.8rem;
|
||||
border-left: 1px solid var(--button-border-color);
|
||||
}
|
||||
&.selected {
|
||||
color: var(--caption-color);
|
||||
background-color: var(--button-bg-color);
|
||||
border-color: var(--button-border-color);
|
||||
box-shadow: var(--accent-shadow);
|
||||
}
|
||||
}
|
||||
.button:not(.selected) + .button:not(.selected)::before {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
</style>
|
@ -33,7 +33,8 @@ export type {
|
||||
PopupPositionElement,
|
||||
ButtonKind,
|
||||
ButtonSize,
|
||||
IconSize
|
||||
IconSize,
|
||||
TabItem
|
||||
} from './types'
|
||||
// export { applicationShortcutKey } from './utils'
|
||||
export { getCurrentLocation, locationToUrl, navigate, location } from './location'
|
||||
@ -93,6 +94,7 @@ export { default as Menu } from './components/Menu.svelte'
|
||||
export { default as TimeShiftPicker } from './components/TimeShiftPicker.svelte'
|
||||
export { default as ErrorPresenter } from './components/ErrorPresenter.svelte'
|
||||
export { default as Scroller } from './components/Scroller.svelte'
|
||||
export { default as TabList } from './components/TabList.svelte'
|
||||
|
||||
export { default as IconAdd } from './components/icons/Add.svelte'
|
||||
export { default as IconBack } from './components/icons/Back.svelte'
|
||||
|
@ -64,6 +64,14 @@ export interface Tab {
|
||||
|
||||
export type TabModel = Tab[]
|
||||
|
||||
export interface TabItem {
|
||||
id: string
|
||||
label?: string
|
||||
labelIntl?: IntlString
|
||||
icon?: Asset | AnySvelteComponent
|
||||
color?: string
|
||||
}
|
||||
|
||||
export type ButtonKind = 'primary' | 'secondary' | 'no-border' | 'transparent' | 'link' | 'link-bordered' | 'dangerous'
|
||||
export type ButtonSize = 'small' | 'medium' | 'large' | 'x-large'
|
||||
export type ButtonShape = 'rectangle' | 'rectangle-left' | 'rectangle-right' | 'circle' | 'round' | undefined
|
||||
|
@ -17,11 +17,10 @@
|
||||
import { Class, DocumentQuery, FindOptions, Ref, SortingOrder } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { DoneState, SpaceWithStates, State, Task } from '@anticrm/task'
|
||||
import Label from '@anticrm/ui/src/components/Label.svelte'
|
||||
import { TabList } from '@anticrm/ui'
|
||||
import type { TabItem } from '@anticrm/ui'
|
||||
import { TableBrowser } from '@anticrm/view-resources'
|
||||
import task from '../plugin'
|
||||
import Lost from './icons/Lost.svelte'
|
||||
import Won from './icons/Won.svelte'
|
||||
import StatesBar from './state/StatesBar.svelte'
|
||||
import type { Filter } from '@anticrm/view'
|
||||
|
||||
@ -34,10 +33,12 @@
|
||||
|
||||
let doneStatusesView: boolean = false
|
||||
let state: Ref<State> | undefined = undefined
|
||||
let selectedDoneStates: Set<Ref<DoneState>> = new Set<Ref<DoneState>>()
|
||||
const selectedDoneStates: Set<Ref<DoneState>> = new Set<Ref<DoneState>>()
|
||||
$: resConfig = updateConfig(config)
|
||||
let query = {}
|
||||
let doneStates: DoneState[] = []
|
||||
let itemsDS: TabItem[] = []
|
||||
let selectedDS: string[] = []
|
||||
let withoutDone: boolean = false
|
||||
|
||||
function updateConfig (config: string[]): string[] {
|
||||
@ -56,7 +57,17 @@
|
||||
{
|
||||
space
|
||||
},
|
||||
(res) => (doneStates = res),
|
||||
(res) => {
|
||||
doneStates = res
|
||||
itemsDS = doneStates.map((s) => {
|
||||
return {
|
||||
id: s._id,
|
||||
label: s.title,
|
||||
color: s._class === task.class.WonState ? 'var(--done-color)' : 'var(--error-color)'
|
||||
}
|
||||
})
|
||||
itemsDS.unshift({ id: 'NoDoneState', labelIntl: task.string.NoDoneState })
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
_class: SortingOrder.Descending,
|
||||
@ -87,92 +98,69 @@
|
||||
|
||||
function doneStateClick (id: Ref<DoneState>): void {
|
||||
withoutDone = false
|
||||
if (selectedDoneStates.has(id)) {
|
||||
selectedDoneStates.delete(id)
|
||||
} else {
|
||||
selectedDoneStates.add(id)
|
||||
if (selectedDoneStates.has(id)) selectedDoneStates.delete(id)
|
||||
else selectedDoneStates.add(id)
|
||||
if (selectedDS.length === 2 && selectedDS.includes('NoDoneState')) {
|
||||
selectedDS = selectedDS.filter((s) => s !== 'NoDoneState')
|
||||
}
|
||||
if (selectedDS.length === 0) {
|
||||
selectedDS = ['NoDoneState']
|
||||
withoutDone = true
|
||||
}
|
||||
selectedDoneStates = selectedDoneStates
|
||||
updateQuery(search, selectedDoneStates)
|
||||
}
|
||||
|
||||
function noDoneClick (): void {
|
||||
withoutDone = !withoutDone
|
||||
withoutDone = true
|
||||
selectedDS = ['NoDoneState']
|
||||
selectedDoneStates.clear()
|
||||
selectedDoneStates = selectedDoneStates
|
||||
updateQuery(search, selectedDoneStates)
|
||||
}
|
||||
|
||||
$: updateQuery(search, selectedDoneStates)
|
||||
</script>
|
||||
|
||||
<div class="flex-between mb-2 header">
|
||||
<div class="flex-row-center buttons">
|
||||
<div
|
||||
class="button flex-center"
|
||||
class:active={!doneStatusesView}
|
||||
on:click={() => {
|
||||
doneStatusesView = false
|
||||
state = undefined
|
||||
withoutDone = false
|
||||
selectedDoneStates.clear()
|
||||
updateQuery(search, selectedDoneStates)
|
||||
<div class="header">
|
||||
<TabList
|
||||
items={[
|
||||
{ id: 'AllStates', labelIntl: task.string.AllStates },
|
||||
{ id: 'DoneStates', labelIntl: task.string.DoneStates }
|
||||
]}
|
||||
multiselect={false}
|
||||
on:select={(result) => {
|
||||
if (result.type === 'select') {
|
||||
const res = result.detail
|
||||
if (res.id === 'AllStates') {
|
||||
doneStatusesView = false
|
||||
state = undefined
|
||||
withoutDone = false
|
||||
selectedDoneStates.clear()
|
||||
updateQuery(search, selectedDoneStates)
|
||||
} else if (res.id === 'DoneStates') {
|
||||
doneStatusesView = true
|
||||
state = undefined
|
||||
selectedDoneStates.clear()
|
||||
updateQuery(search, selectedDoneStates)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if doneStatusesView}
|
||||
<TabList
|
||||
items={itemsDS}
|
||||
bind:selected={selectedDS}
|
||||
multiselect
|
||||
on:select={(result) => {
|
||||
if (result.type === 'select') {
|
||||
const res = result.detail
|
||||
if (res.id === 'NoDoneState') noDoneClick()
|
||||
else doneStateClick(res.id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Label label={task.string.AllStates} />
|
||||
</div>
|
||||
<div
|
||||
class="button flex-center ml-3"
|
||||
class:active={doneStatusesView}
|
||||
on:click={() => {
|
||||
doneStatusesView = true
|
||||
state = undefined
|
||||
selectedDoneStates.clear()
|
||||
updateQuery(search, selectedDoneStates)
|
||||
}}
|
||||
>
|
||||
<Label label={task.string.DoneStates} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex-row-center caption-color states"
|
||||
class:antiStatesBar={doneStatusesView}
|
||||
class:justify-end={doneStatusesView}
|
||||
>
|
||||
{#if doneStatusesView}
|
||||
<div
|
||||
class="doneState withoutDone flex-center whitespace-nowrap"
|
||||
class:disable={!withoutDone}
|
||||
on:click={() => {
|
||||
noDoneClick()
|
||||
}}
|
||||
>
|
||||
<Label label={task.string.NoDoneState} />
|
||||
</div>
|
||||
{#each doneStates as state}
|
||||
<div
|
||||
class="doneState flex-center whitespace-nowrap"
|
||||
class:won={state._class === task.class.WonState}
|
||||
class:lost={state._class === task.class.LostState}
|
||||
class:disable={!selectedDoneStates.has(state._id)}
|
||||
on:click={() => {
|
||||
doneStateClick(state._id)
|
||||
}}
|
||||
>
|
||||
{#if state._class === task.class.WonState}
|
||||
<Won size="medium" />
|
||||
{:else}
|
||||
<Lost size="medium" />
|
||||
{/if}
|
||||
<span class="ml-2">
|
||||
{state.title}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<StatesBar bind:state {space} on:change={() => updateQuery(search, selectedDoneStates)} />
|
||||
{/if}
|
||||
</div>
|
||||
/>
|
||||
{:else}
|
||||
<StatesBar bind:state {space} gap={'none'} on:change={() => updateQuery(search, selectedDoneStates)} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="statustableview-container">
|
||||
<TableBrowser {_class} bind:query config={resConfig} {options} bind:filters showNotification />
|
||||
@ -181,61 +169,23 @@
|
||||
<style lang="scss">
|
||||
.statustableview-container {
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0.75rem;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-left: 2.5rem;
|
||||
margin-right: 1.75rem;
|
||||
.buttons {
|
||||
padding: 0.125rem 0;
|
||||
}
|
||||
.states {
|
||||
max-width: 75%;
|
||||
}
|
||||
}
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
column-gap: 1rem;
|
||||
padding: 0.75rem 0.75rem 0.75rem 2.5rem;
|
||||
width: 100%;
|
||||
min-height: 3.25rem;
|
||||
min-width: 0;
|
||||
background-color: var(--board-bg-color);
|
||||
|
||||
.button {
|
||||
height: 2.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-button-bg-enabled);
|
||||
border-color: var(--theme-button-border-enabled);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--theme-button-bg-enabled);
|
||||
color: var(--theme-caption-color);
|
||||
border-color: var(--theme-button-border-enabled);
|
||||
}
|
||||
}
|
||||
|
||||
.doneState {
|
||||
padding: 0.5rem 0.75rem;
|
||||
height: 2.5rem;
|
||||
border: 1px solid var(--theme-button-border-enabled);
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
|
||||
&.won {
|
||||
background-color: #60b96e;
|
||||
}
|
||||
&.lost {
|
||||
background-color: #f06c63;
|
||||
}
|
||||
&.withoutDone {
|
||||
background-color: var(--theme-bg-focused-color);
|
||||
}
|
||||
&.disable {
|
||||
background-color: var(--theme-button-bg-enabled);
|
||||
}
|
||||
}
|
||||
.doneState + .doneState {
|
||||
margin-left: 0.75rem;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
// border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
</style>
|
||||
|
@ -24,16 +24,21 @@
|
||||
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let state: Ref<State> | undefined = undefined
|
||||
export let gap: 'small' | 'big' = 'small'
|
||||
export let gap: 'none' | 'small' | 'big' = 'small'
|
||||
|
||||
let states: State[] = []
|
||||
|
||||
let div: HTMLElement
|
||||
let divScroll: HTMLElement
|
||||
let divBar: HTMLElement
|
||||
let isScrolling: boolean = false
|
||||
let dX: number
|
||||
let timer: number
|
||||
|
||||
let maskLeft: boolean = false
|
||||
let maskRight: boolean = false
|
||||
let mask: 'left' | 'right' | 'both' | 'none' = 'none'
|
||||
let stepStyle: string
|
||||
$: stepStyle = gap === 'small' ? 'gap-1' : 'gap-2'
|
||||
$: stepStyle = gap === 'small' ? 'gap-1' : gap === 'big' ? 'gap-2' : ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -51,21 +56,82 @@
|
||||
}
|
||||
)
|
||||
|
||||
const checkBar = (): void => {
|
||||
if (divBar && divScroll) {
|
||||
const trackW = divScroll.clientWidth
|
||||
const scrollW = divScroll.scrollWidth
|
||||
const proc = scrollW / trackW
|
||||
divBar.style.width = divScroll.clientWidth / proc + 'px'
|
||||
divBar.style.left = divScroll.scrollLeft / proc + 'px'
|
||||
if (mask === 'none') divBar.style.visibility = 'hidden'
|
||||
else {
|
||||
divBar.style.visibility = 'visible'
|
||||
if (divBar) {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
divBar.style.opacity = '1'
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
if (divBar) divBar.style.opacity = '0'
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
if (divScroll.clientWidth >= divScroll.scrollWidth) divBar.style.visibility = 'hidden'
|
||||
}
|
||||
}
|
||||
|
||||
const onScroll = (event: MouseEvent): void => {
|
||||
if (isScrolling && divBar && divScroll) {
|
||||
const rectScroll = divScroll.getBoundingClientRect()
|
||||
let X = event.clientX - dX
|
||||
if (X < rectScroll.left) X = rectScroll.left
|
||||
if (X > rectScroll.right - divBar.clientWidth) X = rectScroll.right - divBar.clientWidth
|
||||
divBar.style.left = X - rectScroll.x + 'px'
|
||||
const leftBar = X - rectScroll.x
|
||||
const widthScroll = rectScroll.width - divBar.clientWidth
|
||||
const procBar = leftBar / widthScroll
|
||||
divScroll.scrollLeft = (divScroll.scrollWidth - divScroll.clientWidth) * procBar
|
||||
}
|
||||
}
|
||||
const onScrollEnd = (event: MouseEvent): void => {
|
||||
const el: HTMLElement = event.currentTarget as HTMLElement
|
||||
if (el && isScrolling) {
|
||||
document.removeEventListener('mousemove', onScroll)
|
||||
document.body.style.userSelect = 'auto'
|
||||
document.body.style.webkitUserSelect = 'auto'
|
||||
}
|
||||
document.removeEventListener('mouseup', onScrollEnd)
|
||||
isScrolling = false
|
||||
}
|
||||
const onScrollStart = (event: MouseEvent): void => {
|
||||
const el: HTMLElement = event.currentTarget as HTMLElement
|
||||
if (el && divScroll) {
|
||||
dX = event.clientX - el.getBoundingClientRect().x
|
||||
document.addEventListener('mouseup', onScrollEnd)
|
||||
document.addEventListener('mousemove', onScroll)
|
||||
document.body.style.userSelect = 'none'
|
||||
document.body.style.webkitUserSelect = 'none'
|
||||
isScrolling = true
|
||||
}
|
||||
}
|
||||
|
||||
const checkMask = (): void => {
|
||||
maskLeft = !!(div && div.scrollLeft > 1)
|
||||
maskRight = !!(div && div.scrollWidth - div.scrollLeft - div.clientWidth > 1)
|
||||
maskLeft = !!(divScroll && divScroll.scrollLeft > 1)
|
||||
maskRight = !!(divScroll && divScroll.scrollWidth - divScroll.scrollLeft - divScroll.clientWidth > 1)
|
||||
if (maskLeft || maskRight) {
|
||||
if (maskLeft && maskRight) mask = 'both'
|
||||
else if (maskLeft) mask = 'left'
|
||||
else if (maskRight) mask = 'right'
|
||||
} else mask = 'none'
|
||||
|
||||
if (!isScrolling) checkBar()
|
||||
}
|
||||
|
||||
const selectItem = (ev: Event, item: State): void => {
|
||||
const el: HTMLElement = ev.currentTarget as HTMLElement
|
||||
const rect = el.getBoundingClientRect()
|
||||
const rectScroll = div.getBoundingClientRect()
|
||||
div.scrollBy({
|
||||
const rectScroll = divScroll.getBoundingClientRect()
|
||||
divScroll.scrollBy({
|
||||
top: 0,
|
||||
left: rect.left + rect.width / 2 - (rectScroll.left + rectScroll.width / 2),
|
||||
behavior: 'smooth'
|
||||
@ -85,26 +151,98 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (div) {
|
||||
if (divScroll) {
|
||||
const observer = new IntersectionObserver(() => checkMask(), { root: null, threshold: 0.1 })
|
||||
const tempEl = divScroll.querySelector('*') as HTMLElement
|
||||
if (tempEl) observer.observe(tempEl)
|
||||
checkMask()
|
||||
div.addEventListener('scroll', checkMask)
|
||||
divScroll.addEventListener('scroll', checkMask)
|
||||
}
|
||||
})
|
||||
onDestroy(() => {
|
||||
if (div) div.removeEventListener('scroll', checkMask)
|
||||
if (divScroll) divScroll.removeEventListener('scroll', checkMask)
|
||||
})
|
||||
const _resize = (): void => checkMask()
|
||||
</script>
|
||||
|
||||
<div bind:this={div} class="antiStatesBar mask-{mask} {stepStyle}">
|
||||
{#each states as item, i (item._id)}
|
||||
<StatesBarElement
|
||||
label={item.title}
|
||||
position={getPosition(i)}
|
||||
selected={item._id === state}
|
||||
color={getPlatformColor(item.color)}
|
||||
on:click={(ev) => {
|
||||
if (item._id !== state) selectItem(ev, item)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
<svelte:window on:resize={_resize} />
|
||||
<div class="statesbar-container">
|
||||
<div bind:this={divScroll} class="antiStatesBar mask-{mask} {stepStyle}">
|
||||
{#each states as item, i (item._id)}
|
||||
<StatesBarElement
|
||||
label={item.title}
|
||||
position={getPosition(i)}
|
||||
selected={item._id === state}
|
||||
color={getPlatformColor(item.color)}
|
||||
on:click={(ev) => {
|
||||
if (item._id !== state) selectItem(ev, item)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<div
|
||||
class="bar"
|
||||
class:hovered={isScrolling}
|
||||
bind:this={divBar}
|
||||
on:mousedown={onScrollStart}
|
||||
on:mouseleave={checkMask}
|
||||
/>
|
||||
<div class="track" class:hovered={isScrolling} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.statesbar-container {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.track {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: 0;
|
||||
height: 5px;
|
||||
transform-origin: center;
|
||||
transform: scaleX(0);
|
||||
transition: all 0.1s ease-in-out;
|
||||
background-color: var(--scrollbar-track-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.bar {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: 0;
|
||||
height: 5px;
|
||||
min-width: 2rem;
|
||||
max-width: calc(100% - 12px);
|
||||
transform-origin: center;
|
||||
transform: scaleY(0.5);
|
||||
background-color: var(--scrollbar-bar-color);
|
||||
border-radius: 0.125rem;
|
||||
opacity: 0;
|
||||
box-shadow: 0 0 1px 1px var(--board-bg-color);
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover,
|
||||
&.hovered {
|
||||
background-color: var(--scrollbar-bar-hover);
|
||||
transform: scaleY(1);
|
||||
border-radius: 0.25rem;
|
||||
opacity: 1 !important;
|
||||
box-shadow: 0 0 1px black;
|
||||
|
||||
& + .track {
|
||||
visibility: visible;
|
||||
left: 0;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
&.hovered {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -28,17 +28,16 @@
|
||||
let svgBack: SVGElement
|
||||
|
||||
afterUpdate(() => {
|
||||
if (text) lenght = text.clientWidth + 20 > 300 ? 300 : text.clientWidth + 20
|
||||
if (divBar) divBar.style.width = lenght + 20 + 'px'
|
||||
if (svgBack) svgBack.style.width = lenght + 20 + 'px'
|
||||
if (text) lenght = text.clientWidth + 32 > 300 ? 300 : text.clientWidth + 32
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="hidden-text" bind:this={text}>{label}</div>
|
||||
<div class="hidden-text text-md font-medium" bind:this={text}>{label}</div>
|
||||
{#if lenght > 0}
|
||||
<div
|
||||
bind:this={divBar}
|
||||
class="asb-bar"
|
||||
class:selected
|
||||
class:cursor-pointer={!selected}
|
||||
class:cursor-default={selected}
|
||||
on:click|stopPropagation
|
||||
@ -46,7 +45,7 @@
|
||||
<svg
|
||||
bind:this={svgBack}
|
||||
class="asb-bar__back"
|
||||
viewBox="0 0 {lenght + 20} 36"
|
||||
viewBox="0 0 {lenght} 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
@ -54,30 +53,33 @@
|
||||
<path
|
||||
class="asb-bar__{selected ? 'selected' : 'element'}"
|
||||
style={selected ? `fill: ${color};` : ''}
|
||||
d="M0,8c0-4.4,3.6-8,8-8h2h{lenght}h1.8c0.8,0,1.6,0.5,1.9,1.3l6.1,16c0.2,0.5,0.2,1,0,1.4l-6.1,16c-0.3,0.8-1,1.3-1.9,1.3L{lenght +
|
||||
10},36H10 l-2,0c-4.4,0-8-3.6-8-8V8z"
|
||||
d="M0,5.3C0,2.4,2.3,0,5.2,0h1.3h{lenght -
|
||||
13}h1.2c0.5,0,1,0.3,1.2,0.9l4,10.7c0.1,0.3,0.1,0.7,0,0.9l-4,10.7c-0.2,0.5-0.7,0.9-1.2,0.9 l-1.2,0h-{lenght -
|
||||
13}H5.2C2.3,24,0,21.6,0,18.7V5.3z"
|
||||
/>
|
||||
{:else if position === 'middle'}
|
||||
<path
|
||||
class="asb-bar__{selected ? 'selected' : 'element'}"
|
||||
style={selected ? `fill: ${color};` : ''}
|
||||
d="M6.1,17.3l-6-15.9C-0.2,0.7,0.3,0,1,0h9h{lenght}h1.8c0.8,0,1.6,0.5,1.9,1.3l6.1,16c0.2,0.5,0.2,1,0,1.4l-6.1,16 c-0.3,0.8-1,1.3-1.9,1.3H{lenght +
|
||||
10}H10H1c-0.7,0-1.2-0.7-0.9-1.4l6-15.9C6.3,18.3,6.3,17.7,6.1,17.3z"
|
||||
d="M4,11.5L0.1,0.9C-0.1,0.5,0.2,0,0.6,0h5.8h{lenght -
|
||||
13}h1.2c0.5,0,1,0.3,1.2,0.9l4,10.7c0.1,0.3,0.1,0.7,0,0.9l-4,10.7 c-0.2,0.5-0.7,0.9-1.2,0.9h-1.2h-{lenght -
|
||||
13}H0.6c-0.5,0-0.8-0.5-0.6-0.9L4,12.5C4.1,12.2,4.1,11.8,4,11.5z"
|
||||
/>
|
||||
{:else if position === 'end'}
|
||||
<path
|
||||
class="asb-bar__{selected ? 'selected' : 'element'}"
|
||||
style={selected ? `fill: ${color};` : ''}
|
||||
d="M6.1,17.3l-6-15.9C-0.2,0.7,0.3,0,1,0h9h{lenght}h2c4.4,0,8,3.6,8,8v20c0,4.4-3.6,8-8,8h-2H10H1 c-0.7,0-1.2-0.7-0.9-1.4l6-15.9C6.3,18.3,6.3,17.7,6.1,17.3z"
|
||||
d="M4.1,11.5l-4-10.6C-0.1,0.5,0.2,0,0.7,0h{lenght - 7}C{lenght -
|
||||
3},0,{lenght},2.4,{lenght},5.3v13.3c0,2.9-2.4,5.3-5.3,5.3h-{lenght}H0.6c-0.5,0-0.8-0.5-0.6-0.9L4,12.5C4.1,12.2,4.1,11.8,4,11.5z"
|
||||
/>
|
||||
{:else}
|
||||
<path
|
||||
class="asb-bar__{selected ? 'selected' : 'element'}"
|
||||
style={selected ? `fill: ${color};` : ''}
|
||||
d="M0,8c0-4.4,3.6-8,8-8l2,0h{lenght}l2,0c4.4,0,8,3.6,8,8v20c0,4.4-3.6,8-8,8h-2H10H8c-4.4,0-8-3.6-8-8V8z"
|
||||
d="M0,5.3C0,2.4,2.3,0,5.2,0h1.3h{lenght}h1.3C49.7,0,52,2.4,52,5.3v13.3c0,2.9-2.3,5.3-5.2,5.3h-1.3h-{lenght}H5.2 C2.3,24,0,21.6,0,18.7V5.3z"
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
<div class="asb-label__container"><div class="overflow-label">{label}</div></div>
|
||||
<div class="asb-label__container" class:selected><div class="overflow-label">{label}</div></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
Loading…
Reference in New Issue
Block a user