1
0
Fork 0
mirror of https://gitnet.fr/deblan/side_menu.git synced 2025-12-17 21:02:25 +01:00

refactor of menus

begin work on settings
This commit is contained in:
Simon Vieille 2025-04-09 19:15:43 +02:00
parent ecbe2f7d72
commit 52b2d18a03
No known key found for this signature in database
GPG key ID: 579388D585F70417
26 changed files with 1613 additions and 1667 deletions

View file

@ -123,14 +123,14 @@ class Application extends App implements IBootstrap
//Util::addStyle(self::APP_ID, 'sideMenu');
$assets = [
// 'stylesheet' => [
// 'route' => 'side_menu.Css.stylesheet',
// 'type' => 'link',
// 'route_attr' => 'href',
// 'attr' => [
// 'rel' => 'stylesheet',
// ],
// ],
'stylesheet' => [
'route' => 'side_menu.Css.stylesheet',
'type' => 'link',
'route_attr' => 'href',
'attr' => [
'rel' => 'stylesheet',
],
],
// 'script' => [
// 'route' => 'side_menu.Js.script',
// 'type' => 'script',

View file

@ -145,6 +145,8 @@ class JsController extends Controller
'opener-hover' => $this->config->getAppValueBool('opener-hover', '0'),
'external-sites-in-top-menu' => $this->config->getAppValueBool('external-sites-in-top-menu', '0'),
'force-light-icon' => $this->config->getAppValueBool('force-light-icon', '0'),
'display-logo' => $this->config->getAppValueBool('display-logo', '1'),
'use-avatar' => $this->config->getAppValueBool('use-avatar', '0'),
'hide-when-no-apps' => $this->config->getAppValueBool('hide-when-no-apps', '0'),
'loader-enabled' => $this->config->getAppValueBool('loader-enabled', '1'),
'always-displayed' => $this->config->getAppValueBool('always-displayed', '0'),

View file

@ -115,6 +115,7 @@ class NavController extends Controller
$appsCategories[$app['id']][] = $category;
$items[$category]['apps'][$app['id']] = [
'id' => $app['id'],
'name' => $app['name'],
'href' => $app['href'],
'icon' => $app['icon'],

View file

@ -10,10 +10,12 @@
"format": "./node_modules/.bin/prettier src --write"
},
"dependencies": {
"@babel/core": ">=7.12.0 <8.0.0",
"@nextcloud/router": "^3.0.1",
"@nextcloud/vue": "^9.0.0-alpha.8",
"node-polyfill-webpack-plugin": "^4.1.0",
"pinia": "^3.0.1",
"postcss": "^7.0.0 || ^8.0.1",
"vue": "^3.5.13"
},
"browserslist": [

View file

@ -15,261 +15,18 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import './scss/admin.scss'
import { createApp } from 'vue'
import AdminCategoriesCustom from './components/AdminCategoriesCustom.vue'
import { createPinia } from 'pinia'
import { waitContainer } from './lib/dom.js'
let elements = []
import AdminSettings from './pages/AdminSettings.vue'
const selector = '#side-menu-message'
const userConfig = (name, value, callbacks) => {
const url = OC.generateUrl('/apps/side_menu/personalSetting/valueSet')
const formData = []
formData.push('name=' + encodeURIComponent(name))
formData.push('value=' + encodeURIComponent(value))
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.join('&'),
})
.then(callbacks.success)
.catch(callbacks.error)
}
const appConfig = (name, value, callbacks) => {
OCP.AppConfig.setValue('side_menu', name, value, callbacks)
}
const saveSettings = (key) => {
const element = elements[key]
if (!element) {
return
}
let value
let name
if (element.hasAttribute('data-checkbox')) {
name = element.getAttribute('data-name')
value = []
const inputs = document.querySelectorAll('input[name="' + name + '[]"]:checked')
for (let input of inputs) {
value.push(input.value)
}
value = JSON.stringify(value)
} else {
name = element.getAttribute('name')
value = element.value
}
const size = elements.length
if (name === 'cache') {
++value
}
const progress = document.querySelector('#side-menu-save-progress')
progress.style.width = '40px'
progress.style.marginLeft = '5px'
const callbacks = {
success: () => {
const percent = parseInt(((key + 1) * 100) / size)
progress.setAttribute('value', percent)
if (key < size - 1) {
saveSettings(key + 1)
} else {
location.reload()
}
},
error: () => {
OC.msg.finishedError(selector, t('side_menu', 'Error while saving "' + element + '"'))
},
}
if (element.hasAttribute('data-personal')) {
userConfig(name, value, callbacks)
} else {
appConfig(name, value, callbacks)
}
}
const elementToggler = (element) => {
let display = 'none'
if (window.getComputedStyle(element).display === 'none') {
display = 'block'
}
element.style.display = display
}
const updateAppsCategoriesCustom = () => {
let values = {}
for (let item of document.querySelectorAll('.apps-categories-custom')) {
let app = item.getAttribute('data-app')
let value = item.value
if (value) {
values[app] = value
}
}
document.querySelector('#apps-categories-custom').value = JSON.stringify(values)
}
document.addEventListener('DOMContentLoaded', () => {
$('*[data-toggle="tooltip"]').tooltip()
if (document.querySelector('#side-menu-categories-custom')) {
const app = createApp(AdminCategoriesCustom)
app.mount('#side-menu-categories-custom')
}
elements = document.querySelectorAll('.side-menu-setting')
document.querySelector('#side-menu-save').addEventListener('click', (event) => {
event.preventDefault()
OC.msg.startSaving(selector)
saveSettings(0)
})
const resets = document.querySelectorAll('.btn-reset')
for (let btn of resets) {
btn.addEventListener('click', (event) => {
const target = event.target
const values = JSON.parse(target.getAttribute('data-reset'))
target.classList.toggle('btn-reset--progress', true)
for (let i in values) {
document.querySelector(`#${i}`).value = values[i]
}
window.setTimeout(() => {
target.classList.toggle('btn-reset--progress', false)
}, 800)
})
}
const displays = document.querySelectorAll('.side-menu-display')
for (let display of displays) {
display.addEventListener('click', (event) => {
const target = event.target
for (let d of displays) {
d.classList.toggle('is-active', d === display)
}
document.querySelector('#side-menu-always-displayed').value = target.getAttribute('data-alwaysdiplayed')
document.querySelector('#side-menu-big-menu').value = target.getAttribute('data-bigmenu')
document.querySelector('#side-menu-side-with-categories').value = target.getAttribute('data-sidewithcategories')
})
}
for (let item of document.querySelectorAll('.apps-categories-custom')) {
item.addEventListener('change', (event) => {
updateAppsCategoriesCustom()
})
}
for (let item of document.querySelectorAll('.side-menu-setting-live')) {
item.addEventListener('change', (event) => {
const target = event.target
const name = target.getAttribute('name')
let value = target.value
let id = null
if (name === 'background-color-opacity') {
id = '#side-menu-background-color, #side-menu-background-color-to'
} else if (name === 'dark-mode-background-color-opacity') {
id = '#side-menu-dark-mode-background-color, #side-menu-dark-mode-background-color-to'
}
if (id) {
document.querySelector(id).dispatchEvent(new CustomEvent('change'))
return
}
if (name === 'opener') {
const url = OC.generateUrl(`/apps/side_menu/img/${value}.svg`).replace('/index.php', '')
value = `url(${url})`
}
if (name === 'icon-invert-filter' || name === 'icon-opacity') {
value /= 100
}
if (['dark-mode-background-color', 'dark-mode-background-color-to'].indexOf(name) > -1) {
const opacity = parseInt((document.querySelector('#side-menu-dark-mode-background-color-opacity').value * 255) / 100)
value = [value, opacity.toString(16)].join('')
} else if (['background-color', 'background-color-to'].indexOf(name) > -1) {
const opacity = parseInt((document.querySelector('#side-menu-background-color-opacity').value * 255) / 100)
value = [value, opacity.toString(16)].join('')
}
document.documentElement.style.setProperty('--side-menu-' + name, value)
})
}
for (let toggler of document.querySelectorAll('.side-menu-toggler')) {
toggler.addEventListener('click', (event) => {
const target = event.target
const element = document.querySelector(target.getAttribute('data-target'))
elementToggler(element)
})
}
sortable('#categories-list .side-menu-setting-list', {
placeholderClass: 'side-menu-setting-list-drop',
})
try {
sortable('#categories-list .side-menu-setting-list')[0].addEventListener('sortstop', (e) => {
let value = []
for (let item of document.querySelectorAll('#categories-list .side-menu-setting-list-item')) {
value.push(item.getAttribute('data-id'))
}
document.querySelector('input[name="categories-order"]').value = JSON.stringify(value)
})
} catch (e) {}
sortable('#apps-order-list .side-menu-setting-list', {
placeholderClass: 'side-menu-setting-list-drop',
})
try {
sortable('#apps-order-list .side-menu-setting-list')[0].addEventListener('sortstop', (e) => {
let value = []
for (let item of document.querySelectorAll('#apps-order-list .side-menu-setting-list-item')) {
value.push(item.getAttribute('data-id'))
}
document.querySelector('input[name="apps-order"]').value = JSON.stringify(value)
})
} catch (e) {}
waitContainer('#side-menu-admin-settings').then((selector) => {
const pinia = createPinia()
const app = createApp(AdminSettings)
app.use(pinia)
app.mixin({ methods: { t, n }})
app.mount(selector)
})

View file

@ -102,101 +102,92 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</template>
<script>
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
<script setup>
import { NcModal, NcActions, NcActionButton } from '@nextcloud/vue'
import { ref } from 'vue'
export default {
name: 'AdminCategoriesCustom',
components: {
NcModal,
NcActions,
NcActionButton,
},
data() {
return {
input: null,
values: [],
langs: [],
addForm: false,
editForm: false,
newValue: {},
editValue: {},
}
},
mounted() {
this.input = document.querySelector('input[name="categories-custom"]')
this.init()
},
methods: {
init() {
this.values = JSON.parse(this.input.value)
this.langs = JSON.parse(this.input.getAttribute('data-langs'))
},
update() {
this.input.value = JSON.stringify(this.values)
},
showAddForm() {
this.newValue = { id: 'cat' + Math.random().toString().replace('0.', '') }
const { langs, values } = defineProps(['lang', 'values'])
this.addForm = true
},
showEditForm(value) {
this.editValue = { id: value.id }
const input = ref(null)
const values = ref([])
const langs = ref([])
const addForm = ref(false)
const editForm = ref(false)
const newValue = ref({})
const editValue = ref({})
for (let i of this.langs) {
this.editValue[i] = typeof value[i] !== 'undefined' ? value[i] : ''
}
this.editForm = true
},
saveAdd() {
for (let i of this.langs) {
if (!this.newValue[i] || /^\s*$/.test(this.newValue[i])) {
return
}
}
this.values.push(this.newValue)
this.update()
this.hideAddForm()
this.newValue = {}
},
saveEdit() {
for (let i of this.langs) {
if (!this.editValue[i] || /^\s*$/.test(this.editValue[i])) {
return
}
}
for (let i in this.values) {
if (this.values[i].id === this.editValue.id) {
this.values[i] = this.editValue
}
}
this.update()
this.hideEditForm()
},
removeEdit() {
for (let i in this.values) {
if (this.values[i].id === this.editValue.id) {
this.values.splice(i, 1)
}
}
this.update()
this.hideEditForm()
},
hideAddForm() {
this.addForm = false
},
hideEditForm() {
this.editForm = false
},
},
}
// mounted() {
// this.input = document.querySelector('input[name="categories-custom"]')
// this.init()
// },
// methods: {
// init() {
// this.values = JSON.parse(this.input.value)
// this.langs = JSON.parse(this.input.getAttribute('data-langs'))
// },
// update() {
// this.input.value = JSON.stringify(this.values)
// },
// showAddForm() {
// this.newValue = { id: 'cat' + Math.random().toString().replace('0.', '') }
//
// this.addForm = true
// },
// showEditForm(value) {
// this.editValue = { id: value.id }
//
// for (let i of this.langs) {
// this.editValue[i] = typeof value[i] !== 'undefined' ? value[i] : ''
// }
//
// this.editForm = true
// },
// saveAdd() {
// for (let i of this.langs) {
// if (!this.newValue[i] || /^\s*$/.test(this.newValue[i])) {
// return
// }
// }
//
// this.values.push(this.newValue)
// this.update()
// this.hideAddForm()
// this.newValue = {}
// },
// saveEdit() {
// for (let i of this.langs) {
// if (!this.editValue[i] || /^\s*$/.test(this.editValue[i])) {
// return
// }
// }
//
// for (let i in this.values) {
// if (this.values[i].id === this.editValue.id) {
// this.values[i] = this.editValue
// }
// }
//
// this.update()
// this.hideEditForm()
// },
// removeEdit() {
// for (let i in this.values) {
// if (this.values[i].id === this.editValue.id) {
// this.values.splice(i, 1)
// }
// }
//
// this.update()
// this.hideEditForm()
// },
// hideAddForm() {
// this.addForm = false
// },
// hideEditForm() {
// this.editForm = false
// },
// },
// }
</script>
<style>

View file

@ -0,0 +1,33 @@
<template>
</template>
<script setup>
// import AlwaysDisplayImg from '../../img/admin/layout-always-displayed.svg'
// import WideMenuImg from '../../img/admin/layout-big-menu.svg'
// import DefaultImg from '../../img/admin/layout-default.svg'
// import WithCategoriesImg from '../../img/admin/layout-width-categories.svg'
const choices = [
// {id: 1, url: AlwaysDisplayImg},
// {id: 2, url: WideMenuImg},
// {id: 3, url: DefaultImg},
// {id: 4, url: WithCategoriesImg},
]
const carouselConfig = {
itemsToShow: 1.3,
wrapAround: true
}
const { mode, alwaysDisplayed } = defineProps({
mode: {
type: String,
required: true,
},
alwaysDisplayed: {
type: Boolean,
required: true,
}
})
</script>

View file

@ -0,0 +1,5 @@
<template>
<div class="side-menu-setting-table">
<slot></slot>
</div>
</template>

View file

@ -0,0 +1,33 @@
<template>
<div class="side-menu-setting-label" :class="{
'side-menu-setting-label-short': short,
'side-menu-setting-label--top': top,
'side-menu-setting-label--middle': middle,
}">
{{ t('side_menu', label) }}
</div>
</template>
<script setup>
const { short, label } = defineProps({
short: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: true,
},
middle: {
type: Boolean,
required: false,
default: false,
},
top: {
type: Boolean,
required: false,
default: true,
},
})
</script>

View file

@ -0,0 +1,5 @@
<template>
<div class="side-menu-setting-row">
<slot></slot>
</div>
</template>

View file

@ -0,0 +1,15 @@
<template>
<div class="side-menu-setting-form" :class="{'side-menu-setting-form-long': long}">
<slot></slot>
</div>
</template>
<script setup>
const { long } = defineProps({
long: {
type: Boolean,
required: false,
default: false,
},
})
</script>

View file

@ -5,7 +5,7 @@ const getActiveAppId = () => {
for (let id in apps) {
if (apps[id].active) {
return id
return apps[id].id
}
}

12
src/lib/menu.js Normal file
View file

@ -0,0 +1,12 @@
const focusActiveApp = (menu) => {
window.setTimeout(() => {
const a = menu.querySelector('.side-menu-app.active a')
|| menu.querySelector('.side-menu-app a')
if (a) {
a.focus()
}
}, 500)
}
export { focusActiveApp }

View file

@ -40,6 +40,7 @@ const display = ref(null)
const hasPageLoader = ref(false)
const isOpen = ref(false)
const hasApps = ref(false)
const openerHover = ref(false)
const toggleMenu = (value) => {
isOpen.value = value
@ -78,6 +79,10 @@ const createOpener = () => {
}
opener.addEventListener('click', () => toggleMenu(true), true)
if (openerHover.value) {
opener.addEventListener('mouseenter', () => toggleMenu(true), true)
}
}
const createLoader = () => {
@ -111,6 +116,7 @@ onMounted(async () => {
}
hasPageLoader.value = config.value['loader-enabled']
openerHover.value = config.value['opener-hover']
if (hasApps.value) {
createOpener()

View file

@ -77,10 +77,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'
import { ref, useTemplateRef, onMounted, watch } from 'vue'
import { useNavStore } from '../store/nav.js'
import { useConfigStore } from '../store/config.js'
import { getActiveAppId } from '../lib/app.js'
import { focusActiveApp } from '../lib/menu.js'
import { containsAppsMatchingSearch, isAppMatchingSearch } from '../lib/search.js'
import OpenerButton from '../components/OpenerButton'
@ -108,6 +109,12 @@ const openerHover = ref(false)
const menu = useTemplateRef('menu')
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches
watch(() => open, (val) => {
if (val) {
focusActiveApp(menu.value)
}
})
onMounted(async () => {
const config = await configStore.getConfig()

View file

@ -21,8 +21,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
ref="menu"
>
<div
v-if="settings || !openerHover || (!avatar && !alwaysDisplayed && logo) || avatar"
class="side-menu-header"
v-if="settings || displayLogo || open || !openerHover"
>
<SettingsButton
v-if="settings && open"
@ -32,26 +32,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
<AppSearch v-model="search" v-if="open" />
<OpenerButton
v-if="(!alwaysDisplayed && !openerHover) || isTouchDevice"
v-if="!openerHover || isTouchDevice"
@click="$emit('toggle')"
/>
<Logo
v-if="!avatar && !alwaysDisplayed && logo"
v-if="displayLogo"
:classes="{ 'side-menu-logo': true, avatardiv: false }"
:image="logo"
:link="logoLink"
/>
<Logo
v-if="avatar && !alwaysDisplayed"
:classes="{ 'side-menu-logo': true, avatardiv: true }"
:image="avatar"
:image="useAvatarAsLogo ? avatar : logo"
:link="logoLink"
/>
</div>
<ul
class="side-menu-apps-list"
:class="{ 'side-menu-apps-list--with-settings': !!settings }"
:class="{ 'side-menu-apps-list--with-logo': displayLogo }"
>
<template
v-for="(app, key) in apps"
@ -59,7 +53,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<SideMenuApp
v-if="isAppMatchingSearch(app, search)"
:classes="{ 'side-menu-app': true, active: app.active }"
:classes="{ 'side-menu-app': true, active: app.id === activeApp }"
:icon="app.icon"
:label="app.name"
:href="app.href"
@ -74,7 +68,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { ref, useTemplateRef, onMounted, watch } from 'vue'
import { useConfigStore } from '../store/config.js'
import { useNavStore } from '../store/nav.js'
import { focusActiveApp } from '../lib/menu.js'
import { isAppMatchingSearch } from '../lib/search.js'
import { getActiveAppId } from '../lib/app.js'
import OpenerButton from '../components/OpenerButton'
import SettingsButton from '../components/SettingsButton'
@ -94,12 +90,15 @@ const navStore = useNavStore()
const configStore = useConfigStore()
const targetBlankApps = ref(null)
const forceLightIcon = ref(null)
const activeApp = ref(null)
const avatar = ref(null)
const logo = ref(null)
const logoLink = ref(null)
const settings = ref(null)
const openerHover = ref(false)
const alwaysDisplayed = ref(false)
const displayLogo = ref(false)
const useAvatarAsLogo = ref(false)
const search = ref('')
const apps = ref([])
const menu = useTemplateRef('menu')
@ -109,6 +108,12 @@ watch(apps, (val) => {
document.querySelector('html').classList.toggle('side-menu-always-displayed', alwaysDisplayed.value && val.length)
})
watch(() => open, (val) => {
if (val) {
focusActiveApp(menu.value)
}
})
function getFiltredAndSortedApps(items, order, topMenuApps, topSideMenuApps) {
const data = []
@ -136,14 +141,22 @@ function getFiltredAndSortedApps(items, order, topMenuApps, topSideMenuApps) {
onMounted(async () => {
const config = await configStore.getConfig()
alwaysDisplayed.value = config['always-displayed']
targetBlankApps.value = config['target-blank-apps']
forceLightIcon.value = config['force-light-icon']
avatar.value = config['avatar']
logo.value = config['logo']
useAvatarAsLogo.value = config['use-avatar']
displayLogo.value = config['display-logo'] && !alwaysDisplayed.value && (
(!useAvatarAsLogo.value && logo.value) ||
(useAvatarAsLogo.value && avatar.value)
)
logoLink.value = config['logo-link']
settings.value = config['settings']
openerHover.value = config['opener-hover'] && !isTouchDevice
alwaysDisplayed.value = config['always-displayed']
activeApp.value = getActiveAppId()
apps.value = getFiltredAndSortedApps(await navStore.getApps(), config['apps-order'], config['top-menu-apps'], config['top-side-menu-apps'])
@ -151,7 +164,7 @@ onMounted(async () => {
menu.value.addEventListener('mouseleave', () => emit('close'))
}
if (alwaysDisplayed.value) {
if (alwaysDisplayed.value && openerHover.value) {
menu.value.addEventListener('mouseenter', () => emit('open'))
menu.value.addEventListener('mouseleave', () => emit('close'))
}

View file

@ -81,10 +81,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'
import { ref, useTemplateRef, onMounted, watch } from 'vue'
import { useNavStore } from '../store/nav.js'
import { useConfigStore } from '../store/config.js'
import { getActiveAppId } from '../lib/app.js'
import { focusActiveApp } from '../lib/menu.js'
import { containsAppsMatchingSearch, isAppMatchingSearch } from '../lib/search.js'
import OpenerButton from '../components/OpenerButton'
@ -113,6 +114,12 @@ const openerHover = ref(false)
const menu = useTemplateRef('menu')
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches
watch(() => open, (val) => {
if (val) {
focusActiveApp(menu.value)
}
})
onMounted(async () => {
const config = await configStore.getConfig()

104
src/pages/AdminSettings.vue Normal file
View file

@ -0,0 +1,104 @@
<template>
<NcContent app-name="side_menu">
<NcAppContent>
<div class="side-menu-setting">
<NcButton
v-for="item in menu"
@click="setSection(item.section)"
:variant="item.section === section ? 'primary' : 'secondary'"
>{{ trans(item.label) }}</NcButton>
</div>
<TableContainer :class="sectionClass('panel')">
</TableContainer>
<TableContainer :class="sectionClass('color')">
<TableRow>
<TableLabel label="Background color" :short="true" :middle="true"></TableLabel>
<TableValue>
<NcColorPicker v-model="color" class="side-menu-setting-color-picker">
<div :style="{'background-color': color}" class="side-menu-setting-color-picker-value" />
</NcColorPicker>
<NcColorPicker v-model="color" class="side-menu-setting-color-picker">
<div :style="{'background-color': color}" class="side-menu-setting-color-picker-value" />
</NcColorPicker>
</TableValue>
</TableRow>
<TableRow>
<TableLabel label="Background color of current app" :short="true" :middle="true"></TableLabel>
<TableValue>
<NcColorPicker v-model="color" class="side-menu-setting-color-picker">
<div :style="{'background-color': color}" class="side-menu-setting-color-picker-value" />
</NcColorPicker>
</TableValue>
</TableRow>
<TableRow>
<TableLabel label="Text color" :short="true" :middle="true"></TableLabel>
<TableValue>
<NcColorPicker v-model="color" class="side-menu-setting-color-picker">
<div :style="{'background-color': color}" class="side-menu-setting-color-picker-value" />
</NcColorPicker>
</TableValue>
</TableRow>
</TableContainer>
</NcAppContent>
</NcContent>
</template>
<style scoped>
.wrapper {
display: flex;
gap: 12px;
padding-top: calc(var(--default-grid-baseline) * 5);
padding-left: calc(var(--default-grid-baseline) * 7);
padding-right: calc(var(--default-grid-baseline) * 7);
}
.hidden {
display: none;
}
.color-picker {
width: 100px;
height: 34px;
border-radius: 6px;
}
.flex {
display: flex;
}
</style>
<script setup>
import { NcContent, NcAppContent, NcColorPicker, NcButton } from '@nextcloud/vue'
import MenuDisplay from '../components/settings/MenuDisplay'
import TableContainer from '../components/settings/TableContainer'
import TableRow from '../components/settings/TableRow'
import TableLabel from '../components/settings/TableLabel'
import TableValue from '../components/settings/TableValue'
import { ref, onMounted } from 'vue'
const menu = [
{label: 'Panel', section: 'panel'},
{label: 'Colors', section: 'color'},
]
const section = ref('panel')
const color = ref('#ff9')
const setSection = (value) => {
section.value = value
}
const trans = (value) => {
return t('side_menu', value)
}
const sectionClass = (value) => {
return {
hidden: value !== section.value,
}
}
</script>

View file

@ -114,6 +114,7 @@
.side-menu-setting-table {
display: table;
width: 100%;
padding: 10px;
}
.side-menu-setting-row {
@ -141,6 +142,10 @@
vertical-align: top;
}
.side-menu-setting-label--middle {
vertical-align: middle;
}
.side-menu-setting-form {
display: table-cell;
min-width: 300px;
@ -218,3 +223,23 @@
border-color: #c681d4;
color: #fff;
}
.side-menu-setting {
padding: 10px;
display: flex;
gap: 12px;
}
.side-menu-setting-color-picker {
display: inline-block;
margin-right: 12px;
width: 60px;
height: 30px;
&-value {
cursor: pointer;
width: 60px;
height: 30px;
border-radius: 6px;
}
}

View file

@ -41,12 +41,40 @@
display: block;
}
}
.side-menu-opener {
margin-top: 1px !important;
}
&.side-menu-big {
max-width: 100%;
height: auto;
}
&.side-menu-with-categories {
max-width: 290px;
height: 100vh;
.side-menu-categories {
display: block;
padding: 0;
width: 100%;
}
.side-menu-category {
padding: 10px 0;
}
}
&.side-menu-big, &.side-menu-with-categories {
height: auto;
}
}
#header {
.side-menu-opener {
margin-left: 0px;
margin-top: -1px;
margin-top: 0px;
}
}
@ -111,20 +139,19 @@
display: none;
}
#side-menu.hide-opener .side-menu-opener,
.side-menu-opener.hide,
#side-menu.hide {
display: none !important;
}
.side-menu-apps-list {
height: calc(100vh - 150px);
height: calc(100vh - 49px);
top: 49px;
z-index: 2200;
position: fixed;
top: 150px;
width: 100%;
max-width: 290px;
overflow: auto;
&.side-menu-apps-list--with-logo {
height: calc(100vh - 160px);
top: 160px;
}
}
.side-menu-app-icon {
@ -149,7 +176,8 @@
a:hover,
a:focus,
&:active {
&:active,
&.active {
background: var(--side-menu-current-app-background-color, #444);
}
}
@ -165,7 +193,6 @@
}
.side-menu-header {
height: 150px;
width: 100%;
z-index: 2300;
max-width: 290px;
@ -183,10 +210,6 @@
max-width: 295px;
}
#side-menu.hide-opener .side-menu-logo {
margin-top: 10px;
}
#side-menu-loader {
position: fixed;
top: 0;
@ -202,28 +225,24 @@
}
}
#side-menu.side-menu-big,
#side-menu.side-menu-with-categories {
max-width: 100%;
height: auto;
}
.side-menu-big .side-menu-header,
.side-menu-with-categories .side-menu-header {
height: auto;
}
.side-menu-big .side-menu-apps-list,
.side-menu-with-categories .side-menu-apps-list {
.side-menu-big, .side-menu-with-categories {
.side-menu-apps-list {
height: auto;
position: static;
max-width: 100vw;
overflow: auto;
}
}
.side-menu-big .side-menu-app a,
.side-menu-with-categories .side-menu-app a {
.side-menu-app {
a {
padding: 7px 0 7px 7px;
}
}
.side-menu-app-icon {
vertical-align: middle;
margin-top: -2px;
}
}
.side-menu-categories-wrapper {
@ -265,11 +284,6 @@
stroke: var(--side-menu-text-color, #fff);
}
.side-menu-with-categories .side-menu-app-icon,
.side-menu-big .side-menu-app-icon {
vertical-align: middle;
margin-top: -2px;
}
.side-menu-always-displayed {
body {
@ -287,12 +301,6 @@
}
.side-menu-apps-list {
height: 100vh;
top: 0;
overflow: hidden;
}
.side-menu-apps-list--with-settings {
height: calc(100vh - 49px);
top: 49px;
}
@ -331,32 +339,6 @@
right: 0 !important;
margin-left: 0 !important;
}
.side-menu-search {
display: none;
}
#body-settings,
#body-settings.body-settings-side-menu {
overflow-x: visible;
}
}
#side-menu.side-menu-with-categories {
max-width: 290px;
height: 100vh;
}
.side-menu-with-categories {
.side-menu-categories {
display: block;
padding: 0;
width: 100%;
}
.side-menu-category {
padding: 10px 0;
}
}
.app-menu {
@ -390,10 +372,6 @@
height: 100vh;
}
#side-menu.hide-opener.side-menu-big .side-menu-search {
float: none;
}
.side-menu-categories {
display: block;
padding: 0;

View file

@ -8,49 +8,13 @@
<?php endforeach; ?>
}
<?php if (empty($_['top-menu-apps']) && empty($_['top-side-menu-apps'])): ?>
#appmenu {
display: none;
}
#appmenu + nav {
display: none;
}
<?php else: ?>
.app-hidden {
opacity: 0;
}
<?php endif; ?>
<?php if ($_['opener-only']): ?>
#nextcloud {
display: none;
}
<?php endif; ?>
<?php if (!$_['display-logo']): ?>
.side-menu-logo {
display: none;
}
.side-menu-header {
height: 50px;
}
.side-menu-apps-list {
height: calc(100vh - 49px);
top: 49px;
}
#side-menu.hide-opener .side-menu-header .side-menu-opener.side-menu-closer {
visibility: hidden;
}
#side-menu.hide-opener.side-menu-with-categories .side-menu-search {
float: none;
}
<?php if ($_['size-text'] === 'hidden'): ?>
<?php if ($_['size-text'] === 'hidden'): ?>
#side-menu, .side-menu-apps-list {
<?php if ($_['size-icon'] === 'big'): ?>
width: 55px;
@ -66,7 +30,6 @@
margin-left: 0px;
<?php endif; ?>
}
<?php endif; ?>
<?php endif; ?>
<?php if ($_['size-icon'] === 'hidden'): ?>

View file

@ -1,42 +0,0 @@
<?php
(function() {
const body = document.querySelector('body')
const html = document.querySelector('html')
const nextcloud = document.querySelector('#nextcloud')
const logo = document.querySelector('.header-left .logo')
const isTouchDevice = window.matchMedia("(pointer: coarse)").matches
const sideMenuFocus = () => {
let a = document.querySelector('#side-menu .side-menu-app.active a')
|| document.querySelector('#side-menu .side-menu-app a')
if (a) {
a.focus()
}
}
document.querySelector('body').addEventListener('side-menu.apps', (e) => {
const apps = e.detail.apps;
})
body.addEventListener('side-menu.ready', () => {
const sideMenu = document.querySelector('#side-menu')
const sideMenuObserver = new MutationObserver((e) => {
if (body.getAttribute('id') !== 'body-settings') {
return
}
body.classList.toggle('body-settings-side-menu', sideMenu.classList.contains('open'))
})
sideMenuObserver.observe(sideMenu, {
attributes: true,
attributeFilter: ['class'],
childList: false,
characterData: false
})
})
})();

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,7 @@ module.exports = {
devtool: "inline-source-map",
entry: {
menu: path.resolve(path.join('src', 'menu.js')),
// admin: path.resolve(path.join('src', 'admin.js')),
admin: path.resolve(path.join('src', 'admin.js')),
},
output: {
path: path.resolve('./js'),

View file

@ -3,6 +3,10 @@ module.exports = {
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
css: {
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
vue: {
test: /\.vue$/,
loader: 'vue-loader',