Skip to content

Commit a8f829d

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

3 files changed

Lines changed: 245 additions & 5 deletions

File tree

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: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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+
type="multiselect"
15+
:options="[0, 1]"
16+
:v-model="lowestOption">
17+
{{ t('forms', 'Lowest value') }}
18+
</NcActionInput>
19+
<NcActionInput
20+
type="multiselect"
21+
:options="[2, 3, 4, 5, 6, 7, 8, 9, 10]"
22+
:v-model="highestOption">
23+
{{ t('forms', 'Highest value"') }}
24+
</NcActionInput>
25+
<NcActionInput>
26+
{{ t('forms', 'Label for lowest value"') }}
27+
</NcActionInput>
28+
<NcActionInput>
29+
{{ t('forms', 'Label for highest value"') }}
30+
</NcActionInput>
31+
</template>
32+
33+
<fieldset :name="name || undefined" :aria-labelledby="titleId">
34+
<NcNoteCard v-if="hasError" :id="errorId" type="error">
35+
{{ errorMessage }}
36+
</NcNoteCard>
37+
<NcCheckboxRadioSwitch
38+
v-for="option in scaleOptions"
39+
:key="option"
40+
:disabled="!readOnly"
41+
:aria-errormessage="hasError ? errorId : undefined"
42+
:aria-invalid="hasError ? 'true' : undefined"
43+
:checked="questionValues"
44+
:value="option.toString()"
45+
:name="`${id}-answer`"
46+
button-variant
47+
button-variant-grouped="horizontal"
48+
type="radio"
49+
:required="checkRequired(option)"
50+
@update:checked="onChange"
51+
@keydown.enter.exact.prevent="onKeydownEnter">
52+
{{ option }}
53+
</NcCheckboxRadioSwitch>
54+
</fieldset>
55+
</Question>
56+
</template>
57+
58+
<script>
59+
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
60+
import NcActionInput from '@nextcloud/vue/components/NcActionInput'
61+
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
62+
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
63+
64+
import QuestionMixin from '../../mixins/QuestionMixin.js'
65+
66+
export default {
67+
name: 'QuestionLinearScale',
68+
69+
components: {
70+
NcActionInput,
71+
NcCheckboxRadioSwitch,
72+
NcNoteCard,
73+
},
74+
75+
mixins: [QuestionMixin],
76+
77+
data() {
78+
return {
79+
/**
80+
* The shown error message
81+
*/
82+
errorMessage: null,
83+
84+
isLoading: false,
85+
86+
lowestOption: 1,
87+
highestOption: 5,
88+
}
89+
},
90+
91+
computed: {
92+
scaleOptions() {
93+
return Array.from(
94+
{ length: this.highestOption - this.lowestOption + 1 },
95+
(_, i) => i + this.lowestOption,
96+
)
97+
},
98+
99+
isUnique() {
100+
return this.answerType.unique === true
101+
},
102+
103+
hasError() {
104+
return !!this.errorMessage
105+
},
106+
107+
questionValues() {
108+
return this.isUnique ? this.values?.[0] : this.values
109+
},
110+
111+
titleId() {
112+
return `q${this.index}_title`
113+
},
114+
115+
errorId() {
116+
return `q${this.index}_error`
117+
},
118+
},
119+
120+
methods: {
121+
async validate() {
122+
if (!this.isUnique) {
123+
// Validate limits
124+
const max = this.extraSettings.optionsLimitMax ?? 0
125+
const min = this.extraSettings.optionsLimitMin ?? 0
126+
if (max && this.values.length > max) {
127+
this.errorMessage = n(
128+
'forms',
129+
'You must choose at most one option',
130+
'You must choose a maximum of %n options',
131+
max,
132+
)
133+
return false
134+
}
135+
if (min && this.values.length < min) {
136+
this.errorMessage = n(
137+
'forms',
138+
'You must choose at least one option',
139+
'You must choose at least %n options',
140+
min,
141+
)
142+
return false
143+
}
144+
}
145+
146+
this.errorMessage = null
147+
return true
148+
},
149+
150+
onChange(value) {
151+
this.$emit('update:values', this.isUnique ? [value].flat() : value)
152+
},
153+
154+
/**
155+
* Is the provided answer required ?
156+
* This is needed for checkboxes as html5
157+
* doesn't allow to require at least ONE checked.
158+
* So we require the one that are checked or all
159+
* if none are checked yet.
160+
*
161+
* @return {boolean}
162+
*/
163+
checkRequired() {
164+
// false, if question not required
165+
if (!this.isRequired) {
166+
return false
167+
}
168+
169+
// true for Radiobuttons
170+
if (this.isUnique) {
171+
return true
172+
}
173+
174+
// For checkboxes, only required if no other is checked
175+
return this.areNoneChecked
176+
},
177+
},
178+
}
179+
</script>
180+
181+
<style lang="scss" scoped>
182+
</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)