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 18f6b9c
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 133 deletions.
102 changes: 60 additions & 42 deletions src/components/Questions/AnswerInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,36 @@
<!-- 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>
<NcActionButton
ref="buttonUp"
:disabled="index === 0"
@click="onMoveUp">
<template #icon>
<IconArrowUp :size="20" />
</template>
{{ t('forms', 'Move option up') }}
</NcActionButton>
<NcActionButton
ref="buttonDown"
:disabled="index === maxIndex"
@click="onMoveDown">
<template #icon>
<IconArrowDown :size="20" />
</template>
{{ t('forms', 'Move option down') }}
</NcActionButton>
</NcActions>
<NcButton
ref="buttonDown"
class="option__actions-button"
:aria-label="t('forms', 'Move option down')"
:disabled="index === maxIndex"
size="small"
type="tertiary"
@click="onMoveDown">
<template #icon>
<IconArrowDown :size="20" />
</template>
</NcButton>
<NcButton
class="option__actions-button"
:aria-label="t('forms', 'Delete answer')"
size="small"
type="tertiary"
@click="deleteEntry">
<template #icon>
Expand All @@ -74,13 +76,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 +98,10 @@ export default {
IconArrowUp,
IconCheckboxBlankOutline,
IconDelete,
IconDragIndicator,
IconRadioboxBlank,
NcActions,
NcActionButton,
NcButton,
},

Expand Down Expand Up @@ -145,6 +154,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 +184,6 @@ export default {
this.$emit('tabbed-out')
},

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

/**
* Option changed, processing the data
*
Expand Down Expand Up @@ -356,31 +362,43 @@ export default {

&__pseudoInput {
color: var(--color-primary-element);
margin-inline-start: calc(-1 * var(--default-grid-baseline));
margin-inline-start: -2px;
z-index: 1;
}

.option__actions {
display: flex;
position: absolute;
gap: var(--default-grid-baseline);
inset-inline-end: 16px;
height: var(--default-clickable-area);
}

.option__actions-button {
margin-block: auto;
inset-inline-end: 12px;
height: 100%;

.option__drag-handle {
top: 4px;
opacity: 0.5;
cursor: grab;

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

&:last-of-type {
margin-inline: 5px;
&:active {
cursor: grabbing;
}

> * {
cursor: grab;
}
}
}

.question__input {
width: calc(100% - var(--default-clickable-area));
position: relative;
inset-inline-start: -12px;
margin-block: 0 !important;
margin-inline-end: -12px !important;

&--shifted {
Expand Down
67 changes: 43 additions & 24 deletions src/components/Questions/QuestionDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,41 @@
<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
: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>
<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"
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" />
</transition-group>
</Draggable>
</template>

<!-- Add multiple options modal -->
Expand All @@ -71,6 +85,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 +98,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
Loading

0 comments on commit 18f6b9c

Please sign in to comment.