Add animations and pin create card input
Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
@@ -23,26 +23,29 @@
|
||||
<template>
|
||||
<div class="board-wrapper">
|
||||
<Controls :board="board" />
|
||||
<div v-if="loading" class="emptycontent">
|
||||
<div class="icon icon-loading" />
|
||||
<h2>{{ t('deck', 'Loading board') }}</h2>
|
||||
<p />
|
||||
</div>
|
||||
<div v-else-if="board" class="board">
|
||||
<Container lock-axix="y"
|
||||
orientation="horizontal"
|
||||
:drag-handle-selector="dragHandleSelector"
|
||||
@drop="onDropStack">
|
||||
<Draggable v-for="stack in stacksByBoard" :key="stack.id">
|
||||
<Stack :stack="stack" />
|
||||
</Draggable>
|
||||
</Container>
|
||||
</div>
|
||||
<div v-else class="emptycontent">
|
||||
<div class="icon icon-deck" />
|
||||
<h2>{{ t('deck', 'Board not found') }}</h2>
|
||||
<p />
|
||||
</div>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="loading" class="emptycontent" key="loading">
|
||||
<div class="icon icon-loading" />
|
||||
<h2>{{ t('deck', 'Loading board') }}</h2>
|
||||
<p />
|
||||
</div>
|
||||
<div v-else-if="board && !loading" class="board" key="board">
|
||||
<Container lock-axix="y"
|
||||
orientation="horizontal"
|
||||
:drag-handle-selector="dragHandleSelector"
|
||||
@drop="onDropStack">
|
||||
<Draggable v-for="stack in stacksByBoard" :key="stack.id">
|
||||
<Stack :stack="stack" />
|
||||
</Draggable>
|
||||
</Container>
|
||||
</div>
|
||||
<div v-else class="emptycontent" key="notfound">
|
||||
<div class="icon icon-deck" />
|
||||
<h2>{{ t('deck', 'Board not found') }}</h2>
|
||||
<p />
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -102,8 +105,12 @@ export default {
|
||||
methods: {
|
||||
async fetchData() {
|
||||
this.loading = true
|
||||
await this.$store.dispatch('loadBoardById', this.id)
|
||||
await this.$store.dispatch('loadStacks', this.id)
|
||||
try {
|
||||
await this.$store.dispatch('loadBoardById', this.id)
|
||||
await this.$store.dispatch('loadStacks', this.id)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
@@ -125,6 +132,8 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@import "../../css/animations.scss";
|
||||
|
||||
$board-spacing: 15px;
|
||||
$stack-spacing: 10px;
|
||||
$stack-width: 300px;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<template>
|
||||
<div class="stack">
|
||||
<div class="stack--header">
|
||||
<div v-click-outside="stopCardCreation" class="stack--header">
|
||||
<transition name="fade" mode="out-in">
|
||||
<h3 v-if="!canManage">
|
||||
{{ stack.title }}
|
||||
@@ -32,7 +32,7 @@
|
||||
{{ stack.title }}
|
||||
</h3>
|
||||
<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')"
|
||||
class="icon-confirm"
|
||||
type="submit"
|
||||
@@ -45,31 +45,35 @@
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
<Actions v-if="canEdit && !showArchived">
|
||||
<ActionButton icon="icon-add" @click="showAddCard=true">
|
||||
<ActionButton icon="icon-add" @click.stop="showAddCard=true">
|
||||
{{ t('deck', 'Add card') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</div>
|
||||
|
||||
<form v-if="showAddCard"
|
||||
class="stack--card-add"
|
||||
:class="{ 'icon-loading-small': stateCardCreating }"
|
||||
@submit.prevent="clickAddCard()">
|
||||
<label for="new-stack-input-main" class="hidden-visually">{{ t('deck', 'Add a new card') }}</label>
|
||||
<input id="new-stack-input-main"
|
||||
v-model="newCardTitle"
|
||||
v-focus
|
||||
type="text"
|
||||
class="no-close"
|
||||
:disabled="stateCardCreating"
|
||||
placeholder="Add a new card"
|
||||
required>
|
||||
<transition name="slide-top" appear>
|
||||
<div v-if="showAddCard" class="stack--card-add">
|
||||
<form :class="{ 'icon-loading-small': stateCardCreating }"
|
||||
@submit.prevent.stop="clickAddCard()">
|
||||
<label for="new-stack-input-main" class="hidden-visually">{{ t('deck', 'Add a new card') }}</label>
|
||||
<input id="new-stack-input-main"
|
||||
ref="newCardInput"
|
||||
v-model="newCardTitle"
|
||||
v-focus
|
||||
type="text"
|
||||
class="no-close"
|
||||
:disabled="stateCardCreating"
|
||||
placeholder="Add a new card"
|
||||
required
|
||||
@keydown.esc="stopCardCreation">
|
||||
|
||||
<input v-show="!stateCardCreating"
|
||||
class="icon-confirm"
|
||||
type="submit"
|
||||
value="">
|
||||
</form>
|
||||
<input v-show="!stateCardCreating"
|
||||
class="icon-confirm"
|
||||
type="submit"
|
||||
value="">
|
||||
</form>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<Container :get-child-payload="payloadForCard(stack.id)"
|
||||
group-name="stack"
|
||||
@@ -78,7 +82,11 @@
|
||||
@should-accept-drop="canEdit"
|
||||
@drop="($event) => onDropCard(stack.id, $event)">
|
||||
<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>
|
||||
</Container>
|
||||
</div>
|
||||
@@ -114,6 +122,7 @@ export default {
|
||||
newCardTitle: '',
|
||||
showAddCard: false,
|
||||
stateCardCreating: false,
|
||||
animate: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -133,6 +142,16 @@ export default {
|
||||
},
|
||||
|
||||
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) {
|
||||
const { addedIndex, removedIndex, payload } = event
|
||||
const card = Object.assign({}, payload)
|
||||
@@ -172,13 +191,19 @@ export default {
|
||||
async clickAddCard() {
|
||||
this.stateCardCreating = true
|
||||
try {
|
||||
await this.$store.dispatch('addCard', {
|
||||
this.animate = true
|
||||
const newCard = await this.$store.dispatch('addCard', {
|
||||
title: this.newCardTitle,
|
||||
stackId: this.stack.id,
|
||||
boardId: this.stack.boardId,
|
||||
})
|
||||
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) {
|
||||
OCP.Toast.error('Could not create card: ' + e.response.data.message)
|
||||
} finally {
|
||||
@@ -208,8 +233,8 @@ export default {
|
||||
padding: 3px;
|
||||
margin: 3px -3px;
|
||||
margin-right: -10px;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
background-color: var(--color-main-background-translucent);
|
||||
|
||||
h3, form {
|
||||
@@ -223,10 +248,25 @@ export default {
|
||||
}
|
||||
|
||||
.stack--card-add {
|
||||
position: sticky;
|
||||
top: 52px;
|
||||
height: 52px;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 0 3px var(--color-box-shadow);
|
||||
border-radius: 3px;
|
||||
background-color: var(--color-main-background);
|
||||
margin-left: -10px;
|
||||
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-dark:after {
|
||||
@@ -241,9 +281,22 @@ export default {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
.stack .smooth-dnd-container.vertical {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
|
||||
@@ -236,7 +236,7 @@ export default {
|
||||
descriptionSaveTimeout: null,
|
||||
descriptionSaving: false,
|
||||
hasActivity: capabilities && capabilities.activity,
|
||||
hasComments: !!OC.appswebroots['comments']
|
||||
hasComments: !!OC.appswebroots['comments'],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -48,16 +48,21 @@
|
||||
</form>
|
||||
|
||||
<div v-if="!editing" class="right">
|
||||
<div v-if="card.duedate" :class="dueIcon">
|
||||
<span>{{ relativeDate }}</span>
|
||||
</div>
|
||||
<transition name="zoom">
|
||||
<div v-if="card.duedate" :class="dueIcon">
|
||||
<span>{{ relativeDate }}</span>
|
||||
</div>
|
||||
</transition>
|
||||
</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)">
|
||||
<span>{{ label.title }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</transition-group>
|
||||
<div v-show="!compactMode" class="card-controls compact-item" @click="openCard">
|
||||
<CardBadges :id="id" />
|
||||
</div>
|
||||
@@ -132,6 +137,13 @@ export default {
|
||||
return moment(this.card.duedate).format('LLLL')
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentCard(newValue) {
|
||||
if (newValue) {
|
||||
this.$nextTick(() => this.$el.scrollIntoView())
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openCard() {
|
||||
this.$router.push({ name: 'card', params: { cardId: this.id } })
|
||||
@@ -154,6 +166,8 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './../../css/animations';
|
||||
|
||||
$card-spacing: 10px;
|
||||
$card-padding: 10px;
|
||||
|
||||
|
||||
25
src/css/animations.scss
Normal file
25
src/css/animations.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user