Skip to content

Commit 184e61d

Browse files
committed
feat: add color question type
Signed-off-by: Christian Hartmann <[email protected]>
1 parent 9de4ea5 commit 184e61d

File tree

11 files changed

+266
-21
lines changed

11 files changed

+266
-21
lines changed

docs/DataStructure.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ Currently supported Question-Types are:
217217
| `time` | Showing a dropdown menu to select a time. |
218218
| `file` | One or multiple files. It is possible to specify which mime types are allowed |
219219
| `linearscale` | A linear or Likert scale question where you choose an option that best fits your opinion |
220+
| `color` | A color answer, hex string representation (e. g. `#123456`) |
220221

221222
## Extra Settings
222223

lib/Constants.php

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,37 +66,39 @@ class Constants {
6666
*/
6767

6868
// Available AnswerTypes
69-
public const ANSWER_TYPE_MULTIPLE = 'multiple';
70-
public const ANSWER_TYPE_MULTIPLEUNIQUE = 'multiple_unique';
71-
public const ANSWER_TYPE_DROPDOWN = 'dropdown';
72-
public const ANSWER_TYPE_SHORT = 'short';
73-
public const ANSWER_TYPE_LONG = 'long';
69+
public const ANSWER_TYPE_COLOR = 'color';
7470
public const ANSWER_TYPE_DATE = 'date';
7571
public const ANSWER_TYPE_DATETIME = 'datetime';
76-
public const ANSWER_TYPE_TIME = 'time';
72+
public const ANSWER_TYPE_DROPDOWN = 'dropdown';
7773
public const ANSWER_TYPE_FILE = 'file';
7874
public const ANSWER_TYPE_LINEARSCALE = 'linearscale';
75+
public const ANSWER_TYPE_LONG = 'long';
76+
public const ANSWER_TYPE_MULTIPLE = 'multiple';
77+
public const ANSWER_TYPE_MULTIPLEUNIQUE = 'multiple_unique';
78+
public const ANSWER_TYPE_SHORT = 'short';
79+
public const ANSWER_TYPE_TIME = 'time';
7980

8081
// All AnswerTypes
8182
public const ANSWER_TYPES = [
82-
self::ANSWER_TYPE_MULTIPLE,
83-
self::ANSWER_TYPE_MULTIPLEUNIQUE,
84-
self::ANSWER_TYPE_DROPDOWN,
85-
self::ANSWER_TYPE_SHORT,
86-
self::ANSWER_TYPE_LONG,
83+
self::ANSWER_TYPE_COLOR,
8784
self::ANSWER_TYPE_DATE,
8885
self::ANSWER_TYPE_DATETIME,
89-
self::ANSWER_TYPE_TIME,
86+
self::ANSWER_TYPE_DROPDOWN,
9087
self::ANSWER_TYPE_FILE,
9188
self::ANSWER_TYPE_LINEARSCALE,
89+
self::ANSWER_TYPE_LONG,
90+
self::ANSWER_TYPE_MULTIPLE,
91+
self::ANSWER_TYPE_MULTIPLEUNIQUE,
92+
self::ANSWER_TYPE_SHORT,
93+
self::ANSWER_TYPE_TIME,
9294
];
9395

9496
// AnswerTypes, that need/have predefined Options
9597
public const ANSWER_TYPES_PREDEFINED = [
96-
self::ANSWER_TYPE_MULTIPLE,
97-
self::ANSWER_TYPE_MULTIPLEUNIQUE,
9898
self::ANSWER_TYPE_DROPDOWN,
9999
self::ANSWER_TYPE_LINEARSCALE,
100+
self::ANSWER_TYPE_MULTIPLE,
101+
self::ANSWER_TYPE_MULTIPLEUNIQUE,
100102
];
101103

102104
// AnswerTypes for date/time questions
@@ -194,10 +196,10 @@ class Constants {
194196
* !! Keep in sync with src/mixins/ShareTypes.js !!
195197
*/
196198
public const SHARE_TYPES_USED = [
197-
IShare::TYPE_USER,
199+
IShare::TYPE_CIRCLE,
198200
IShare::TYPE_GROUP,
199201
IShare::TYPE_LINK,
200-
IShare::TYPE_CIRCLE
202+
IShare::TYPE_USER,
201203
];
202204

203205
/**
@@ -214,18 +216,18 @@ class Constants {
214216

215217
public const PERMISSION_ALL = [
216218
self::PERMISSION_EDIT,
219+
self::PERMISSION_EMBED,
217220
self::PERMISSION_RESULTS,
218221
self::PERMISSION_RESULTS_DELETE,
219222
self::PERMISSION_SUBMIT,
220-
self::PERMISSION_EMBED,
221223
];
222224

223225
/**
224226
* !! Keep in sync with src/FormsEmptyContent.vue !!
225227
* InitialStates for emptyContent to render as...
226228
*/
227-
public const EMPTY_NOTFOUND = 'notfound';
228229
public const EMPTY_EXPIRED = 'expired';
230+
public const EMPTY_NOTFOUND = 'notfound';
229231

230232
/**
231233
* Constants related to extra settings for questions

lib/Service/SubmissionService.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,15 @@ public function validateSubmission(array $questions, array $answers, string $for
453453
throw new \InvalidArgumentException(sprintf('Invalid input for question "%s".', $question['text']));
454454
}
455455

456+
// Handle color questions
457+
if (
458+
$question['type'] === Constants::ANSWER_TYPE_COLOR
459+
&& $answers[$questionId][0] !== ''
460+
&& !preg_match('/^#[a-f0-9]{6}$/i', $answers[$questionId][0])
461+
) {
462+
throw new \InvalidArgumentException(sprintf('Invalid color string for question "%s".', $question['text']));
463+
}
464+
456465
// Handle file questions
457466
if ($question['type'] === Constants::ANSWER_TYPE_FILE) {
458467
$maxAllowedFilesCount = $question['extraSettings']['maxAllowedFilesCount'] ?? 0;

src/components/Icons/IconPalette.vue

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 palette-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="M480-96q-78.72 0-148.8-30.24-70.08-30.24-122.4-82.56-52.32-52.32-82.56-122.4Q96-401.28 96-480q0-80 30.5-149.5t84-122Q264-804 335.5-834t152.75-30q77.39 0 146.07 27Q703-810 754-763t80.5 110Q864-590 864-518q0 96-67.08 163-67.09 67-162.92 67h-67.76q-8.24 0-14.24 5t-6 12.67Q546-255 561-245q15 10 15 53 0 37-27 66.5T480-96ZM264-444q25 0 42.5-17.5T324-504q0-25-17.5-42.5T264-564q-25 0-42.5 17.5T204-504q0 25 17.5 42.5T264-444Zm120-144q25 0 42.5-17.5T444-648q0-25-17.5-42.5T384-708q-25 0-42.5 17.5T324-648q0 25 17.5 42.5T384-588Zm192 0q25 0 42.5-17.5T636-648q0-25-17.5-42.5T576-708q-25 0-42.5 17.5T516-648q0 25 17.5 42.5T576-588Zm120 144q25 0 42.5-17.5T756-504q0-25-17.5-42.5T696-564q-25 0-42.5 17.5T636-504q0 25 17.5 42.5T696-444Z" />
22+
<title v-if="title">{{ title }}</title>
23+
</svg>
24+
</span>
25+
</template>
26+
27+
<script>
28+
export default {
29+
name: 'IconPalette',
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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 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+
<div class="question__content">
13+
<NcColorPicker
14+
:model-value="pickedColor"
15+
advanced-fields
16+
@update:model-value="onUpdatePickedColor">
17+
<NcButton :disabled="!readOnly">{{
18+
colorPickerPlaceholder
19+
}}</NcButton>
20+
</NcColorPicker>
21+
<div :style="{ 'background-color': pickedColor }" class="color__field">
22+
<NcButton
23+
v-if="pickedColor !== '' && !isRequired"
24+
class="color__field__button"
25+
:aria-label="t('forms', 'Clear selected color')"
26+
variant="tertiary"
27+
@click="onUpdatePickedColor('')">
28+
<template #icon>
29+
<IconClose :size="20" />
30+
</template>
31+
</NcButton>
32+
</div>
33+
</div>
34+
</Question>
35+
</template>
36+
37+
<script>
38+
import IconClose from 'vue-material-design-icons/Close.vue'
39+
import NcButton from '@nextcloud/vue/components/NcButton'
40+
import NcColorPicker from '@nextcloud/vue/components/NcColorPicker'
41+
42+
import QuestionMixin from '../../mixins/QuestionMixin.js'
43+
44+
export default {
45+
name: 'QuestionColor',
46+
47+
components: {
48+
IconClose,
49+
NcButton,
50+
NcColorPicker,
51+
},
52+
53+
mixins: [QuestionMixin],
54+
55+
data() {
56+
return {
57+
isLoading: false,
58+
}
59+
},
60+
61+
computed: {
62+
colorPickerPlaceholder() {
63+
return this.readOnly
64+
? this.answerType.submitPlaceholder
65+
: this.answerType.createPlaceholder
66+
},
67+
68+
pickedColor() {
69+
return this.values[0] ?? ''
70+
},
71+
},
72+
73+
methods: {
74+
onUpdatePickedColor(color) {
75+
this.$emit('update:values', [color])
76+
},
77+
},
78+
}
79+
</script>
80+
81+
<style lang="scss" scoped>
82+
.question__content {
83+
display: flex;
84+
gap: var(--clickable-area-small);
85+
}
86+
87+
.color__field {
88+
width: 100px;
89+
height: var(--default-clickable-area);
90+
border-radius: var(--border-radius-element);
91+
92+
&__button {
93+
position: relative;
94+
margin-inline-start: calc(100% - var(--default-clickable-area));
95+
}
96+
}
97+
</style>

src/components/Results/Answer.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
</a>
2323
</p>
2424
</template>
25+
<template v-else-if="questionType === 'color'">
26+
<div class="color__result">
27+
<div
28+
:style="{ 'background-color': answerText }"
29+
class="color__field" />
30+
<NcHighlight :text="answerText" :search="highlight" />
31+
</div>
32+
</template>
2533
<p v-else class="answer__text" dir="auto">
2634
<NcHighlight :text="answerText" :search="highlight" />
2735
</p>
@@ -54,6 +62,10 @@ export default {
5462
type: String,
5563
required: true,
5664
},
65+
questionType: {
66+
type: String,
67+
required: true,
68+
},
5769
highlight: {
5870
type: String,
5971
required: false,
@@ -81,5 +93,20 @@ export default {
8193
top: 4px;
8294
}
8395
}
96+
97+
.color__field {
98+
width: 100px;
99+
height: var(--default-clickable-area);
100+
border-radius: var(--border-radius-element);
101+
position: relative;
102+
inset-block-start: 12px;
103+
margin-block-start: -12px;
104+
}
105+
106+
.color__result {
107+
align-items: baseline;
108+
display: flex;
109+
gap: calc(var(--clickable-area-small) / 2);
110+
}
84111
}
85112
</style>

src/components/Results/ResultsSummary.vue

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,26 @@
4141
<ul v-else class="question-summary__text">
4242
<!-- Do not wrap the following line between tags! `white-space:pre-line` respects `\n` but would produce additional empty first line -->
4343
<!-- eslint-disable-next-line -->
44-
<li v-for="answer in answers" :key="answer.id" dir="auto">
44+
<li v-for="(answer, index) in answers" :key="answer.id" dir="auto">
4545
<template v-if="answer.url">
4646
<a :href="answer.url" target="_blank">
4747
<IconFile :size="20" class="question-summary__text-icon" />
4848
{{ answer.text }}
4949
</a>
5050
</template>
51+
<template v-else-if="question.type === 'color'">
52+
<div class="color__result">
53+
<div
54+
v-if="answer.id !== 0"
55+
:style="{ 'background-color': answer.text }"
56+
:class="
57+
index === 1
58+
? 'color__field color__field__first'
59+
: 'color__field'
60+
" />
61+
{{ answer.text }}
62+
</div>
63+
</template>
5164
<template v-else>
5265
{{ answer.text }}
5366
</template>
@@ -368,5 +381,23 @@ export default {
368381
}
369382
}
370383
}
384+
385+
.color__field {
386+
width: 100px;
387+
height: var(--default-clickable-area);
388+
border-radius: var(--border-radius-element);
389+
position: relative;
390+
inset-block-start: 12px;
391+
392+
&__first {
393+
margin-block-start: -12px;
394+
}
395+
}
396+
397+
.color__result {
398+
align-items: baseline;
399+
display: flex;
400+
gap: calc(var(--clickable-area-small) / 2);
401+
}
371402
}
372403
</style>

src/components/Results/Submission.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
:highlight="highlight"
4040
:answer-text="question.squashedAnswers"
4141
:answers="question.answers"
42-
:question-text="question.text" />
42+
:question-text="question.text"
43+
:question-type="question.type" />
4344
</div>
4445
</template>
4546

@@ -120,6 +121,7 @@ export default {
120121
answeredQuestionsArray.push({
121122
id: question.id,
122123
text: question.text,
124+
type: question.type,
123125
answers: answers.map((answer) => {
124126
return {
125127
id: answer.id,
@@ -138,6 +140,7 @@ export default {
138140
answeredQuestionsArray.push({
139141
id: question.id,
140142
text: question.text,
143+
type: question.type,
141144
squashedAnswers,
142145
})
143146
} else {
@@ -148,6 +151,7 @@ export default {
148151
answeredQuestionsArray.push({
149152
id: question.id,
150153
text: question.text,
154+
type: question.type,
151155
squashedAnswers,
152156
})
153157
}

0 commit comments

Comments
 (0)