Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
115 changes: 68 additions & 47 deletions src/components/Questions/AnswerInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,43 +26,43 @@
@compositionend="onCompositionEnd" />

<!-- 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')"
<div v-if="!answer.local" class="option__actions">
<NcActions
:id="optionDragMenuId"
:container="`#${optionDragMenuId}`"
:aria-label="t('forms', 'Move option actions')"
class="option__drag-handle"
type="tertiary-no-background">
<template #icon>
<IconDragIndicator :size="20" />
</template>
<NcActionButton
ref="buttonOptionUp"
:disabled="index === 0"
size="small"
type="tertiary"
@click="onMoveUp">
<template #icon>
<IconArrowUp :size="20" />
</template>
</NcButton>
<NcButton
ref="buttonDown"
class="option__actions-button"
:aria-label="t('forms', 'Move option down')"
{{ t('forms', 'Move option up') }}
</NcActionButton>
<NcActionButton
ref="buttonOptionDown"
: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>
<IconDelete :size="20" />
</template>
</NcButton>
</template>
{{ t('forms', 'Move option down') }}
</NcActionButton>
</NcActions>
<NcButton
:aria-label="t('forms', 'Delete answer')"
type="tertiary"
@click="deleteEntry">
<template #icon>
<IconDelete :size="20" />
</template>
</NcButton>
</div>
</li>
</template>
Expand All @@ -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'

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

Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -303,27 +314,25 @@ export default {
logger.error('Error while saving answer', { answer, error })
showError(t('forms', 'Error while saving the answer'))
}
return answer
},

/**
* Reorder option but keep focus on the button
*/
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())
},

/**
Expand Down Expand Up @@ -356,31 +365,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);
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;
}
}

.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
4 changes: 2 additions & 2 deletions src/components/Questions/Question.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -397,7 +397,7 @@ export default {
&:hover,
&:focus,
&:focus-within {
opacity: 1;
color: var(--color-main-text);

.question__drag-handle-button {
position: initial;
Expand Down
89 changes: 65 additions & 24 deletions src/components/Questions/QuestionDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,45 @@
<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"
direction="vertical"
handle=".option__drag-handle"
invert-swap
tag="ul"
@change="saveOptionsOrder"
@start="isDragging = true"
@end="isDragging = false">
<TransitionGroup
:name="
isDragging
? 'no-external-transition-on-drag'
: 'options-list-transition'
">
<!-- 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, answer)"
@delete="deleteOption"
@focus-next="focusNextInput"
@move-up="onOptionMoveUp(index)"
@move-down="onOptionMoveDown(index)"
@tabbed-out="checkValidOption" />
</TransitionGroup>
</Draggable>
</template>

<!-- Add multiple options modal -->
Expand All @@ -71,6 +89,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 +102,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 Expand Up @@ -169,4 +192,22 @@ export default {
margin-inline-end: 32px !important;
}
}

.options-list-transition-move,
.options-list-transition-enter-active,
.options-list-transition-leave-active {
transition: all var(--animation-slow) ease;
}

.options-list-transition-enter-from,
.options-list-transition-leave-to {
opacity: 0;
transform: translateX(44px);
}

/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.options-list-transition-leave-active {
position: absolute;
}
</style>
Loading
Loading