Skip to content

Commit

Permalink
feat(frontend): Implement drag-and-drop reordering for dropdown options
Browse files Browse the repository at this point in the history
Signed-off-by: Christian Hartmann <[email protected]>
  • Loading branch information
Chartman123 committed Feb 23, 2025
1 parent e9ebee5 commit 1307729
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 119 deletions.
93 changes: 64 additions & 29 deletions src/components/Questions/AnswerInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,38 @@
<!-- Actions for reordering and deleting the option -->
<div class="option__actions">
<template v-if="!answer.local">
<NcButton
ref="buttonUp"
class="option__actions-button"
:aria-label="t('forms', 'Move option up')"
:disabled="index === 0"
size="small"
type="tertiary"
@click="onMoveUp">
<NcActions
:id="optionDragMenuId"
:container="`#${optionDragMenuId}`"
:aria-label="t('forms', 'Move option actions')"
class="option__drag-handle"
type="tertiary-no-background">
<template #icon>
<IconArrowUp :size="20" />
<IconDragIndicator :size="20" />
</template>
</NcButton>
<NcButton
ref="buttonDown"
<NcActionButton
ref="buttonUp"
class="option__actions-button"
:aria-label="t('forms', 'Move option down')"
:disabled="index === maxIndex"
:disabled="index === 0"
size="small"
type="tertiary"
@click="onMoveDown">
<template #icon>
<IconArrowDown :size="20" />
</template>
</NcButton>
@click="onMoveUp">
<template #icon>
<IconArrowUp :size="20" />
</template>
{{ t('forms', 'Move option up') }}
</NcActionButton>
<NcActionButton
ref="buttonDown"
class="option__actions-button"
:disabled="index === maxIndex"
size="small"
@click="onMoveDown">
<template #icon>
<IconArrowDown :size="20" />
</template>
{{ t('forms', 'Move option down') }}
</NcActionButton>
</NcActions>
<NcButton
class="option__actions-button"
:aria-label="t('forms', 'Delete answer')"
Expand All @@ -74,13 +82,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'

Expand All @@ -92,7 +104,10 @@ export default {
IconArrowUp,
IconCheckboxBlankOutline,
IconDelete,
IconDragIndicator,
IconRadioboxBlank,
NcActions,
NcActionButton,
NcButton,
},

Expand Down Expand Up @@ -145,6 +160,10 @@ export default {
})
},

optionDragMenuId() {
return 'q' + this.answer.questionId + 'o' + this.index + '__drag_menu'
},

placeholder() {
if (this.answer.local) {
return t('forms', 'Add a new answer option')
Expand All @@ -171,13 +190,6 @@ export default {
this.$emit('tabbed-out')
},

/**
* Focus the input
*/
focus() {
this.$refs.input?.focus()
},

/**
* Option changed, processing the data
*
Expand Down Expand Up @@ -374,6 +386,29 @@ export default {
&:last-of-type {
margin-inline: 5px;
}
position: relative;

.option__drag-handle {
position: absolute;
right: calc(4px + var(--default-clickable-area));
top: 4px;
opacity: 0.5;
cursor: grab;

&:hover,
&:focus,
&:focus-within {
opacity: 1;
}

&:active {
cursor: grabbing;
}

> * {
cursor: grab;
}
}
}

.question__input {
Expand Down
64 changes: 41 additions & 23 deletions src/components/Questions/QuestionDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,40 @@
<div v-if="isLoading">
<NcLoadingIcon :size="64" />
</div>
<TransitionList v-else class="question__content">
<!-- Answer text input edit -->
<AnswerInput
v-for="(answer, index) in sortedOptions"
:key="answer.local ? 'option-local' : answer.id"
ref="input"
:answer="answer"
:form-id="formId"
is-dropdown
<Draggable
v-else
v-model="sortedOptions"
animation="200"
tag="ul"
handle=".option__drag-handle"
@change="saveOptionsOrder"
@start="isDragging = true"
@end="isDragging = false">
<transition-group
:name="
isDragging ? 'no-external-transition-on-drag' : 'option-list'
">
<!-- Answer text input edit -->
<AnswerInput
v-for="(answer, index) in sortedOptions"
:key="answer.local ? 'option-local' : answer.id"
ref="input"
:answer="answer"
:form-id="formId"
is-dropdown
:index="index"
:is-unique="!isMultiple"
:max-index="options.length - 1"
:max-option-length="maxStringLengths.optionText"
@create-answer="onCreateAnswer"
@update:answer="updateAnswer(index, $event)"
@delete="deleteOption"
@focus-next="focusNextInput"
@move-up="onOptionMoveUp(index)"
@move-down="onOptionMoveDown(index)"
@tabbed-out="checkValidOption" />
</TransitionList>
:is-unique="!isMultiple"
:max-index="options.length - 1"
:max-option-length="maxStringLengths.optionText"
@create-answer="onCreateAnswer"
@update:answer="updateAnswer(index, $event)"
@delete="deleteOption"
@focus-next="focusNextInput"
@move-up="onOptionMoveUp(index)"
@move-down="onOptionMoveDown(index)"
@tabbed-out="checkValidOption" />
</transition-group>
</Draggable>
</template>

<!-- Add multiple options modal -->
Expand All @@ -71,6 +84,8 @@
</template>

<script>
import Draggable from 'vuedraggable'

import NcActionCheckbox from '@nextcloud/vue/components/NcActionCheckbox'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
Expand All @@ -82,26 +97,29 @@ import AnswerInput from './AnswerInput.vue'
import OptionInputDialog from '../OptionInputDialog.vue'
import QuestionMixin from '../../mixins/QuestionMixin.js'
import QuestionMultipleMixin from '../../mixins/QuestionMultipleMixin.ts'
import TransitionList from '../TransitionList.vue'

export default {
name: 'QuestionDropdown',

components: {
AnswerInput,
Draggable,
IconContentPaste,
NcActionButton,
NcActionCheckbox,
NcLoadingIcon,
NcSelect,
OptionInputDialog,
TransitionList,
},

mixins: [QuestionMixin, QuestionMultipleMixin],

data() {
return { inputValue: '', isOptionDialogShown: false, isLoading: false }
return {
isDragging: false,
isLoading: false,
isOptionDialogShown: false,
}
},

computed: {
Expand Down
97 changes: 55 additions & 42 deletions src/components/Questions/QuestionMultiple.vue
Original file line number Diff line number Diff line change
Expand Up @@ -112,45 +112,57 @@
</template>

<template v-else>
<template v-if="isLoading">
<div>
<NcLoadingIcon :size="64" />
</div>
</template>
<TransitionList v-else class="question__content">
<!-- Answer text input edit -->
<AnswerInput
v-for="(answer, index) in sortedOptions"
:key="answer.local ? 'option-local' : answer.id"
ref="input"
:answer="answer"
:form-id="formId"
:index="index"
:is-unique="isUnique"
:max-index="options.length - 1"
:max-option-length="maxStringLengths.optionText"
@create-answer="onCreateAnswer"
@update:answer="updateAnswer"
@delete="deleteOption"
@focus-next="focusNextInput"
@move-up="onOptionMoveUp(index)"
@move-down="onOptionMoveDown(index)"
@tabbed-out="checkValidOption" />
<li
v-if="allowOtherAnswer"
key="option-add-other"
class="question__item">
<div :is="pseudoIcon" class="question__item__pseudoInput" />
<input
:placeholder="t('forms', 'Other')"
class="question__input"
:disabled="!readonly"
:maxlength="maxStringLengths.optionText"
minlength="1"
type="text"
:readonly="!readOnly" />
</li>
</TransitionList>
<div v-if="isLoading">
<NcLoadingIcon :size="64" />
</div>
<Draggable
v-else
v-model="sortedOptions"
class="question__content"
animation="200"
tag="ul"
handle=".option__drag-handle"
@change="saveOptionsOrder"
@start="isDragging = true"
@end="isDragging = false">
<transition-group
:name="
isDragging ? 'no-external-transition-on-drag' : 'option-list'
">
<!-- Answer text input edit -->
<AnswerInput
v-for="(answer, index) in sortedOptions"
:key="answer.local ? 'option-local' : answer.id"
ref="input"
:answer="answer"
:form-id="formId"
:index="index"
:is-unique="isUnique"
:max-index="options.length - 1"
:max-option-length="maxStringLengths.optionText"
@create-answer="onCreateAnswer"
@update:answer="updateAnswer(index, $event)"
@delete="deleteOption"
@focus-next="focusNextInput"
@move-up="onOptionMoveUp(index)"
@move-down="onOptionMoveDown(index)"
@tabbed-out="checkValidOption" />
</transition-group>
</Draggable>
<li
v-if="allowOtherAnswer"
key="option-add-other"
class="question__item">
<div :is="pseudoIcon" class="question__item__pseudoInput" />
<input
:placeholder="t('forms', 'Other')"
class="question__input"
:disabled="!readonly"
:maxlength="maxStringLengths.optionText"
minlength="1"
type="text"
:readonly="!readOnly" />
</li>
</template>

<!-- Add multiple options modal -->
Expand All @@ -163,6 +175,7 @@
<script>
import { showError } from '@nextcloud/dialogs'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import Draggable from 'vuedraggable'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcActionCheckbox from '@nextcloud/vue/components/NcActionCheckbox'
import NcActionInput from '@nextcloud/vue/components/NcActionInput'
Expand All @@ -179,7 +192,6 @@ import AnswerInput from './AnswerInput.vue'
import QuestionMixin from '../../mixins/QuestionMixin.js'
import OptionInputDialog from '../OptionInputDialog.vue'
import QuestionMultipleMixin from '../../mixins/QuestionMultipleMixin.ts'
import TransitionList from '../TransitionList.vue'

const QUESTION_EXTRASETTINGS_OTHER_PREFIX = 'system-other-answer:'

Expand All @@ -188,6 +200,7 @@ export default {

components: {
AnswerInput,
Draggable,
IconCheckboxBlankOutline,
IconContentPaste,
IconRadioboxBlank,
Expand All @@ -199,7 +212,6 @@ export default {
NcLoadingIcon,
NcNoteCard,
OptionInputDialog,
TransitionList,
},

mixins: [QuestionMixin, QuestionMultipleMixin],
Expand All @@ -217,6 +229,7 @@ export default {
cachedOtherAnswerText: '',
QUESTION_EXTRASETTINGS_OTHER_PREFIX,

isDragging: false,
isOptionDialogShown: false,
isLoading: false,
}
Expand All @@ -232,7 +245,7 @@ export default {
},

shiftDragHandle() {
return !this.readonly && this.options.length !== 0
return !this.readonly && this.options.length !== 0 && !this.isLastEmpty
},

pseudoIcon() {
Expand Down
Loading

0 comments on commit 1307729

Please sign in to comment.