From 046cb872b1b779099286634b8aaabc264466d8ed Mon Sep 17 00:00:00 2001 From: Christian Hartmann Date: Mon, 17 Feb 2025 23:37:01 +0100 Subject: [PATCH] feat(frontend): Implement drag-and-drop reordering for dropdown options Signed-off-by: Christian Hartmann --- CHANGELOG.en.md | 4 + src/components/Questions/AnswerInput.vue | 115 ++++++++++------- src/components/Questions/Question.vue | 4 +- src/components/Questions/QuestionDropdown.vue | 89 +++++++++---- src/components/Questions/QuestionMultiple.vue | 119 +++++++++++------- src/components/TransitionList.vue | 29 ----- src/mixins/QuestionMultipleMixin.ts | 62 +++++---- src/models/Entities.d.ts | 1 + 8 files changed, 254 insertions(+), 169 deletions(-) delete mode 100644 src/components/TransitionList.vue diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 8177c41c6..b89c8482c 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -7,6 +7,10 @@ ## v5.0.0 - tbd +- **Re-order options** + + Options of multiple choice questions (radio/checkbox/dropdown) can now be re-ordered using a menu or drag'n'drop. + - **Unified Search integration** You can now use the Unified Search to search forms based on the title and the description. diff --git a/src/components/Questions/AnswerInput.vue b/src/components/Questions/AnswerInput.vue index c355b59be..174a8245a 100644 --- a/src/components/Questions/AnswerInput.vue +++ b/src/components/Questions/AnswerInput.vue @@ -26,43 +26,43 @@ @compositionend="onCompositionEnd" /> -
- + {{ t('forms', 'Move option down') }} + + + + +
@@ -74,13 +74,17 @@ import axios from '@nextcloud/axios' import debounce from 'debounce' import PQueue from 'p-queue' -import NcButton from '@nextcloud/vue/components/NcButton' import IconArrowDown from 'vue-material-design-icons/ArrowDown.vue' import IconArrowUp from 'vue-material-design-icons/ArrowUp.vue' -import IconDelete from 'vue-material-design-icons/Delete.vue' import IconCheckboxBlankOutline from 'vue-material-design-icons/CheckboxBlankOutline.vue' +import IconDelete from 'vue-material-design-icons/Delete.vue' +import IconDragIndicator from '../Icons/IconDragIndicator.vue' import IconRadioboxBlank from 'vue-material-design-icons/RadioboxBlank.vue' +import NcActions from '@nextcloud/vue/components/NcActions' +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcButton from '@nextcloud/vue/components/NcButton' + import OcsResponse2Data from '../../utils/OcsResponse2Data.js' import logger from '../../utils/Logger.js' @@ -92,7 +96,10 @@ export default { IconArrowUp, IconCheckboxBlankOutline, IconDelete, + IconDragIndicator, IconRadioboxBlank, + NcActions, + NcActionButton, NcButton, }, @@ -145,6 +152,10 @@ export default { }) }, + optionDragMenuId() { + return `q${this.answer.questionId}o${this.answer.id}__drag_menu` + }, + placeholder() { if (this.answer.local) { return t('forms', 'Add a new answer option') @@ -303,7 +314,6 @@ export default { logger.error('Error while saving answer', { answer, error }) showError(t('forms', 'Error while saving the answer')) } - return answer }, /** @@ -311,19 +321,18 @@ export default { */ onMoveDown() { this.$emit('move-down') - if (this.index < this.maxIndex - 1) { - this.$nextTick(() => this.$refs.buttonDown.$el.focus()) - } else { - this.$nextTick(() => this.$refs.buttonUp.$el.focus()) - } + this.focusButton( + this.index < this.maxIndex - 1 + ? 'buttonOptionDown' + : 'buttonOptionUp', + ) }, onMoveUp() { this.$emit('move-up') - if (this.index > 1) { - this.$nextTick(() => this.$refs.buttonUp.$el.focus()) - } else { - this.$nextTick(() => this.$refs.buttonDown.$el.focus()) - } + this.focusButton(this.index > 1 ? 'buttonOptionUp' : 'buttonOptionDown') + }, + focusButton(refName) { + this.$nextTick(() => this.$refs[refName].$el.focus()) }, /** @@ -356,7 +365,7 @@ export default { &__pseudoInput { color: var(--color-primary-element); - margin-inline-start: calc(-1 * var(--default-grid-baseline)); + margin-inline-start: -2px; z-index: 1; } @@ -364,15 +373,28 @@ export default { display: flex; position: absolute; gap: var(--default-grid-baseline); - inset-inline-end: 16px; - height: var(--default-clickable-area); + inset-inline-end: 12px; + height: 100%; } - .option__actions-button { + .option__drag-handle, + .drag-indicator-icon { + color: var(--color-text-maxcontrast); + cursor: grab; margin-block: auto; - &:last-of-type { - margin-inline: 5px; + &:hover, + &:focus, + &:focus-within { + color: var(--color-main-text); + } + + &:active { + cursor: grabbing; + } + + > * { + cursor: grab; } } @@ -380,7 +402,6 @@ export default { width: calc(100% - var(--default-clickable-area)); position: relative; inset-inline-start: -12px; - margin-block: 0 !important; margin-inline-end: -12px !important; &--shifted { diff --git a/src/components/Questions/Question.vue b/src/components/Questions/Question.vue index 2daa0beec..d4776cea4 100644 --- a/src/components/Questions/Question.vue +++ b/src/components/Questions/Question.vue @@ -381,7 +381,7 @@ export default { gap: 12px; width: var(--default-clickable-area); height: 100%; - opacity: 0.5; + color: var(--color-text-maxcontrast); cursor: grab; &-button { @@ -397,7 +397,7 @@ export default { &:hover, &:focus, &:focus-within { - opacity: 1; + color: var(--color-main-text); .question__drag-handle-button { position: initial; diff --git a/src/components/Questions/QuestionDropdown.vue b/src/components/Questions/QuestionDropdown.vue index fd36b4f26..1e117dd01 100644 --- a/src/components/Questions/QuestionDropdown.vue +++ b/src/components/Questions/QuestionDropdown.vue @@ -40,27 +40,45 @@
- - - - + + + + + + @@ -71,6 +89,8 @@