move vertical tab switcher into helpers since it's not meant to be used elsewhere

This commit is contained in:
Henry Jameson 2025-11-25 19:12:18 +02:00
commit ce04595e36
5 changed files with 3 additions and 6 deletions

View file

@ -0,0 +1,207 @@
// eslint-disable-next-line no-unused
import { h, Fragment } from 'vue'
import { mapState } from 'pinia'
import { throttle } from 'lodash'
import { mapState as mapPiniaState } from 'pinia'
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
import './vertical_tab_switcher.scss'
import { useInterfaceStore } from 'src/stores/interface'
const findFirstUsable = (slots) => slots.findIndex(_ => _.props)
export default {
name: 'VerticalTabSwitcher',
props: {
renderOnlyFocused: {
required: false,
type: Boolean,
default: false
},
onSwitch: {
required: false,
type: Function,
default: undefined
},
activeTab: {
required: false,
type: String,
default: undefined
},
bodyScrollLock: {
required: false,
type: Boolean,
default: false
},
parentCollapsed: {
required: false,
type: Boolean,
default: null
}
},
data () {
return {
active: findFirstUsable(this.slots()),
resizeHandler: null,
navSide: 'tabs'
}
},
computed: {
activeIndex () {
// In case of controlled component
if (this.activeTab) {
return this.slots().findIndex(slot => slot && slot.props && this.activeTab === slot.props.key)
} else {
return this.active
}
},
isActive () {
return tabName => {
const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName
return this.$slots.default().findIndex(isWanted) === this.activeIndex
}
},
...mapPiniaState(useInterfaceStore, {
mobileLayout: store => store.layoutType === 'mobile'
}),
},
beforeUpdate () {
const currentSlot = this.slots()[this.active]
if (!currentSlot.props) {
this.active = findFirstUsable(this.slots())
}
},
methods: {
clickTab (index) {
return (e) => {
e.preventDefault()
this.setTab(index)
}
},
setTab (index) {
if (typeof this.onSwitch === 'function') {
this.onSwitch.call(null, this.slots()[index].key)
}
this.active = index
this.changeNavSide('content')
},
changeNavSide (side) {
if (this.navSide !== side) {
this.navSide = side
}
},
// DO NOT put it to computed, it doesn't work (caching?)
slots () {
if (this.$slots.default()[0].type === Fragment) {
return this.$slots.default()[0].children
}
return this.$slots.default()
}
},
render () {
const tabs = this.slots()
.map((slot, index) => {
const props = slot.props
if (!props) return
const classesTab = ['vertical-tab', 'menu-item']
if (this.activeIndex === index && useInterfaceStore().layoutType !== 'mobile') {
classesTab.push('-active')
}
return (
<button
disabled={props.disabled}
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
role="tab"
title={props.label}
>
{!props.icon ? '' : (<FAIcon class="tab-icon" size="1x" fixed-width icon={props.icon}/>)}
<span class="text">
{props.label}
</span>
</button>
)
})
const contents = this.slots().map((slot, index) => {
const props = slot.props
if (!props) return
const active = this.activeIndex === index
const classes = ['tab-content-wrapper', active ? '-active' : '-hidden' ]
if (props.fullHeight) {
classes.push('-full-height')
}
let delayRender = slot.props['delay-render']
if (delayRender && active) {
slot.props['delay-render'] = false
delayRender = false
}
const renderSlot = (!delayRender && (!this.renderOnlyFocused || active))
? slot
: ''
const headerClasses = ['tab-content-label']
const header = (
<h2 class={headerClasses}>
<button
type="button"
onClick={() => this.changeNavSide('tabs')}
class="button-unstyled"
>
<FAIcon
size="lg"
class="back-button-icon"
icon="chevron-left"
/>
</button>
{props.label}
</h2>
)
return (
<div class={classes} >
<div class="tab-mobile-header">
{header}
</div>
<div class="tab-slot-wrapper">
<div class={ ['tab-content', props['full-width'] ? '-full-width' : null].join(' ') } >
{renderSlot}
</div>
</div>
</div>
)
})
const rootClasses = ['vertical-tab-switcher']
if (useInterfaceStore().layoutType === 'mobile') {
rootClasses.push('-mobile')
}
if (this.navSide === 'tabs') {
rootClasses.push('-nav-tabs')
} else {
rootClasses.push('-nav-contents')
}
return (
<div ref="root" class={ rootClasses.join(' ') }>
<div
class="tabs"
role="tablist"
ref="nav"
>
{tabs}
</div>
<div
role="tabpanel"
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
v-body-scroll-lock={this.bodyScrollLock}
ref="contents"
>
{contents}
</div>
</div>
)
}
}

View file

@ -0,0 +1,176 @@
.vertical-tab-switcher {
display: flex;
flex-direction: row;
container-type: inline-size;
> .tabs {
flex: 0 0 15em;
flex-direction: column;
overflow: hidden auto;
white-space: nowrap;
text-overflow: ellipsis;
width: 15em;
min-width: 15em;
border-right: 1px solid;
border-right-color: var(--border);
box-sizing: border-box;
> .menu-item {
padding: 0.5em 1em;
.tab-icon {
vertical-align: middle;
margin-right: 0.75em;
}
}
}
> .contents {
flex: 1 0 35em;
.tab-content {
align-self: center;
height: 100%;
&:not(.-full-width) {
max-width: 40em;
}
&.-full-width {
align-self: stretch;
}
}
.tab-content-label {
box-sizing: border-box;
margin: 0;
border-bottom: 1px solid var(--border);
display: none;
button {
box-sizing: border-box;
padding: 0.5em;
}
}
.tab-slot-wrapper {
flex: 1 1 auto;
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.tab-content-wrapper {
flex: 1 1 auto;
height: 100%;
&.-hidden {
display: none;
}
}
}
&.-mobile {
> .contents {
.tab-content-label {
display: block
}
}
&.-nav-contents {
> .contents {
display: block;
flex-grow: 1;
flex-shrink: 1;
}
> .tabs {
display: none;
flex-grow: 0;
flex-shrink: 1;
}
}
&.-nav-tabs {
> .tabs {
display: block;
flex-grow: 1;
}
> .contents {
display: none;
flex-grow: 0;
flex-shrink: 1;
}
}
}
@supports (container-type: inline-size) {
&,
&.-mobile {
&.-nav-contents,
&.-nav-tabs {
/* I THINK it's a false positive and eslint doesn't understand the @-rule */
/* stylelint-disable no-descending-specificity */
> .contents {
display: block;
flex-grow: 1;
}
> .tabs {
display: block;
flex-grow: 0;
}
/* stylelint-enable no-descending-specificity */
}
}
@container (width < 50em) {
> .contents {
.tab-content-label {
display: block
}
}
&.-mobile {
> .contents {
.tab-content-label {
display: block
}
}
}
&,
&.-mobile {
&.-nav-contents {
> .contents {
display: block;
flex-grow: 1;
}
> .tabs {
display: none;
flex-grow: 0;
flex-shrink: 1;
}
}
&.-nav-tabs {
/* stylelint-disable no-descending-specificity */
> .tabs {
display: block;
flex-grow: 1;
}
> .contents {
display: none;
flex-grow: 0;
flex-shrink: 1;
}
/* stylelint-enable no-descending-specificity */
}
}
}
}
}

View file

@ -1,4 +1,4 @@
import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx'
import VerticalTabSwitcher from './helpers/vertical_tab_switcher.jsx'
import InstanceTab from './admin_tabs/instance_tab.vue'
import LimitsTab from './admin_tabs/limits_tab.vue'

View file

@ -1,4 +1,4 @@
import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx'
import VerticalTabSwitcher from './helpers/vertical_tab_switcher.jsx'
import DataImportExportTab from './tabs/data_import_export_tab.vue'
import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'

View file

@ -1,5 +1,3 @@
import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx'
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
@ -88,8 +86,7 @@ const AppearanceTab = {
UnitSetting,
ProfileSettingIndicator,
Preview,
PaletteEditor,
VerticalTabSwitcher
PaletteEditor
},
mounted () {
useInterfaceStore().getThemeData()