Compare commits

..

4 Commits

Author SHA1 Message Date
Simon Vieille 711d169d22 Merge pull request 'add check if menu exists before adding event listeners (#210)' (#211) from feature/issue210 into v2.x
Reviewed-on: https://gitnet.fr/deblan/side_menu/pulls/211
2023-01-21 21:49:02 +01:00
Simon Vieille ddcd10f7b0 release v2.5.2 2023-01-21 21:45:41 +01:00
Simon Vieille ab8b745496 add check if menu exists before adding event listeners (#210) 2023-01-21 21:41:22 +01:00
Simon Vieille 401b351fbd include v2.x in CI 2022-10-11 22:49:02 +02:00
42 changed files with 1638 additions and 1731 deletions
-1
View File
@@ -4,4 +4,3 @@
/releases /releases
/package-lock.json /package-lock.json
!/l10n/.gitkeep !/l10n/.gitkeep
/yarn*.log
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"rules": { "rules": {
"indentation": 2 "indentation": 4
} }
} }
+6 -6
View File
@@ -1,21 +1,21 @@
pipeline: pipeline:
dependencies: dependencies:
image: gitnet.fr/deblan/devenv image: deblan/devenv
commands: commands:
- make dep - npm install
when: when:
event: [tag, push, pull_request] event: [tag, push, pull_request]
branch: [master, develop, feature/*] branch: [master, develop, v2.x, feature/*]
build: build:
image: gitnet.fr/deblan/devenv image: deblan/devenv
commands: commands:
- make build - make npm-build
when: when:
event: [push, pull_request] event: [push, pull_request]
package: package:
image: gitnet.fr/deblan/devenv image: deblan/devenv
volumes: volumes:
- /var/www/html/artifacts:/var/www/html/artifacts - /var/www/html/artifacts:/var/www/html/artifacts
secrets: [app_certificate] secrets: [app_certificate]
+2 -15
View File
@@ -1,21 +1,8 @@
## [Unreleased] ## [Unreleased]
## 3.1.0 ## 2.5.2
### Added
* add global custom app sorting for the top menu
### Fixed ### Fixed
* fix admin list/modal look * add check if menu exists before adding event listeners (#210)
## 3.0.1
### Fixed
* Remove the gap between the window's top and menu categories (large menu)
## 3.0.0
### Added
* Add compatibility with NC25 (#136/#135)
### Removed
* Nextcloud 20-24 are not supported anymore
* AppOrder is not supported anymore
## 2.5.1 ## 2.5.1
### Fixed ### Fixed
+3 -7
View File
@@ -1,15 +1,11 @@
build: dep npm-build:
npm run build npm run build
watch: dep npm-watch:
npm run watch npm run watch
dep:
npm i
npm link @nextcloud/vue || sudo npm link @nextcloud/vue
.ONESHELL: .ONESHELL:
release: build translations release: npm-build translations
if [ -z "$$VERSION" ]; then if [ -z "$$VERSION" ]; then
echo "VERSION required" echo "VERSION required"
exit 1 exit 1
+3 -3
View File
@@ -6,7 +6,7 @@ You can also add and sort custom categories, define apps that must be displayed
This application is rather suitable for instances that activate a lot of applications. This application is rather suitable for instances that activate a lot of applications.
You can customize colors depending of the theme (Dark theme and Breeze Dark). You can customize colors depending of the theme (Dark theme and Breeze Dark). Comptatible with AppOrder.
* [Installation and upgrade](#installation-and-upgrade) * [Installation and upgrade](#installation-and-upgrade)
* [How to contribute?](#how-to-contribute) * [How to contribute?](#how-to-contribute)
@@ -56,9 +56,9 @@ If you are a developer:
* fork the repository * fork the repository
* install an instance of Nextcloud * install an instance of Nextcloud
* go to `apps/` and clone your repository * go to `apps/` and clone your repository
* go to `apps/side_menu` and run `make dep` * go to `apps/side_menu` and run `npm install`
Build javascripts using `make build` (or `make watch` to build them in real time). Build javascripts using `make npm-build` (or `make npm-watch` to build them in real time).
Then commit and create a pull request. Then commit and create a pull request.
+3 -5
View File
@@ -11,7 +11,7 @@ This application is rather suitable for instances that activate a lot of applica
Use the shortcut `Ctrl`+`o` to open and to hide the side menu. Use `tab` to navigate. Use the shortcut `Ctrl`+`o` to open and to hide the side menu. Use `tab` to navigate.
You can customize colors depending of the theme (Dark theme and Breeze Dark). You can customize colors depending of the theme (Dark theme and Breeze Dark). Comptatible with AppOrder.
You can report a bug or request a feature by opening an issue. You can report a bug or request a feature by opening an issue.
@@ -26,7 +26,7 @@ If you like this application and if you want to support the development:
* [Donate with liberapay](https://liberapay.com/deblan) * [Donate with liberapay](https://liberapay.com/deblan)
* [Leave a comment](https://apps.nextcloud.com/apps/side_menu#comments) * [Leave a comment](https://apps.nextcloud.com/apps/side_menu#comments)
]]></description> ]]></description>
<version>3.1.0</version> <version>2.5.2</version>
<licence>agpl</licence> <licence>agpl</licence>
<author mail="contact@deblan.fr" homepage="https://www.deblan.io/">Simon Vieille</author> <author mail="contact@deblan.fr" homepage="https://www.deblan.io/">Simon Vieille</author>
<namespace>SideMenu</namespace> <namespace>SideMenu</namespace>
@@ -45,10 +45,8 @@ If you like this application and if you want to support the development:
<screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc18_menu_always_displayed.png</screenshot> <screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc18_menu_always_displayed.png</screenshot>
<screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc20_big_menu_responsive.png</screenshot> <screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc20_big_menu_responsive.png</screenshot>
<screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/personal_settings.png</screenshot> <screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/personal_settings.png</screenshot>
<screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc25_big_menu.png</screenshot>
<screenshot>https://gitnet.fr/deblan/side_menu/raw/branch/master/screenshots/nc25_default_menu.png</screenshot>
<dependencies> <dependencies>
<nextcloud min-version="25" max-version="25"/> <nextcloud min-version="20" max-version="24"/>
<php min-version="7.4"/> <php min-version="7.4"/>
</dependencies> </dependencies>
<settings> <settings>
+8 -28
View File
@@ -18,8 +18,6 @@
#side-menu-section input[type="color"] { #side-menu-section input[type="color"] {
width: 100px; width: 100px;
margin: 10px 0 10px 0; margin: 10px 0 10px 0;
padding: 0;
border-radius: 0;
} }
#-dropside-menu-section input[type="checkbox"] { #-dropside-menu-section input[type="checkbox"] {
@@ -73,21 +71,14 @@
.side-menu-setting-list { .side-menu-setting-list {
margin: 10px 4px 4px 0px; margin: 10px 4px 4px 0px;
border: 1px solid var(--color-border-dark);
border-radius: 15px;
} }
.side-menu-setting-list-item { .side-menu-setting-list-item {
padding: 5px 10px; padding: 5px 10px;
border-bottom: 1px solid var(--color-border-dark); border: 1px solid var(--color-border-dark);
max-width: 300px; max-width: 300px;
margin: -1px 0 0 0; margin: -1px 0 0 0;
cursor: pointer; cursor: pointer;
line-height: 32px;
}
.side-menu-setting-list-item:last-child {
border-bottom: 0;
} }
.side-menu-setting-list-drop { .side-menu-setting-list-drop {
@@ -102,15 +93,15 @@
} }
.side-menu-setting-list-item input { .side-menu-setting-list-item input {
margin-top: 0; min-height: auto;
height: 21px !important; margin-top: -1px;
min-height: auto !important;
} }
#apps-categories-custom-list select { #apps-categories-custom-list select {
width: 100%; width: 100%;
} }
.side-menu-setting-table { .side-menu-setting-table {
display: table; display: table;
width: 100%; width: 100%;
@@ -118,22 +109,11 @@
.side-menu-setting-row { .side-menu-setting-row {
display: table; display: table;
margin-bottom: 10px;
}
.side-menu-setting-row code {
margin-left: 2px;
margin-bottom: 1px;
padding: 3px 10px;
border-radius: 5px;
display: inline-block;
right: 2px;
border: 1px solid var(--color-border-dark);
} }
.side-menu-setting-label { .side-menu-setting-label {
display: table-cell; display: table-cell;
width: 430px; width: 400px;
padding-right: 20px; padding-right: 20px;
} }
@@ -160,7 +140,7 @@
.btn-reset { .btn-reset {
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
position: relative; position: absolute;
top: -8px; margin-top: 17px;
left: 5px; margin-left: 5px;
} }
+17 -16
View File
@@ -21,7 +21,7 @@
left: 0; left: 0;
height: 100vh; height: 100vh;
width: 100%; width: 100%;
max-width: 290px; max-width: 250px;
background: linear-gradient(90deg, var(--side-menu-background-color, #333) 0%, var(--side-menu-background-color-to, #333) 100%); background: linear-gradient(90deg, var(--side-menu-background-color, #333) 0%, var(--side-menu-background-color-to, #333) 100%);
z-index: 3000; z-index: 3000;
color: var(--side-menu-text-color, #fff); color: var(--side-menu-text-color, #fff);
@@ -103,19 +103,27 @@
position: fixed; position: fixed;
top: 150px; top: 150px;
width: 100%; width: 100%;
max-width: 290px; max-width: 250px;
overflow: auto; overflow: auto;
} }
.side-menu-app-icon { .side-menu-app-icon {
width: 20px; width: 20px;
vertical-align: middle; vertical-align: top;
margin-top: -4px;
margin-right: 10px; margin-right: 10px;
filter: invert(var(--side-menu-icon-invert-filter, 0%)); filter: invert(var(--side-menu-icon-invert-filter, 0%));
opacity: var(--side-menu-icon-opacity, 1); opacity: var(--side-menu-icon-opacity, 1);
} }
.side-menu-app-icon svg {
vertical-align: middle;
margin-top: -3px;
}
.side-menu-app-icon .app-icon-notification {
display: none;
}
.side-menu-app a { .side-menu-app a {
line-height: 30px; line-height: 30px;
color: var(--side-menu-text-color, #fff); color: var(--side-menu-text-color, #fff);
@@ -139,11 +147,11 @@
max-height: 100px; max-height: 100px;
} }
.enu-header { .side-menu-header {
height: 150px; height: 150px;
width: 100%; width: 100%;
z-index: 2300; z-index: 2300;
max-width: 290px; max-width: 250px;
position: fixed; position: fixed;
padding-top: 2px; padding-top: 2px;
top: 0; top: 0;
@@ -197,10 +205,10 @@
} }
.side-menu-categories { .side-menu-categories {
max-height: calc(100vh - 5px); max-height: calc(100vh - 50px);
overflow: auto; overflow: auto;
position: relative; position: relative;
top: 5px; top: 50px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
@@ -232,6 +240,7 @@
margin-top: -2px; margin-top: -2px;
} }
.side-menu-always-displayed #header,
.side-menu-always-displayed body { .side-menu-always-displayed body {
width: calc(100% - 50px) !important; width: calc(100% - 50px) !important;
} }
@@ -303,14 +312,6 @@
overflow-x: visible; overflow-x: visible;
} }
.app-menu {
visibility: hidden;
}
.app-menu.show {
visibility: visible;
}
@media screen and (max-width: 1024px) { @media screen and (max-width: 1024px) {
#side-menu.side-menu-big { #side-menu.side-menu-big {
max-width: 290px; max-width: 290px;
-2
View File
@@ -95,7 +95,6 @@ class JsController extends Controller
{ {
$topMenuApps = $this->config->getAppValueArray('top-menu-apps', '[]'); $topMenuApps = $this->config->getAppValueArray('top-menu-apps', '[]');
$topSideMenuApps = $this->config->getAppValueArray('top-side-menu-apps', '[]'); $topSideMenuApps = $this->config->getAppValueArray('top-side-menu-apps', '[]');
$topMenuAppsOrder = $this->config->getAppValueArray('top-menu-apps-order', '[]');
$targetBlankApps = $this->config->getAppValueArray('target-blank-apps', '[]'); $targetBlankApps = $this->config->getAppValueArray('target-blank-apps', '[]');
$useAvatar = $this->config->getAppValueBool('use-avatar', '0'); $useAvatar = $this->config->getAppValueBool('use-avatar', '0');
$isForced = $this->config->getAppValueBool('force', '0'); $isForced = $this->config->getAppValueBool('force', '0');
@@ -165,7 +164,6 @@ class JsController extends Controller
'avatar' => $avatar, 'avatar' => $avatar,
'top-menu-apps' => $topMenuApps, 'top-menu-apps' => $topMenuApps,
'top-side-menu-apps' => $topSideMenuApps, 'top-side-menu-apps' => $topSideMenuApps,
'top-menu-apps-order' => $topMenuAppsOrder,
'target-blank-apps' => $targetBlankApps, 'target-blank-apps' => $targetBlankApps,
'settings' => $settings, 'settings' => $settings,
'logo' => $this->themingDefaults->getLogo(), 'logo' => $this->themingDefaults->getLogo(),
-19
View File
@@ -105,23 +105,4 @@ class AppRepository
return $visibleApps; return $visibleApps;
} }
public function getTopMenuOrderedApps()
{
$apps = $this->getVisibleApps();
$orders = $this->config->getAppValueArray('top-menu-apps-order', '[]');
usort($apps, function ($a, $b) use ($orders) {
$ak = array_keys($orders, $a['id'])[0] ?? null;
$bk = array_keys($orders, $b['id'])[0] ?? null;
if ($ak === null || $bk === null) {
return ($a['name'] < $b['name']) ? -1 : 1;
}
return $ak < $bk ? -1 : 1;
});
return $apps;
}
} }
-2
View File
@@ -159,11 +159,9 @@ class Admin implements ISettings
'force' => $this->config->getAppValue('force', '0'), 'force' => $this->config->getAppValue('force', '0'),
'target-blank-apps' => $this->config->getAppValueArray('target-blank-apps', '[]'), 'target-blank-apps' => $this->config->getAppValueArray('target-blank-apps', '[]'),
'top-menu-apps' => $this->config->getAppValueArray('top-menu-apps', '[]'), 'top-menu-apps' => $this->config->getAppValueArray('top-menu-apps', '[]'),
'top-menu-apps-order' => $this->config->getAppValueArray('top-menu-apps-order', '[]'),
'top-side-menu-apps' => $this->config->getAppValueArray('top-side-menu-apps', '[]'), 'top-side-menu-apps' => $this->config->getAppValueArray('top-side-menu-apps', '[]'),
'default-enabled' => $this->config->getAppValue('default-enabled', '1'), 'default-enabled' => $this->config->getAppValue('default-enabled', '1'),
'apps' => $this->appRepository->getVisibleApps(), 'apps' => $this->appRepository->getVisibleApps(),
'top-menu-ordered-apps' => $this->appRepository->getTopMenuOrderedApps(),
'apps-categories-custom' => $this->config->getAppValueArray('apps-categories-custom', '[]'), 'apps-categories-custom' => $this->config->getAppValueArray('apps-categories-custom', '[]'),
'categories-order-type' => $this->config->getAppValue('categories-order-type', 'default'), 'categories-order-type' => $this->config->getAppValue('categories-order-type', 'default'),
'categories-order' => $this->config->getAppValueArray('categories-order', '[]'), 'categories-order' => $this->config->getAppValueArray('categories-order', '[]'),
+11 -14
View File
@@ -2,15 +2,17 @@
"license": "agpl", "license": "agpl",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "NODE_ENV=production ./node_modules/.bin/webpack-cli --progress --config webpack.js", "build": "NODE_ENV=production webpack --progress --config webpack.js",
"dev": "NODE_ENV=development ./node_modules/.bin/webpack-cli --progress --config webpack.js", "dev": "NODE_ENV=development webpack --progress --config webpack.js",
"watch": "NODE_ENV=development ./node_modules/.bin/webpack-cli --progress --watch --config webpack.js", "watch": "NODE_ENV=development webpack --progress --watch --config webpack.js",
"lint": "./node_modules/.bin/eslint --ext .js,.vue src", "lint": "eslint --ext .js,.vue src",
"lint:fix": "./node_modules/.bin/eslint --ext .js,.vue src --fix", "lint:fix": "eslint --ext .js,.vue src --fix",
"stylelint": "./node_modules/.bin/stylelint src", "stylelint": "stylelint src",
"stylelint:fix": "./node_modules/.bin/stylelint src --fix" "stylelint:fix": "stylelint src --fix"
}, },
"dependencies": { "dependencies": {
"@nextcloud/axios": "^1.8.0",
"@nextcloud/vue": "^1.5.0",
"axios": "^0.24.0", "axios": "^0.24.0",
"trim": "^1.0.1", "trim": "^1.0.1",
"vue": "^2.6.11" "vue": "^2.6.11"
@@ -25,12 +27,8 @@
"@babel/core": "^7.9.0", "@babel/core": "^7.9.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.9.0", "@babel/preset-env": "^7.9.0",
"@nextcloud/axios": "^1.8.0",
"@nextcloud/browserslist-config": "^1.0.0", "@nextcloud/browserslist-config": "^1.0.0",
"@nextcloud/eslint-config": "^8.1.2", "@nextcloud/eslint-config": "^8.1.2",
"@nextcloud/initial-state": "^2.0.0",
"@nextcloud/l10n": "^1.6.0",
"@nextcloud/vue": "^7.0.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"css-loader": "^3.4.2", "css-loader": "^3.4.2",
@@ -52,9 +50,8 @@
"stylelint-scss": "^4.0.0", "stylelint-scss": "^4.0.0",
"stylelint-webpack-plugin": "^3.3.0", "stylelint-webpack-plugin": "^3.3.0",
"url-loader": "^4.0.0", "url-loader": "^4.0.0",
"vue-loader": "^15", "vue-loader": "^15.9.1",
"vue-style-loader": "^4.1.3", "vue-template-compiler": "^2.6.11",
"vue-template-compiler": "^2.7.13",
"webpack": "^5.0.0", "webpack": "^5.0.0",
"webpack-cli": "^4.0.0", "webpack-cli": "^4.0.0",
"webpack-merge": "^4.2.2", "webpack-merge": "^4.2.2",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

+38 -31
View File
@@ -22,72 +22,79 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</li> </li>
</ul> </ul>
<NcActions> <Actions>
<NcActionButton @click="showAddForm" icon="icon-add"></NcActionButton> <ActionButton @click="showAddForm" icon="icon-add"></ActionButton>
</NcActions> </Actions>
<NcModal v-if="addForm" @close="hideAddForm"> <Modal v-if="addForm" @close="hideAddForm">
<div class="modal__content"> <div class="modal__content">
<div v-for="lang in langs"> <div v-for="lang in langs">
<span class="lang" v-text="lang"></span> <span class="lang" v-text="lang"></span>
<input type="text" v-model="newValue[lang]" required style="width: calc(100% - 100px)"> <input type="text" v-model="newValue[lang]" required>
</div> </div>
<NcActions> <Actions>
<NcActionButton @click="saveAdd" icon="icon-checkmark"></NcActionButton> <ActionButton @click="saveAdd" icon="icon-checkmark"></ActionButton>
</NcActions> </Actions>
</div> </div>
</NcModal> </Modal>
<NcModal v-if="editForm" @close="hideEditForm"> <Modal v-if="editForm" @close="hideEditForm">
<div class="modal__content"> <div class="modal__content">
<div v-for="lang in langs"> <div v-for="lang in langs">
<span class="lang" v-text="lang"></span> <span class="lang" v-text="lang"></span>
<input type="text" v-model="editValue[lang]" required style="width: calc(100% - 100px)"> <input type="text" v-model="editValue[lang]" required>
</div> </div>
<div class="pull-right"> <div class="pull-right">
<NcActions> <Actions>
<NcActionButton @click="removeEdit" icon="icon-delete"></NcActionButton> <ActionButton @click="removeEdit" icon="icon-delete"></ActionButton>
</NcActions> </Actions>
</div> </div>
<NcActions> <Actions>
<NcActionButton @click="saveEdit" icon="icon-checkmark"></NcActionButton> <ActionButton @click="saveEdit" icon="icon-checkmark"></ActionButton>
</NcActions> </Actions>
</div> </div>
</NcModal> </Modal>
</div> </div>
</template> </template>
<style> <style scoped>
.modal__content { .modal__content {
width: 200px;
padding: 10px; padding: 10px;
} }
.modal__content .lang { .modal__content .lang {
width: 60px; width: 60px;
display: inline-block; display: inline-block;
padding: 4px; padding: 4px;
box-sizing: border-box; box-sizing: border-box;
} }
.pull-right { .modal__content input[type=text] {
width: calc(100% - 85px);
display: inline-block;
margin: 0;
}
.pull-right {
float: right; float: right;
} }
</style> </style>
<script> <script>
import NcModal from '@nextcloud/vue/dist/Components/NcModal' import Modal from '@nextcloud/vue/dist/Components/Modal'
import NcActions from '@nextcloud/vue/dist/Components/NcActions' import Actions from '@nextcloud/vue/dist/Components/Actions'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton' import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
export default { export default {
name: 'AdminCategoriesCustom', name: 'AdminCategoriesCustom',
components: { components: {
NcModal, Modal,
NcActions, Actions,
NcActionButton, ActionButton,
}, },
data() { data() {
return { return {
-309
View File
@@ -1,309 +0,0 @@
<!--
- @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<nav class="app-menu show">
<ul class="app-menu-main" v-if="apps !== null">
<li v-for="app in mainAppList()"
:key="app.id"
:data-app-id="app.id"
class="app-menu-entry"
:class="{ 'app-menu-entry__active': app.active }"
:style="makeStyle(app)"
>
<a :href="app.href"
:class="{ 'has-unread': app.unread > 0 }"
:aria-label="appLabel(app)"
:aria-current="app.active ? 'page' : false">
<img :src="app.icon" alt="">
<div class="app-menu-entry--label">
{{ app.name }}
<span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span>
</div>
</a>
</li>
</ul>
<NcActions class="app-menu-more" :aria-label="t('core', 'More apps')" v-if="apps !== null">
<NcActionLink v-for="app in popoverAppList()"
:key="app.id"
:aria-label="appLabel(app)"
:aria-current="app.active ? 'page' : false"
:href="app.href"
class="app-menu-popover-entry">
<template #icon>
<div class="app-icon" :class="{ 'has-unread': app.unread > 0 }">
<img :src="app.icon" alt="">
</div>
</template>
{{ app.name }}
<span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span>
</NcActionLink>
</NcActions>
</nav>
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import { NcActions, NcActionLink } from '@nextcloud/vue'
export default {
name: 'AppMenu',
components: {
NcActions, NcActionLink,
},
data() {
return {
apps: null,
appLimit: 0,
observer: null,
}
},
mounted() {
const ncApps = loadState('core', 'apps', {})
this.apps = {}
let orders = {}
Array.from(window.topMenuAppsOrder).forEach((app, order) => {
orders[app] = order + 1
})
Array.from(window.topMenuApps).forEach((id) => {
if (ncApps.hasOwnProperty(id)) {
this.apps[id] = ncApps[id]
this.apps[id].order = orders[id] || null
}
})
this.observer = new ResizeObserver(this.resize)
this.observer.observe(this.$el)
this.resize()
},
beforeDestroy() {
this.observer.disconnect()
},
methods: {
appLabel() {
return (app) => app.name
+ (app.active ? ' (' + t('core', 'Currently open') + ')' : '')
+ (app.unread > 0 ? ' (' + n('core', '{count} notification', '{count} notifications', app.unread, { count: app.unread }) + ')' : '')
},
appList() {
return Object.values(this.apps)
},
mainAppList() {
return this.appList().slice(0, this.appLimit)
},
popoverAppList() {
return this.appList().slice(this.appLimit)
},
setNavigationCounter(id, counter) {
this.$set(this.apps[id], 'unread', counter)
},
resize() {
const availableWidth = this.$el.offsetWidth
let appCount = Math.floor(availableWidth / 50) - 1
const popoverAppCount = this.appList.length - appCount
if (popoverAppCount === 1) {
appCount--
}
if (appCount < 1) {
appCount = 0
}
this.appLimit = appCount
},
makeStyle(app) {
if (app.order !== null) {
return `order: ${app.order}`
}
}
},
}
</script>
<style lang="scss" scoped>
$header-icon-size: 20px;
.app-menu {
width: 100%;
display: flex;
flex-shrink: 1;
flex-wrap: wrap;
}
.app-menu-main {
display: flex;
flex-wrap: nowrap;
.app-menu-entry {
width: 50px;
height: 50px;
position: relative;
display: flex;
opacity: .7;
&.app-menu-entry__active {
opacity: 1;
&::before {
content: " ";
position: absolute;
pointer-events: none;
border-bottom-color: var(--color-main-background);
transform: translateX(-50%);
width: 12px;
height: 5px;
border-radius: 3px;
background-color: var(--color-primary-text);
left: 50%;
bottom: 6px;
display: block;
transition: all 0.1s ease-in-out;
opacity: 1;
}
.app-menu-entry--label {
font-weight: bold;
}
}
a {
width: calc(100% - 4px);
height: calc(100% - 4px);
margin: 2px;
color: var(--color-primary-text);
position: relative;
}
img {
transition: margin 0.1s ease-in-out;
width: $header-icon-size;
height: $header-icon-size;
padding: calc((100% - $header-icon-size) / 2);
filter: var(--primary-invert-if-bright);
}
.app-menu-entry--label {
opacity: 0;
position: absolute;
font-size: 12px;
color: var(--color-primary-text);
text-align: center;
bottom: -5px;
left: 50%;
display: block;
min-width: 100%;
transform: translateX(-50%);
transition: all 0.1s ease-in-out;
width: 100%;
text-overflow: ellipsis;
overflow: hidden;
}
&:hover,
&:focus-within {
opacity: 1;
.app-menu-entry--label {
opacity: 1;
font-weight: bold;
font-size: 14px;
bottom: 0;
width: auto;
overflow: visible;
}
}
}
// Show labels
&:hover,
&:focus-within,
.app-menu-entry:hover,
.app-menu-entry:focus {
opacity: 1;
img {
margin-top: -6px;
}
.app-menu-entry--label {
opacity: 1;
bottom: 0;
}
&::before, .app-menu-entry::before {
opacity: 0;
}
}
}
::v-deep .app-menu-more .button-vue--vue-tertiary {
color: var(--color-primary-text);
opacity: .7;
margin: 3px;
&:hover {
opacity: 1;
background-color: transparent !important;
}
&:focus-visible {
opacity: 1;
background-color: transparent !important;
border-radius: var(--border-radius);
outline: none;
box-shadow: 0 0 0 2px var(--color-primary-text);
}
}
.app-menu-popover-entry {
.app-icon {
position: relative;
height: 44px;
&.has-unread::after {
background-color: var(--color-main-text);
}
img {
filter: var(--background-invert-if-bright);
width: $header-icon-size;
height: $header-icon-size;
padding: calc((50px - $header-icon-size) / 2);
}
}
}
.has-unread::after {
content: "";
width: 8px;
height: 8px;
background-color: var(--color-primary-text);
border-radius: 50%;
position: absolute;
display: block;
top: 10px;
right: 10px;
}
.unread-counter {
display: none;
}
</style>
-20
View File
@@ -1,20 +0,0 @@
const createElement = require('./lib/createElement')
const PageLoader = () => {
const pageLoader = createElement('div', {id: 'side-menu-loader'})
const pageLoaderBar = createElement('div', {id: 'side-menu-loader-bar'})
pageLoader.appendChild(pageLoaderBar)
document.querySelector('body').appendChild(pageLoader)
let pageLoaderValue = 0
window.addEventListener('beforeunload', () => {
setInterval(() => {
pageLoaderBar.style.width = pageLoaderValue.toString() + '%'
pageLoaderValue = Math.min(pageLoaderValue + .2, 100)
}, 25)
})
}
module.exports = PageLoader
+1 -1
View File
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
{{ label }} {{ label }}
<span class="avatardiv avatardiv-shown"> <span class="avatardiv avatardiv-shown">
<img v-bind:src="avatar"> <img v-bind:src="avatar" v-bind:alt="name" v-bind:title="name">
</span> </span>
</a> </a>
</div> </div>
+12 -33
View File
@@ -16,56 +16,35 @@
*/ */
import Vue from 'vue' import Vue from 'vue'
import AppMenu from './AppMenu.vue'
import SideMenu from './SideMenu.vue' import SideMenu from './SideMenu.vue'
import SideMenuBig from './SideMenuBig.vue' import SideMenuBig from './SideMenuBig.vue'
import SideMenuWithCategories from './SideMenuWithCategories.vue' import SideMenuWithCategories from './SideMenuWithCategories.vue'
import PageLoader from './PageLoader'
import SMcreateElement from './lib/createElement'
Vue.prototype.OC = OC Vue.prototype.OC = OC
Vue.prototype.t = OC.L10N.translate
window.SMcreateElement = SMcreateElement
window.PageLoader = PageLoader
const mountSideMenuComponent = () => { const mountSideMenuComponent = () => {
const container = document.querySelector('#side-menu') const sideMenuContainer = document.querySelector('#side-menu')
if (!container) { if (sideMenuContainer) {
return window.setTimeout(mountSideMenuComponent, 50) let component
}
const component = (() => { if (sideMenuContainer.getAttribute('data-bigmenu')) {
if (container.getAttribute('data-bigmenu')) { component = SideMenuBig
return SideMenuBig } else if(sideMenuContainer.getAttribute('data-sidewithcategories')) {
} else if(container.getAttribute('data-sidewithcategories')) { component = SideMenuWithCategories
return SideMenuWithCategories
} else { } else {
return SideMenu component = SideMenu
} }
})()
const View = Vue.extend(component) const View = Vue.extend(component)
const App = new View({}) const sideMenu = new View({})
App.$mount('#side-menu') sideMenu.$mount('#side-menu')
document.querySelector('body').dispatchEvent(new CustomEvent('side-menu.ready')) document.querySelector('body').dispatchEvent(new CustomEvent('side-menu.ready'))
} } else {
window.setTimeout(mountSideMenuComponent, 50)
const mountAppMenu = () => {
const container = document.querySelector('#header .app-menu')
if (!container) {
return window.setTimeout(mountAppMenu, 50)
} }
const View = Vue.extend(AppMenu)
const App = new View({})
App.$mount('#header .app-menu')
} }
mountSideMenuComponent() mountSideMenuComponent()
mountAppMenu()
+65 -36
View File
@@ -40,9 +40,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<ul class="side-menu-apps-list"> <ul class="side-menu-apps-list">
<SideMenuApp <SideMenuApp
v-for="(app, key) in apps" v-for="app in apps"
v-bind:classes="{'side-menu-app': true, 'active': app.active}" v-bind:classes="{'side-menu-app': true, 'active': app.active}"
v-bind:key="key"
v-bind:icon="app.icon" v-bind:icon="app.icon"
v-bind:label="app.name" v-bind:label="app.name"
v-bind:href="app.href" v-bind:href="app.href"
@@ -59,7 +58,6 @@ import OpenerButton from './OpenerButton'
import SettingsButton from './SettingsButton' import SettingsButton from './SettingsButton'
import SideMenuApp from './SideMenuApp' import SideMenuApp from './SideMenuApp'
import Logo from './Logo' import Logo from './Logo'
import { loadState } from '@nextcloud/initial-state'
export default { export default {
name: 'SideMenu', name: 'SideMenu',
@@ -82,58 +80,89 @@ export default {
}, },
methods: { methods: {
retrieveApps() { retrieveApps() {
const ncApps = loadState('core', 'apps', {}) this.apps = []
let orders = {} const links = document.querySelectorAll('#appmenu a')
let finalApps = [] const menu = document.querySelector('#appmenu')
let menuIsHidden = true
Array.from(window.topMenuAppsOrder).forEach((app, order) => { if (menu) {
orders[app] = order + 1 menuIsHidden = window.getComputedStyle(menu, null).getPropertyValue('display') === 'none'
}
for (let element of links) {
let href = element.getAttribute('href')
let parent = element.parentNode
if (!parent) {
continue
}
let dataId = parent.getAttribute('data-id')
dataId = dataId !== null ? dataId : ''
if (!parent.classList.contains('app-top-side-menu') && !parent.classList.contains('app-hidden') && !menuIsHidden) {
continue
}
if (href !== '#') {
let svg = element.querySelector('svg').outerHTML
svg = svg
.replace(/(height|width)="20"/, '')
.replace('id="invertMenuMain', 'id="invertSideMenu')
.replace('url(#invertMenuMain', 'url(#invertSideMenu')
if (this.forceLightIcon) {
svg = svg.replace(/filter="url[^"]+"/, '')
}
this.apps.push({
id: dataId,
href: href,
name: trim(element.querySelector('span').innerHTML),
icon: svg,
active: element.classList.contains('active')
}) })
}
for (let id in ncApps) {
if (window.topMenuApps.includes(id) && !window.topSideMenuApps.includes(id)) {
continue;
} }
let app = ncApps[id] (function(apps) {
app.order = orders[id] || null window.setTimeout(function() {
finalApps.push(app)
}
finalApps.sort((a, b) => {
if (a.order === null || b.order === null) {
return a.name < b.name ? -1 : 1;
}
return a.order < b.order ? -1 : 1;
})
this.apps = finalApps
document.querySelector('body').dispatchEvent(new CustomEvent('side-menu.apps', { document.querySelector('body').dispatchEvent(new CustomEvent('side-menu.apps', {
detail: {apps: this.apps}, detail: {apps: apps},
})) }))
}, 1000)
})(this.apps)
}, },
retrieveConfig() { retrieveConfig() {
let that = this
axios axios
.get(OC.generateUrl('/apps/side_menu/js/config')) .get(OC.generateUrl('/apps/side_menu/js/config'))
.then((response) => { .then(function(response) {
const config = response.data const config = response.data
this.targetBlankApps = config['target-blank-apps'] that.targetBlankApps = config['target-blank-apps']
this.forceLightIcon = config['force-light-icon'] that.forceLightIcon = config['force-light-icon']
this.avatar = config['avatar'] that.avatar = config['avatar']
this.logo = config['logo'] that.logo = config['logo']
this.logoLink = config['logo-link'] that.logoLink = config['logo-link']
this.settings = config['settings'] that.settings = config['settings']
}) })
}, },
}, },
mounted() { mounted() {
this.retrieveConfig() this.retrieveConfig()
this.retrieveApps() this.retrieveApps()
const menu = document.querySelector('#appmenu')
if (menu) {
const config = {attributes: true, childList: true, subtree: true}
const observer = new MutationObserver(this.retrieveApps)
observer.observe(menu, config)
}
} }
} }
</script> </script>
+1 -1
View File
@@ -17,7 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template> <template>
<li v-bind:class="classes"> <li v-bind:class="classes">
<a v-bind:href="href" :target="target" v-bind:title="label"> <a v-bind:href="href" :target="target" v-bind:title="label">
<img class="side-menu-app-icon" v-bind:src="icon" v-bind:alt="label" /> <span class="side-menu-app-icon" v-html="icon"></span>
<span class="side-menu-app-text" v-text="label"></span> <span class="side-menu-app-text" v-text="label"></span>
</a> </a>
</li> </li>
+13 -8
View File
@@ -33,13 +33,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="side-menu-categories"> <div class="side-menu-categories">
<Loader v-if="!items.length" /> <Loader v-if="!items.length" />
<div class="side-menu-category" v-for="(category, key) in items" v-bind:key="key"> <div class="side-menu-category" v-for="category in items">
<h2 class="side-menu-category-title" v-if="category.name != ''" v-text="category.name"></h2> <h2 class="side-menu-category-title" v-if="category.name != ''" v-text="category.name"></h2>
<ul class="side-menu-apps-list"> <ul class="side-menu-apps-list">
<SideMenuBigApp <SideMenuBigApp
v-for="(app, appId) in category.apps" v-for="(app, appId) in category.apps"
v-bind:key="appId"
v-bind:classes="{'side-menu-app': true, 'active': activeApp === appId}" v-bind:classes="{'side-menu-app': true, 'active': activeApp === appId}"
v-bind:icon="app.icon" v-bind:icon="app.icon"
v-bind:label="app.name" v-bind:label="app.name"
@@ -81,13 +80,17 @@ export default {
}, },
methods: { methods: {
retrieveApps() { retrieveApps() {
this.apps = []
let that = this
axios axios
.get(OC.generateUrl('/apps/side_menu/nav/items')) .get(OC.generateUrl('/apps/side_menu/nav/items'))
.then((response) => { .then(function(response) {
this.items = response.data.items that.items = response.data.items
let apps = [] let apps = []
for (let category of this.items) { for (let category of that.items) {
for (let a in category.apps) { for (let a in category.apps) {
apps.push(category.apps[a]) apps.push(category.apps[a])
} }
@@ -105,13 +108,15 @@ export default {
}, },
retrieveConfig() { retrieveConfig() {
let that = this
axios axios
.get(OC.generateUrl('/apps/side_menu/js/config')) .get(OC.generateUrl('/apps/side_menu/js/config'))
.then((response) => { .then(function(response) {
const config = response.data const config = response.data
this.targetBlankApps = config['target-blank-apps'] that.targetBlankApps = config['target-blank-apps']
this.settings = config['settings'] that.settings = config['settings']
}) })
}, },
}, },
+13 -8
View File
@@ -31,13 +31,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="side-menu-categories"> <div class="side-menu-categories">
<Loader v-if="!items.length" /> <Loader v-if="!items.length" />
<div class="side-menu-category" v-for="(category, key) in items" v-bind:key="key"> <div class="side-menu-category" v-for="category in items">
<h2 class="side-menu-category-title" v-if="category.name != ''" v-text="category.name"></h2> <h2 class="side-menu-category-title" v-if="category.name != ''" v-text="category.name"></h2>
<ul class="side-menu-apps-list"> <ul class="side-menu-apps-list">
<SideMenuBigApp <SideMenuBigApp
v-for="(app, appId) in category.apps" v-for="(app, appId) in category.apps"
v-bind:key="appId"
v-bind:classes="{'side-menu-app': true, 'active': activeApp === appId}" v-bind:classes="{'side-menu-app': true, 'active': activeApp === appId}"
v-bind:icon="app.icon" v-bind:icon="app.icon"
v-bind:label="app.name" v-bind:label="app.name"
@@ -79,13 +78,17 @@ export default {
}, },
methods: { methods: {
retrieveApps() { retrieveApps() {
this.apps = []
let that = this
axios axios
.get(OC.generateUrl('/apps/side_menu/nav/items')) .get(OC.generateUrl('/apps/side_menu/nav/items'))
.then((response) => { .then(function(response) {
this.items = response.data.items that.items = response.data.items
let apps = [] let apps = []
for (let category of this.items) { for (let category of that.items) {
for (let a in category.apps) { for (let a in category.apps) {
apps.push(category.apps[a]) apps.push(category.apps[a])
} }
@@ -103,13 +106,15 @@ export default {
}, },
retrieveConfig() { retrieveConfig() {
let that = this
axios axios
.get(OC.generateUrl('/apps/side_menu/js/config')) .get(OC.generateUrl('/apps/side_menu/js/config'))
.then((response) => { .then(function(response) {
const config = response.data const config = response.data
this.targetBlankApps = config['target-blank-apps'] that.targetBlankApps = config['target-blank-apps']
this.settings = config['settings'] that.settings = config['settings']
}) })
}, },
}, },
-18
View File
@@ -257,22 +257,4 @@ document.addEventListener('DOMContentLoaded', () => {
}) })
} catch (e) { } catch (e) {
} }
sortable('#top-menu-apps-order-list .side-menu-setting-list', {
placeholderClass: 'side-menu-setting-list-drop'
})
try {
sortable('#top-menu-apps-order-list .side-menu-setting-list')[0].addEventListener('sortstop', (e) => {
let value = []
for (let item of document.querySelectorAll('#top-menu-apps-order-list .side-menu-setting-list-item')) {
console.log(item.getAttribute('data-id'))
value.push(item.getAttribute('data-id'))
}
document.querySelector('input[name="top-menu-apps-order"]').value = JSON.stringify(value)
})
} catch (e) {
}
}) })
+1 -2
View File
@@ -40,6 +40,7 @@
"Panel": "Panel" "Panel": "Panel"
"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Otevřít nabídku při najetím ukazatelem na tlačítko nabídky (automaticky vypnuto pro dotykové obrazovky)." "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Otevřít nabídku při najetím ukazatelem na tlačítko nabídky (automaticky vypnuto pro dotykové obrazovky)."
"Display the big menu": "Zobrazit velkou nabídku" "Display the big menu": "Zobrazit velkou nabídku"
"This menu is not compatible with AppOrder.": "Nabídka není kompatibilní s jinou aplikací (doplňkem) „Pořadí aplikací“."
"Display the logo": "Zobrazit logo" "Display the logo": "Zobrazit logo"
"This feature is not compatible with the <code>big menu</code> display.": "Tato funkce není kompatibilní se zobrazením <code>velké nabídky</code>." "This feature is not compatible with the <code>big menu</code> display.": "Tato funkce není kompatibilní se zobrazením <code>velké nabídky</code>."
"Icons and texts": "Ikony a texty" "Icons and texts": "Ikony a texty"
@@ -89,5 +90,3 @@
"Small text": "Malý text" "Small text": "Malý text"
"Normal text": "Normální text" "Normal text": "Normální text"
"Big text": "Velký text" "Big text": "Velký text"
"Applications": "Applications"
"This feature is not compatible with <code>big menu</code> and <code>with categories</code> displays.": "This feature is not compatible with <code>big menu</code> and <code>with categories</code> displays."
+1 -2
View File
@@ -40,6 +40,7 @@
"Panel": "Panel" "Panel": "Panel"
"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Öffne das Menü, wenn die Maus über das Menü-Symbol bewegt wird (auf Touchscreens automatisch deaktiviert)." "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Öffne das Menü, wenn die Maus über das Menü-Symbol bewegt wird (auf Touchscreens automatisch deaktiviert)."
"Display the big menu": "Großes Menü anzeigen" "Display the big menu": "Großes Menü anzeigen"
"This menu is not compatible with AppOrder.": "Dieses Menü ist nicht mit <code>AppOrder</code> kompatibel."
"Display the logo": "Logo anzeigen" "Display the logo": "Logo anzeigen"
"This feature is not compatible with the <code>big menu</code> display.": "Diese Funktion ist nicht mit dem <code>großen Menü</code> kompatibel." "This feature is not compatible with the <code>big menu</code> display.": "Diese Funktion ist nicht mit dem <code>großen Menü</code> kompatibel."
"Icons and texts": "Symbole und Texte" "Icons and texts": "Symbole und Texte"
@@ -89,5 +90,3 @@
"Small text": "Kleiner Text" "Small text": "Kleiner Text"
"Normal text": "Normaler Text" "Normal text": "Normaler Text"
"Big text": "Großer Text" "Big text": "Großer Text"
"Applications": "Applications"
"This feature is not compatible with <code>big menu</code> and <code>with categories</code> displays.": "This feature is not compatible with <code>big menu</code> and <code>with categories</code> displays."
+2 -3
View File
@@ -48,8 +48,9 @@
"Panel": "Panneau" "Panel": "Panneau"
"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Ouvrir le menu au passage de la souris (automatiquement désactivé sur les écrans tactiles)" "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "Ouvrir le menu au passage de la souris (automatiquement désactivé sur les écrans tactiles)"
"Display the big menu": "Afficher le menu large" "Display the big menu": "Afficher le menu large"
"This menu is not compatible with AppOrder.": "Ce menu n'est pas compatible avec l'application AppOrder"
"Display the logo": "Afficher le logo" "Display the logo": "Afficher le logo"
"This feature is not compatible with the <code>big menu</code> display.": "Cette fonctionnalité n'est pas compatible avec l'affichage <code>Menu large</code>." "This feature is not compatible with the <code>big menu</code> display.": "Cette fonctionnalité n'est pas compatible avec l'affichage du menu large."
"Icons and texts": "Icônes et textes" "Icons and texts": "Icônes et textes"
"Loader enabled": "Activation de l'indicateur de chargement" "Loader enabled": "Activation de l'indicateur de chargement"
"Tips": "Astuces" "Tips": "Astuces"
@@ -89,5 +90,3 @@
"Apps only visible in the top menu": "Applications visibles uniquement dans le menu supérieur" "Apps only visible in the top menu": "Applications visibles uniquement dans le menu supérieur"
"Apps visible in the top and side menus": "Applications visibles dans le menus supérieur et latéral" "Apps visible in the top and side menus": "Applications visibles dans le menus supérieur et latéral"
"Reset to default": "Restaurer les valeurs par défaut" "Reset to default": "Restaurer les valeurs par défaut"
"Applications": "Applications"
"This feature is not compatible with <code>big menu</code> and <code>with categories</code> displays.": "Cette fonctionnalité n'est pas compatible avec les affichages <code>Menu large</code> et <code>Avec les catégories</code>"
+1 -2
View File
@@ -40,6 +40,7 @@
"Panel": "面板" "Panel": "面板"
"Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "鼠标悬停时打开菜单 (触摸屏时将自动禁用)" "Open the menu when the mouse is hover the opener (automatically disabled on touch screens)": "鼠标悬停时打开菜单 (触摸屏时将自动禁用)"
"Display the big menu": "显示大型菜单" "Display the big menu": "显示大型菜单"
"This menu is not compatible with AppOrder.": "型菜单与应用顺序不兼容"
"Display the logo": "显示logo" "Display the logo": "显示logo"
"This feature is not compatible with the <code>big menu<\/code> display.": "此功能与显示<code>大型菜单<\/code>不兼容。" "This feature is not compatible with the <code>big menu<\/code> display.": "此功能与显示<code>大型菜单<\/code>不兼容。"
"Icons and texts": "图标与文字" "Icons and texts": "图标与文字"
@@ -89,5 +90,3 @@
"Small text": "小文本" "Small text": "小文本"
"Normal text": "普通文本" "Normal text": "普通文本"
"Big text": "大文本" "Big text": "大文本"
"Applications": "Applications"
"This feature is not compatible with <code>big menu</code> and <code>with categories</code> displays.": "This feature is not compatible with <code>big menu</code> and <code>with categories</code> displays."
-11
View File
@@ -1,11 +0,0 @@
module.exports = (tagName, attributes) => {
const element = document.createElement(tagName)
if (typeof attributes === 'object') {
for (let i in attributes) {
element.setAttribute(i, attributes[i])
}
}
return element
}
-12
View File
@@ -114,15 +114,3 @@
font-size: 16px; font-size: 16px;
} }
<?php endif; ?> <?php endif; ?>
<?php if ($_['always-displayed']): ?>
#content {
left: 53px;
width: calc(100% - (var(--body-container-margin) * 2) - 62px);
}
#content-vue {
width: calc(100% - (var(--body-container-margin) * 2) - 60px);
margin-left: 11px;
}
<?php endif; ?>
+86
View File
@@ -0,0 +1,86 @@
const alwaysDisplayed = function() {
const elements = querySelectorAll('*')
const fixedElements = []
for (let element of elements) {
if (typeof element !== 'object') {
continue
}
const position = window.getComputedStyle(element, null).getPropertyValue('position')
if (position !== 'fixed') {
continue
}
const id = element.getAttribute('id')
if (id === 'header' || id === 'side-menu' || id === 'side-menu-loader') {
continue
}
if (element.classList.contains('oc-dialog')) {
continue
}
let elementIsInSideMenu = false
let parent = element.parentNode
while (parent && !elementIsInSideMenu) {
try {
if (parent.getAttribute('id') === 'side-menu') {
elementIsInSideMenu = true
}
} catch (e) {
}
parent = parent.parentNode
}
if (elementIsInSideMenu) {
continue
}
fixedElements.push(element)
}
for (let i in fixedElements) {
const element = fixedElements[i]
const computedStyle = window.getComputedStyle(element, null)
const left = computedStyle.getPropertyValue('left')
const right = computedStyle.getPropertyValue('right')
if (right !== '0px') {
const intValue = parseInt(left.replace('px', '')) + 50
element.style.setProperty('transform', 'translateX(' + intValue.toString() + 'px)')
}
}
}
const content = querySelector('#content')
if (content && content.classList.contains('app-settings')) {
let loaded = false
const config = {
attributes: false,
childList: true,
subtree: true
}
const observer = new MutationObserver(() => {
if (loaded) {
return
}
const element = content.querySelector('#app-category-your-apps') || content.querySelector('#app-navigation ul')
if (element) {
loaded = true
alwaysDisplayed()
}
})
observer.observe(content, config)
} else {
window.setTimeout(alwaysDisplayed, 200)
}
+14
View File
@@ -0,0 +1,14 @@
let pageLoader = createElement('div', {id: 'side-menu-loader'})
let pageLoaderBar = createElement('div', {id: 'side-menu-loader-bar'})
pageLoader.appendChild(pageLoaderBar)
querySelector('body').appendChild(pageLoader)
let pageLoaderValue = 0
window.addEventListener('beforeunload', () => {
setInterval(() => {
pageLoaderBar.style.width = pageLoaderValue.toString() + '%'
pageLoaderValue = Math.min(pageLoaderValue + .2, 100)
}, 25)
})
+216
View File
@@ -0,0 +1,216 @@
let menuCache = null
const breakpointMobileWidth = 1024
const usePercentualAppMenuLimit = 0.8
const minAppsDesktop = 8
const handleMenuClick = (e, icon) => {
let element = e.target
while (element.tagName !== 'LI') {
element = element.parentNode
}
const a = querySelector('a', element)
if (a.getAttribute('target') !== '_blank' && e.which === 1 && !e.ctrlKey && !e.metaKey) {
for (let tag of ['svg', 'div']) {
let el = querySelector(tag, element)
if (el) {
el.remove()
}
}
const loader = createElement('div', {'class': icon})
a.insertBefore(loader, querySelector('span', a))
}
}
const updateTopMenu = function() {
const isMobile = window.innerWidth < breakpointMobileWidth
const menu = querySelector('#appmenu')
const moreApps = querySelector('#more-apps')
const navigation = querySelector('#navigation')
const navigationApps = querySelector('#apps ul')
let apps = querySelectorAll('li', menu)
let lastShownApp = null
let appShown = []
if ((menu.innerHTML + menu.nextSibling.innerHTML) === menuCache) {
return
}
let navigationAppsHtml = ''
for (let app of apps) {
const dataId = app.getAttribute('data-id')
if (dataId === null) {
continue
}
if (topMenuApps.indexOf(dataId) === -1 && topSideMenuApps.indexOf(dataId) === -1) {
app.classList.add('hidden')
app.classList.add('app-hidden')
} else {
app.classList.remove('hidden')
app.classList.add('app-external-site')
if (topSideMenuApps.indexOf(dataId) !== -1) {
app.classList.add('app-top-side-menu')
}
appShown.push(app)
navigationAppsHtml = navigationAppsHtml + app.outerHTML
}
if (targetBlankApps.indexOf(dataId) !== -1) {
querySelector('a', app).setAttribute('target', '_blank')
}
}
navigationApps.innerHTML = navigationAppsHtml
const rightHeaderWidth = querySelector('.header-right').offsetWidth
const headerWidth = querySelector('header').offsetWidth
let availableWidth = headerWidth
availableWidth -= nextcloud.offsetWidth
availableWidth -= querySelector('#header .side-menu-opener').offsetWidth
availableWidth -= rightHeaderWidth > 230 ? rightHeaderWidth : 230
availableWidth *= isMobile ? usePercentualAppMenuLimit : 1
let appCount = Math.floor(availableWidth / querySelector('#appmenu li:not(.hidden)').offsetWidth)
if (isMobile && appCount > minAppsDesktop) {
appCount = minAppsDesktop
} else if (!isMobile && appCount < minAppsDesktop) {
appCount = minAppsDesktop
}
menu.style.opacity = 1
if (appShown.length - 1 - appCount >= 1) {
appCount--
}
for (let item of querySelectorAll('a', moreApps)) {
item.classList.remove('active')
}
let k = 0
let notInHeader = 0
for (let app of appShown) {
const name = app.getAttribute('data-id')
const li = querySelector('#apps li[data-id=' + name + '].app-external-site')
if (k < appCount && appCount > 0) {
app.classList.remove('hidden')
li.classList.add('in-header')
lastShownApp = app
} else {
app.classList.add('hidden')
li.classList.remove('in-header')
notInHeader++
const a = querySelector('a', app)
if (appCount > 0 && a.classList.contains('active')) {
lastShownApp.classList.add('hidden')
app.classList.remove('hidden')
notInHeader++
li.classList.add('in-header')
}
}
k++
}
// Hack for:
// - https://github.com/nextcloud/server/blob/master/core/src/components/MainMenu.js#L97-L119
// - https://github.com/nextcloud/server/blob/master/core/src/components/MainMenu.js#L97-L119
jQuery(menu).undelegate('li:not(#more-apps) > a', 'click')
jQuery(navigation).undelegate('a', 'click')
const confs = [
{
items: querySelectorAll('#navigation li'),
icon: 'icon-loading-small'
},
{
items: querySelectorAll('li:not(#more-apps)', menu),
icon: OCA.Theming && OCA.Theming.inverted ? 'icon-loading-small' : 'icon-loading-small-dark'
},
]
for (let conf of confs) {
for (let item of conf.items) {
item.addEventListener('click', (e) => {
handleMenuClick(e, conf.icon)
})
}
}
for (let app of querySelectorAll('#apps li.app-external-site')) {
const appId = app.getAttribute('data-id')
if (app.classList.contains('in-header')) {
for (let defs of querySelectorAll('svg defs', app)) {
defs.remove()
}
} else {
const svg = querySelector('svg', app)
if (querySelectorAll('svg defs', app).length > 0) {
continue
}
const defs = `
<defs>
<filter id="invertMenuMore-${appId}">
<feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix>
</filter>
</defs>`
svg.innerHTML = defs + svg.innerHTML
for (let image of querySelectorAll('image', svg)) {
image.setAttribute('filter', `url(#invertMenuMore-${appId})`)
}
svg.innerHTML = svg.innerHTML.replace(/fecolormatrix/g, 'feColorMatrix')
}
}
if (notInHeader === 0) {
moreApps.style.display = 'none'
navigation.style.display = 'none'
} else {
moreApps.style.display = 'flex'
}
menuCache = menu.innerHTML + menu.nextSibling.innerHTML
}
for (let i = 0; i < 4000; i+= 100) {
setTimeout(updateTopMenu, i)
}
let resizeTimeout = null;
window.addEventListener('resize', () => {
if (resizeTimeout !== null) {
clearTimeout(resizeTimeout)
}
resizeTimeout = setTimeout(updateTopMenu, 100)
})
+60 -19
View File
@@ -13,32 +13,56 @@ if ($_['always-displayed']) {
?> ?>
(function() { (function() {
const sideMenuContainer = SMcreateElement('div', {id: 'side-menu-container'}) const querySelector = function(selector, element) {
const sideMenuOpener = SMcreateElement('button', {'class': 'side-menu-opener'}) if (element) {
const sideMenu = SMcreateElement('div', {id: 'side-menu'}) return element.querySelector(selector)
}
const body = document.querySelector('body') return document.querySelector(selector)
const html = document.querySelector('html') }
const nextcloud = document.querySelector('#nextcloud')
const querySelectorAll = function(selector, element) {
if (element) {
return element.querySelectorAll(selector)
}
return document.querySelectorAll(selector)
}
const createElement = function(tagName, attributes) {
const element = document.createElement(tagName)
if (typeof attributes === 'object') {
for (let i in attributes) {
element.setAttribute(i, attributes[i])
}
}
return element
}
const sideMenuContainer = createElement('div', {id: 'side-menu-container'})
const sideMenuOpener = createElement('button', {'class': 'side-menu-opener'})
const sideMenu = createElement('div', {id: 'side-menu'})
const body = querySelector('body')
const html = querySelector('html')
const nextcloud = querySelector('#nextcloud')
const isTouchDevice = window.matchMedia("(pointer: coarse)").matches const isTouchDevice = window.matchMedia("(pointer: coarse)").matches
const targetBlankApps = <?php echo json_encode($_['target-blank-apps']) ?> const targetBlankApps = <?php echo json_encode($_['target-blank-apps']) ?>
window.topMenuApps = <?php echo json_encode($_['top-menu-apps']), "\n"; ?>
window.topMenuAppsOrder = <?php echo json_encode($_['top-menu-apps-order']), "\n"; ?>
window.topSideMenuApps = <?php echo json_encode($_['top-side-menu-apps']); ?>
<?php if ($display === 'big-menu'): ?> <?php if ($display === 'big-menu'): ?>
sideMenu.setAttribute('data-bigmenu', '1') sideMenu.setAttribute('data-bigmenu', '1')
<?php elseif ($display === 'side-with-categories'): ?> <?php elseif ($display === 'side-with-categories'): ?>
sideMenu.setAttribute('data-sidewithcategories', '1') sideMenu.setAttribute('data-sidewithcategories', '1')
<?php endif; ?> <?php endif; ?>
document.querySelector('body').addEventListener('side-menu.apps', (e) => { querySelector('body').addEventListener('side-menu.apps', (e) => {
const apps = e.detail.apps; const apps = e.detail.apps;
<?php if ($_['hide-when-no-apps']): ?> <?php if ($_['hide-when-no-apps']): ?>
const sideMenu = document.querySelector('#side-menu') const sideMenu = querySelector('#side-menu')
if (apps.length === 0) { if (apps.length === 0) {
sideMenu.classList.remove('open') sideMenu.classList.remove('open')
@@ -68,19 +92,23 @@ if ($_['always-displayed']) {
}) })
body.addEventListener('side-menu.ready', () => { body.addEventListener('side-menu.ready', () => {
const sideMenu = document.querySelector('#side-menu') const sideMenu = querySelector('#side-menu')
const headerMenuOpener = document.querySelector('#header .side-menu-opener') const headerMenuOpener = querySelector('#header .side-menu-opener')
const sideMenuOpener = document.querySelectorAll('#side-menu .side-menu-opener') const sideMenuOpener = querySelectorAll('#side-menu .side-menu-opener')
if (!headerMenuOpener) {
return
}
sideMenuFocus = () => { sideMenuFocus = () => {
let a = document.querySelector('.side-menu-app.active a', sideMenu) let a = querySelector('.side-menu-app.active a', sideMenu)
if (!a) { if (!a) {
return return
} }
if (a.length === 0) { if (a.length === 0) {
a = sideMenu.querySelector('.side-menu-app:first-child a') a = querySelector('.side-menu-app:first-child a', sideMenu)
} }
if (a.length > 0) { if (a.length > 0) {
@@ -120,7 +148,7 @@ if ($_['always-displayed']) {
headerMenuOpener.addEventListener('click', () => { headerMenuOpener.addEventListener('click', () => {
sideMenu.classList.add('open') sideMenu.classList.add('open')
const a = sideMenu.querySelector('.side-menu-app.active a') const a = querySelector('.side-menu-app.active a', sideMenu)
if (a !== null) { if (a !== null) {
a.focus() a.focus()
@@ -170,12 +198,25 @@ if ($_['always-displayed']) {
sideMenuContainer.appendChild(sideMenu) sideMenuContainer.appendChild(sideMenu)
<?php if ($_['loader-enabled'] === true): ?> <?php if ($_['loader-enabled'] === true): ?>
PageLoader() <?php require_once __DIR__.'/_loaderEnabled.js'; ?>
<?php endif; ?> <?php endif; ?>
if (nextcloud) {
<?php if ($_['opener-position'] === 'before'): ?> <?php if ($_['opener-position'] === 'before'): ?>
nextcloud.parentNode.insertBefore(sideMenuOpener, nextcloud) nextcloud.parentNode.insertBefore(sideMenuOpener, nextcloud)
<?php else: ?> <?php else: ?>
nextcloud.parentNode.insertBefore(sideMenuOpener, nextcloud.nextSibling) nextcloud.parentNode.insertBefore(sideMenuOpener, nextcloud.nextSibling)
<?php endif; ?> <?php endif; ?>
}
<?php if (!empty($_['top-menu-apps']) || !empty($_['top-side-menu-apps'])): ?>
const topMenuApps = <?php echo json_encode($_['top-menu-apps']), "\n"; ?>
const topSideMenuApps = <?php echo json_encode($_['top-side-menu-apps']); ?>
<?php require_once __DIR__.'/_topMenuApps.js'; ?>
<?php endif; ?>
<?php if ($display === 'always-displayed'): ?>
<?php require_once __DIR__.'/_alwaysDisplayed.js'; ?>
<?php endif; ?>
})(); })();
+4 -39
View File
@@ -509,6 +509,7 @@ $labelReset = 'Reset to default';
<?php p($l->t('With categories')); ?> <?php p($l->t('With categories')); ?>
</label> </label>
</div> </div>
<p><em><?php echo $l->t('This menu is not compatible with AppOrder.'); ?></em></p>
<p> <p>
<img <img
class="side-menu-display <?php echo $displays['side-with-categories'] ? 'is-active' : '' ?>" class="side-menu-display <?php echo $displays['side-with-categories'] ? 'is-active' : '' ?>"
@@ -523,6 +524,7 @@ $labelReset = 'Reset to default';
<?php p($l->t('Big menu')); ?> <?php p($l->t('Big menu')); ?>
</label> </label>
</div> </div>
<p><em><?php echo $l->t('This menu is not compatible with AppOrder.'); ?></em></p>
<p> <p>
<img <img
class="side-menu-display <?php echo $displays['big-menu'] ? 'is-active' : '' ?>" class="side-menu-display <?php echo $displays['big-menu'] ? 'is-active' : '' ?>"
@@ -796,7 +798,9 @@ $labelReset = 'Reset to default';
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="side-menu-setting-table">
<div class="side-menu-setting-row"> <div class="side-menu-setting-row">
<div class="side-menu-setting-label"> <div class="side-menu-setting-label">
<?php p($l->t('Apps visible in the top and side menus')); ?> <?php p($l->t('Apps visible in the top and side menus')); ?>
@@ -830,45 +834,6 @@ $labelReset = 'Reset to default';
</div> </div>
</div> </div>
<div class="section">
<h2>
<?php p($l->t('Applications')); ?>
</h2>
<div class="side-menu-setting-table">
<div class="side-menu-setting-row">
<div class="side-menu-setting-label">
<?php p($l->t('Customize sorting')); ?>
<br>
<em>
<?php echo $l->t('This feature is not compatible with <code>big menu</code> and <code>with categories</code> displays.'); ?>
</em>
</div>
<div class="side-menu-setting-form">
<a class="side-menu-toggler" data-target="#top-menu-apps-order-list" href="#_">
🖱️ <?php p($l->t($labelShowHideApps)); ?>
</a>
<div id="top-menu-apps-order-list" style="display: none">
<ul class="side-menu-setting-list">
<?php foreach ($_['top-menu-ordered-apps'] as $key => $app): ?>
<li data-id="<?php echo $app['id']; ?>" class="side-menu-setting-list-item">
<span class="arrow">
⇅
</span>
<?php echo p($l->t($app['name'])); ?>
</li>
<?php endforeach; ?>
</ul>
</div>
<input type="hidden" value='<?php echo json_encode(array_keys($_['top-menu-apps-order'])) ?>' name="top-menu-apps-order" class="side-menu-setting">
</div>
</div>
</div>
</div>
<div class="section"> <div class="section">
<h2> <h2>
-1
View File
@@ -3,7 +3,6 @@ const { VueLoaderPlugin } = require('vue-loader')
const StyleLintPlugin = require('stylelint-webpack-plugin') const StyleLintPlugin = require('stylelint-webpack-plugin')
module.exports = { module.exports = {
devtool: "source-map",
entry: { entry: {
'admin': path.join(__dirname, 'src', 'admin.js'), 'admin': path.join(__dirname, 'src', 'admin.js'),
'sideMenu': path.join(__dirname, 'src', 'SideMenu.js'), 'sideMenu': path.join(__dirname, 'src', 'SideMenu.js'),