forked from geolba/tethys.backend
Arno Kaimbacher
87e9314b00
- 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
902 lines
26 KiB
Vue
902 lines
26 KiB
Vue
<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>
|