Add animations and pin create card input

Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Julius Härtl
2020-04-22 18:01:48 +02:00
parent 63c547b9c7
commit ff5d092436
5 changed files with 157 additions and 56 deletions

View File

@@ -23,26 +23,29 @@
<template> <template>
<div class="board-wrapper"> <div class="board-wrapper">
<Controls :board="board" /> <Controls :board="board" />
<div v-if="loading" class="emptycontent"> <transition name="fade" mode="out-in">
<div class="icon icon-loading" /> <div v-if="loading" class="emptycontent" key="loading">
<h2>{{ t('deck', 'Loading board') }}</h2> <div class="icon icon-loading" />
<p /> <h2>{{ t('deck', 'Loading board') }}</h2>
</div> <p />
<div v-else-if="board" class="board"> </div>
<Container lock-axix="y" <div v-else-if="board && !loading" class="board" key="board">
orientation="horizontal" <Container lock-axix="y"
:drag-handle-selector="dragHandleSelector" orientation="horizontal"
@drop="onDropStack"> :drag-handle-selector="dragHandleSelector"
<Draggable v-for="stack in stacksByBoard" :key="stack.id"> @drop="onDropStack">
<Stack :stack="stack" /> <Draggable v-for="stack in stacksByBoard" :key="stack.id">
</Draggable> <Stack :stack="stack" />
</Container> </Draggable>
</div> </Container>
<div v-else class="emptycontent"> </div>
<div class="icon icon-deck" /> <div v-else class="emptycontent" key="notfound">
<h2>{{ t('deck', 'Board not found') }}</h2> <div class="icon icon-deck" />
<p /> <h2>{{ t('deck', 'Board not found') }}</h2>
</div> <p />
</div>
</transition>
</div> </div>
</template> </template>
@@ -102,8 +105,12 @@ export default {
methods: { methods: {
async fetchData() { async fetchData() {
this.loading = true this.loading = true
await this.$store.dispatch('loadBoardById', this.id) try {
await this.$store.dispatch('loadStacks', this.id) await this.$store.dispatch('loadBoardById', this.id)
await this.$store.dispatch('loadStacks', this.id)
} catch (e) {
console.error(e)
}
this.loading = false this.loading = false
}, },
@@ -125,6 +132,8 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../css/animations.scss";
$board-spacing: 15px; $board-spacing: 15px;
$stack-spacing: 10px; $stack-spacing: 10px;
$stack-width: 300px; $stack-width: 300px;

View File

@@ -23,7 +23,7 @@
<template> <template>
<div class="stack"> <div class="stack">
<div class="stack--header"> <div v-click-outside="stopCardCreation" class="stack--header">
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<h3 v-if="!canManage"> <h3 v-if="!canManage">
{{ stack.title }} {{ stack.title }}
@@ -32,7 +32,7 @@
{{ stack.title }} {{ stack.title }}
</h3> </h3>
<form v-else @submit.prevent="finishedEdit(stack)"> <form v-else @submit.prevent="finishedEdit(stack)">
<input v-model="copiedStack.title" type="text" autofocus> <input v-model="copiedStack.title" v-focus type="text">
<input v-tooltip="t('deck', 'Add a new stack')" <input v-tooltip="t('deck', 'Add a new stack')"
class="icon-confirm" class="icon-confirm"
type="submit" type="submit"
@@ -45,31 +45,35 @@
</ActionButton> </ActionButton>
</Actions> </Actions>
<Actions v-if="canEdit && !showArchived"> <Actions v-if="canEdit && !showArchived">
<ActionButton icon="icon-add" @click="showAddCard=true"> <ActionButton icon="icon-add" @click.stop="showAddCard=true">
{{ t('deck', 'Add card') }} {{ t('deck', 'Add card') }}
</ActionButton> </ActionButton>
</Actions> </Actions>
</div> </div>
<form v-if="showAddCard" <transition name="slide-top" appear>
class="stack--card-add" <div v-if="showAddCard" class="stack--card-add">
:class="{ 'icon-loading-small': stateCardCreating }" <form :class="{ 'icon-loading-small': stateCardCreating }"
@submit.prevent="clickAddCard()"> @submit.prevent.stop="clickAddCard()">
<label for="new-stack-input-main" class="hidden-visually">{{ t('deck', 'Add a new card') }}</label> <label for="new-stack-input-main" class="hidden-visually">{{ t('deck', 'Add a new card') }}</label>
<input id="new-stack-input-main" <input id="new-stack-input-main"
v-model="newCardTitle" ref="newCardInput"
v-focus v-model="newCardTitle"
type="text" v-focus
class="no-close" type="text"
:disabled="stateCardCreating" class="no-close"
placeholder="Add a new card" :disabled="stateCardCreating"
required> placeholder="Add a new card"
required
@keydown.esc="stopCardCreation">
<input v-show="!stateCardCreating" <input v-show="!stateCardCreating"
class="icon-confirm" class="icon-confirm"
type="submit" type="submit"
value=""> value="">
</form> </form>
</div>
</transition>
<Container :get-child-payload="payloadForCard(stack.id)" <Container :get-child-payload="payloadForCard(stack.id)"
group-name="stack" group-name="stack"
@@ -78,7 +82,11 @@
@should-accept-drop="canEdit" @should-accept-drop="canEdit"
@drop="($event) => onDropCard(stack.id, $event)"> @drop="($event) => onDropCard(stack.id, $event)">
<Draggable v-for="card in cardsByStack" :key="card.id"> <Draggable v-for="card in cardsByStack" :key="card.id">
<CardItem v-if="card" :id="card.id" /> <transition :appear="animate && !card.animated && (card.animated=true)"
:appear-class="'zoom-appear-class'"
:appear-active-class="'zoom-appear-active-class'">
<CardItem :id="card.id" />
</transition>
</Draggable> </Draggable>
</Container> </Container>
</div> </div>
@@ -114,6 +122,7 @@ export default {
newCardTitle: '', newCardTitle: '',
showAddCard: false, showAddCard: false,
stateCardCreating: false, stateCardCreating: false,
animate: false,
} }
}, },
computed: { computed: {
@@ -133,6 +142,16 @@ export default {
}, },
methods: { methods: {
stopCardCreation(e) {
// For some reason the submit event triggers a MouseEvent that is bubbling to the outside
// so we have to ignore it
e.stopPropagation()
if (this.$refs.newCardInput && this.$refs.newCardInput.parentElement === e.target.parentElement) {
return false
}
this.showAddCard = false
return false
},
async onDropCard(stackId, event) { async onDropCard(stackId, event) {
const { addedIndex, removedIndex, payload } = event const { addedIndex, removedIndex, payload } = event
const card = Object.assign({}, payload) const card = Object.assign({}, payload)
@@ -172,13 +191,19 @@ export default {
async clickAddCard() { async clickAddCard() {
this.stateCardCreating = true this.stateCardCreating = true
try { try {
await this.$store.dispatch('addCard', { this.animate = true
const newCard = await this.$store.dispatch('addCard', {
title: this.newCardTitle, title: this.newCardTitle,
stackId: this.stack.id, stackId: this.stack.id,
boardId: this.stack.boardId, boardId: this.stack.boardId,
}) })
this.newCardTitle = '' this.newCardTitle = ''
this.showAddCard = false this.showAddCard = true
this.$nextTick(() => {
this.$refs.newCardInput.focus()
this.animate = false
})
this.$router.push({ name: 'card', params: { cardId: newCard.id } })
} catch (e) { } catch (e) {
OCP.Toast.error('Could not create card: ' + e.response.data.message) OCP.Toast.error('Could not create card: ' + e.response.data.message)
} finally { } finally {
@@ -208,8 +233,8 @@ export default {
padding: 3px; padding: 3px;
margin: 3px -3px; margin: 3px -3px;
margin-right: -10px; margin-right: -10px;
margin-bottom: 5px;
margin-top: 0; margin-top: 0;
margin-bottom: 0;
background-color: var(--color-main-background-translucent); background-color: var(--color-main-background-translucent);
h3, form { h3, form {
@@ -223,10 +248,25 @@ export default {
} }
.stack--card-add { .stack--card-add {
position: sticky;
top: 52px;
height: 52px;
z-index: 100;
display: flex; display: flex;
margin-bottom: 10px; background-color: var(--color-main-background);
box-shadow: 0 0 3px var(--color-box-shadow); margin-left: -10px;
border-radius: 3px; margin-right: -10px;
padding-top: 3px;
form {
display: flex;
width: 100%;
margin: 10px;
margin-top: 0;
margin-bottom: 10px;
box-shadow: 0 0 3px var(--color-box-shadow);
border-radius: 3px;
}
&.icon-loading-small:after, &.icon-loading-small:after,
&.icon-loading-small-dark:after { &.icon-loading-small-dark:after {
@@ -241,9 +281,22 @@ export default {
border: none; border: none;
} }
} }
.stack .smooth-dnd-container.vertical {
margin-top: 3px;
}
/** /**
* Rules to handle scrolling behaviour are inherited from Board.vue * Rules to handle scrolling behaviour are inherited from Board.vue
*/ */
.slide-top-enter-active,
.slide-top-leave-active {
transition: all 100ms ease;
}
.slide-top-enter, .slide-top-leave-to {
transform: translateY(-10px);
opacity: 0;
height: 0px;
}
</style> </style>

View File

@@ -236,7 +236,7 @@ export default {
descriptionSaveTimeout: null, descriptionSaveTimeout: null,
descriptionSaving: false, descriptionSaving: false,
hasActivity: capabilities && capabilities.activity, hasActivity: capabilities && capabilities.activity,
hasComments: !!OC.appswebroots['comments'] hasComments: !!OC.appswebroots['comments'],
} }
}, },
computed: { computed: {

View File

@@ -48,16 +48,21 @@
</form> </form>
<div v-if="!editing" class="right"> <div v-if="!editing" class="right">
<div v-if="card.duedate" :class="dueIcon"> <transition name="zoom">
<span>{{ relativeDate }}</span> <div v-if="card.duedate" :class="dueIcon">
</div> <span>{{ relativeDate }}</span>
</div>
</transition>
</div> </div>
</div> </div>
<ul v-if="card.labels && card.labels.length > 0" class="labels" @click="openCard"> <transition-group name="zoom"
tag="ul"
class="labels"
@click="openCard">
<li v-for="label in card.labels" :key="label.id" :style="labelStyle(label)"> <li v-for="label in card.labels" :key="label.id" :style="labelStyle(label)">
<span>{{ label.title }}</span> <span>{{ label.title }}</span>
</li> </li>
</ul> </transition-group>
<div v-show="!compactMode" class="card-controls compact-item" @click="openCard"> <div v-show="!compactMode" class="card-controls compact-item" @click="openCard">
<CardBadges :id="id" /> <CardBadges :id="id" />
</div> </div>
@@ -132,6 +137,13 @@ export default {
return moment(this.card.duedate).format('LLLL') return moment(this.card.duedate).format('LLLL')
}, },
}, },
watch: {
currentCard(newValue) {
if (newValue) {
this.$nextTick(() => this.$el.scrollIntoView())
}
},
},
methods: { methods: {
openCard() { openCard() {
this.$router.push({ name: 'card', params: { cardId: this.id } }) this.$router.push({ name: 'card', params: { cardId: this.id } })
@@ -154,6 +166,8 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import './../../css/animations';
$card-spacing: 10px; $card-spacing: 10px;
$card-padding: 10px; $card-padding: 10px;

25
src/css/animations.scss Normal file
View File

@@ -0,0 +1,25 @@
.fade-enter-active {
transition: opacity 250ms;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
.zoom-appear-active-class,
.zoom-enter-active {
animation: zoom-in var(--animation-quick);
}
.zoom-leave-active {
animation: zoom-in var(--animation-quick) reverse;
will-change: transform;
}
@keyframes zoom-in {
0% {
transform: scale(0);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}