feat: simplify heading blur logic (#727)

Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#727
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
This commit is contained in:
dpschen 2021-09-10 12:57:59 +00:00 committed by konrad
parent 0376ef53e3
commit dae441a373
3 changed files with 29 additions and 46 deletions

View File

@ -7,16 +7,17 @@
<h1 <h1
class="title input" class="title input"
:class="{'disabled': !canWrite}" :class="{'disabled': !canWrite}"
@focusout="save()" @blur="save($event.target.textContent)"
@keydown.enter.prevent.stop="save()" @keydown.enter.prevent.stop="$event.target.blur()"
:contenteditable="canWrite ? 'true' : 'false'" :contenteditable="canWrite ? 'true' : 'false'"
spellcheck="false"
ref="taskTitle">{{ task.title.trim() }}</h1> ref="taskTitle">{{ task.title.trim() }}</h1>
<transition name="fade"> <transition name="fade">
<span class="is-inline-flex is-align-items-center" v-if="loading && saving"> <span class="is-inline-flex is-align-items-center" v-if="loading && saving">
<span class="loader is-inline-block mr-2"></span> <span class="loader is-inline-block mr-2"></span>
{{ $t('misc.saving') }} {{ $t('misc.saving') }}
</span> </span>
<span class="has-text-success is-inline-flex is-align-content-center" v-if="!loading && saved"> <span class="has-text-success is-inline-flex is-align-content-center" v-if="!loading && showSavedMessage">
<icon icon="check" class="mr-2"/> <icon icon="check" class="mr-2"/>
{{ $t('misc.saved') }} {{ $t('misc.saved') }}
</span> </span>
@ -25,22 +26,22 @@
</template> </template>
<script> <script>
import {LOADING} from '@/store/mutation-types'
import {mapState} from 'vuex' import {mapState} from 'vuex'
export default { export default {
name: 'heading', name: 'heading',
data() { data() {
return { return {
task: {title: '', identifier: '', index: ''}, showSavedMessage: false,
taskTitle: '',
saved: false,
saving: false, // Since loading is global state, this variable ensures we're only showing the saving icon when saving the description. saving: false, // Since loading is global state, this variable ensures we're only showing the saving icon when saving the description.
} }
}, },
computed: mapState({ computed: {
loading: LOADING, ...mapState(['loading']),
}), task() {
return this.value
},
},
props: { props: {
value: { value: {
required: true, required: true,
@ -50,50 +51,29 @@ export default {
default: false, default: false,
}, },
}, },
watch: {
value(newVal) {
this.task = newVal
this.taskTitle = this.task.title
},
},
mounted() {
this.task = this.value
this.taskTitle = this.task.title
},
methods: { methods: {
save() { save(title) {
this.$refs.taskTitle.spellcheck = false // We only want to save if the title was actually changed.
// Because the contenteditable does not have a change event
// Pull the task title from the contenteditable // we're building it ourselves and only continue
let taskTitle = this.$refs.taskTitle.textContent
this.task.title = taskTitle
// We only want to save if the title was actually change.
// Because the contenteditable does not have a change event,
// we're building it ourselves and only calling saveTask()
// if the task title changed. // if the task title changed.
if (this.task.title !== this.taskTitle) { if (title === this.task.title) {
this.$refs.taskTitle.blur()
this.saveTask()
this.taskTitle = taskTitle
}
},
saveTask() {
// When only saving with enter, the focusout event is called as well. This then leads to the saveTask
// method being called twice, overriding some task attributes in the second run.
// If we simply check if we're already in the process of saving, we can prevent that.
if (this.saving) {
return return
} }
this.saving = true this.saving = true
this.$store.dispatch('tasks/update', this.task) const newTask = {
.then(() => { ...this.task,
this.$emit('input', this.task) title,
this.saved = true }
this.$store.dispatch('tasks/update', newTask)
.then((task) => {
this.$emit('input', task)
this.showSavedMessage = true
setTimeout(() => { setTimeout(() => {
this.saved = false this.showSavedMessage = false
}, 2000) }, 2000)
}) })
.catch(e => { .catch(e => {

View File

@ -15,6 +15,7 @@ export default class TaskModel extends AbstractModel {
super(data) super(data)
this.id = Number(this.id) this.id = Number(this.id)
this.title = this.title?.trim()
this.listId = Number(this.listId) this.listId = Number(this.listId)
// Make date objects from timestamps // Make date objects from timestamps

View File

@ -39,6 +39,8 @@ export default class TaskService extends AbstractService {
processModel(model) { processModel(model) {
model.title = model.title?.trim()
// Ensure that listId is an int // Ensure that listId is an int
model.listId = Number(model.listId) model.listId = Number(model.listId)