diff --git a/src/components/Questions/QuestionMultiple.vue b/src/components/Questions/QuestionMultiple.vue index 69b64bc67..b6824727d 100644 --- a/src/components/Questions/QuestionMultiple.vue +++ b/src/components/Questions/QuestionMultiple.vue @@ -52,7 +52,7 @@
{{ t('forms', 'Other:') }} - +
@@ -97,9 +98,9 @@
item.startsWith(this.QUESTION_EXTRASETTINGS_OTHER_PREFIX)) - return checkedOtherAnswer[0] !== undefined - }, - }, - - watch: { - inputOtherAnswer() { - if (this.isUnique) { - this.onChange(this.valueOtherAnswer) - return - } - - const values = this.values.filter(item => !item.startsWith(this.QUESTION_EXTRASETTINGS_OTHER_PREFIX)) - if (this.inputOtherAnswer !== '') { - values.push(this.valueOtherAnswer) + /** + * Text of the "other" answer field + */ + otherAnswerText() { + if (this.otherAnswer) { + return this.otherAnswer.slice(QUESTION_EXTRASETTINGS_OTHER_PREFIX.length) } + return '' + }, - this.onChange(values) + /** + * The full "other" answer including prefix, undefined if no "other answer" + */ + otherAnswer() { + return this.values.find((v) => v.startsWith(QUESTION_EXTRASETTINGS_OTHER_PREFIX)) }, }, @@ -228,6 +221,20 @@ export default { this.$emit('update:values', this.isUnique ? [value] : value) }, + /** + * Called when the value of the "other" anwer is changed input + * @param {string} value the new text of the "other" answer + */ + onChangeOther(value) { + // Prefix the value + const prefixedValue = `${QUESTION_EXTRASETTINGS_OTHER_PREFIX}${value}` + // emit the values and add the "other" answer + this.$emit( + 'update:values', + this.isUnique ? [prefixedValue] : [...this.values.filter((v) => !v.startsWith(QUESTION_EXTRASETTINGS_OTHER_PREFIX)), prefixedValue], + ) + }, + /** * Is the provided answer required ? * This is needed for checkboxes as html5 @@ -280,6 +287,7 @@ export default { /** * Update the options + * This will handle updating the form (emitting the changes) and update last changed property * * @param {Array} options options to change */ @@ -304,22 +312,24 @@ export default { /** * Add a new empty answer locally + * @param {InputEvent} event The input event that triggered adding a new entry */ - addNewEntry() { + addNewEntry({ target }) { // Add local entry const options = this.options.slice() options.push({ id: GenRandomId(), questionId: this.id, - text: this.inputValue, + text: target?.value ?? '', local: true, }) - this.inputValue = '' - - // Update question + // Update questions this.updateOptions(options) + // Reset the "new answer" input + this.$refs.pseudoInput.value = '' + this.$nextTick(() => { this.focusIndex(options.length - 1) @@ -412,14 +422,9 @@ export default { * * @param {boolean} allowOtherAnswer show/hide field for other answer */ - onAllowOtherAnswerChange(allowOtherAnswer) { + onAllowOtherAnswerChange(allowOtherAnswer) { return this.onExtraSettingsChange('allowOtherAnswer', allowOtherAnswer) }, - - valueToInputOtherAnswer() { - const otherAnswer = this.values.filter(item => item.startsWith(this.QUESTION_EXTRASETTINGS_OTHER_PREFIX)) - return otherAnswer[0] !== undefined ? otherAnswer[0].substring(this.QUESTION_EXTRASETTINGS_OTHER_PREFIX.length) : '' - }, }, } diff --git a/src/views/Submit.vue b/src/views/Submit.vue index 504b4f033..1e6707e52 100644 --- a/src/views/Submit.vue +++ b/src/views/Submit.vue @@ -95,11 +95,11 @@ :answer-type="answerTypes[question.type]" :index="index + 1" :max-string-lengths="maxStringLengths" + :values="answers[question.id]" v-bind="question" - :values.sync="answers[question.id]" @keydown.enter="onKeydownEnter" @keydown.ctrl.enter="onKeydownCtrlEnter" - @update:values="addFormFieldToLocalStorage(question)" /> + @update:values="(values) => onUpdate(question, values)" /> { // All questions must have a valid title @@ -279,7 +272,7 @@ export default { this.resetData() // Fetch full form on change this.fetchFullForm(this.form.id) - this.initFromLocalHost() + this.initFromLocalStorage() SetWindowTitle(this.formTitle) }, }, @@ -300,15 +293,30 @@ export default { } SetWindowTitle(this.formTitle) if (this.isLoggedIn) { - this.initFromLocalHost() + this.initFromLocalStorage() } }, methods: { - initFromLocalHost() { - if (localStorage.getItem(`nextcloud_forms_${this.publicView ? this.shareHash : this.hash}`)) { - for (const key in this.formValuesForLocalStorage) { - const answer = this.formValuesForLocalStorage[key] + /** + * Load saved values for current form from LocalStorage + * @return {Record} + */ + getFormValuesFromLocalStorage() { + const fromLocalStorage = localStorage.getItem(`nextcloud_forms_${this.publicView ? this.shareHash : this.hash}`) + if (fromLocalStorage) { + return JSON.parse(fromLocalStorage) + } + return null + }, + + /** + * Initialize answers from saved state in LocalStorage + */ + initFromLocalStorage() { + const savedState = this.getFormValuesFromLocalStorage() + if (savedState) { + for (const [key, answer] of Object.entries(savedState)) { const answers = [] switch (answer?.type) { case 'QuestionMultiple': @@ -324,6 +332,44 @@ export default { } } }, + + /** + * Save updated answers for question to LocalStorage in case of browser crash / closes / etc + * @param {*} question Question to update + */ + addFormFieldToLocalStorage(question) { + if (!this.isLoggedIn) { + return + } + // We make sure the values are updated by the `values.sync` handler + const state = { + ...(this.getFormValuesFromLocalStorage() ?? {}), + [`${question.id}`]: { + value: this.answers[question.id], + type: answerTypes[question.type].component.name, + }, + } + const stringified = JSON.stringify(state) + localStorage.setItem(`nextcloud_forms_${this.publicView ? this.shareHash : this.hash}`, stringified) + }, + + deleteFormFieldFromLocalStorage() { + if (!this.isLoggedIn) { + return + } + localStorage.removeItem(`nextcloud_forms_${this.publicView ? this.shareHash : this.hash}`) + }, + + /** + * Update answers of a give value + * @param {{id: number}} question The question to answer + * @param {unknown[]} values The new values + */ + onUpdate(question, values) { + this.answers = { ...this.answers, [question.id]: values } + this.addFormFieldToLocalStorage(question) + }, + /** * On Enter, focus next form-element * Last form element is the submit button, the form submits on enter then @@ -346,20 +392,6 @@ export default { // Using button-click event to not bypass validity-checks and use our specified behaviour this.$refs.submitButton.click() }, - addFormFieldToLocalStorage(question) { - if (!this.isLoggedIn) { - return - } - this.formValuesForLocalStorage[`${question.id}`] = { value: this.answers[question.id], type: answerTypes[question.type].component.name } - const parsed = JSON.stringify(this.formValuesForLocalStorage) - localStorage.setItem(`nextcloud_forms_${this.publicView ? this.shareHash : this.hash}`, parsed) - }, - deleteFormFieldFromLocalStorage() { - if (!this.isLoggedIn) { - return - } - localStorage.removeItem(`nextcloud_forms_${this.publicView ? this.shareHash : this.hash}`) - }, /* * Methods for catching unwanted unload events