- added NcModal.vue, NcActions.vue, NcButton.vue, FirstrunWizard.vue, Card.vue, Page0.vue, Page1.vue, Page2.vue, Page3.vue and some icons
Some checks failed
CI Pipeline / japa-tests (push) Failing after 51s
Some checks failed
CI Pipeline / japa-tests (push) Failing after 51s
- added lime color inside tailwind.config.js - added some utilities scripts needed for components - npm updates - changed postcss.config.js for nesting css styles - added about function to NavBar.vue
This commit is contained in:
parent
cefd9081ae
commit
87e9314b00
683
package-lock.json
generated
683
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -85,6 +85,7 @@
|
||||||
"clamscan": "^2.1.2",
|
"clamscan": "^2.1.2",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
|
"focus-trap": "^7.5.4",
|
||||||
"http-status-codes": "^2.2.0",
|
"http-status-codes": "^2.2.0",
|
||||||
"leaflet": "^1.9.3",
|
"leaflet": "^1.9.3",
|
||||||
"luxon": "^3.2.1",
|
"luxon": "^3.2.1",
|
||||||
|
@ -92,7 +93,7 @@
|
||||||
"pg": "^8.9.0",
|
"pg": "^8.9.0",
|
||||||
"proxy-addr": "^2.0.7",
|
"proxy-addr": "^2.0.7",
|
||||||
"redis": "^4.6.10",
|
"redis": "^4.6.10",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.2.1",
|
||||||
"saxon-js": "^2.5.0",
|
"saxon-js": "^2.5.0",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
|
// 'postcss-import': {},
|
||||||
|
'tailwindcss/nesting': {},
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,6 +14,101 @@
|
||||||
@import '@fontsource/inter/index.css';
|
@import '@fontsource/inter/index.css';
|
||||||
@import '@fontsource/archivo-black/index.css';
|
@import '@fontsource/archivo-black/index.css';
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--color-main-background: #ffffff;
|
||||||
|
--color-main-background-rgb: 255,255,255;
|
||||||
|
--color-main-background-translucent: rgba(var(--color-main-background-rgb), .97);
|
||||||
|
--color-main-background-blur: rgba(var(--color-main-background-rgb), .8);
|
||||||
|
--filter-background-blur: blur(25px);
|
||||||
|
--gradient-main-background: var(--color-main-background) 0%, var(--color-main-background-translucent) 85%, transparent 100%;
|
||||||
|
--color-background-hover: #f5f5f5;
|
||||||
|
/** Can be used e.g. to colorize selected table rows */
|
||||||
|
--color-background-dark: #ededed;
|
||||||
|
/** This should only be used for elements, not as a text background! Otherwise it will not work for accessibility. */
|
||||||
|
--color-background-darker: #dbdbdb;
|
||||||
|
--color-placeholder-light: #e6e6e6;
|
||||||
|
--color-placeholder-dark: #cccccc;
|
||||||
|
--color-main-text: #222222;
|
||||||
|
--color-text-maxcontrast: #6b6b6b;
|
||||||
|
--color-text-maxcontrast-default: #6b6b6b;
|
||||||
|
--color-text-maxcontrast-background-blur: #595959;
|
||||||
|
/** @deprecated use ` --color-main-text` instead */
|
||||||
|
--color-text-light: var(--color-main-text);
|
||||||
|
/** @deprecated use `--color-text-maxcontrast` instead */
|
||||||
|
--color-text-lighter: var(--color-text-maxcontrast);
|
||||||
|
--color-scrollbar: rgba(34,34,34, .15);
|
||||||
|
--color-error: #d91812;
|
||||||
|
--color-error-rgb: 217,24,18;
|
||||||
|
--color-error-hover: #dd342f;
|
||||||
|
--color-error-text: #c61610;
|
||||||
|
--color-warning: #b88100;
|
||||||
|
--color-warning-rgb: 184,129,0;
|
||||||
|
--color-warning-hover: #c69a32;
|
||||||
|
--color-warning-text: #855d00;
|
||||||
|
--color-success: #2d7b41;
|
||||||
|
--color-success-rgb: 45,123,65;
|
||||||
|
--color-success-hover: #448955;
|
||||||
|
--color-success-text: #286c39;
|
||||||
|
--color-info: #0071ad;
|
||||||
|
--color-info-rgb: 0,113,173;
|
||||||
|
--color-info-hover: #197fb5;
|
||||||
|
--color-info-text: #006499;
|
||||||
|
--color-loading-light: #cccccc;
|
||||||
|
--color-loading-dark: #444444;
|
||||||
|
--color-box-shadow-rgb: 77,77,77;
|
||||||
|
--color-box-shadow: rgba(var(--color-box-shadow-rgb), 0.5);
|
||||||
|
--color-border: #ededed;
|
||||||
|
--color-border-dark: #dbdbdb;
|
||||||
|
--color-border-maxcontrast: #949494;
|
||||||
|
--font-face: system-ui, -apple-system, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
|
--default-font-size: 15px;
|
||||||
|
--animation-quick: 100ms;
|
||||||
|
--animation-slow: 300ms;
|
||||||
|
--border-radius: 3px;
|
||||||
|
--border-radius-large: 10px;
|
||||||
|
--border-radius-rounded: 28px;
|
||||||
|
--border-radius-pill: 100px;
|
||||||
|
--default-clickable-area: 44px;
|
||||||
|
--clickable-area: 44;
|
||||||
|
--default-line-height: 24px;
|
||||||
|
--default-grid-baseline: 4px;
|
||||||
|
--header-height: 50px;
|
||||||
|
--navigation-width: 300px;
|
||||||
|
--sidebar-min-width: 300px;
|
||||||
|
--sidebar-max-width: 500px;
|
||||||
|
--list-min-width: 200px;
|
||||||
|
--list-max-width: 300px;
|
||||||
|
--header-menu-item-height: 44px;
|
||||||
|
--header-menu-profile-item-height: 66px;
|
||||||
|
--breakpoint-mobile: 1024px;
|
||||||
|
--background-invert-if-dark: no;
|
||||||
|
--background-invert-if-bright: invert(100%);
|
||||||
|
--background-image-invert-if-bright: no;
|
||||||
|
--primary-invert-if-bright: no;
|
||||||
|
--primary-invert-if-dark: invert(100%);
|
||||||
|
--color-primary: #00679e;
|
||||||
|
--color-primary-default: #0082c9;
|
||||||
|
--color-primary-text: #ffffff;
|
||||||
|
--color-primary-hover: #3285b1;
|
||||||
|
--color-primary-light: #e5eff5;
|
||||||
|
--color-primary-light-text: #00293f;
|
||||||
|
--color-primary-light-hover: #dbe4ea;
|
||||||
|
/* --color-primary-element: #00679e; */
|
||||||
|
--color-primary-element: #bfce40;
|
||||||
|
/* --color-primary-element-hover: #052E37; */
|
||||||
|
--color-primary-element-hover: rgba(5,46,55,0.7);
|
||||||
|
--color-primary-element-text: #ffffff;
|
||||||
|
--color-primary-element-text-dark: #f5f5f5;
|
||||||
|
--color-primary-element-light: #e5eff5;
|
||||||
|
--color-primary-element-light-hover: #dbe4ea;
|
||||||
|
--color-primary-element-light-text: #00293f;
|
||||||
|
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
|
||||||
|
/* --image-background-default: url('./img/background/kamil-porembinski-clouds.jpg'); */
|
||||||
|
--color-background-plain: #0082c9;
|
||||||
|
--radius: 15;
|
||||||
|
--pi: 3.14159265358979;
|
||||||
|
}
|
||||||
|
|
||||||
/* @layer base {
|
/* @layer base {
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
|
312
resources/js/Components/FirstrunWizard/FirstrunWizard.vue
Normal file
312
resources/js/Components/FirstrunWizard/FirstrunWizard.vue
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
<!--
|
||||||
|
- @copyright Copyright (c) 2023 Arno Kaimbacher <arno.kaimbacher@outlook.at>
|
||||||
|
-
|
||||||
|
- @author Arno Kaimbacher <arno.kaimbacher@outlook.at>
|
||||||
|
-
|
||||||
|
- @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>
|
||||||
|
<NcModal v-if="showModal" id="firstrunwizard" class="first-run-wizard" size="normal" :has-next="hasNext"
|
||||||
|
:has-previous="hasPrevious" @close="close" @next="goToNextPage" @previous="goToPreviousPage">
|
||||||
|
<!-- page 0 for entry -->
|
||||||
|
<Page0 v-if="page === 0" @next="goToNextPage" />
|
||||||
|
|
||||||
|
<div v-else class="first-run-wizard__wrapper">
|
||||||
|
<Transition :name="circleSlideDirection">
|
||||||
|
<div v-if="page === 1" class="first-run-wizard__background-circle" />
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<div class="first-run-wizard__background-bar rounded-t-xl" />
|
||||||
|
|
||||||
|
<!-- previous header button -->
|
||||||
|
<NcButton v-if="page > 1" type="tertiary" class="first-run-wizard__back-button"
|
||||||
|
aria-label="'Go to previous page'" @click="goToPreviousPage" :icon="mdiArrowLeft" :size="20"
|
||||||
|
:rounded-full="true">
|
||||||
|
</NcButton>
|
||||||
|
<!-- close header button -->
|
||||||
|
<NcButton :type="page === 1 ? 'primary' : 'tertiary'" class="first-run-wizard__close-button"
|
||||||
|
aria-label="'Close dialog'" @click="close" :icon="mdiClose" :size="20" :rounded-full="true">
|
||||||
|
</NcButton>
|
||||||
|
|
||||||
|
<!-- show logo on first page -->
|
||||||
|
<div v-if="page === 1" class="first-run-wizard__logo"> <!-- :style="logoStyle" -->
|
||||||
|
<JustboilLogo class="w-auto h-16" />
|
||||||
|
</div>
|
||||||
|
<div v-if="page === 1" class="first-run-wizard__logo" />
|
||||||
|
<Transition :name="pageSlideDirection" mode="out-in">
|
||||||
|
<Page1 v-if="page === 1" />
|
||||||
|
<Page2 v-else-if="page === 2" />
|
||||||
|
<Page3 v-else-if="page === 3" />
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<!-- last wider button at footer for every page -->
|
||||||
|
<NcButton type="primary" alignment="center-reverse" :wide="true" @click="handleButtonCLick" :label="buttonText"
|
||||||
|
:icon="mdiArrowRight" :size="20">
|
||||||
|
</NcButton>
|
||||||
|
</div>
|
||||||
|
</NcModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
// import { NcModal, BaseButton } from '@nextcloud/vue';
|
||||||
|
import NcModal from '@/Components/NcModal.vue';
|
||||||
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
|
import NcButton from '@/Components/NcButton.vue';
|
||||||
|
// import { imagePath, generateUrl } from '@nextcloud/router';
|
||||||
|
// import axios from '@nextcloud/axios';
|
||||||
|
import JustboilLogo from '@/Components/JustboilLogo.vue';
|
||||||
|
|
||||||
|
import Page0 from './components/Page0.vue';
|
||||||
|
import Page1 from './components/Page1.vue';
|
||||||
|
import Page2 from './components/Page2.vue';
|
||||||
|
import Page3 from './components/Page3.vue';
|
||||||
|
|
||||||
|
import { mdiClose, mdiArrowRight, mdiArrowLeft } from '@mdi/js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FirstrunWizard',
|
||||||
|
components: {
|
||||||
|
NcModal,
|
||||||
|
Page0,
|
||||||
|
Page1,
|
||||||
|
Page2,
|
||||||
|
BaseButton,
|
||||||
|
NcButton,
|
||||||
|
Page3,
|
||||||
|
JustboilLogo
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showModal: false,
|
||||||
|
page: 1,
|
||||||
|
// logoURL: '/logo.svg',// imagePath('firstrunwizard', 'nextcloudLogo.svg'),
|
||||||
|
pageSlideDirection: undefined,
|
||||||
|
circleSlideDirection: undefined,
|
||||||
|
mdiClose: mdiClose,
|
||||||
|
mdiArrowRight: mdiArrowRight,
|
||||||
|
mdiArrowLeft,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
// logoStyle() {
|
||||||
|
// // return { backgroundImage: 'url(' + this.logoURL + ')' };
|
||||||
|
// return { backgroundImage: 'url(' + this.logoURL + ')' };
|
||||||
|
// },
|
||||||
|
|
||||||
|
hasPrevious() {
|
||||||
|
if (window.innerWidth <= 512) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return this.page > 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hasNext() {
|
||||||
|
if (window.innerWidth <= 512) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return this.page < 3;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
buttonText() {
|
||||||
|
if (this.page === 1) {
|
||||||
|
return 'TethysCloud on all your devices';
|
||||||
|
} else if (this.page === 2) {
|
||||||
|
return 'More about TethysCloud';
|
||||||
|
} else if (this.page === 3) {
|
||||||
|
return 'Get started!';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
open() {
|
||||||
|
this.page = 1;
|
||||||
|
this.showModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.page = 1;
|
||||||
|
this.showModal = false;
|
||||||
|
// axios.delete(generateUrl('/apps/firstrunwizard/wizard'));
|
||||||
|
},
|
||||||
|
|
||||||
|
goToNextPage() {
|
||||||
|
this.pageSlideDirection = 'slide-left';
|
||||||
|
if (this.page === 1) {
|
||||||
|
this.circleSlideDirection = 'slide-up';
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.page++;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
goToPreviousPage() {
|
||||||
|
this.pageSlideDirection = 'slide-right';
|
||||||
|
if (this.page === 2) {
|
||||||
|
this.circleSlideDirection = 'slide-down';
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.page--;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleButtonCLick() {
|
||||||
|
if (this.page < 3) {
|
||||||
|
this.goToNextPage();
|
||||||
|
} else {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.first-run-wizard {
|
||||||
|
&__wrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: calc(var(--default-grid-baseline) * 5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__background-circle {
|
||||||
|
height: 6000px;
|
||||||
|
width: 6000px;
|
||||||
|
border-radius: 3000px;
|
||||||
|
background-color: var(--color-primary-element);
|
||||||
|
position: absolute;
|
||||||
|
top: -5900px;
|
||||||
|
left: calc(-3000px + 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__background-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 10px;
|
||||||
|
background-color: var(--color-primary-element);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__back-button {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--default-grid-baseline);
|
||||||
|
left: var(--default-grid-baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--default-grid-baseline);
|
||||||
|
right: var(--default-grid-baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__logo {
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 40px;
|
||||||
|
/* top: 50%; */
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: var(--color-primary-element-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-wrapper .modal-container {
|
||||||
|
overflow: hidden;
|
||||||
|
/* max-width: 90%;
|
||||||
|
width: 600px;
|
||||||
|
max-height: min(90%, 100% - 100px); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-wrapper .modal-container__content {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 512px) {
|
||||||
|
:deep(.modal-wrapper .modal-container) {
|
||||||
|
height: 100dvh;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.modal-header) {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hide close button predefined by NCModal.vue */
|
||||||
|
:deep(.modal-container__close) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-enter-active,
|
||||||
|
.slide-right-leave-active,
|
||||||
|
.slide-left-enter-active,
|
||||||
|
.slide-left-leave-active,
|
||||||
|
.slide-up-enter-active,
|
||||||
|
.slide-up-leave-active,
|
||||||
|
.slide-down-enter-active,
|
||||||
|
.slide-down-leave-active {
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left-enter {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-enter {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up-enter {
|
||||||
|
top: calc(-5900px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up-leave-to {
|
||||||
|
top: calc(-5900px - 80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-down-enter {
|
||||||
|
top: calc(-5900px - 80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-down-leave-to {
|
||||||
|
top: calc(-5900px);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,8 +1,7 @@
|
||||||
<!--
|
<!--
|
||||||
- @copyright Copyright (c) 2023 Marco Ambrosini <marcoambrosini@proton.me>
|
- @copyright Copyright (c) 2023 Arno Kaimbacher <arno.kaimbacher@outlook.at>
|
||||||
-
|
-
|
||||||
- @author Simon Lindner <szaimen@e.mail.de>
|
- @author Arno Kaimbacher <arno.kaimbacher@outlook.at>
|
||||||
- @author Marco Ambrosini <marcoambrosini@proton.me>
|
|
||||||
-
|
-
|
||||||
- @license GNU AGPL version 3 or any later version
|
- @license GNU AGPL version 3 or any later version
|
||||||
-
|
-
|
||||||
|
@ -21,18 +20,18 @@
|
||||||
-
|
-
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<element :is="isLink ? 'a' : 'div'" :href="href || undefined" class="card" :class="{ 'card--link': isLink }"
|
<component :is="isLink ? 'a' : 'div'" :href="href || undefined" class="card" :class="{ 'card--link': isLink }"
|
||||||
:target="!isLink ? undefined : '_blank'" :rel="!isLink ? undefined : 'noreferrer'">
|
:target="!isLink ? undefined : '_blank'" :rel="!isLink ? undefined : 'noreferrer'">
|
||||||
<div v-if="!isLink" class="card__icon">
|
<div v-if="!isLink" class="card__icon">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<div class="card__text">
|
<div class="card__text">
|
||||||
<h3 class="card__heading">
|
<h3 class="text-base text-ellipsis card__heading">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h3>
|
</h3>
|
||||||
<p>{{ subtitle }}</p>
|
<p>{{ subtitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
</element>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
85
resources/js/Components/FirstrunWizard/components/Page0.vue
Normal file
85
resources/js/Components/FirstrunWizard/components/Page0.vue
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<!--<div class="video-wrapper">
|
||||||
|
<video ref="video" playsinline autoplay muted @ended="handleEnded">
|
||||||
|
<source :src="videoWebm" type="video/webm" />
|
||||||
|
<source :src="videoMp4" type="video/mp4" />
|
||||||
|
{{ videoFallbackText }}
|
||||||
|
</video>
|
||||||
|
</div>-->
|
||||||
|
<div id="page__wrapper" class="flex flex-col justify-between min-h-520">
|
||||||
|
<div class="page__scroller">
|
||||||
|
<h2 id="page__heading" class="text-xl font-bold leading-tight dark:text-slate-400 text-center">
|
||||||
|
{{ 'Seamless integration with your devices.' }}
|
||||||
|
</h2>
|
||||||
|
<!-- <p id="page__subtitle" class="max-w-md m-auto text-center">
|
||||||
|
{{ subtitleText }}
|
||||||
|
</p> -->
|
||||||
|
<div class="page__content">
|
||||||
|
Discover the power of Tethys, the cutting-edge web backend solution that revolutionizes the way you handle
|
||||||
|
research
|
||||||
|
data. At the heart of Tethys lies our meticulously developed research data repository, which leverages
|
||||||
|
state-of-the-art
|
||||||
|
CI/CD techniques to deliver a seamless and efficient experience.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
// import { generateFilePath } from '@nextcloud/router';
|
||||||
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Page0',
|
||||||
|
components: {
|
||||||
|
CardBox
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// videoMp4: generateFilePath('firstrunwizard', 'img', 'Nextcloud.mp4'),
|
||||||
|
// videoWebm: generateFilePath('firstrunwizard', 'img', 'Nextcloud.webm'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// videoFallbackText() {
|
||||||
|
// return 'Welcome to {cloudName}!', { cloudName: "test" };
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
handleEnded() {
|
||||||
|
this.$emit('next');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="css">
|
||||||
|
/* video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.video-wrapper {
|
||||||
|
background-color: var(--color-primary-element);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
|
||||||
|
&__scroller {
|
||||||
|
/* overflow-y: scroll; */
|
||||||
|
margin-top: calc(var(--default-grid-baseline) * 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: calc(var(--default-grid-baseline) * 6);
|
||||||
|
justify-content: center;
|
||||||
|
margin: calc(var(--default-grid-baseline) * 10) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,24 +1,38 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="page__wrapper">
|
<div id="page__wrapper" class="flex flex-col justify-between min-h-520">
|
||||||
<div class="page__scroller first-page">
|
<div class="page__scroller first-page">
|
||||||
<h2 class="page__heading">
|
<h2 id="page__heading" class="text-xl font-bold leading-tight dark:text-slate-400 text-center">
|
||||||
{{ t('firstrunwizard', 'A collaboration platform that puts you in control') }}
|
{{ 'A researchdata platform that puts you in control' }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="page__content">
|
<div class="page__content">
|
||||||
<Card :title="t('firstrunwizard', 'Privacy')"
|
<!-- <Card :title="'Privacy'"
|
||||||
:subtitle="t('firstrunwizard', 'Host your data and files where you decide.')">
|
:subtitle="'Host your data and files where you decide.'">
|
||||||
<Lock :size="20" />
|
<Lock :size="20" />
|
||||||
</Card>
|
</Card>
|
||||||
<Card :title="t('firstrunwizard', 'Productivity')"
|
<Card :title="'Productivity'"
|
||||||
:subtitle="t('firstrunwizard', 'Collaborate and communicate across any platform.')">
|
:subtitle="'Collaborate and communicate across any platform.'">
|
||||||
<BriefcaseCheck :size="20" />
|
<BriefcaseCheck :size="20" />
|
||||||
</Card>
|
</Card> -->
|
||||||
<Card :title="t('firstrunwizard', 'Interoperability')"
|
<div class="max-w-60 h-fit box-border flex">
|
||||||
:subtitle="t('firstrunwizard', 'Import and export anything you want with open standards.')">
|
|
||||||
|
<!-- <div class="card__icon">
|
||||||
|
<BriefcaseCheck :size="20" />
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Discover the power of TethysCloud, the cutting-edge web backend solution that revolutionizes the way you
|
||||||
|
handle
|
||||||
|
research
|
||||||
|
data. At the heart of Tethys lies our meticulously developed research data repository, which
|
||||||
|
leverages
|
||||||
|
state-of-the-art
|
||||||
|
CI/CD techniques to deliver a seamless and efficient experience.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Card :title="'Interoperability'" :subtitle="'Import and export anything you want with open standards.'">
|
||||||
<SwapHorizontal :size="20" />
|
<SwapHorizontal :size="20" />
|
||||||
</Card>
|
</Card>
|
||||||
<Card :title="t('firstrunwizard', 'Community')"
|
<Card :title="'Community'" :subtitle="'Enjoy constant improvements from a thriving open-source community.'">
|
||||||
:subtitle="t('firstrunwizard', 'Enjoy constant improvements from a thriving open-source community.')">
|
|
||||||
<AccountGroup :size="20" />
|
<AccountGroup :size="20" />
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,21 +40,18 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import Card from './Card.vue'
|
import Card from './Card.vue'
|
||||||
|
|
||||||
import Lock from 'vue-material-design-icons/Lock.vue'
|
import SwapHorizontal from '@/Components/Icons/SwapHorizontal.vue';
|
||||||
import BriefcaseCheck from 'vue-material-design-icons/BriefcaseCheck.vue'
|
import AccountGroup from '@/Components/Icons/AccountGroup.vue'
|
||||||
import SwapHorizontal from 'vue-material-design-icons/SwapHorizontal.vue'
|
|
||||||
import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Page1',
|
name: 'Page1',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Card,
|
Card,
|
||||||
Lock,
|
|
||||||
BriefcaseCheck,
|
|
||||||
SwapHorizontal,
|
SwapHorizontal,
|
||||||
AccountGroup,
|
AccountGroup,
|
||||||
},
|
},
|
||||||
|
@ -48,7 +59,25 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
/* @import "pageStyles"; */
|
.page {
|
||||||
|
&__scroller {
|
||||||
|
/* overflow-y: scroll; */
|
||||||
|
margin-top: calc(var(--default-grid-baseline) * 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: calc(var(--default-grid-baseline) * 6);
|
||||||
|
justify-content: center;
|
||||||
|
margin: calc(var(--default-grid-baseline) * 10) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card__icon {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 44px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.first-page {
|
.first-page {
|
||||||
margin-top: 100px;
|
margin-top: 100px;
|
||||||
|
|
88
resources/js/Components/FirstrunWizard/components/Page2.vue
Normal file
88
resources/js/Components/FirstrunWizard/components/Page2.vue
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<!--
|
||||||
|
- @copyright Copyright (c) 2023 Arno Kaimbacher <arno.kaimbacher@outlook.at>
|
||||||
|
-
|
||||||
|
- @author Marco Ambrosini <arno.kaimbacher@outlook.at>
|
||||||
|
-
|
||||||
|
- @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>
|
||||||
|
<div id="page__wrapper" class="flex flex-col justify-between min-h-520">
|
||||||
|
<div class="page__scroller">
|
||||||
|
<h2 id="page__heading" class="text-xl font-bold leading-tight dark:text-slate-400 text-center">
|
||||||
|
{{ 'Seamless integration with your devices.' }}
|
||||||
|
</h2>
|
||||||
|
<p id="page__subtitle" class="max-w-md m-auto text-center">
|
||||||
|
{{ subtitleText }}
|
||||||
|
</p>
|
||||||
|
<div class="page__content">
|
||||||
|
<!-- <AppStoreBadge type="android" />
|
||||||
|
<AppStoreBadge type="ios" /> -->
|
||||||
|
<Card :href="desktop" :title="'Desktop app ↗'" :subtitle="'Download For Windows, Mac OS and Linux.'" />
|
||||||
|
<!-- <Card :href="syncClientsUrl" :title="'Calendar and contacts ↗'"
|
||||||
|
:subtitle="'Connect your calendar and contacts with your devices.'" /> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Card from './Card.vue';
|
||||||
|
// import AppStoreBadge from './AppStoreBadge.vue';
|
||||||
|
// import { generateUrl } from '@nextcloud/router';
|
||||||
|
import { loadState } from '@/utils/initialState';
|
||||||
|
|
||||||
|
const desktop = loadState('firstrunwizard', 'desktop');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Page2',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Card,
|
||||||
|
// AppStoreBadge,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
subtitleText: 'Sync your files across your devices with the desktop and mobile apps, and connect your calendar and contacts.',
|
||||||
|
// syncClientsUrl: generateUrl('settings/user/sync-clients'),
|
||||||
|
desktop,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
/* @import 'pageStyles'; */
|
||||||
|
.page {
|
||||||
|
|
||||||
|
&__scroller {
|
||||||
|
/* overflow-y: scroll; */
|
||||||
|
margin-top: calc(var(--default-grid-baseline) * 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: calc(var(--default-grid-baseline) * 6);
|
||||||
|
justify-content: center;
|
||||||
|
margin: calc(var(--default-grid-baseline) * 10) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
68
resources/js/Components/FirstrunWizard/components/Page3.vue
Normal file
68
resources/js/Components/FirstrunWizard/components/Page3.vue
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<div id="page__wrapper" class="flex flex-col justify-between min-h-520">
|
||||||
|
<div class="page__scroller">
|
||||||
|
<h2 id="page__heading" class="text-xl font-bold leading-tight dark:text-slate-400 text-center">
|
||||||
|
{{ 'More about TethysCloud' }}
|
||||||
|
</h2>
|
||||||
|
<div id="page__content" class="flex flex-wrap justify-center page__content">
|
||||||
|
<Card href="https://gitea.geologie.ac.at/geolba" :title="'Explore more apps ↗'"
|
||||||
|
:subtitle="'Extend the functionality of TethysCloud.'" />
|
||||||
|
<Card href="https://gitea.geologie.ac.at/geolba" :title="'Get involved! ↗'"
|
||||||
|
:subtitle="'Be a part of the community that helps build, design, translate and promote TethysCloud!'" />
|
||||||
|
<Card href="https://gitea.geologie.ac.at/geolba" :title="'Need help? ↗'"
|
||||||
|
:subtitle="'Find out more about your TethysCloud setup with the admin, user or developer documentation.'" />
|
||||||
|
<Card href="https://gitea.geologie.ac.at/geolba" :title="'For large organisations ↗'"
|
||||||
|
:subtitle="'Get TethysCloud Enterprise for mission critical environments where advanced security and compliance are important.'" />
|
||||||
|
</div>
|
||||||
|
<p class="version-number">
|
||||||
|
{{ versionNumber }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Card from './Card.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Page3',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Card,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
versionNumber() {
|
||||||
|
return 'This TethysCloud is on version 2.0.0'; // + OC.config.versionstring
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.page {
|
||||||
|
|
||||||
|
&__scroller {
|
||||||
|
/* overflow-y: scroll; */
|
||||||
|
margin-top: calc(var(--default-grid-baseline) * 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* &__content {
|
||||||
|
|
||||||
|
gap: calc(var(--default-grid-baseline) * 6);
|
||||||
|
margin: calc(var(--default-grid-baseline) * 10) 0;
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.page__content {
|
||||||
|
gap: calc(var(--default-grid-baseline) * 6);
|
||||||
|
margin: calc(var(--default-grid-baseline) * 6) 0 calc(var(--default-grid-baseline) * 4) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-number {
|
||||||
|
margin: 0px 0 calc(var(--default-grid-baseline) * 4) 0;
|
||||||
|
color: var(--color-text-maxcontrast);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
31
resources/js/Components/Icons/DotsHorizontal.vue
Normal file
31
resources/js/Components/Icons/DotsHorizontal.vue
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<span v-bind="$attrs" :aria-hidden="!title" :aria-label="title" class="material-design-icon dots-horizontal-icon"
|
||||||
|
role="img" @click="$emit('click', $event)">
|
||||||
|
<svg :fill="fillColor" class="material-design-icon__svg" :width="size" :height="size" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z">
|
||||||
|
<title v-if="title">{{ title }}</title>
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "DotsHorizontalIcon",
|
||||||
|
emits: ['click'],
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
fillColor: {
|
||||||
|
type: String,
|
||||||
|
default: "currentColor"
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 24
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
30
resources/js/Components/Icons/Pause.vue
Normal file
30
resources/js/Components/Icons/Pause.vue
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<span v-bind="$attrs" :aria-hidden="!title" :aria-label="title" class="material-design-icon pause-icon" role="img"
|
||||||
|
@click="$emit('click', $event)">
|
||||||
|
<svg :fill="fillColor" class="material-design-icon__svg" :width="size" :height="size" viewBox="0 0 24 24">
|
||||||
|
<path d="M14,19H18V5H14M6,19H10V5H6V19Z">
|
||||||
|
<title v-if="title">{{ title }}</title>
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "PauseIcon",
|
||||||
|
emits: ['click'],
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
fillColor: {
|
||||||
|
type: String,
|
||||||
|
default: "currentColor"
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 24
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
30
resources/js/Components/Icons/Play.vue
Normal file
30
resources/js/Components/Icons/Play.vue
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<span v-bind="$attrs" :aria-hidden="!title" :aria-label="title" class="material-design-icon play-icon" role="img"
|
||||||
|
@click="$emit('click', $event)">
|
||||||
|
<svg :fill="fillColor" class="material-design-icon__svg" :width="size" :height="size" viewBox="0 0 24 24">
|
||||||
|
<path d="M8,5.14V19.14L19,12.14L8,5.14Z">
|
||||||
|
<title v-if="title">{{ title }}</title>
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "PlayIcon",
|
||||||
|
emits: ['click'],
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
fillColor: {
|
||||||
|
type: String,
|
||||||
|
default: "currentColor"
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 24
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -27,7 +27,8 @@ import {
|
||||||
mdiGithub,
|
mdiGithub,
|
||||||
mdiThemeLightDark,
|
mdiThemeLightDark,
|
||||||
mdiViewDashboard,
|
mdiViewDashboard,
|
||||||
mdiMapSearch
|
mdiMapSearch,
|
||||||
|
mdiInformationVariant,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import NavBarItem from '@/Components/NavBarItem.vue';
|
import NavBarItem from '@/Components/NavBarItem.vue';
|
||||||
import NavBarItemLabel from '@/Components/NavBarItemLabel.vue';
|
import NavBarItemLabel from '@/Components/NavBarItemLabel.vue';
|
||||||
|
@ -38,8 +39,8 @@ import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
// import NavBarSearch from '@/Components/NavBarSearch.vue';
|
// import NavBarSearch from '@/Components/NavBarSearch.vue';
|
||||||
import { stardust } from '@eidellev/adonis-stardust/client';
|
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||||
import type { User } from '@/Dataset';
|
import type { User } from '@/Dataset';
|
||||||
// import FirstrunWizard from '@/Components/FirstrunWizard/FirstrunWizard.vue'
|
import FirstrunWizard from '@/Components/FirstrunWizard/FirstrunWizard.vue'
|
||||||
import Lock from 'vue-material-design-icons/Lock.vue'
|
// import Lock from 'vue-material-design-icons/Lock.vue'
|
||||||
// import BriefcaseCheck from 'vue-material-design-icons/BriefcaseCheck.vue'
|
// import BriefcaseCheck from 'vue-material-design-icons/BriefcaseCheck.vue'
|
||||||
// import SwapHorizontal from 'vue-material-design-icons/SwapHorizontal.vue'
|
// import SwapHorizontal from 'vue-material-design-icons/SwapHorizontal.vue'
|
||||||
// import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
|
// import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
|
||||||
|
@ -89,11 +90,20 @@ const logout = async () => {
|
||||||
await router.post(stardust.route('logout'));
|
await router.post(stardust.route('logout'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const about = ref();
|
||||||
|
const showAbout = async () => {
|
||||||
|
// router.post(route('logout'));
|
||||||
|
about.value.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<nav class="text-base top-0 left-0 right-0 fixed bg-gray-50 h-14 z-40 w-screen transition-position lg:w-auto dark:bg-slate-800"
|
<nav class="text-base top-0 left-0 right-0 fixed bg-gray-50 h-14 z-40 w-screen transition-position lg:w-auto dark:bg-slate-800"
|
||||||
:class="{ 'xl:pl-60': props.showBurger == true }">
|
:class="{ 'xl:pl-60': props.showBurger == true }">
|
||||||
|
<FirstrunWizard ref="about"></FirstrunWizard>
|
||||||
<div class="flex lg:items-stretch" :class="containerMaxW">
|
<div class="flex lg:items-stretch" :class="containerMaxW">
|
||||||
<div class="flex-1 items-stretch flex h-14">
|
<div class="flex-1 items-stretch flex h-14">
|
||||||
<NavBarItem type="flex lg:hidden" @click.prevent="layoutStore.asideMobileToggle()" v-if="props.showBurger">
|
<NavBarItem type="flex lg:hidden" @click.prevent="layoutStore.asideMobileToggle()" v-if="props.showBurger">
|
||||||
|
@ -165,6 +175,9 @@ const logout = async () => {
|
||||||
<!-- <NavBarItem>
|
<!-- <NavBarItem>
|
||||||
<NavBarItemLabel :icon="mdiEmail" label="Messages" />
|
<NavBarItemLabel :icon="mdiEmail" label="Messages" />
|
||||||
</NavBarItem> -->
|
</NavBarItem> -->
|
||||||
|
<NavBarItem @click="showAbout">
|
||||||
|
<NavBarItemLabel :icon="mdiInformationVariant" label="About" />
|
||||||
|
</NavBarItem>
|
||||||
<BaseDivider nav-bar />
|
<BaseDivider nav-bar />
|
||||||
<NavBarItem @click="logout">
|
<NavBarItem @click="logout">
|
||||||
<NavBarItemLabel :icon="mdiLogout" label="Log Out" />
|
<NavBarItemLabel :icon="mdiLogout" label="Log Out" />
|
||||||
|
@ -178,9 +191,12 @@ const logout = async () => {
|
||||||
<NavBarItem href="https://gitea.geologie.ac.at/geolba/tethys" target="_blank" is-desktop-icon-only>
|
<NavBarItem href="https://gitea.geologie.ac.at/geolba/tethys" target="_blank" is-desktop-icon-only>
|
||||||
<NavBarItemLabel v-bind:icon="mdiGithub" label="GitHub" is-desktop-icon-only />
|
<NavBarItemLabel v-bind:icon="mdiGithub" label="GitHub" is-desktop-icon-only />
|
||||||
</NavBarItem>
|
</NavBarItem>
|
||||||
|
<NavBarItem is-desktop-icon-only @click="showAbout">
|
||||||
|
<NavBarItemLabel v-bind:icon="mdiInformationVariant" label="About" is-desktop-icon-only />
|
||||||
|
</NavBarItem>
|
||||||
<NavBarItem is-desktop-icon-only @click="logout">
|
<NavBarItem is-desktop-icon-only @click="logout">
|
||||||
<NavBarItemLabel v-bind:icon="mdiLogout" label="Log out" is-desktop-icon-only />
|
<NavBarItemLabel v-bind:icon="mdiLogout" label="Log out" is-desktop-icon-only />
|
||||||
</NavBarItem>
|
</NavBarItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
774
resources/js/Components/NcActions.vue
Normal file
774
resources/js/Components/NcActions.vue
Normal file
|
@ -0,0 +1,774 @@
|
||||||
|
<script lang="ts">
|
||||||
|
// import NcButton from '../NcButton/index.js'
|
||||||
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
|
// import NcPopover from '../NcPopover/index.js'
|
||||||
|
import GenRandomId from '../utils/GenRandomId';
|
||||||
|
// import { t } from '../../l10n.js'
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import DotsHorizontal from '@/Components/Icons/DotsHorizontal.vue';
|
||||||
|
|
||||||
|
const focusableSelector = '.focusable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Actions component can be used to display one ore more actions.
|
||||||
|
* If only a single action is provided, it will be rendered as an inline icon.
|
||||||
|
* For more, a menu indicator will be shown and a popovermenu containing the
|
||||||
|
* actions will be opened on click.
|
||||||
|
*
|
||||||
|
* @since 0.10.0
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 'NcActions',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
// NcButton,
|
||||||
|
BaseButton,
|
||||||
|
DotsHorizontal,
|
||||||
|
// NcPopover,
|
||||||
|
},
|
||||||
|
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* NcActions can be used as:
|
||||||
|
* - Application menu (has menu role)
|
||||||
|
* - Navigation (has no specific role, should be used an element with navigation role)
|
||||||
|
* - Popover with plain text or text inputs (has no specific role)
|
||||||
|
* Depending on the usage (used items), the menu and its items should have different roles for a11y.
|
||||||
|
* Provide the role for NcAction* components in the NcActions content.
|
||||||
|
* @type {import('vue').ComputedRef<boolean>}
|
||||||
|
*/
|
||||||
|
'NcActions:isSemanticMenu': computed(() => this.isSemanticMenu),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* Specify the open state of the popover menu
|
||||||
|
*/
|
||||||
|
open: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This disables the internal open management,
|
||||||
|
* so the actions menu only respects the `open` prop.
|
||||||
|
* This is e.g. necessary for the NcAvatar component
|
||||||
|
* to only open the actions menu after loading it's entries has finished.
|
||||||
|
*/
|
||||||
|
manualOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force the actions to display in a three dot menu
|
||||||
|
*/
|
||||||
|
forceMenu: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force the name to show for single actions
|
||||||
|
*/
|
||||||
|
forceName: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify the menu name
|
||||||
|
*/
|
||||||
|
menuName: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply primary styling for this menu
|
||||||
|
*/
|
||||||
|
primary: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the button type used for trigger and single actions buttons
|
||||||
|
* Accepted values: primary, secondary, tertiary, tertiary-no-background, tertiary-on-primary, error, warning, success. If left empty,
|
||||||
|
* the default button style will be applied.
|
||||||
|
*/
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
validator(value: string) {
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
'primary',
|
||||||
|
'secondary',
|
||||||
|
'tertiary',
|
||||||
|
'tertiary-no-background',
|
||||||
|
'tertiary-on-primary',
|
||||||
|
'error',
|
||||||
|
'warning',
|
||||||
|
'success',
|
||||||
|
].indexOf(value) !== -1
|
||||||
|
);
|
||||||
|
},
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon to show for the toggle menu button
|
||||||
|
* when more than one action is inside the actions component.
|
||||||
|
* Only replace the default three-dot icon if really necessary.
|
||||||
|
*/
|
||||||
|
defaultIcon: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aria label for the actions menu.
|
||||||
|
*
|
||||||
|
* If `menuName` is defined this will not be used to prevent
|
||||||
|
* any accessible name conflicts. This ensures that the
|
||||||
|
* element can be activated via voice input.
|
||||||
|
*/
|
||||||
|
ariaLabel: {
|
||||||
|
type: String,
|
||||||
|
default: 'Actions',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated To be removed in @nextcloud/vue 9. Migration guide: remove ariaHidden prop from NcAction* components.
|
||||||
|
* @todo Add a check in @nextcloud/vue 9 that this prop is not provided,
|
||||||
|
* otherwise root element will inherit incorrect aria-hidden.
|
||||||
|
*/
|
||||||
|
ariaHidden: {
|
||||||
|
type: Boolean,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wanted direction of the menu
|
||||||
|
*/
|
||||||
|
placement: {
|
||||||
|
type: String,
|
||||||
|
default: 'bottom',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DOM element for the actions' popover boundaries
|
||||||
|
*/
|
||||||
|
boundariesElement: {
|
||||||
|
type: Element,
|
||||||
|
default: () => document.querySelector('body'),
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selector for the actions' popover container
|
||||||
|
*/
|
||||||
|
container: {
|
||||||
|
type: [String, Object, Element, Boolean],
|
||||||
|
default: 'body',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disabled state of the main button (single action or menu toggle)
|
||||||
|
*/
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display x items inline out of the dropdown menu
|
||||||
|
* Will be ignored if `forceMenu` is set
|
||||||
|
*/
|
||||||
|
inline: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['open', 'update:open', 'close', 'focus', 'blur'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
opened: this.open,
|
||||||
|
focusIndex: 0,
|
||||||
|
randomId: `menu-${GenRandomId()}`,
|
||||||
|
isSemanticMenu: false,
|
||||||
|
isSemanticNavigation: false,
|
||||||
|
isSemanticPopoverLike: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
triggerBtnType() {
|
||||||
|
// If requested, we use a primary button
|
||||||
|
return (
|
||||||
|
this.type ||
|
||||||
|
(this.primary
|
||||||
|
? 'primary'
|
||||||
|
: // If it has a name, we use a secondary button
|
||||||
|
this.menuName
|
||||||
|
? 'secondary'
|
||||||
|
: 'tertiary')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
// Watch parent prop
|
||||||
|
open(state) {
|
||||||
|
if (state === this.opened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.opened = state;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Do we have exactly one Action and
|
||||||
|
* is it allowed as a standalone element?
|
||||||
|
*
|
||||||
|
* @param {Array} action The action to check
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isValidSingleAction(action) {
|
||||||
|
const componentName = action?.componentOptions?.Ctor?.extendOptions?.name ?? action?.componentOptions?.tag;
|
||||||
|
return ['NcActionButton', 'NcActionLink', 'NcActionRouter'].includes(componentName);
|
||||||
|
},
|
||||||
|
|
||||||
|
// MENU STATE MANAGEMENT
|
||||||
|
openMenu() {
|
||||||
|
if (this.opened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.opened = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when the popover menu open state is changed
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this.$emit('update:open', true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when the popover menu is opened
|
||||||
|
*/
|
||||||
|
this.$emit('open');
|
||||||
|
},
|
||||||
|
closeMenu(returnFocus = true) {
|
||||||
|
if (!this.opened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.opened = false;
|
||||||
|
|
||||||
|
this.$refs.popover.clearFocusTrap({ returnFocus });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when the popover menu open state is changed
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this.$emit('update:open', false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when the popover menu is closed
|
||||||
|
*/
|
||||||
|
this.$emit('close');
|
||||||
|
|
||||||
|
// close everything
|
||||||
|
this.focusIndex = 0;
|
||||||
|
|
||||||
|
// focus back the menu button
|
||||||
|
this.$refs.menuButton.$el.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
onOpen(event) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.focusFirstAction(event);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// MENU KEYS & FOCUS MANAGEMENT
|
||||||
|
// focus nearest focusable item on mouse move
|
||||||
|
// DO NOT change the focus if the target is already focused
|
||||||
|
// this will prevent issues with input being unfocused
|
||||||
|
// on mouse move
|
||||||
|
onMouseFocusAction(event) {
|
||||||
|
if (document.activeElement === event.target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItem = event.target.closest('li');
|
||||||
|
if (menuItem && this.$refs.menu.contains(menuItem)) {
|
||||||
|
const focusableItem = menuItem.querySelector(focusableSelector);
|
||||||
|
if (focusableItem) {
|
||||||
|
const focusList = this.$refs.menu.querySelectorAll(focusableSelector);
|
||||||
|
const focusIndex = [...focusList].indexOf(focusableItem);
|
||||||
|
if (focusIndex > -1) {
|
||||||
|
this.focusIndex = focusIndex;
|
||||||
|
this.focusAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Dispatches the keydown listener to different handlers
|
||||||
|
*
|
||||||
|
* @param {object} event The keydown event
|
||||||
|
*/
|
||||||
|
onKeydown(event) {
|
||||||
|
if (event.key === 'Tab' && !this.isSemanticPopoverLike) {
|
||||||
|
this.closeMenu(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
this.focusPreviousAction(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
this.focusNextAction(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'PageUp') {
|
||||||
|
this.focusFirstAction(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'PageDown') {
|
||||||
|
this.focusLastAction(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
this.closeMenu();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeCurrentActive() {
|
||||||
|
const currentActiveElement = this.$refs.menu.querySelector('li.active');
|
||||||
|
if (currentActiveElement) {
|
||||||
|
currentActiveElement.classList.remove('active');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focusAction() {
|
||||||
|
// TODO: have a global disabled state for non input elements
|
||||||
|
const focusElement = this.$refs.menu.querySelectorAll(focusableSelector)[this.focusIndex];
|
||||||
|
if (focusElement) {
|
||||||
|
this.removeCurrentActive();
|
||||||
|
const liMenuParent = focusElement.closest('li.action');
|
||||||
|
focusElement.focus();
|
||||||
|
if (liMenuParent) {
|
||||||
|
liMenuParent.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focusPreviousAction(event) {
|
||||||
|
if (this.opened) {
|
||||||
|
if (this.focusIndex === 0) {
|
||||||
|
this.focusLastAction(event);
|
||||||
|
} else {
|
||||||
|
this.preventIfEvent(event);
|
||||||
|
this.focusIndex = this.focusIndex - 1;
|
||||||
|
}
|
||||||
|
this.focusAction();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focusNextAction(event) {
|
||||||
|
if (this.opened) {
|
||||||
|
const indexLength = this.$refs.menu.querySelectorAll(focusableSelector).length - 1;
|
||||||
|
if (this.focusIndex === indexLength) {
|
||||||
|
this.focusFirstAction(event);
|
||||||
|
} else {
|
||||||
|
this.preventIfEvent(event);
|
||||||
|
this.focusIndex = this.focusIndex + 1;
|
||||||
|
}
|
||||||
|
this.focusAction();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focusFirstAction(event) {
|
||||||
|
if (this.opened) {
|
||||||
|
this.preventIfEvent(event);
|
||||||
|
// In case a button is considered aria-selected we will use this one as a initial focus
|
||||||
|
const firstSelectedIndex = [...this.$refs.menu.querySelectorAll(focusableSelector)].findIndex((button) => {
|
||||||
|
return button.parentElement.getAttribute('aria-selected');
|
||||||
|
});
|
||||||
|
this.focusIndex = firstSelectedIndex > -1 ? firstSelectedIndex : 0;
|
||||||
|
this.focusAction();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focusLastAction(event) {
|
||||||
|
if (this.opened) {
|
||||||
|
this.preventIfEvent(event);
|
||||||
|
this.focusIndex = this.$refs.menu.querySelectorAll(focusableSelector).length - 1;
|
||||||
|
this.focusAction();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preventIfEvent(event) {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFocus(event) {
|
||||||
|
this.$emit('focus', event);
|
||||||
|
},
|
||||||
|
onBlur(event) {
|
||||||
|
this.$emit('blur', event);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The render function to display the component
|
||||||
|
*
|
||||||
|
* @param {Function} h The function to create VNodes
|
||||||
|
* @return {object|undefined} The created VNode
|
||||||
|
*/
|
||||||
|
render(h) {
|
||||||
|
/**
|
||||||
|
* Filter the Actions, so that we only get allowed components.
|
||||||
|
* This also ensure that we don't get 'text' elements, which would
|
||||||
|
* become problematic later on.
|
||||||
|
*/
|
||||||
|
const actions = ([this.$slots.default] || []).filter(
|
||||||
|
(action) => action?.componentOptions?.tag || action?.componentOptions?.Ctor?.extendOptions?.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
const getActionName = (action) => action?.componentOptions?.Ctor?.extendOptions?.name ?? action?.componentOptions?.tag;
|
||||||
|
|
||||||
|
const menuItemsActions = ['NcActionButton', 'NcActionButtonGroup', 'NcActionCheckbox', 'NcActionRadio'];
|
||||||
|
const textInputActions = ['NcActionInput', 'NcActionTextEditable'];
|
||||||
|
const linkActions = ['NcActionLink', 'NcActionRouter'];
|
||||||
|
|
||||||
|
const hasTextInputAction = actions.some((action) => textInputActions.includes(getActionName(action)));
|
||||||
|
const hasMenuItemAction = actions.some((action) => menuItemsActions.includes(getActionName(action)));
|
||||||
|
const hasLinkAction = actions.some((action) => linkActions.includes(getActionName(action)));
|
||||||
|
|
||||||
|
// We consider the NcActions to have role="menu" if it consists some button-like action and not text inputs
|
||||||
|
this.isSemanticMenu = hasMenuItemAction && !hasTextInputAction;
|
||||||
|
// We consider the NcActions to be navigation if it consists some link-like action
|
||||||
|
this.isSemanticNavigation = hasLinkAction && !hasMenuItemAction && !hasTextInputAction;
|
||||||
|
// If it is no a manu and not a navigation, it is a popover with items: a form or just a text
|
||||||
|
this.isSemanticPopoverLike = !this.isSemanticMenu && !this.isSemanticNavigation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter and list actions that are allowed to be displayed inline
|
||||||
|
*/
|
||||||
|
let inlineActions = actions.filter(this.isValidSingleAction);
|
||||||
|
if (this.forceMenu && inlineActions.length > 0 && this.inline > 0) {
|
||||||
|
// Vue.util.warn('Specifying forceMenu will ignore any inline actions rendering.');
|
||||||
|
inlineActions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we have at least one action
|
||||||
|
if (actions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the provided action
|
||||||
|
*
|
||||||
|
* @param {import('vue').VNode} action the action to render
|
||||||
|
* @return {Function} the vue render function
|
||||||
|
*/
|
||||||
|
const renderInlineAction = (action) => {
|
||||||
|
const icon =
|
||||||
|
action?.data?.scopedSlots?.icon()?.[0] || h('span', { class: ['icon', action?.componentOptions?.propsData?.icon] });
|
||||||
|
const attrs = action?.data?.attrs || {};
|
||||||
|
const clickListener = action?.componentOptions?.listeners?.click;
|
||||||
|
|
||||||
|
const text = action?.componentOptions?.children?.[0]?.text?.trim?.();
|
||||||
|
const ariaLabel = action?.componentOptions?.propsData?.ariaLabel || text;
|
||||||
|
const buttonText = this.forceName ? text : '';
|
||||||
|
|
||||||
|
let title = action?.componentOptions?.propsData?.title;
|
||||||
|
// Show a default title for single actions if none is present
|
||||||
|
if (!(this.forceName || title)) {
|
||||||
|
title = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return h(
|
||||||
|
'BaseButton',
|
||||||
|
{
|
||||||
|
class: ['action-item action-item--single', action?.data?.staticClass, action?.data?.class],
|
||||||
|
attrs: {
|
||||||
|
...attrs,
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
ref: action?.data?.ref,
|
||||||
|
props: {
|
||||||
|
// If it has a menuName, we use a secondary button
|
||||||
|
type: this.type || (buttonText ? 'secondary' : 'tertiary'),
|
||||||
|
disabled: this.disabled || action?.componentOptions?.propsData?.disabled,
|
||||||
|
...action?.componentOptions?.propsData,
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
focus: this.onFocus,
|
||||||
|
blur: this.onBlur,
|
||||||
|
// If we have a click listener,
|
||||||
|
// we bind it to execute on click and forward the click event
|
||||||
|
...(!!clickListener && {
|
||||||
|
click: (event) => {
|
||||||
|
if (clickListener) {
|
||||||
|
clickListener(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[h('template', { slot: 'icon' }, [icon]), buttonText],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the actions popover
|
||||||
|
*
|
||||||
|
* @param {Array} actions the actions to render within
|
||||||
|
* @return {Function} the vue render function
|
||||||
|
*/
|
||||||
|
const renderActionsPopover = (actions) => {
|
||||||
|
const triggerIcon =
|
||||||
|
this.$slots.icon?.[0] ||
|
||||||
|
(this.defaultIcon
|
||||||
|
? h('span', { class: ['icon', this.defaultIcon] })
|
||||||
|
: h('DotsHorizontal', {
|
||||||
|
props: {
|
||||||
|
size: 20,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return h(
|
||||||
|
'NcPopover',
|
||||||
|
{
|
||||||
|
ref: 'popover',
|
||||||
|
props: {
|
||||||
|
delay: 0,
|
||||||
|
handleResize: true,
|
||||||
|
shown: this.opened,
|
||||||
|
placement: this.placement,
|
||||||
|
boundary: this.boundariesElement,
|
||||||
|
container: this.container,
|
||||||
|
popoverBaseClass: 'action-item__popper',
|
||||||
|
// Menu and navigation should not have focus trap
|
||||||
|
// Tab should close the menu and move focus to the next UI element
|
||||||
|
setReturnFocus: !this.isSemanticPopoverLike ? null : this.$refs.menuButton?.$el,
|
||||||
|
focusTrap: this.isSemanticPopoverLike,
|
||||||
|
},
|
||||||
|
// For some reason the popover component
|
||||||
|
// does not react to props given under the 'props' key,
|
||||||
|
// so we use both 'attrs' and 'props'
|
||||||
|
attrs: {
|
||||||
|
delay: 0,
|
||||||
|
handleResize: true,
|
||||||
|
shown: this.opened,
|
||||||
|
placement: this.placement,
|
||||||
|
boundary: this.boundariesElement,
|
||||||
|
container: this.container,
|
||||||
|
...(this.manualOpen && { triggers: [] }),
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
'show': this.openMenu,
|
||||||
|
'after-show': this.onOpen,
|
||||||
|
'hide': this.closeMenu,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h(
|
||||||
|
'BaseButton',
|
||||||
|
{
|
||||||
|
class: 'action-item__menutoggle',
|
||||||
|
props: {
|
||||||
|
type: this.triggerBtnType,
|
||||||
|
disabled: this.disabled,
|
||||||
|
},
|
||||||
|
slot: 'trigger',
|
||||||
|
ref: 'menuButton',
|
||||||
|
attrs: {
|
||||||
|
'aria-haspopup': this.isSemanticMenu ? 'menu' : null,
|
||||||
|
'aria-label': this.menuName ? null : this.ariaLabel,
|
||||||
|
'aria-controls': this.opened ? this.randomId : null,
|
||||||
|
'aria-expanded': this.opened ? 'true' : 'false',
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
focus: this.onFocus,
|
||||||
|
blur: this.onBlur,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[h('template', { slot: 'icon' }, [triggerIcon]), this.menuName],
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: {
|
||||||
|
open: this.opened,
|
||||||
|
},
|
||||||
|
attrs: {
|
||||||
|
tabindex: '-1',
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
keydown: this.onKeydown,
|
||||||
|
mousemove: this.onMouseFocusAction,
|
||||||
|
},
|
||||||
|
ref: 'menu',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h(
|
||||||
|
'ul',
|
||||||
|
{
|
||||||
|
attrs: {
|
||||||
|
id: this.randomId,
|
||||||
|
tabindex: '-1',
|
||||||
|
role: this.isSemanticMenu ? 'menu' : undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[actions],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we have a single action only and didn't force a menu,
|
||||||
|
* we render the action as a standalone button
|
||||||
|
*/
|
||||||
|
if (actions.length === 1 && inlineActions.length === 1 && !this.forceMenu) {
|
||||||
|
return renderInlineAction(inlineActions[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we completely re-render the children
|
||||||
|
// we need to focus the first action again
|
||||||
|
// Mostly used when clicking a menu item
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.opened && this.$refs.menu) {
|
||||||
|
const isAnyActive = this.$refs.menu.querySelector('li.active') || [];
|
||||||
|
if (isAnyActive.length === 0) {
|
||||||
|
this.focusFirstAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we some inline actions to render, render them, then the menu
|
||||||
|
*/
|
||||||
|
if (inlineActions.length > 0 && this.inline > 0) {
|
||||||
|
const renderedInlineActions = inlineActions.slice(0, this.inline);
|
||||||
|
// Filter already rendered actions
|
||||||
|
const menuActions = actions.filter((action) => !renderedInlineActions.includes(action));
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: ['action-items', `action-item--${this.triggerBtnType}`],
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// Render inline actions
|
||||||
|
...renderedInlineActions.map(renderInlineAction),
|
||||||
|
// render the rest within the popover menu
|
||||||
|
menuActions.length > 0
|
||||||
|
? h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: [
|
||||||
|
'action-item',
|
||||||
|
{
|
||||||
|
'action-item--open': this.opened,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[renderActionsPopover(menuActions)],
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Otherwise, we render the actions in a popover
|
||||||
|
*/
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: [
|
||||||
|
'action-item action-item--default-popover',
|
||||||
|
`action-item--${this.triggerBtnType}`,
|
||||||
|
{
|
||||||
|
'action-item--open': this.opened,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[renderActionsPopover(actions)],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
/* Inline buttons */
|
||||||
|
.action-items {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
/* Spacing between buttons */
|
||||||
|
/* &>button {
|
||||||
|
margin-right: math.div(28, 2);
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item {
|
||||||
|
/* --open-background-color: var(--color-background-hover, $action-background-hover); */
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&.action-item--primary {
|
||||||
|
--open-background-color: var(--color-primary-element-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action-item--secondary {
|
||||||
|
--open-background-color: var(--color-primary-element-light-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action-item--error {
|
||||||
|
--open-background-color: var(--color-error-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action-item--warning {
|
||||||
|
--open-background-color: var(--color-warning-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action-item--success {
|
||||||
|
--open-background-color: var(--color-success-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action-item--tertiary-no-background {
|
||||||
|
--open-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action-item--open .action-item__menutoggle {
|
||||||
|
background-color: var(--open-background-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="css">
|
||||||
|
/* // We overwrote the popover base class, so we can style
|
||||||
|
// the popover__inner for actions only. */
|
||||||
|
.v-popper--theme-dropdown.v-popper__popper.action-item__popper .v-popper__wrapper {
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.v-popper__inner {
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
padding: 4px;
|
||||||
|
max-height: calc(50vh - 16px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
544
resources/js/Components/NcButton.vue
Normal file
544
resources/js/Components/NcButton.vue
Normal file
|
@ -0,0 +1,544 @@
|
||||||
|
<template>
|
||||||
|
<component :is="is" :class="componentClass" :target="target" href="href" aria-label="ariaLabel" aria-pressed="pressed"
|
||||||
|
:disabled="disabled" :type="type" v-on:click="click">
|
||||||
|
|
||||||
|
<span class="button-vue__wrapper">
|
||||||
|
<!-- <span v-if="hasIcon" class="button-vue__icon" aria-hidden="true">
|
||||||
|
{{ this.$slots.icon }}-->
|
||||||
|
<BaseIcon v-if="icon" :path="icon" class="button-vue__icon" aria-hidden="true" :size="size" />
|
||||||
|
|
||||||
|
<!-- <span v-if="hasText" class="button-vue__text">{{ this.$slots.default }}</span> -->
|
||||||
|
<span v-if="label" class="font-bold whitespace-nowrap text-ellipsis overflow-hidden" :class="labelClass">{{
|
||||||
|
label }}</span>
|
||||||
|
</span>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
|
// import { h } from 'vue';
|
||||||
|
export default {
|
||||||
|
name: 'NcButton',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
BaseIcon
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
size: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 16,
|
||||||
|
},
|
||||||
|
small: Boolean,
|
||||||
|
roundedFull: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Set the text and icon alignment
|
||||||
|
*
|
||||||
|
* @default 'center'
|
||||||
|
*/
|
||||||
|
alignment: {
|
||||||
|
type: String,
|
||||||
|
default: 'center',
|
||||||
|
validator: (alignment: string) => ['start', 'start-reverse', 'center', 'center-reverse', 'end', 'end-reverse'].includes(alignment),
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the disabled state of the button on and off.
|
||||||
|
*/
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the button type
|
||||||
|
* Accepted values: primary, secondary, tertiary, tertiary-no-background, tertiary-on-primary, error, warning, success. If left empty,
|
||||||
|
* the default button style will be applied.
|
||||||
|
*/
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
validator(value: string) {
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
'primary',
|
||||||
|
'secondary',
|
||||||
|
'tertiary',
|
||||||
|
'tertiary-no-background',
|
||||||
|
'tertiary-on-primary',
|
||||||
|
'error',
|
||||||
|
'warning',
|
||||||
|
'success',
|
||||||
|
].indexOf(value) !== -1
|
||||||
|
);
|
||||||
|
},
|
||||||
|
default: 'secondary',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the button native type
|
||||||
|
* Accepted values: submit, reset, button. If left empty,
|
||||||
|
* the default "button" type will be used.
|
||||||
|
*/
|
||||||
|
nativeType: {
|
||||||
|
type: String,
|
||||||
|
validator(value: string) {
|
||||||
|
return ['submit', 'reset', 'button'].indexOf(value) !== -1;
|
||||||
|
},
|
||||||
|
default: 'button',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies whether the button should span all the available width.
|
||||||
|
* By default, buttons span the whole width of the container.
|
||||||
|
*/
|
||||||
|
wide: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always try to provide an aria-label to your button. Make it more
|
||||||
|
* specific than the button's name by provide some more context. E.g. if
|
||||||
|
* the name of the button is "send" in the Mail app, the aria label could
|
||||||
|
* be "Send email".
|
||||||
|
*/
|
||||||
|
ariaLabel: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Providing the href attribute turns the button component into an `a`
|
||||||
|
* element.
|
||||||
|
*/
|
||||||
|
href: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Providing the download attribute with href downloads file when clicking.
|
||||||
|
*/
|
||||||
|
download: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Providing the to attribute turns the button component into a `router-link`
|
||||||
|
* element. Takes precedence over the href attribute.
|
||||||
|
*/
|
||||||
|
to: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass in `true` if you want the matching behaviour of `router-link` to
|
||||||
|
* be non-inclusive: https://router.vuejs.org/api/#exact
|
||||||
|
*/
|
||||||
|
exact: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @deprecated To be removed in @nextcloud/vue 9. Migration guide: remove ariaHidden prop from NcAction* components.
|
||||||
|
* @todo Add a check in @nextcloud/vue 9 that this prop is not provided,
|
||||||
|
* otherwise root element will inherit incorrect aria-hidden.
|
||||||
|
*/
|
||||||
|
ariaHidden: {
|
||||||
|
type: Boolean,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pressed state of the button if it has a checked state
|
||||||
|
* This will add the `aria-pressed` attribute and for the button to have the primary style in checked state.
|
||||||
|
*/
|
||||||
|
pressed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['update:pressed', 'click'],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
labelClass() {
|
||||||
|
return (this.small && this.icon) ? 'px-1' : 'px-2';
|
||||||
|
},
|
||||||
|
is() {
|
||||||
|
if (this.href) {
|
||||||
|
return 'a';
|
||||||
|
}
|
||||||
|
return 'button';
|
||||||
|
},
|
||||||
|
hasText() {
|
||||||
|
// return !!this.$slots.default;
|
||||||
|
return this.label != undefined;
|
||||||
|
},
|
||||||
|
hasIcon() {
|
||||||
|
// return this.$slots?.icon;
|
||||||
|
return this.icon != undefined;
|
||||||
|
},
|
||||||
|
// type() {
|
||||||
|
// return this.href ? null : this.nativeType;
|
||||||
|
// },
|
||||||
|
|
||||||
|
componentClass() {
|
||||||
|
const base = [
|
||||||
|
'button-vue',
|
||||||
|
this.roundedFull ? 'rounded-full' : 'rounded',
|
||||||
|
'inline-flex',
|
||||||
|
'cursor-pointer',
|
||||||
|
'justify-center',
|
||||||
|
'items-center',
|
||||||
|
'whitespace-nowrap',
|
||||||
|
'focus:outline-none',
|
||||||
|
'transition-colors',
|
||||||
|
// 'focus:ring-2',
|
||||||
|
{
|
||||||
|
'button-vue--icon-only': this.hasIcon && !this.hasText,
|
||||||
|
'button-vue--text-only': this.hasText && !this.hasIcon,
|
||||||
|
'button-vue--icon-and-text': this.hasIcon && this.hasText,
|
||||||
|
[`button-vue--vue-${this.realType}`]: this.realType,
|
||||||
|
'button-vue--wide': this.wide,
|
||||||
|
[`button-vue--${this.flexAlignment}`]: this.flexAlignment !== 'center',
|
||||||
|
'button-vue--reverse': this.isReverseAligned,
|
||||||
|
// Add other classes based on conditions as needed
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (this.small) {
|
||||||
|
base.push('text-sm', this.roundedFull ? 'px-3 py-1' : 'p-1');
|
||||||
|
} else {
|
||||||
|
base.push('py-2', this.roundedFull ? 'px-6' : 'px-3');
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
},
|
||||||
|
|
||||||
|
target() {
|
||||||
|
return (!this.to && this.href) ? '_self' : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The real type to be used for the button, enforces `primary` for pressed state and, if stateful button, any other type for not pressed state
|
||||||
|
* Otherwise the type property is used.
|
||||||
|
*/
|
||||||
|
realType() {
|
||||||
|
// Force *primary* when pressed
|
||||||
|
if (this.pressed) {
|
||||||
|
return 'primary';
|
||||||
|
}
|
||||||
|
// If not pressed but button is configured as stateful button then the type must not be primary
|
||||||
|
if (this.pressed === false && this.type === 'primary') {
|
||||||
|
return 'secondary';
|
||||||
|
}
|
||||||
|
return this.type;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The flexbox alignment of the button content
|
||||||
|
*/
|
||||||
|
flexAlignment() {
|
||||||
|
return this.alignment.split('-')[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the button content should be reversed (icon on the end)
|
||||||
|
*/
|
||||||
|
isReverseAligned() {
|
||||||
|
return this.alignment.includes('-');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
click($event) {
|
||||||
|
// Update pressed prop on click if it is set
|
||||||
|
if (typeof this.pressed === 'boolean') {
|
||||||
|
/**
|
||||||
|
* Update the current pressed state of the button (if the `pressed` property was configured)
|
||||||
|
*
|
||||||
|
* @property {boolean} newValue The new `pressed`-state
|
||||||
|
*/
|
||||||
|
this.$emit('update:pressed', !this.pressed)
|
||||||
|
}
|
||||||
|
// We have to both navigate and emit the click event
|
||||||
|
this.$emit('click', $event)
|
||||||
|
// navigate?.($event)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.button-vue {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
/* width: fit-content;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0; */
|
||||||
|
padding: 0;
|
||||||
|
/* font-size: var(--default-font-size);
|
||||||
|
font-weight: bold; */
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
/* justify-content: center; */
|
||||||
|
|
||||||
|
/* Cursor pointer on element and all children */
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
& *,
|
||||||
|
span {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* border-radius: math.div($clickable-area, 2); */
|
||||||
|
/* transition-property: color, */
|
||||||
|
/* border-color,
|
||||||
|
background-color;
|
||||||
|
transition-duration: 0.1s;
|
||||||
|
transition-timing-function: linear; */
|
||||||
|
|
||||||
|
/* border-radius: math.div($clickable-area, 2); */
|
||||||
|
/* border-radius: calc(50% - var(--clickable-area) / 2);
|
||||||
|
transition-property: color, border-color, background-color;
|
||||||
|
transition-duration: 0.1s;
|
||||||
|
transition-timing-function: linear; */
|
||||||
|
|
||||||
|
/* No outline feedback for focus. Handled with a toggled class in js (see data) */
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
& * {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
opacity: 0.5;
|
||||||
|
/* // Gives a wash out effect */
|
||||||
|
filter: saturate(0.7l);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* // Default button type */
|
||||||
|
color: var(--color-primary-element-light-text);
|
||||||
|
background-color: var(--color-primary-element-light);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: var(--color-primary-element-light-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Back to the default color for this button when active */
|
||||||
|
/* TODO: add ripple effect */
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-primary-element-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--end &__wrapper {
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--start &__wrapper {
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--reverse &__wrapper {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--reverse &--icon-and-text {
|
||||||
|
padding-inline: calc(var(--default-grid-baseline) * 4) var(--default-grid-baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
height: 44px;
|
||||||
|
width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* &__text {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
padding: 2px 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* Icon-only button */
|
||||||
|
&--icon-only {
|
||||||
|
width: 44px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-only button */
|
||||||
|
&--text-only {
|
||||||
|
padding: 0 12px;
|
||||||
|
|
||||||
|
& .button-vue__text {
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon and text button */
|
||||||
|
&--icon-and-text {
|
||||||
|
padding-block: 0;
|
||||||
|
padding-inline: var(--default-grid-baseline) calc(var(--default-grid-baseline) * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wide button spans the whole width of the container */
|
||||||
|
&--wide {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--color-main-text) !important;
|
||||||
|
box-shadow: 0 0 0 4px var(--color-main-background) !important;
|
||||||
|
|
||||||
|
&.button-vue--vue-tertiary-on-primary {
|
||||||
|
outline: 2px solid var(--color-primary-element-text);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button types */
|
||||||
|
|
||||||
|
/* // Primary */
|
||||||
|
&--vue-primary {
|
||||||
|
background-color: var(--color-primary-element);
|
||||||
|
color: var(--color-primary-element-text);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: var(--color-primary-element-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Back to the default color for this button when active
|
||||||
|
TODO: add ripple effect */
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-primary-element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary */
|
||||||
|
&--vue-secondary {
|
||||||
|
color: var(--color-primary-element-light-text);
|
||||||
|
background-color: var(--color-primary-element-light);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
color: var(--color-primary-element-light-text);
|
||||||
|
background-color: var(--color-primary-element-light-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tertiary */
|
||||||
|
&--vue-tertiary {
|
||||||
|
color: var(--color-main-text);
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: var(--color-background-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tertiary, no background */
|
||||||
|
&--vue-tertiary-no-background {
|
||||||
|
color: var(--color-main-text);
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tertiary on primary color (like the header) */
|
||||||
|
&--vue-tertiary-on-primary {
|
||||||
|
color: var(--color-primary-element-text);
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success */
|
||||||
|
&--vue-success {
|
||||||
|
background-color: var(--color-success);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: var(--color-success-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Back to the default color for this button when active
|
||||||
|
: add ripple effect */
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warning */
|
||||||
|
&--vue-warning {
|
||||||
|
background-color: var(--color-warning);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: var(--color-warning-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Back to the default color for this button when active
|
||||||
|
TODO: add ripple effect */
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
&--vue-error {
|
||||||
|
background-color: var(--color-error);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: var(--color-error-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Back to the default color for this button when active
|
||||||
|
TODO: add ripple effect */
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
901
resources/js/Components/NcModal.vue
Normal file
901
resources/js/Components/NcModal.vue
Normal file
|
@ -0,0 +1,901 @@
|
||||||
|
<template>
|
||||||
|
<transition name="fade" appear @after-enter="useFocusTrap" @before-leave="clearFocusTrap">
|
||||||
|
<div v-show="showModal" ref="mask" class="modal-mask"
|
||||||
|
:class="{ 'modal-mask--dark': dark || !closeButtonContained || hasPrevious || hasNext }" :style="cssVariables"
|
||||||
|
role="dialog" aria-modal="true" :aria-labelledby="'modal-name-' + randId"
|
||||||
|
:aria-describedby="'modal-description-' + randId" tabindex="-1">
|
||||||
|
<!-- Header -->
|
||||||
|
<transition name="fade-visibility" appear>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 v-if="name.trim() !== ''" :id="'modal-name-' + randId" class="modal-name">
|
||||||
|
{{ name }}
|
||||||
|
</h2>
|
||||||
|
<div class="icons-menu">
|
||||||
|
<!-- Play-pause toggle -->
|
||||||
|
<button v-if="hasNext && enableSlideshow" :class="{ 'play-pause-icons--paused': slideshowPaused }"
|
||||||
|
class="play-pause-icons" type="button" @click="togglePlayPause">
|
||||||
|
<!-- Play/pause icons -->
|
||||||
|
<Play v-if="!playing" :size="iconSize" class="play-pause-icons__play" />
|
||||||
|
<Pause v-else :size="iconSize" class="play-pause-icons__pause" />
|
||||||
|
<span class="hidden-visually">
|
||||||
|
{{ playPauseName }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Progress circle, css animated -->
|
||||||
|
<svg v-if="playing" class="progress-ring" height="50" width="50">
|
||||||
|
<circle class="progress-ring__circle" stroke="white" stroke-width="2" fill="transparent"
|
||||||
|
r="15" cx="25" cy="25" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Actions menu -->
|
||||||
|
<!-- @slot List of actions to show -->
|
||||||
|
<NcActions class="header-actions" :inline="inlineActions">
|
||||||
|
<slot name="actions" />
|
||||||
|
</NcActions>
|
||||||
|
|
||||||
|
<!-- Close modal -->
|
||||||
|
<NcButton v-if="canClose && !closeButtonContained" :aria-label="closeButtonAriaLabel"
|
||||||
|
class="header-close" type="tertiary" @click="close" :icon="mdiClose">
|
||||||
|
<!-- <template #icon>
|
||||||
|
<Close :size="iconSize" />
|
||||||
|
</template> -->
|
||||||
|
</NcButton>
|
||||||
|
<!-- <BaseButton v-if="canClose && !closeButtonContained" :aria-label="closeButtonAriaLabel" :color="button" outline @click="cancel" >
|
||||||
|
</BaseButton> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- modal-wrapper -->
|
||||||
|
<transition :name="modalTransitionName" appear>
|
||||||
|
<div v-show="showModal"
|
||||||
|
:class="[`modal-wrapper--${size}`, { 'modal-wrapper--spread-navigation': spreadNavigation }]"
|
||||||
|
class="modal-wrapper" @mousedown.self="handleClickModalWrapper">
|
||||||
|
|
||||||
|
<!-- Navigation button left -->
|
||||||
|
<transition name="fade-visibility" appear>
|
||||||
|
<NcButton v-show="hasPrevious" type="tertiary-no-background" class="prev"
|
||||||
|
:aria-label="prevButtonAriaLabel" @click="previous" :icon="mdiChevronLeft" :size="40">
|
||||||
|
</NcButton>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- modal-container: Content with close button -->
|
||||||
|
<div :id="'modal-description-' + randId" class="modal-container">
|
||||||
|
<!-- Close modal -default hidden by FirstrunWizard.vue -->
|
||||||
|
<NcButton v-if="canClose && closeButtonContained" type="tertiary" class="modal-container__close"
|
||||||
|
:aria-label="'closeButtonAriaLabel'" @click="close" :icon="mdiClose" :size="20"
|
||||||
|
:rounded-full="true">
|
||||||
|
</NcButton>
|
||||||
|
<div class="modal-container__content">
|
||||||
|
<!-- @slot Modal content to render -->
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation button right -->
|
||||||
|
<transition name="fade-visibility" appear>
|
||||||
|
<NcButton v-show="hasNext" type="tertiary-no-background" class="next"
|
||||||
|
:aria-label="nextButtonAriaLabel" @click="next" :icon="mdiChevronRight" :size="40">
|
||||||
|
</NcButton>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { getTrapStack } from '../utils/focusTrap';
|
||||||
|
// import { t } from '../../l10n.js';
|
||||||
|
import GenRandomId from '../utils/GenRandomId';
|
||||||
|
// import l10n from '../../mixins/l10n.js';
|
||||||
|
import NcActions from '@/Components/NcActions.vue';
|
||||||
|
import NcButton from '@/Components/NcButton.vue';
|
||||||
|
import Timer from '../utils/Timer.js';
|
||||||
|
// import Tooltip from '../../directives/Tooltip/index.js';
|
||||||
|
|
||||||
|
import Pause from '@/Components/Icons/Pause.vue';
|
||||||
|
import Play from '@/Components/Icons/Play.vue';
|
||||||
|
import { mdiClose, mdiChevronLeft, mdiChevronRight } from '@mdi/js';
|
||||||
|
|
||||||
|
import { createFocusTrap } from 'focus-trap';
|
||||||
|
// import { useSwipe } from '@vueuse/core';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NcModal',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
NcActions,
|
||||||
|
Pause,
|
||||||
|
Play,
|
||||||
|
NcButton,
|
||||||
|
},
|
||||||
|
|
||||||
|
// directives: {
|
||||||
|
// tooltip: Tooltip,
|
||||||
|
// },
|
||||||
|
|
||||||
|
// mixins: [l10n],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* Name to be shown with the modal
|
||||||
|
*/
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Declare if a previous slide is available
|
||||||
|
*/
|
||||||
|
hasPrevious: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Declare if a next slide is available
|
||||||
|
*/
|
||||||
|
hasNext: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Declare if hiding the modal should be animated
|
||||||
|
*/
|
||||||
|
outTransition: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Declare if the slideshow functionality should be enabled
|
||||||
|
*/
|
||||||
|
enableSlideshow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Declare the slide interval
|
||||||
|
*/
|
||||||
|
slideshowDelay: {
|
||||||
|
type: Number,
|
||||||
|
default: 5000,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Allow to pause an ongoing slideshow
|
||||||
|
*/
|
||||||
|
slideshowPaused: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Enable swipe between slides
|
||||||
|
*/
|
||||||
|
enableSwipe: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
spreadNavigation: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Defines the modal size.
|
||||||
|
* Default is 'normal'.
|
||||||
|
* Available are 'small', 'normal', 'large' and 'full'.
|
||||||
|
* All sizes except 'small' change automatically to full-screen on mobile.
|
||||||
|
*/
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'normal',
|
||||||
|
validator: (size: string) => {
|
||||||
|
return ['small', 'normal', 'large', 'full'].includes(size);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare if the modal can be closed
|
||||||
|
*/
|
||||||
|
canClose: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal if the user clicked outside of the modal
|
||||||
|
* Only relevant if `canClose` is set to true.
|
||||||
|
*/
|
||||||
|
closeOnClickOutside: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the modal backdrop black if true
|
||||||
|
* Will be overwritten if some buttons are shown outside
|
||||||
|
*/
|
||||||
|
dark: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selector for the modal container, pass `null` to prevent automatic container mounting
|
||||||
|
*/
|
||||||
|
// container: {
|
||||||
|
// type: [String, null],
|
||||||
|
// default: 'body',
|
||||||
|
// },
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass in false if you want the modal 'close' button to be displayed
|
||||||
|
* outside the modal boundaries, in the top right corner of the window
|
||||||
|
*/
|
||||||
|
closeButtonContained: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional elements to add to the focus trap
|
||||||
|
*/
|
||||||
|
additionalTrapElements: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display x items inline
|
||||||
|
*
|
||||||
|
* @see Actions component usage
|
||||||
|
*/
|
||||||
|
inlineActions: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['previous', 'next', 'close', 'update:show'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mc: null,
|
||||||
|
playing: false,
|
||||||
|
slideshowTimeout: null,
|
||||||
|
iconSize: 24,
|
||||||
|
focusTrap: null,
|
||||||
|
randId: GenRandomId(),
|
||||||
|
internalShow: true,
|
||||||
|
mdiClose,
|
||||||
|
mdiChevronLeft,
|
||||||
|
mdiChevronRight,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
showModal() {
|
||||||
|
return this.show === undefined ? this.internalShow : this.show;
|
||||||
|
},
|
||||||
|
modalTransitionName() {
|
||||||
|
return `modal-${this.outTransition ? 'out' : 'in'}`;
|
||||||
|
},
|
||||||
|
playPauseName() {
|
||||||
|
return this.playing ? 'Pause slideshow' : 'Start slideshow';
|
||||||
|
},
|
||||||
|
cssVariables() {
|
||||||
|
return {
|
||||||
|
'--slideshow-duration': this.slideshowDelay + 'ms',
|
||||||
|
'--icon-size': this.iconSize + 'px',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
closeButtonAriaLabel() {
|
||||||
|
return 'Close';
|
||||||
|
},
|
||||||
|
prevButtonAriaLabel() {
|
||||||
|
return 'Previous';
|
||||||
|
},
|
||||||
|
nextButtonAriaLabel() {
|
||||||
|
return 'Next';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
/**
|
||||||
|
* Handle play/pause of an ongoing slideshow
|
||||||
|
*
|
||||||
|
* @param {boolean} paused is the player paused
|
||||||
|
*/
|
||||||
|
slideshowPaused(paused) {
|
||||||
|
if (this.slideshowTimeout) {
|
||||||
|
if (paused) {
|
||||||
|
this.slideshowTimeout.pause();
|
||||||
|
} else {
|
||||||
|
this.slideshowTimeout.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
additionalTrapElements(elements) {
|
||||||
|
if (this.focusTrap) {
|
||||||
|
const contentContainer = this.$refs.mask;
|
||||||
|
this.focusTrap.updateContainerElements([contentContainer, ...elements]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeMount() {
|
||||||
|
window.addEventListener('keydown', this.handleKeydown);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('keydown', this.handleKeydown);
|
||||||
|
this.mc.stop();
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// init clear view
|
||||||
|
this.useFocusTrap();
|
||||||
|
// this.mc = useSwipe(this.$refs.mask, {
|
||||||
|
// onSwipeEnd: this.handleSwipe,
|
||||||
|
// });
|
||||||
|
|
||||||
|
if (this.container) {
|
||||||
|
if (this.container === 'body') {
|
||||||
|
// force mount the component to body
|
||||||
|
document.body.insertBefore(this.$el, document.body.lastChild);
|
||||||
|
} else {
|
||||||
|
const container = document.querySelector(this.container);
|
||||||
|
container.appendChild(this.$el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
this.clearFocusTrap();
|
||||||
|
this.$el.remove();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// Events emitters
|
||||||
|
previous(event) {
|
||||||
|
// do not send the event if nothing is available
|
||||||
|
if (this.hasPrevious) {
|
||||||
|
// if data is set, then it's a user mouse event
|
||||||
|
// and not the slideshow handler, therefore
|
||||||
|
// we reset the timer
|
||||||
|
if (event) {
|
||||||
|
this.resetSlideshow();
|
||||||
|
}
|
||||||
|
this.$emit('previous', event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
next(event) {
|
||||||
|
// do not send the event if nothing is available
|
||||||
|
if (this.hasNext) {
|
||||||
|
// if data is set, then it's a mouse event
|
||||||
|
// and not the slideshow handler, therefore
|
||||||
|
// we reset the timer
|
||||||
|
if (event) {
|
||||||
|
this.resetSlideshow();
|
||||||
|
}
|
||||||
|
this.$emit('next', event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close(data) {
|
||||||
|
// do not fire event if forbidden
|
||||||
|
if (this.canClose) {
|
||||||
|
// We set internalShow here, so the out transitions properly run before the component is destroyed
|
||||||
|
this.internalShow = false;
|
||||||
|
this.$emit('update:show', false);
|
||||||
|
|
||||||
|
// delay closing for animation
|
||||||
|
setTimeout(() => {
|
||||||
|
/**
|
||||||
|
* Emitted when the closing animation is finished
|
||||||
|
*/
|
||||||
|
this.$emit('close', data);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle click on modal wrapper
|
||||||
|
* If `closeOnClickOutside` is set the modal will be closed
|
||||||
|
*
|
||||||
|
* @param {MouseEvent} event The click event
|
||||||
|
*/
|
||||||
|
handleClickModalWrapper(event) {
|
||||||
|
if (this.closeOnClickOutside) {
|
||||||
|
this.close(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {KeyboardEvent} event - keyboard event
|
||||||
|
*/
|
||||||
|
handleKeydown(event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
const trapStack = getTrapStack();
|
||||||
|
// Only close the most recent focus trap modal
|
||||||
|
if (trapStack.length > 0 && trapStack[trapStack.length - 1] !== this.focusTrap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return this.close(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrowHandlers = {
|
||||||
|
ArrowLeft: this.previous,
|
||||||
|
ArrowRight: this.next,
|
||||||
|
};
|
||||||
|
if (arrowHandlers[event.key]) {
|
||||||
|
// Ignore arrow navigation, if there is a current focus outside the modal.
|
||||||
|
// For example, when the focus is in Sidebar or NcActions's items,
|
||||||
|
// arrow navigation should not be intercept by modal slider
|
||||||
|
if (document.activeElement && !this.$el.contains(document.activeElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return arrowHandlers[event.key](event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle the swipe event
|
||||||
|
*
|
||||||
|
* @param {TouchEvent} e The touch event
|
||||||
|
* @param {import('@vueuse/core').SwipeDirection} direction Swipe direction
|
||||||
|
*/
|
||||||
|
handleSwipe(e, direction) {
|
||||||
|
if (this.enableSwipe) {
|
||||||
|
if (direction === 'left') {
|
||||||
|
// swiping to left to go to the next item
|
||||||
|
this.next(e);
|
||||||
|
} else if (direction === 'right') {
|
||||||
|
// swiping to right to go back to the previous item
|
||||||
|
this.previous(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the slideshow state
|
||||||
|
*/
|
||||||
|
togglePlayPause() {
|
||||||
|
this.playing = !this.playing;
|
||||||
|
if (this.playing) {
|
||||||
|
this.handleSlideshow();
|
||||||
|
} else {
|
||||||
|
this.clearSlideshowTimeout();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the slideshow timer and keep going if it was on
|
||||||
|
*/
|
||||||
|
resetSlideshow() {
|
||||||
|
this.playing = !this.playing;
|
||||||
|
this.clearSlideshowTimeout();
|
||||||
|
this.$nextTick(function () {
|
||||||
|
this.togglePlayPause();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the slideshow timer and next event
|
||||||
|
*/
|
||||||
|
handleSlideshow() {
|
||||||
|
this.playing = true;
|
||||||
|
if (this.hasNext) {
|
||||||
|
this.slideshowTimeout = new Timer(() => {
|
||||||
|
this.next();
|
||||||
|
this.handleSlideshow();
|
||||||
|
}, this.slideshowDelay);
|
||||||
|
} else {
|
||||||
|
this.playing = false;
|
||||||
|
this.clearSlideshowTimeout();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear slideshowTimeout if ongoing
|
||||||
|
*/
|
||||||
|
clearSlideshowTimeout() {
|
||||||
|
if (this.slideshowTimeout) {
|
||||||
|
this.slideshowTimeout.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Add focus trap for accessibility.
|
||||||
|
*/
|
||||||
|
async useFocusTrap() {
|
||||||
|
// Don't do anything if the modal is hidden,
|
||||||
|
// or we have a focus trap already
|
||||||
|
if (!this.showModal || this.focusTrap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentContainer = this.$refs.mask;
|
||||||
|
// wait until all children are mounted and available in the DOM before focusTrap can be added
|
||||||
|
await this.$nextTick();
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
allowOutsideClick: true,
|
||||||
|
fallbackFocus: contentContainer,
|
||||||
|
trapStack: getTrapStack(),
|
||||||
|
// Esc can be used without stop in content or additionalTrapElements where it should not deacxtivate modal's focus trap.
|
||||||
|
// Focus trap is deactivated on modal close anyway.
|
||||||
|
escapeDeactivates: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Init focus trap
|
||||||
|
this.focusTrap = createFocusTrap([contentContainer, ...this.additionalTrapElements], options);
|
||||||
|
this.focusTrap.activate();
|
||||||
|
},
|
||||||
|
clearFocusTrap() {
|
||||||
|
if (!this.focusTrap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.focusTrap?.deactivate();
|
||||||
|
this.focusTrap = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.modal-mask {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9998;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
&--dark {
|
||||||
|
background-color: rgba(0, 0, 0, 0.92);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10001;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
/* prevent vue show to use display:none and reseting */
|
||||||
|
/* the circle animation loop */
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--header-height);
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
opacity 250ms,
|
||||||
|
visibility 250ms;
|
||||||
|
|
||||||
|
.modal-name {
|
||||||
|
overflow-x: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
/* maximum actions is 3 */
|
||||||
|
padding: 0 var(--clickable-area * 3) 0 12px;
|
||||||
|
transition: padding ease 100ms;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: #fff;
|
||||||
|
/* font-size: var(--icon-margin); */
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On wider screens the name can be centered
|
||||||
|
@media only screen and (min-width: $breakpoint-mobile) {
|
||||||
|
.modal-name {
|
||||||
|
padding-left: #{$clickable-area * 3}; // maximum actions is 3
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
.icons-menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.header-close {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* margin: math.div($header-height - $clickable-area, 2); */
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-pause-icons {
|
||||||
|
position: relative;
|
||||||
|
width: var(--header-height);
|
||||||
|
height: var(header-height);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
|
||||||
|
.play-pause-icons__play,
|
||||||
|
.play-pause-icons__pause {
|
||||||
|
opacity: var(--opacity_full);
|
||||||
|
/* border-radius: math.div($clickable-area, 2); */
|
||||||
|
background-color: var(--icon-focus-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__play,
|
||||||
|
&__pause {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: var(--default-clickable-area);
|
||||||
|
height: var(--default-clickable-area);
|
||||||
|
margin: var(--header-height) - var(--default-clickable-area);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: var(--opacity_normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:deep() .action-item {
|
||||||
|
/* margin: math.div($header-height - $clickable-area, 2); */
|
||||||
|
|
||||||
|
&--single {
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* width: $clickable-area;
|
||||||
|
height: $clickable-area; */
|
||||||
|
cursor: pointer;
|
||||||
|
background-position: center;
|
||||||
|
background-size: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(button) {
|
||||||
|
/* force white instead of default main text */
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force the Actions menu icon to be the same size as other icons */
|
||||||
|
&:deep(.action-item__menutoggle) {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
span,
|
||||||
|
svg {
|
||||||
|
width: var(--icon-size);
|
||||||
|
height: var(--icon-size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
/* Navigation buttons */
|
||||||
|
.prev,
|
||||||
|
.next {
|
||||||
|
z-index: 10000;
|
||||||
|
height: 35vh;
|
||||||
|
min-height: 300px;
|
||||||
|
position: absolute;
|
||||||
|
transition: opacity 250ms;
|
||||||
|
/* // hover the mask */
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
/* Override NcButton focus styles */
|
||||||
|
box-shadow: 0 0 0 2px var(--color-primary-element-text);
|
||||||
|
background-color: var(--color-box-shadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev {
|
||||||
|
left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next {
|
||||||
|
right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.modal-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 0;
|
||||||
|
transition: transform 300ms ease;
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
background-color: var(--color-main-background);
|
||||||
|
color: var(--color-main-text);
|
||||||
|
box-shadow: 0 0 40px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
&__close {
|
||||||
|
/* // Ensure the close button is always ontop of the content */
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
width: 100%;
|
||||||
|
/* At least the close button shall fit in */
|
||||||
|
min-height: 52px;
|
||||||
|
/* avoids unecessary hacks if the content should be bigger than the modal */
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We allow 90% max-height, but we need to ensure the header does not overlap the modal
|
||||||
|
as the modal is centered, we need the space on top and bottom */
|
||||||
|
/* $max-modal-height: min(90%, calc(100% - 2 * $header-height)); */
|
||||||
|
|
||||||
|
/* // Sizing */
|
||||||
|
&--small {
|
||||||
|
.modal-container {
|
||||||
|
width: 400px;
|
||||||
|
max-width: 90%;
|
||||||
|
/* max-height: $max-modal-height; */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--normal {
|
||||||
|
.modal-container {
|
||||||
|
max-width: 90%;
|
||||||
|
width: 600px;
|
||||||
|
/* max-height: $max-modal-height; */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--large {
|
||||||
|
.modal-container {
|
||||||
|
max-width: 90%;
|
||||||
|
width: 900px;
|
||||||
|
/* max-height: $max-modal-height; */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--full {
|
||||||
|
.modal-container {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - var(--header-height));
|
||||||
|
position: absolute;
|
||||||
|
/* top: $header-height; */
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* // Make modal full screen on mobile */
|
||||||
|
@media only screen and (max-width: 640px) {
|
||||||
|
.modal-container {
|
||||||
|
max-width: initial;
|
||||||
|
width: 100%;
|
||||||
|
max-height: initial;
|
||||||
|
height: calc(100% - var(--header-height));
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TRANSITIONS */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 250ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-visibility-enter,
|
||||||
|
.fade-visibility-leave-to {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-in-enter-active,
|
||||||
|
.modal-in-leave-active,
|
||||||
|
.modal-out-enter-active,
|
||||||
|
.modal-out-leave-active {
|
||||||
|
transition: opacity 250ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-in-enter,
|
||||||
|
.modal-in-leave-to,
|
||||||
|
.modal-out-enter,
|
||||||
|
.modal-out-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-in-enter .modal-container,
|
||||||
|
.modal-in-leave-to .modal-container {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-out-enter .modal-container,
|
||||||
|
.modal-out-leave-to .modal-container {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* // animated circle */
|
||||||
|
/* $radius: 15;
|
||||||
|
$pi: 3.14159265358979; */
|
||||||
|
|
||||||
|
.modal-mask .play-pause-icons {
|
||||||
|
.progress-ring {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
|
||||||
|
.progress-ring__circle {
|
||||||
|
transition: 100ms stroke-dashoffset;
|
||||||
|
/* // axis compensation */
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
animation: progressring linear var(--slideshow-duration) infinite;
|
||||||
|
|
||||||
|
stroke-linecap: round;
|
||||||
|
/* radius * 2 * PI */
|
||||||
|
stroke-dashoffset: 15 * 2 * 3.14159265358979;
|
||||||
|
/* radius * 2 * PI */
|
||||||
|
stroke-dasharray: 15 * 2 * 3.14159265358979;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--paused {
|
||||||
|
.icon-pause {
|
||||||
|
animation: breath 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-ring__circle {
|
||||||
|
animation-play-state: paused !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* keyframes get scoped too and break the animation name, we need them unscoped */
|
||||||
|
@keyframes progressring {
|
||||||
|
from {
|
||||||
|
/* radius * 2 * PI */
|
||||||
|
stroke-dashoffset: 15 * 2 * 3.14159265358979;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes breath {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -146,7 +146,7 @@ const datasets = computed(() => mainService.datasets);
|
||||||
<TableSampleClients />
|
<TableSampleClients />
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
|
||||||
<CardBox>
|
<!-- <CardBox>
|
||||||
<p class="mb-3 text-gray-500 dark:text-gray-400">
|
<p class="mb-3 text-gray-500 dark:text-gray-400">
|
||||||
Discover the power of Tethys, the cutting-edge web backend solution that revolutionizes the way you handle research
|
Discover the power of Tethys, the cutting-edge web backend solution that revolutionizes the way you handle research
|
||||||
data. At the heart of Tethys lies our meticulously developed research data repository, which leverages state-of-the-art
|
data. At the heart of Tethys lies our meticulously developed research data repository, which leverages state-of-the-art
|
||||||
|
@ -163,7 +163,7 @@ const datasets = computed(() => mainService.datasets);
|
||||||
eliminates errors and minimizes downtime. Our CI/CD pipeline automatically verifies each code commit, runs comprehensive
|
eliminates errors and minimizes downtime. Our CI/CD pipeline automatically verifies each code commit, runs comprehensive
|
||||||
tests, and deploys the repository seamlessly, ensuring that your research data is always up-to-date and accessible.
|
tests, and deploys the repository seamlessly, ensuring that your research data is always up-to-date and accessible.
|
||||||
</p>
|
</p>
|
||||||
</CardBox>
|
</CardBox> -->
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
<!-- </section> -->
|
<!-- </section> -->
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
|
|
|
@ -6,8 +6,10 @@ export const basic = {
|
||||||
asideMenuItemActive: 'font-bold text-cyan-300',
|
asideMenuItemActive: 'font-bold text-cyan-300',
|
||||||
asideMenuDropdown: 'bg-gray-700/50',
|
asideMenuDropdown: 'bg-gray-700/50',
|
||||||
navBarItemLabel: 'text-black',
|
navBarItemLabel: 'text-black',
|
||||||
navBarItemLabelHover: 'hover:text-blue-500',
|
// navBarItemLabelHover: 'hover:text-blue-500',
|
||||||
navBarItemLabelActiveColor: 'text-blue-600',
|
navBarItemLabelHover: 'hover:text-lime-dark',
|
||||||
|
// navBarItemLabelActiveColor: 'text-blue-600',
|
||||||
|
navBarItemLabelActiveColor: 'text-lime-dark',
|
||||||
overlay: 'from-gray-700 via-gray-900 to-gray-700',
|
overlay: 'from-gray-700 via-gray-900 to-gray-700',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
8
resources/js/utils/GenRandomId.js
Normal file
8
resources/js/utils/GenRandomId.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
const GenRandomId = (length) => {
|
||||||
|
return Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.replace(/[^a-z]+/g, '')
|
||||||
|
.slice(0, length || 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GenRandomId
|
43
resources/js/utils/Timer.js
Normal file
43
resources/js/utils/Timer.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* @param {Function} callback The function to call
|
||||||
|
* @param {number} delay The time to wait
|
||||||
|
*/
|
||||||
|
export default function timer(callback, delay) {
|
||||||
|
let id;
|
||||||
|
let started;
|
||||||
|
let remaining = delay;
|
||||||
|
let running;
|
||||||
|
|
||||||
|
this.start = function () {
|
||||||
|
running = true;
|
||||||
|
started = new Date();
|
||||||
|
id = setTimeout(callback, remaining);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pause = function () {
|
||||||
|
running = false;
|
||||||
|
clearTimeout(id);
|
||||||
|
remaining -= new Date() - started;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.clear = function () {
|
||||||
|
running = false;
|
||||||
|
clearTimeout(id);
|
||||||
|
remaining = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getTimeLeft = function () {
|
||||||
|
if (running) {
|
||||||
|
this.pause();
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
return remaining;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getStateRunning = function () {
|
||||||
|
return running;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.start();
|
||||||
|
}
|
11
resources/js/utils/focusTrap.js
Normal file
11
resources/js/utils/focusTrap.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/**
|
||||||
|
* Return the default global focus trap stack
|
||||||
|
*
|
||||||
|
* @return {import('focus-trap').FocusTrap[]}
|
||||||
|
*/
|
||||||
|
export const getTrapStack = function() {
|
||||||
|
// Create global stack if undefined
|
||||||
|
Object.assign(window, { _nc_focus_trap: window._nc_focus_trap || [] })
|
||||||
|
|
||||||
|
return window._nc_focus_trap
|
||||||
|
}
|
16
resources/js/utils/initialState.ts
Normal file
16
resources/js/utils/initialState.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export function loadState<T>(app: string, key: string, fallback?: T): T {
|
||||||
|
const elem = <HTMLInputElement>document.querySelector(`#initial-state-${app}-${key}`);
|
||||||
|
if (elem === null) {
|
||||||
|
if (fallback !== undefined) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
throw new Error(`Could not find initial state ${key} of ${app}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value = atob(elem.value);
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Could not parse initial state ${key} of ${app}`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +1,27 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{{-- <link rel="icon" type="image/png" href="/favicon.ico"> --}}
|
{{--
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" type="image/png" href="/favicon.ico"> --}}
|
||||||
{{-- <link rel="icon" href="/apps/theming/favicon/settings?v=ad28c447"> --}}
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
|
{{--
|
||||||
|
<link rel="icon" href="/apps/theming/favicon/settings?v=ad28c447"> --}}
|
||||||
|
<input type="hidden" id="initial-state-firstrunwizard-desktop"
|
||||||
|
value="Imh0dHBzOi8vZ2l0ZWEuZ2VvbG9naWUuYWMuYXQvZ2VvbGJhL3RldGh5cy5iYWNrZW5kIg==">
|
||||||
|
|
||||||
@routes
|
@routes
|
||||||
@entryPointStyles('app')
|
@entryPointStyles('app')
|
||||||
@entryPointScripts('app')
|
@entryPointScripts('app')
|
||||||
|
|
||||||
{{-- <title>myapp</title> --}}
|
{{-- <title>myapp</title> --}}
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@inertia()
|
@inertia()
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
|
@ -1,6 +1,7 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
const plugin = require('tailwindcss/plugin');
|
const plugin = require('tailwindcss/plugin');
|
||||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||||
|
const { registerables } = require('chart.js');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ['./resources/**/*.{edge,js,ts,jsx,tsx,vue}'],
|
content: ['./resources/**/*.{edge,js,ts,jsx,tsx,vue}'],
|
||||||
|
@ -13,7 +14,21 @@ module.exports = {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
'primary': '#22C55E',
|
'primary': '#22C55E',
|
||||||
'primary-dark': '#DCFCE7',
|
'primary-dark': '#DCFCE7',
|
||||||
|
'lime': {
|
||||||
|
DEFAULT: '#BFCE40',
|
||||||
|
dark: 'rgba(5,46,55,0.7)',
|
||||||
|
50: '#FBFCF7',
|
||||||
|
100: '#F8FBE1',
|
||||||
|
200: '#EEF69E',
|
||||||
|
300: '#DCEC53',
|
||||||
|
400: '#A8D619',
|
||||||
|
500: '#65DC21',
|
||||||
|
600: '#429E04',
|
||||||
|
700: '#357C06',
|
||||||
|
800: '#295B09',
|
||||||
|
900: '#20450A',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', ...defaultTheme.fontFamily.sans],
|
sans: ['Inter', ...defaultTheme.fontFamily.sans],
|
||||||
|
|
Loading…
Reference in New Issue
Block a user