Skip to content

Commit 7db54c5

Browse files
committed
feat: add linear scale answer type to frontend
Signed-off-by: Christian Hartmann <chris-hartmann@gmx.de>
1 parent c97d8e2 commit 7db54c5

5 files changed

Lines changed: 248 additions & 6 deletions

File tree

lib/ResponseDefinitions.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
* optionsLabelWorst?: string,
3131
* optionsLimitMax?: int,
3232
* optionsLimitMin?: int,
33-
* optionsStart?: int
33+
* optionsStart?: int,
3434
* shuffleOptions?: bool,
3535
* validationRegex?: string,
3636
* validationType?: string

openapi.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,16 @@
426426
"type": "integer",
427427
"format": "int64"
428428
},
429+
"optionsEnd": {
430+
"type": "integer",
431+
"format": "int64"
432+
},
433+
"optionsLabelBest": {
434+
"type": "string"
435+
},
436+
"optionsLabelWorst": {
437+
"type": "string"
438+
},
429439
"optionsLimitMax": {
430440
"type": "integer",
431441
"format": "int64"
@@ -434,6 +444,10 @@
434444
"type": "integer",
435445
"format": "int64"
436446
},
447+
"optionsStart": {
448+
"type": "integer",
449+
"format": "int64"
450+
},
437451
"shuffleOptions": {
438452
"type": "boolean"
439453
},
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Christian Hartmann <chris-hartmann@gmx.de>
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<span
8+
:aria-hidden="!title"
9+
:aria-label="title"
10+
class="material-design-icon drag-indicator-icon"
11+
role="img"
12+
v-bind="$attrs"
13+
@click="$emit('click', $event)">
14+
<svg
15+
:fill="fillColor"
16+
class="material-design-icon__svg"
17+
:height="size"
18+
:width="size"
19+
viewBox="0 -960 960 960">
20+
<path
21+
d="M672-288q-71 0-123.38-44.36Q496.24-376.73 482.9-444H281q-11 26-35 43t-54 17q-40.32 0-68.16-27.77Q96-439.55 96-479.77 96-520 123.84-548q27.84-28 68.16-28 30 0 54 17t35.46 43H484q13.21-67.28 65.1-111.64Q601-672 672-672q79.68 0 135.84 56.23 56.16 56.22 56.16 136Q864-400 807.84-344 751.68-288 672-288Zm0-72q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Z" />
22+
<title v-if="title">{{ title }}</title>
23+
</svg>
24+
</span>
25+
</template>
26+
27+
<script>
28+
export default {
29+
name: 'IconLinearScale',
30+
props: {
31+
title: {
32+
type: String,
33+
default: '',
34+
},
35+
fillColor: {
36+
type: String,
37+
default: 'currentColor',
38+
},
39+
size: {
40+
type: Number,
41+
default: 20,
42+
},
43+
},
44+
}
45+
</script>
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<Question
8+
v-bind="questionProps"
9+
:title-placeholder="answerType.titlePlaceholder"
10+
:warning-invalid="answerType.warningInvalid"
11+
v-on="commonListeners">
12+
<template #actions>
13+
<NcActionInput
14+
v-model="lowestOption"
15+
type="multiselect"
16+
:clearable="false"
17+
:label="t('forms', 'Lowest value')"
18+
label-outside
19+
:options="[0, 1]"
20+
required>
21+
<template #icon>
22+
<IconPencil :size="20" />
23+
</template>
24+
</NcActionInput>
25+
<NcActionInput
26+
v-model="highestOption"
27+
type="multiselect"
28+
:clearable="false"
29+
:label="t('forms', 'Highest value')"
30+
label-outside
31+
:options="[2, 3, 4, 5, 6, 7, 8, 9, 10]"
32+
required>
33+
<template #icon>
34+
<IconPencil :size="20" />
35+
</template>
36+
</NcActionInput>
37+
<NcActionInput
38+
v-model="labelLowest"
39+
:label="t('forms', 'Label for lowest value')"
40+
label-outside
41+
:show-trailing-button="false">
42+
<template #icon>
43+
<IconPencil :size="20" />
44+
</template>
45+
{{ t('forms', 'Label for lowest value') }}
46+
</NcActionInput>
47+
<NcActionInput
48+
v-model="labelHighest"
49+
:label="t('forms', 'Label for highest value')"
50+
label-outside
51+
:show-trailing-button="false">
52+
<template #icon>
53+
<IconPencil :size="20" />
54+
</template>
55+
{{ t('forms', 'Label for highest value') }}
56+
</NcActionInput>
57+
</template>
58+
59+
<div class="question__content">
60+
<template v-if="labelLowest !== ''">{{ labelLowest }}</template>
61+
<NcCheckboxRadioSwitch
62+
v-for="option in scaleOptions"
63+
:key="option"
64+
class="question__content"
65+
:disabled="!readOnly"
66+
:checked="questionValues"
67+
:value="option.toString()"
68+
:name="`${id}-answer`"
69+
button-variant
70+
button-variant-grouped="horizontal"
71+
type="radio"
72+
:required="checkRequired(option)"
73+
@update:checked="onChange"
74+
@keydown.enter.exact.prevent="onKeydownEnter">
75+
{{ option }}
76+
</NcCheckboxRadioSwitch>
77+
<template v-if="labelHighest !== ''">{{ labelHighest }}</template>
78+
</div>
79+
</Question>
80+
</template>
81+
82+
<script>
83+
import NcActionInput from '@nextcloud/vue/components/NcActionInput'
84+
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
85+
86+
import QuestionMixin from '../../mixins/QuestionMixin.js'
87+
88+
import IconPencil from 'vue-material-design-icons/Pencil.vue'
89+
90+
export default {
91+
name: 'QuestionLinearScale',
92+
93+
components: {
94+
IconPencil,
95+
NcActionInput,
96+
NcCheckboxRadioSwitch,
97+
},
98+
99+
mixins: [QuestionMixin],
100+
101+
data() {
102+
return {
103+
isLoading: false,
104+
105+
lowestOption: 1,
106+
highestOption: 5,
107+
labelLowest: 'Lowest',
108+
labelHighest: 'Highest',
109+
}
110+
},
111+
112+
computed: {
113+
scaleOptions() {
114+
return Array.from(
115+
{ length: this.highestOption - this.lowestOption + 1 },
116+
(_, i) => i + this.lowestOption,
117+
)
118+
},
119+
120+
isUnique() {
121+
return this.answerType.unique === true
122+
},
123+
124+
questionValues() {
125+
return this.isUnique ? this.values?.[0] : this.values
126+
},
127+
},
128+
129+
methods: {
130+
onChange(value) {
131+
this.$emit('update:values', this.isUnique ? [value].flat() : value)
132+
},
133+
134+
/**
135+
* Is the provided answer required ?
136+
* This is needed for checkboxes as html5
137+
* doesn't allow to require at least ONE checked.
138+
* So we require the one that are checked or all
139+
* if none are checked yet.
140+
*
141+
* @return {boolean}
142+
*/
143+
checkRequired() {
144+
// false, if question not required
145+
if (!this.isRequired) {
146+
return false
147+
}
148+
149+
// true for Radiobuttons
150+
if (this.isUnique) {
151+
return true
152+
}
153+
154+
// For checkboxes, only required if no other is checked
155+
return false
156+
},
157+
},
158+
}
159+
</script>
160+
161+
<style lang="scss" scoped>
162+
.question__content {
163+
all: unset;
164+
display: flex;
165+
flex-direction: row;
166+
justify-content: center;
167+
width: 100%;
168+
align-items: center;
169+
}
170+
</style>

src/models/AnswerTypes.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@
66
import QuestionDate from '../components/Questions/QuestionDate.vue'
77
import QuestionDropdown from '../components/Questions/QuestionDropdown.vue'
88
import QuestionFile from '../components/Questions/QuestionFile.vue'
9+
import QuestionLinearScale from '../components/Questions/QuestionLinearScale.vue'
910
import QuestionLong from '../components/Questions/QuestionLong.vue'
1011
import QuestionMultiple from '../components/Questions/QuestionMultiple.vue'
1112
import QuestionShort from '../components/Questions/QuestionShort.vue'
1213

13-
import IconCheckboxOutline from 'vue-material-design-icons/CheckboxOutline.vue'
14-
import IconRadioboxMarked from 'vue-material-design-icons/RadioboxMarked.vue'
1514
import IconArrowDownDropCircleOutline from 'vue-material-design-icons/ArrowDownDropCircleOutline.vue'
16-
import IconTextShort from 'vue-material-design-icons/TextShort.vue'
17-
import IconTextLong from 'vue-material-design-icons/TextLong.vue'
18-
import IconFile from 'vue-material-design-icons/File.vue'
1915
import IconCalendar from 'vue-material-design-icons/Calendar.vue'
16+
import IconCheckboxOutline from 'vue-material-design-icons/CheckboxOutline.vue'
2017
import IconClockOutline from 'vue-material-design-icons/ClockOutline.vue'
18+
import IconFile from 'vue-material-design-icons/File.vue'
19+
import IconLinearScale from '../components/Icons/IconLinearScale.vue'
20+
import IconRadioboxMarked from 'vue-material-design-icons/RadioboxMarked.vue'
21+
import IconTextLong from 'vue-material-design-icons/TextLong.vue'
22+
import IconTextShort from 'vue-material-design-icons/TextShort.vue'
2123

2224
/**
2325
* @typedef {object} AnswerTypes
@@ -29,6 +31,7 @@ import IconClockOutline from 'vue-material-design-icons/ClockOutline.vue'
2931
* @property {string} date Date Answer
3032
* @property {string} datetime Date and Time Answer
3133
* @property {string} time Time Answer
34+
* @property {string} linearscale Linear Scale Answer
3235
*/
3336
export default {
3437
/**
@@ -179,4 +182,14 @@ export default {
179182
storageFormat: 'HH:mm',
180183
momentFormat: 'LT',
181184
},
185+
186+
linearscale: {
187+
component: QuestionLinearScale,
188+
icon: IconLinearScale,
189+
label: t('forms', 'Linear scale'),
190+
predefined: true,
191+
192+
titlePlaceholder: t('forms', 'Linear scale question title'),
193+
warningInvalid: t('forms', 'This question needs a title!'),
194+
},
182195
}

0 commit comments

Comments
 (0)