mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
Rework Notifications: separate and bundle to an amount of 4 (#13300)
* WIP notifications only show 2 at once * separate and bundle notifications to pairs * notification fadein/-out/move animations - remove notifications on an interval instead of calculated timeouts * easier way to import all sprite css files * add stories + fix sizes / paddings + click to hide + animation fixes * keep notification at the top but always under the toolbars * change animations to ease-in 0.25s + add prop to change the delay between deletion and add * fix adding logic in a rare case of added notifications when only one item is currently visible + add debug mode * disable lint for notification console * add more notification example trigger buttons * potential fix of animation / queue * increase amount of notifications to 4 * fix sanity * fix test:unit call again * new notification styles - fix animations * keep error notifications visible until manually removed + refactor adding/removal logic * fix margins * prevent multiple filling + different delay on filling * stop and restart removal timer on new notifications * reduce line-height / apply different margin for icons * move sprites.scss out and use it in app.vue as well * update sprites back to 31
This commit is contained in:
@@ -2,38 +2,9 @@
|
||||
import { configure } from '@storybook/vue';
|
||||
import './margin.css';
|
||||
import '../../src/assets/scss/index.scss';
|
||||
import '../../src/assets/css/sprites.css';
|
||||
|
||||
import '../../src/assets/css/sprites/spritesmith-main-0.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-1.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-2.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-3.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-4.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-5.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-6.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-7.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-8.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-9.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-10.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-11.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-12.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-13.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-14.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-15.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-16.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-17.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-18.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-19.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-20.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-21.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-22.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-23.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-24.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-25.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-26.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-27.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-28.css';
|
||||
import '../../src/assets/css/sprites/spritesmith-main-29.css';
|
||||
import '../../src/assets/scss/sprites.scss';
|
||||
|
||||
import Vue from 'vue';
|
||||
import BootstrapVue from 'bootstrap-vue';
|
||||
import StoreModule from '@/libs/store';
|
||||
|
||||
@@ -520,37 +520,5 @@ export default {
|
||||
<style src="axios-progress-bar/dist/nprogress.css"></style>
|
||||
<style src="@/assets/scss/index.scss" lang="scss"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-largeSprites-0.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-0.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-1.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-2.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-3.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-4.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-5.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-6.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-7.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-8.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-9.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-10.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-11.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-12.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-13.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-14.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-15.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-16.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-17.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-18.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-19.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-20.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-21.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-22.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-23.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-24.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-25.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-26.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-27.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-28.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-29.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-30.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-31.css"></style>
|
||||
<style src="@/assets/css/sprites.css"></style>
|
||||
<style src="@/assets/scss/sprites.scss" lang="scss"></style>
|
||||
<style src="smartbanner.js/dist/smartbanner.min.css"></style>
|
||||
|
||||
5
website/client/src/assets/scss/sprites.scss
Normal file
5
website/client/src/assets/scss/sprites.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
@import url("../css/sprites.css");
|
||||
|
||||
@for $i from 0 through 31 {
|
||||
@import url("../css/sprites/spritesmith-main-#{$i}.css");
|
||||
}
|
||||
215
website/client/src/components/snackbars/notification.stories.js
Normal file
215
website/client/src/components/snackbars/notification.stories.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { storiesOf } from '@storybook/vue';
|
||||
import { boolean, withKnobs } from '@storybook/addon-knobs';
|
||||
|
||||
import Notification from './notification';
|
||||
import Notifications from './notifications';
|
||||
import notificationsMixin from '../../mixins/notifications';
|
||||
|
||||
const stories = storiesOf('Notifications', module);
|
||||
|
||||
stories.addDecorator(withKnobs);
|
||||
|
||||
stories
|
||||
.add('notifications overview', () => ({
|
||||
components: {
|
||||
Notification,
|
||||
},
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: flex-start">
|
||||
<Notification v-for="notification of notifications"
|
||||
:notification="notification"
|
||||
:style="{outline: showBounds ? '1px solid green': ''}"
|
||||
style="margin-right: 1rem">
|
||||
|
||||
</Notification> <br/>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
data () {
|
||||
const notifications = [];
|
||||
|
||||
notifications.push({
|
||||
type: 'hp',
|
||||
sign: '+',
|
||||
text: '+2',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'hp',
|
||||
sign: '-',
|
||||
text: '-2',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'mp',
|
||||
sign: '+',
|
||||
text: '+2',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'mp',
|
||||
sign: '-',
|
||||
text: '-2',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'xp',
|
||||
sign: '+',
|
||||
text: '+12',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'xp',
|
||||
sign: '-',
|
||||
text: '-12',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'gp',
|
||||
sign: '+',
|
||||
text: '+12',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'gp',
|
||||
sign: '-',
|
||||
text: '-12',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'streak',
|
||||
text: '12',
|
||||
});
|
||||
|
||||
|
||||
notifications.push({
|
||||
type: 'damage',
|
||||
sign: '+',
|
||||
text: '12',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'drop',
|
||||
icon: 'shop_weapon_wizard_2',
|
||||
text: 'Dropped something with a longer text to try',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'drop',
|
||||
icon: 'Pet_Egg_FlyingPig',
|
||||
text: 'Dropped flying pig egg',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'drop',
|
||||
icon: 'Pet_Food_Strawberry',
|
||||
text: 'You’ve found a Strawberry!',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'info',
|
||||
text: 'Info',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'success',
|
||||
text: 'Success!',
|
||||
});
|
||||
notifications.push({
|
||||
type: 'crit',
|
||||
text: 'Crit!',
|
||||
});
|
||||
notifications.push({
|
||||
type: 'lvl',
|
||||
text: 'Lvl Up',
|
||||
});
|
||||
|
||||
notifications.push({
|
||||
type: 'error',
|
||||
text: 'This is an error message. If it is too long, we can wrap to show the rest of the message',
|
||||
});
|
||||
|
||||
return {
|
||||
notifications,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
showBounds: {
|
||||
default: boolean('show bounds', false),
|
||||
},
|
||||
},
|
||||
}))
|
||||
.add('trigger notifications', () => ({
|
||||
components: {
|
||||
Notifications,
|
||||
},
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
<button @click="addNotification()">Add Notifications</button>
|
||||
|
||||
<button @click="crit(1337)">Crit</button>
|
||||
|
||||
<button @click="drop('Drop', {type:'weapon', key: 'wizard_2'})">Drop</button>
|
||||
|
||||
<button @click="quest('quest', 'val')">Quest</button>
|
||||
<button @click="damage(-13)">Damage</button>
|
||||
<button @click="exp(42)">Exp</button>
|
||||
<button @click="error('some error')">Error</button>
|
||||
|
||||
<br/>
|
||||
|
||||
<button @click="gp(23, 0)">Gold</button>
|
||||
|
||||
|
||||
<button @click="hp(23)">HP</button>
|
||||
<button @click="mp(23)">MP</button>
|
||||
|
||||
<button @click="lvl()">LVL</button>
|
||||
|
||||
<button @click="streak('Streak')">Streak</button>
|
||||
|
||||
<br/>
|
||||
<button @click="markdown('You cast a skill')">Markdown</button>
|
||||
|
||||
|
||||
<Notifications :prevent-queue="preventQueue"
|
||||
:debug-mode="debugMode"
|
||||
:style="{outline: showBounds ? '1px solid green': ''}">
|
||||
</Notifications>
|
||||
</div>
|
||||
`,
|
||||
props: {
|
||||
showBounds: {
|
||||
default: boolean('show bounds', false),
|
||||
},
|
||||
preventQueue: {
|
||||
default: boolean('prevent removing', false),
|
||||
},
|
||||
debugMode: {
|
||||
default: boolean('debug mode', true),
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {};
|
||||
},
|
||||
mixins: [notificationsMixin],
|
||||
methods: {
|
||||
addNotification () {
|
||||
this.text('notification!!');
|
||||
this.text('notification2!!');
|
||||
this.text('notification3!!');
|
||||
this.error('This should stay visible');
|
||||
this.text('notification4!!');
|
||||
this.exp(125);
|
||||
this.damage(-2);
|
||||
|
||||
this.error('This should stay visible too');
|
||||
this.text('notification5!!');
|
||||
this.exp(125);
|
||||
this.damage(-2);
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -1,42 +1,48 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div class="notification-animation-holder">
|
||||
<div class="notification-holder"
|
||||
@click="handleOnClick()">
|
||||
<div v-if="notification.type === 'drop'"
|
||||
class="icon-item">
|
||||
<div :class="notification.icon" class="icon-negative-margin"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="show"
|
||||
class="notification callout animated pt-0"
|
||||
class="notification callout pt-0"
|
||||
:class="classes"
|
||||
@click="handleOnClick()"
|
||||
>
|
||||
<div
|
||||
v-if="notification.type === 'error'"
|
||||
class="row"
|
||||
>
|
||||
<div class="text col-12">
|
||||
<div class="text">
|
||||
<div v-html="notification.text"></div>
|
||||
</div>
|
||||
<close-icon />
|
||||
</div>
|
||||
<div
|
||||
v-if="notification.type === 'streak'"
|
||||
class="row"
|
||||
>
|
||||
<div class="text col-7 offset-1">
|
||||
<div class="text">
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
<div class="icon col-4">
|
||||
<div class="icon d-flex align-items-center">
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icons.gold"
|
||||
></div>
|
||||
<div v-html="notification.text"></div>
|
||||
<div class="icon-text" v-html="notification.text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="['hp', 'gp', 'xp', 'mp'].indexOf(notification.type) !== -1"
|
||||
class="row"
|
||||
>
|
||||
<div class="text col-7 offset-1">
|
||||
<div class="text">
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
<div class="icon col-4 d-flex align-items-center">
|
||||
<div class="icon d-flex align-items-center">
|
||||
<div
|
||||
v-if="notification.type === 'hp'"
|
||||
class="svg-icon"
|
||||
@@ -57,29 +63,29 @@
|
||||
class="svg-icon"
|
||||
v-html="icons.mana"
|
||||
></div>
|
||||
<div v-html="notification.text"></div>
|
||||
<div class="icon-text" v-html="notification.text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="notification.type === 'damage'"
|
||||
class="row"
|
||||
>
|
||||
<div class="text col-7 offset-1">
|
||||
<div class="text">
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
<div class="icon col-4">
|
||||
<div class="icon d-flex align-items-center">
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icons.sword"
|
||||
></div>
|
||||
<div v-html="notification.text"></div>
|
||||
<div class="icon-text" v-html="notification.text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="['info', 'success', 'crit', 'lvl'].indexOf(notification.type) !== -1"
|
||||
class="row"
|
||||
>
|
||||
<div class="text col-12">
|
||||
<div class="text">
|
||||
<div v-html="notification.text"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,103 +93,119 @@
|
||||
v-if="notification.type === 'drop'"
|
||||
class="row"
|
||||
>
|
||||
<div class="col-3">
|
||||
<div class="icon-item">
|
||||
<div :class="notification.icon"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text col-8">
|
||||
<div class="text">
|
||||
<div v-html="notification.text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.notification-holder {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
margin-bottom: 0.5rem;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 330px;
|
||||
}
|
||||
|
||||
.notification {
|
||||
border-radius: 30px;
|
||||
background-color: #24cc8f;
|
||||
max-width: 330px;
|
||||
border-radius: 4px;
|
||||
background-color: $green-50;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
color: white;
|
||||
width: 300px;
|
||||
margin-left: 1em;
|
||||
margin-bottom: 1em;
|
||||
margin-left: 0.5rem;
|
||||
padding-left: 1rem !important;
|
||||
padding-right: 1rem !important;
|
||||
|
||||
transition: opacity .5s, top .5s;
|
||||
|
||||
.row {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: #46a7d9;
|
||||
padding-top: .5em;
|
||||
background-color: $blue-50;
|
||||
padding-top: .5rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #f74e52;
|
||||
border-radius: 60px;
|
||||
width: 320px !important;
|
||||
padding: 10px 5px;
|
||||
margin-left: 0;
|
||||
color: #fff;
|
||||
background-color: $maroon-100;
|
||||
color: $white;
|
||||
position: relative;
|
||||
padding-right: 1.5rem !important;
|
||||
cursor: pointer;
|
||||
|
||||
::v-deep button {
|
||||
height: 9px;
|
||||
width: 9px;
|
||||
top: 0.525rem;
|
||||
right: 0.525rem;
|
||||
padding: 0;
|
||||
|
||||
opacity: 0.5;
|
||||
|
||||
svg path {
|
||||
stroke: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
::v-deep button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.negative {
|
||||
background-color: #f74e52;
|
||||
background-color: $maroon-100;
|
||||
}
|
||||
|
||||
.text {
|
||||
text-align: center;
|
||||
padding: .5em 1.5em;
|
||||
padding: .5rem 0;
|
||||
|
||||
::v-deep p:last-of-type {
|
||||
margin-bottom: 0; // remove last markdown padding
|
||||
}
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.hp .icon {
|
||||
color: #f74e52;
|
||||
}
|
||||
|
||||
.mp .icon {
|
||||
color: #2995cd;
|
||||
}
|
||||
|
||||
.damage .icon {
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.icon {
|
||||
background: #fff;
|
||||
color: #ffa623;
|
||||
border-radius: 0 1000px 1000px 0;
|
||||
padding: .5em;
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0.35rem;
|
||||
}
|
||||
|
||||
.drop {
|
||||
background-color: #4e4a57;
|
||||
background-color: $gray-50;
|
||||
}
|
||||
|
||||
.icon-item {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
border-radius: 50%;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 3px 6px 0 rgba(26, 24, 29, 0.16), 0 3px 6px 0 rgba(26, 24, 29, 0.24);
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity .5s
|
||||
.icon-text {
|
||||
color: $white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0
|
||||
.icon-negative-margin {
|
||||
margin: -0.5rem;
|
||||
}
|
||||
|
||||
.notification-animation-holder {
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -193,12 +215,13 @@ import gold from '@/assets/svg/gold.svg';
|
||||
import star from '@/assets/svg/star.svg';
|
||||
import mana from '@/assets/svg/mana.svg';
|
||||
import sword from '@/assets/svg/sword.svg';
|
||||
import CloseIcon from '../shared/closeIcon';
|
||||
|
||||
export default {
|
||||
props: ['notification'],
|
||||
components: { CloseIcon },
|
||||
props: ['notification', 'visibleAmount'],
|
||||
data () {
|
||||
return {
|
||||
timer: null,
|
||||
icons: Object.freeze({
|
||||
health,
|
||||
gold,
|
||||
@@ -206,7 +229,6 @@ export default {
|
||||
mana,
|
||||
sword,
|
||||
}),
|
||||
show: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -233,34 +255,12 @@ export default {
|
||||
return `${this.notification.type} ${this.negative}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
show () {
|
||||
this.$store.dispatch('snackbars:remove', this.notification);
|
||||
},
|
||||
},
|
||||
created () {
|
||||
const timeout = (
|
||||
this.notification
|
||||
&& this.notification.timeout !== undefined
|
||||
&& this.notification.timeout !== null
|
||||
) ? this.notification.timeout : true;
|
||||
|
||||
if (timeout) {
|
||||
let delay = this.notification.delay || 1500;
|
||||
delay += this.$store.state.notificationStore.length * 1000;
|
||||
this.timer = setTimeout(() => {
|
||||
this.show = false;
|
||||
}, delay);
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
clearTimeout(this.timer);
|
||||
},
|
||||
methods: {
|
||||
handleOnClick () {
|
||||
if (typeof this.notification.onClick === 'function') {
|
||||
this.notification.onClick();
|
||||
}
|
||||
this.$emit('clicked');
|
||||
this.show = false;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<template>
|
||||
<div
|
||||
class="notifications"
|
||||
:class="notificationsTopPos"
|
||||
:class="notificationsTopPosClass"
|
||||
:style="{'--current-scrollY': notificationTopY}"
|
||||
>
|
||||
<div
|
||||
v-for="notification in notificationStore"
|
||||
<transition-group
|
||||
name="notifications"
|
||||
class="animations-holder"
|
||||
appear
|
||||
>
|
||||
<notification
|
||||
v-for="(notification, index) in visibleNotifications"
|
||||
:key="notification.uuid"
|
||||
>
|
||||
<notification :notification="notification" />
|
||||
</div>
|
||||
:notification="notification"
|
||||
class="notification-item"
|
||||
:visible-amount="index"
|
||||
@clicked="notificationRemoved(notification)"
|
||||
@hidden="notificationRemoved($event)"
|
||||
/>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -19,40 +29,294 @@
|
||||
width: 350px;
|
||||
z-index: 1400; // 1400 is above modal backgrounds
|
||||
|
||||
&-top-pos {
|
||||
&-normal {
|
||||
top: 65px;
|
||||
top: var(--current-scrollY);
|
||||
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-sleeping {
|
||||
top: 105px;
|
||||
.animations-holder {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
transition: transform 0.25s ease-in, opacity 0.25s ease-in;
|
||||
}
|
||||
|
||||
.notifications-move {
|
||||
// transition: transform .5s;
|
||||
}
|
||||
|
||||
.notifications-enter-active {
|
||||
// transition: opacity .5s;
|
||||
}
|
||||
|
||||
.notifications-leave-active {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.notifications-enter,
|
||||
.notifications-leave-to {
|
||||
opacity: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import notification from './notification';
|
||||
import { sleepAsync } from '../../../../common/script/libs/sleepAsync';
|
||||
|
||||
const NOTIFICATIONS_VISIBLE_AT_ONCE = 4;
|
||||
const REMOVAL_INTERVAL = 2500;
|
||||
const DELAY_DELETE_AND_NEW = 60;
|
||||
const DELAY_FILLING_ENTRIES = 240;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
notification,
|
||||
},
|
||||
props: {
|
||||
preventQueue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
debugMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
visibleNotifications: [],
|
||||
allowedToFillAgain: true,
|
||||
removalIntervalId: null,
|
||||
notificationTopY: '0px',
|
||||
preventMultipleWatchExecution: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
notificationStore: 'notificationStore',
|
||||
userSleeping: 'user.data.preferences.sleep',
|
||||
}),
|
||||
notificationsTopPos () {
|
||||
notificationsTopPosClass () {
|
||||
const base = 'notifications-top-pos-';
|
||||
let modifier = '';
|
||||
|
||||
if (this.userSleeping) {
|
||||
modifier = 'sleeping';
|
||||
} else {
|
||||
modifier = 'normal';
|
||||
}
|
||||
return `${base}${modifier}`;
|
||||
|
||||
return `${base}${modifier} scroll-${this.scrollY}`;
|
||||
},
|
||||
notificationBannerHeight () {
|
||||
let scrollPosToCheck = 0;
|
||||
if (this.userSleeping) {
|
||||
scrollPosToCheck = 98;
|
||||
} else {
|
||||
scrollPosToCheck = 56;
|
||||
}
|
||||
return scrollPosToCheck;
|
||||
},
|
||||
visibleNotificationsWithoutErrors () {
|
||||
return this.visibleNotifications.filter(n => n.type !== 'error');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
notificationStore: async function notificationStore (notifications) {
|
||||
if (this.preventMultipleWatchExecution) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.preventMultipleWatchExecution = true;
|
||||
|
||||
this.debug('notifications changed', {
|
||||
notifications: notifications.length,
|
||||
});
|
||||
|
||||
// if the timer is already running, stop it
|
||||
// otherwise a newly added notification might just disappear faster
|
||||
this.stopNotificationsRemovalTimer();
|
||||
|
||||
const fillingPromise = this.triggerFillUntilFull();
|
||||
|
||||
this.triggerRemovalTimerIfAllowed();
|
||||
|
||||
await fillingPromise;
|
||||
|
||||
this.preventMultipleWatchExecution = false;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('scroll', this.updateScrollY, { passive: true });
|
||||
this.updateScrollY();
|
||||
},
|
||||
|
||||
destroyed () {
|
||||
window.removeEventListener('scroll', this.updateScrollY, { passive: true });
|
||||
},
|
||||
methods: {
|
||||
debug (...args) {
|
||||
if (this.debugMode) {
|
||||
console.info(...args); // eslint-disable-line no-console
|
||||
}
|
||||
},
|
||||
notificationRemoved ($event) {
|
||||
// findIndex+splice is the way to go on removing, instead of .filter
|
||||
// due to the way vue handles new arrays / entries even if you use :key="uuid"
|
||||
// the notification object was replaced and prevented to stop the right timer => to remove it
|
||||
const foundNotification = this.visibleNotifications.findIndex(n => n.uuid === $event.uuid);
|
||||
|
||||
this.visibleNotifications.splice(foundNotification, 1);
|
||||
|
||||
this.updateAllowedToFillAgain();
|
||||
this.$store.dispatch('snackbars:remove', $event);
|
||||
|
||||
this.debug('removed', {
|
||||
allowedToFillAgain: this.allowedToFillAgain,
|
||||
storeLength: this.notificationStore.length,
|
||||
});
|
||||
|
||||
if (this.allowedToFillAgain) {
|
||||
// reset the flag so that the next call (for a new notification) will be immediately
|
||||
if (this.visibleNotificationsWithoutErrors.length !== 0) {
|
||||
this.debug('start timeout to fill again');
|
||||
setTimeout(() => {
|
||||
this.debug('before fill new notifications');
|
||||
this.triggerFillUntilFull();
|
||||
|
||||
this.triggerRemovalTimerIfAllowed();
|
||||
}, DELAY_DELETE_AND_NEW);
|
||||
}
|
||||
}
|
||||
},
|
||||
fillVisibleNotifications (notifications) {
|
||||
this.debug({
|
||||
fillAgain: this.allowedToFillAgain,
|
||||
visible: this.visibleNotifications.length,
|
||||
notifications: notifications.length,
|
||||
});
|
||||
|
||||
// the generic checks - new notification array has enough items
|
||||
// is allowed to be filled and don't do anything while the visible items are 2
|
||||
if (notifications.length === 0 || !this.allowedToFillAgain
|
||||
|| this.visibleNotifications.length === NOTIFICATIONS_VISIBLE_AT_ONCE) {
|
||||
this.debug('stop fill - 1');
|
||||
return;
|
||||
}
|
||||
|
||||
// this checks if the visible items are the current ones in the notifications array
|
||||
// if so - there is no need to continue, for example on only one visible notification,
|
||||
// it doesn't need to go with the loop again
|
||||
if (notifications.length === this.visibleNotifications.length) {
|
||||
const visibleIds = this.visibleNotifications.map(n => n.uuid);
|
||||
|
||||
const allTheSame = notifications.every(n => visibleIds.includes(n.uuid));
|
||||
|
||||
if (allTheSame) {
|
||||
this.debug('stop fill - 2', {
|
||||
visibleIds,
|
||||
notifications: notifications.length,
|
||||
notificationsStore: this.notificationStore.length,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// fill the new items that needs to be visible
|
||||
if (this.visibleNotifications.length < NOTIFICATIONS_VISIBLE_AT_ONCE) {
|
||||
const visibleIds = this.visibleNotifications.map(n => n.uuid);
|
||||
|
||||
const notAddedYet = notifications.filter(n => !visibleIds.includes(n.uuid));
|
||||
|
||||
this.debug({
|
||||
visibleIds,
|
||||
notAddedYet: notAddedYet.length,
|
||||
});
|
||||
|
||||
if (notAddedYet.length > 0) {
|
||||
this.visibleNotifications.push(notAddedYet[0]);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateAllowedToFillAgain();
|
||||
},
|
||||
async triggerFillUntilFull () {
|
||||
for (let i = 0; i < NOTIFICATIONS_VISIBLE_AT_ONCE; i += 1) {
|
||||
this.debug(`fill ${i}`);
|
||||
this.fillVisibleNotifications(this.notificationStore);
|
||||
|
||||
await sleepAsync(DELAY_FILLING_ENTRIES); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
},
|
||||
triggerRemovalTimerIfAllowed () {
|
||||
// this is only for storybook
|
||||
if (this.preventQueue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.notificationStore.length !== 0) {
|
||||
this.startNotificationRemovalTimer();
|
||||
}
|
||||
},
|
||||
startNotificationRemovalTimer () {
|
||||
if (this.removalIntervalId != null) {
|
||||
// current interval still running - wait until its done
|
||||
return;
|
||||
}
|
||||
|
||||
this.debug('start removal interval');
|
||||
this.removalIntervalId = setInterval(() => {
|
||||
const nonErrorNotifications = this.visibleNotifications.filter(n => n.type !== 'error');
|
||||
|
||||
if (nonErrorNotifications.length !== 0) {
|
||||
const firstEntry = nonErrorNotifications[0];
|
||||
|
||||
this.debug('removed entry', firstEntry);
|
||||
this.notificationRemoved(firstEntry);
|
||||
}
|
||||
|
||||
if (nonErrorNotifications.length === 0) {
|
||||
this.stopNotificationsRemovalTimer();
|
||||
|
||||
this.updateAllowedToFillAgain();
|
||||
}
|
||||
}, REMOVAL_INTERVAL);
|
||||
},
|
||||
|
||||
stopNotificationsRemovalTimer () {
|
||||
if (this.removalIntervalId == null) {
|
||||
// current interval still running - wait until its done
|
||||
return;
|
||||
}
|
||||
|
||||
this.debug('clear removal interval');
|
||||
clearInterval(this.removalIntervalId);
|
||||
this.removalIntervalId = null;
|
||||
},
|
||||
|
||||
updateAllowedToFillAgain () {
|
||||
const notificationsAmount = this.visibleNotificationsWithoutErrors.length;
|
||||
this.allowedToFillAgain = notificationsAmount < NOTIFICATIONS_VISIBLE_AT_ONCE;
|
||||
|
||||
this.debug({
|
||||
allowedToFillAgain: this.allowedToFillAgain,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* This updates the position of all notifications so that its always at the top,
|
||||
* unless the header is visible then its under the header
|
||||
*/
|
||||
updateScrollY () {
|
||||
const topY = Math.min(window.scrollY, this.notificationBannerHeight) - 10;
|
||||
|
||||
this.notificationTopY = `${this.notificationBannerHeight - topY}px`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -91,6 +91,7 @@ import updateTask from './ops/updateTask';
|
||||
import * as statHelpers from './statHelpers';
|
||||
import { unEquipByType } from './ops/unequip';
|
||||
import getOfficialPinnedItems from './libs/getOfficialPinnedItems';
|
||||
import { sleepAsync } from './libs/sleepAsync';
|
||||
|
||||
const api = {};
|
||||
api.content = content;
|
||||
@@ -149,6 +150,7 @@ api.onboarding = onboarding;
|
||||
api.setDebuffPotionItems = setDebuffPotionItems;
|
||||
api.getDebuffPotionItems = getDebuffPotionItems;
|
||||
api.getOfficialPinnedItems = getOfficialPinnedItems;
|
||||
api.sleepAsync = sleepAsync;
|
||||
|
||||
api.fns = {
|
||||
autoAllocate,
|
||||
|
||||
5
website/common/script/libs/sleepAsync.js
Normal file
5
website/common/script/libs/sleepAsync.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export function sleepAsync (ms) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
export default async function (seconds = 1) {
|
||||
// using import common from '../../common';
|
||||
// the test:unit can't be compiled
|
||||
// so the sleepAsync can't be used here (to prevent duplicated code)
|
||||
|
||||
export default function (seconds = 1) {
|
||||
const milliseconds = seconds * 1000;
|
||||
|
||||
return new Promise(resolve => {
|
||||
|
||||
Reference in New Issue
Block a user