vertical tab switcher initial implementation
This commit is contained in:
parent
a3a35e76a8
commit
a96f533777
10 changed files with 254 additions and 18 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx'
|
||||||
|
|
||||||
import InstanceTab from './admin_tabs/instance_tab.vue'
|
import InstanceTab from './admin_tabs/instance_tab.vue'
|
||||||
import LimitsTab from './admin_tabs/limits_tab.vue'
|
import LimitsTab from './admin_tabs/limits_tab.vue'
|
||||||
|
|
@ -31,7 +31,7 @@ library.add(
|
||||||
|
|
||||||
const SettingsModalAdminContent = {
|
const SettingsModalAdminContent = {
|
||||||
components: {
|
components: {
|
||||||
TabSwitcher,
|
VerticalTabSwitcher,
|
||||||
|
|
||||||
InstanceTab,
|
InstanceTab,
|
||||||
LimitsTab,
|
LimitsTab,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<tab-switcher
|
<vertical-tab-switcher
|
||||||
v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)"
|
v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)"
|
||||||
ref="tabSwitcher"
|
ref="tabSwitcher"
|
||||||
class="settings_tab-switcher"
|
class="settings_tab-switcher"
|
||||||
|
|
@ -71,7 +71,7 @@
|
||||||
>
|
>
|
||||||
<EmojiTab />
|
<EmojiTab />
|
||||||
</div>
|
</div>
|
||||||
</tab-switcher>
|
</vertical-tab-switcher>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./settings_modal_admin_content.js"></script>
|
<script src="./settings_modal_admin_content.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx'
|
||||||
|
|
||||||
import DataImportExportTab from './tabs/data_import_export_tab.vue'
|
import DataImportExportTab from './tabs/data_import_export_tab.vue'
|
||||||
import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
|
import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
|
||||||
|
|
@ -42,7 +42,7 @@ library.add(
|
||||||
|
|
||||||
const SettingsModalContent = {
|
const SettingsModalContent = {
|
||||||
components: {
|
components: {
|
||||||
TabSwitcher,
|
VerticalTabSwitcher,
|
||||||
|
|
||||||
DataImportExportTab,
|
DataImportExportTab,
|
||||||
MutesAndBlocksTab,
|
MutesAndBlocksTab,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<tab-switcher
|
<vertical-tab-switcher
|
||||||
ref="tabSwitcher"
|
ref="tabSwitcher"
|
||||||
class="settings_tab-switcher"
|
class="settings_tab-switcher"
|
||||||
:side-tab-bar="true"
|
:side-tab-bar="true"
|
||||||
|
|
@ -94,7 +94,7 @@
|
||||||
>
|
>
|
||||||
<VersionTab />
|
<VersionTab />
|
||||||
</div>
|
</div>
|
||||||
</tab-switcher>
|
</vertical-tab-switcher>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./settings_modal_user_content.js"></script>
|
<script src="./settings_modal_user_content.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx'
|
||||||
|
|
||||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||||
|
|
@ -97,7 +97,7 @@ const AppearanceTab = {
|
||||||
FontControl,
|
FontControl,
|
||||||
Preview,
|
Preview,
|
||||||
PaletteEditor,
|
PaletteEditor,
|
||||||
TabSwitcher
|
VerticalTabSwitcher
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
useInterfaceStore().getThemeData()
|
useInterfaceStore().getThemeData()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<tab-switcher
|
<vertical-tab-switcher
|
||||||
class="appearance-tab"
|
class="appearance-tab"
|
||||||
:label="$t('settings.appearance')"
|
:label="$t('settings.appearance')"
|
||||||
ref="tabSwitcher"
|
ref="tabSwitcher"
|
||||||
|
|
@ -446,7 +446,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</tab-switcher>
|
</vertical-tab-switcher>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./appearance_tab.js"></script>
|
<script src="./appearance_tab.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx'
|
||||||
|
|
||||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||||
|
|
@ -92,7 +92,7 @@ const GeneralTab = {
|
||||||
ProfileSettingIndicator,
|
ProfileSettingIndicator,
|
||||||
ScopeSelector,
|
ScopeSelector,
|
||||||
Select,
|
Select,
|
||||||
TabSwitcher,
|
VerticalTabSwitcher,
|
||||||
FontControl
|
FontControl
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<tab-switcher
|
<vertical-tab-switcher
|
||||||
:label="$t('settings.general')"
|
:label="$t('settings.general')"
|
||||||
ref="tabSwitcher"
|
ref="tabSwitcher"
|
||||||
class="settings_tab-switcher"
|
class="settings_tab-switcher"
|
||||||
:side-tab-bar="true"
|
|
||||||
:scrollable-tabs="true"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:label="$t('settings.behavior')"
|
:label="$t('settings.behavior')"
|
||||||
|
|
@ -551,7 +549,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</tab-switcher>
|
</vertical-tab-switcher>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./general_tab.js"></script>
|
<script src="./general_tab.js"></script>
|
||||||
|
|
|
||||||
166
src/components/tab_switcher/vertical_tab_switcher.jsx
Normal file
166
src/components/tab_switcher/vertical_tab_switcher.jsx
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
active: findFirstUsable(this.slots()),
|
||||||
|
resizeHandler: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
console.log(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 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()
|
||||||
|
},
|
||||||
|
setTab (index) {
|
||||||
|
if (typeof this.onSwitch === 'function') {
|
||||||
|
this.onSwitch.call(null, this.slots()[index].key)
|
||||||
|
}
|
||||||
|
this.active = index
|
||||||
|
this.$refs.contents.scrollTop = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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) {
|
||||||
|
classesTab.push('-active')
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
disabled={props.disabled}
|
||||||
|
onClick={this.clickTab(index)}
|
||||||
|
class={classesTab.join(' ')}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
{!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 = [ 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 header = (
|
||||||
|
<h1 class="tab-content-label">
|
||||||
|
{props.label}
|
||||||
|
</h1>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref="contents" class={classes}>
|
||||||
|
{header}
|
||||||
|
{renderSlot}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref="root" class="vertical-tab-switcher">
|
||||||
|
<div
|
||||||
|
class="tabs -navigation-mode -tabs"
|
||||||
|
role="tablist"
|
||||||
|
ref="nav"
|
||||||
|
>
|
||||||
|
{tabs}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
|
||||||
|
v-body-scroll-lock={this.bodyScrollLock}
|
||||||
|
>
|
||||||
|
{contents}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/components/tab_switcher/vertical_tab_switcher.scss
Normal file
72
src/components/tab_switcher/vertical_tab_switcher.scss
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
.vertical-tab-switcher {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
> .tabs {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
overflow: hidden auto;
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 1px solid;
|
||||||
|
border-right-color: var(--border);
|
||||||
|
min-width: 10em;
|
||||||
|
|
||||||
|
> .menu-item {
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 0.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .contents {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-height:not(.hidden) {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> *:not(.tab-content-label) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content-label {
|
||||||
|
font-size: 1.5em;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
margin: 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tabs,
|
||||||
|
> .content {
|
||||||
|
transition: width 2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-tabs {
|
||||||
|
> .tabs {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .content {
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-content {
|
||||||
|
> .tabs {
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue