aboutsummaryrefslogtreecommitdiff
path: root/resources
diff options
context:
space:
mode:
authorJan-Hendrik Willms <tleilax+studip@gmail.com>2024-04-26 07:17:44 +0000
committerJan-Hendrik Willms <tleilax+studip@gmail.com>2024-04-26 07:17:44 +0000
commitc140af0d6c46ca96ddffd8ea49e1190d05dd9c8f (patch)
tree63dccd1aaf745e50a5642b554c997469e19f329b /resources
parent572b9b5116f83670fd2d482930588144bec03ef0 (diff)
unify questionnaire editing vue components, re #3914, fixes #3915
Closes #3915 and #3914 Merge request studip/studip!2833
Diffstat (limited to 'resources')
-rw-r--r--resources/assets/stylesheets/scss/questionnaire.scss170
-rw-r--r--resources/vue/components/questionnaires/FreetextEdit.vue39
-rw-r--r--resources/vue/components/questionnaires/InputArray.vue304
-rw-r--r--resources/vue/components/questionnaires/LikertEdit.vue189
-rw-r--r--resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue44
-rw-r--r--resources/vue/components/questionnaires/RangescaleEdit.vue216
-rw-r--r--resources/vue/components/questionnaires/VoteEdit.vue51
-rw-r--r--resources/vue/mixins/QuestionnaireComponent.js24
8 files changed, 289 insertions, 748 deletions
diff --git a/resources/assets/stylesheets/scss/questionnaire.scss b/resources/assets/stylesheets/scss/questionnaire.scss
index f55b50f..fde7d32 100644
--- a/resources/assets/stylesheets/scss/questionnaire.scss
+++ b/resources/assets/stylesheets/scss/questionnaire.scss
@@ -1,8 +1,6 @@
$width: 270px;
.questionnaire_edit {
-
-
.editor {
display: flex;
flex-direction: row-reverse;
@@ -14,14 +12,16 @@ $width: 270px;
min-width: $width;
width: $width;
.questions_container {
- padding: 0px;
+ padding: 0;
.questions {
display: flex;
flex-direction: column;
}
}
- > .admin, > .add_question, .questions > * {
+ > .admin,
+ > .add_question,
+ .questions > * {
width: calc(100% - 8px);
padding: 4px;
border-bottom: 1px solid var(--content-color-40);
@@ -42,8 +42,8 @@ $width: 270px;
&::before {
content: '';
position: absolute;
- height: 0px;
- width: 0px;
+ height: 0;
+ width: 0;
border-top: 25px transparent solid;
border-bottom: 25px transparent solid;
border-left: 7px var(--content-color-40) solid;
@@ -52,8 +52,8 @@ $width: 270px;
&::after {
content: '';
position: absolute;
- height: 0px;
- width: 0px;
+ height: 0;
+ width: 0;
border-top: 25px transparent solid;
border-bottom: 25px transparent solid;
border-left: 7px var(--yellow-40) solid;
@@ -93,42 +93,11 @@ $width: 270px;
border: 1px solid var(--content-color-40);
border-left: none;
flex-grow: 1;
- padding: 10px;
- padding-left: 15px;
+ padding: 10px 10px 10px 15px;
min-height: 150px;
min-width: 0;
}
- .vote_edit {
- .options {
- > li {
- display: flex;
- align-items: center;
- > * {
- margin-right: 10px;
- }
- }
- }
- }
- .rangescale_edit table.default > thead > tr > th.number {
- padding-left: 12px;
- }
-
- .dragcolumn {
- max-width: 1px;
- padding-bottom: 0px;
- > .dragarea {
- display: inline-block;
- height: 27px;
- }
- }
-
- .input-array {
- margin-left: 4px;
- }
- .likert_edit .input-array {
- margin-left: 7px;
- }
.inline_editing {
width: 100%;
display: flex;
@@ -150,114 +119,25 @@ $width: 270px;
justify-items: center;
}
}
- .drag-handle {
- display: inline-block;
- height: 24px;
- }
- }
-
- /* ab hier der alte kram */
-
- section {
- border: thin solid var(--black);
- margin: 3px;
- }
-
- .options {
- padding: 0;
- list-style-type: none;
-
- > li {
- margin-top: 5px;
- margin-bottom: 5px;
-
- > .move {
- cursor: move;
- display: inline-block;
- vertical-align: middle;
- }
-
- > input {
- display: inline-block;
- vertical-align: middle;
- }
-
- > input[type=text] {
- width: calc(100% - 70px);
- }
- .delete {
+ .dragcolumn {
+ max-width: 1px;
+ padding-bottom: 0;
+ > .dragarea {
display: inline-block;
- vertical-align: middle;
- cursor: pointer;
- }
-
- .add {
- display: none;
- vertical-align: middle;
- cursor: pointer;
+ height: 27px;
}
}
- > li:last-child .delete {
- display: none;
- }
-
- > li:last-child .add {
+ .drag-handle {
display: inline-block;
+ height: 24px;
}
- > li:only-child .move {
- display: none;
- }
-
- }
-
- .all_questions {
- .question:first-child .move_up {
- display: none;
- }
-
- .question:last-child .move_down {
- display: none;
- }
- }
-
- .add_questions {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- align-items: stretch;
- border: thin dashed var(--content-color-40);
-
- > a {
- background-color: transparent;
- margin: 10px;
- border: thin solid var(--content-color-20);
- padding: 5px;
- width: 100px;
- min-width: 100px;
- max-width: 100px;
- height: 100px;
- min-height: 100px;
- max-height: 100px;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- justify-content: space-around;
- align-items: center;
+ .option-cell {
text-align: center;
-
- > img {
- margin-left: auto;
- margin-right: auto;
- }
}
}
-
- .questionnaire_metadata {
- margin-top: 10px;
- }
}
.questionnaire_results {
@@ -308,7 +188,8 @@ $width: 270px;
}
-.questionnaire_answer, .questionnaire_results {
+.questionnaire_answer,
+.questionnaire_results {
.description_container {
display: flex;
> .icon_container {
@@ -335,7 +216,7 @@ $width: 270px;
border: none;
> :first-child {
- margin-top: 0px;
+ margin-top: 0;
}
.invalidation_notice {
@@ -351,9 +232,6 @@ $width: 270px;
font-size: 0.7em;
padding-left: 5px;
}
- .rangescale_center {
- text-align: center;
- }
.centerline {
border-top: 1px solid var(--base-color);
position: relative;
@@ -389,6 +267,14 @@ $width: 270px;
}
}
+.questionnaire_edit,
+.questionnaire_answer,
+.questionnaire_results {
+ .option-cell {
+ text-align: center;
+ }
+}
+
.courseselector,
.instituteselector,
.statusgroupselector {
diff --git a/resources/vue/components/questionnaires/FreetextEdit.vue b/resources/vue/components/questionnaires/FreetextEdit.vue
index 29c6f34..58848ed 100644
--- a/resources/vue/components/questionnaires/FreetextEdit.vue
+++ b/resources/vue/components/questionnaires/FreetextEdit.vue
@@ -2,7 +2,7 @@
<div>
<div class="formpart" tabindex="0" ref="autofocus">
{{ $gettext('Frage') }}
- <studip-wysiwyg v-model="val_clone.description" :key="question_id"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
<label>
@@ -13,40 +13,19 @@
</template>
<script>
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
export default {
name: 'freetext-edit',
- components: {
- StudipWysiwyg
+ mixins: [ QuestionnaireComponent ],
+ created() {
+ this.setDefaultValues({
+ description: '',
+ mandatory: '0',
+ });
},
- props: {
- value: {
- type: Object,
- required: false,
- default: function () {
- return {};
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data: function () {
- return {
- val_clone: ''
- };
- },
- mounted: function () {
- this.val_clone = this.value;
+ mounted() {
this.$refs.autofocus.focus();
- },
- watch: {
- value (new_val) {
- this.val_clone = new_val;
- }
}
-
}
</script>
diff --git a/resources/vue/components/questionnaires/InputArray.vue b/resources/vue/components/questionnaires/InputArray.vue
index 0fd67db..f57ca58 100644
--- a/resources/vue/components/questionnaires/InputArray.vue
+++ b/resources/vue/components/questionnaires/InputArray.vue
@@ -1,206 +1,170 @@
<template>
<div class="input-array">
<span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span>
- <draggable v-model="options" handle=".dragarea" tag="ol" class="clean options">
- <li v-for="(option, index) in options" :key="index">
- <a class="dragarea"
- v-if="options.length > 1"
- tabindex="0"
- :ref="'draghandle_' + index"
- :title="$gettextInterpolate('Sortierelement für Option %{option}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {option: option})"
- @keydown="keyHandler($event, index)">
- <span class="drag-handle"></span>
- </a>
- <input type="text"
- :placeholder="$gettext('Option')"
- :ref="'option_' + index"
- @paste="(ev) => onPaste(ev, index)"
- v-model="options[index]">
- <button class="as-link"
- :title="$gettext('Option löschen')"
- @click.prevent="askForDeletingOption(index)">
- <studip-icon shape="trash" :role="options.length > 1 ? 'clickable' : 'inactive'" :size="20" alt=""></studip-icon>
- </button>
- </li>
- </draggable>
- <button class="as-link"
- :title="$gettext('Option hinzufügen')"
- @click.prevent="addOption">
- <studip-icon shape="add" :size="20" alt=""></studip-icon>
- </button>
-
- <studip-dialog
- v-if="askForDeleting"
- :title="$gettext('Bitte bestätigen Sie die Aktion.')"
- :question="$gettext('Wirklich löschen?')"
- :confirmText="$gettext('Ja')"
- :closeText="$gettext('Nein')"
- closeClass="cancel"
- height="180"
- @confirm="deleteOption"
- @close="askForDeleting = false"
- >
- </studip-dialog>
+ <table class="default nohover">
+ <colgroup>
+ <col style="width: 16px">
+ <col>
+ <col v-for="i in additionalColspan" :key="`colspan-${i}`">
+ <col style="width: 24px">
+ </colgroup>
+ <thead>
+ <tr>
+ <th class="dragcolumn"></th>
+ <th>{{ labelPlural }}</th>
+ <slot name="header-cells" />
+ <th class="actions"></th>
+ </tr>
+ </thead>
+ <Draggable v-model="options" handle=".dragarea" tag="tbody" class="statements">
+ <tr v-for="(option, index) in options" :key="index">
+ <td class="dragcolumn">
+ <a class="dragarea"
+ tabindex="0"
+ :title="$gettextInterpolate($gettext(`Sortierelement für %{label} %{option}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.`), {option, label})"
+ @keydown="keyHandler($event, index)"
+ ref="draghandle">
+ <span class="drag-handle"></span>
+ </a>
+ </td>
+ <td>
+ <input type="text"
+ ref="inputs"
+ :placeholder="label"
+ @paste="(ev) => onPaste(ev, index)"
+ v-model="options[index]">
+ </td>
+ <slot name="body-cells" />
+ <td class="actions">
+ <StudipIcon name="delete"
+ shape="trash"
+ :size="20"
+ @click.prevent="deleteOption(index)"
+ :title="$gettextInterpolate($gettext('%{label} löschen'), {label})"
+ />
+ </td>
+ </tr>
+ </Draggable>
+ <tfoot>
+ <tr>
+ <td :colspan="3 + additionalColspan">
+ <button class="as-link"
+ :title="$gettextInterpolate($gettext('%{label} hinzufügen'), {label})"
+ @click.prevent="addOption()">
+ <StudipIcon shape="add" :size="20" alt="" />
+ </button>
+ </td>
+ </tr>
+ </tfoot>
+ </table>
</div>
</template>
<script>
-import StudipIcon from "../StudipIcon.vue";
-import StudipDialog from "../StudipDialog.vue";
-import draggable from 'vuedraggable';
+import Draggable from 'vuedraggable';
+import { $gettext } from '../../../assets/javascripts/lib/gettext';
+
export default {
name: 'input-array',
- components: {
- StudipIcon,
- StudipDialog,
- draggable
- },
+ components: { Draggable },
props: {
- value: {
- type: Array,
- required: false
- }
+ additionalColspan: {
+ type: Number,
+ default: 0,
+ },
+ label: {
+ type: String,
+ default: $gettext('Option'),
+ },
+ labelPlural: {
+ type: String,
+ default: $gettext('Optionen'),
+ },
+ value: Array,
},
- data: function () {
+ data() {
return {
options: [],
- askForDeleting: false,
- indexOfDeletingOption: 0,
- unique_id: null,
- assistiveLive: ''
+ assistiveLive: '',
};
},
methods: {
- addOption: function (val, position) {
- let data = this.value;
- if (val.target) {
- val = '';
- }
- if (typeof position === "undefined") {
- data.push(val || '');
- position = this.value.length - 1
- } else {
- data.splice(position, 0, val || '');
- }
- this.$emit('input', data);
- let v = this;
- this.$nextTick(function () {
- v.$refs['option_' + position][0].focus();
- });
- },
- askForDeletingOption: function (index) {
- if (this.options.length <= 1) {
- return;
- }
+ addOption(val = '', position = this.options.length) {
+ this.$set(this.options, position, val.trim());
- this.indexOfDeletingOption = index;
- if (this.value[index]) {
- this.askForDeleting = true;
- } else {
- this.deleteOption();
- }
+ this.$nextTick(() => {
+ this.$refs.inputs[position].focus();
+ });
},
- deleteOption: function () {
- this.$delete(this.value, this.indexOfDeletingOption);
- this.askForDeleting = false;
+ deleteOption(index) {
+ const question = this.options[index] ? this.$gettext('Wirklich löschen?') : true;
+ STUDIP.Dialog.confirm(question).done(() => {
+ this.$delete(this.options, index);
+ });
},
- onPaste: function (ev, position) {
- let data = ev.clipboardData.getData("text").split("\n");
- for (let i = 0; i < data.length; i++) {
- if (data[i].trim()) {
- this.addOption(data[i], position + i);
- }
- }
+ onPaste(ev, position) {
+ ev.clipboardData
+ .getData('text')
+ .split("\n")
+ .filter(str => str.trim().length > 0)
+ .forEach((value, index) => this.addOption(value, position + index));
+ ev.preventDefault();
},
keyHandler(e, index) {
- switch (e.keyCode) {
- case 38: // up
- e.preventDefault();
- if (index > 0) {
- this.moveUp(index);
- this.$nextTick(function () {
- this.$refs['draghandle_' + (index - 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- 'Aktuelle Position in der Liste: %{pos} von %{listLength}.'
- , {pos: index, listLength: this.options.length}
- );
- });
- }
- break;
- case 40: // down
- e.preventDefault();
- if (index < this.options.length - 1) {
- this.moveDown(index);
- this.$nextTick(function () {
- this.$refs['draghandle_' + (index + 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- 'Aktuelle Position in der Liste: %{pos} von %{listLength}.'
- , {pos: index + 2, listLength: this.options.length}
- );
- });
- }
- break;
- }
- },
- moveDown: function (index) {
- if (index == this.options.length - 1) {
+ if (e.keyCode !== 38 && e.keyCode !== 40) {
return;
}
- let option = this.options[index];
- this.options[index] = this.options[index + 1];
- this.options[index + 1] = option;
- this.$forceUpdate();
+
+ e.preventDefault();
+
+ const moveUp = e.keyCode === 38;
+
+ this.moveElement(index, moveUp ? -1 : 1).then((newIndex) => {
+ this.assistiveLive = this.$gettextInterpolate(
+ this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
+ {pos: newIndex + 1, listLength: this.options.length}
+ );
+
+ this.$nextTick(() => {
+ this.$refs['draghandle'][newIndex].focus();
+ });
+ })
},
- moveUp: function (index) {
- if (index === 0) {
- return;
+ moveElement(index, direction) {
+ if (this.options[index + direction] === undefined) {
+ return Promise.resolve(index);
}
- let option = this.options[index];
- this.options[index] = this.options[index - 1];
- this.options[index - 1] = option;
- this.$forceUpdate();
+
+ const indices = [index, index + direction].sort();
+
+ this.options.splice(
+ Math.min(...indices),
+ 2,
+ ...indices.reverse().map(idx => this.options[idx])
+ );
+
+ return Promise.resolve(index + direction);
}
},
- mounted: function () {
- this.options = this.value;
- this.unique_id = 'array_input_' + Math.floor(Math.random() * 100000000);
- },
watch: {
- options (new_data, old_data) {
- if (typeof old_data === 'undefined' || typeof new_data === 'undefined') {
- return;
- }
- this.$emit('input', new_data);
+ options: {
+ handler(current) {
+ this.$emit('input', current);
+ },
+ deep: true
},
- value (new_val) {
- this.options = new_val;
+ value: {
+ handler(current) {
+ this.options = current;
+ },
+ immediate: true
}
}
}
</script>
-<style lang="scss" scoped>
-.input-array {
- display: grid;
- grid-template-areas:
- "sr sr"
- "options button";
- grid-template-columns: calc(100% - 24px) 24px;
- grid-template-rows: auto;
-
- > .sr-only {
- grid-area: sr;
- }
-
- > .options {
- grid-area: options;
- }
-
- > button.as-link {
- align-self: end;
- grid-area: button;
- justify-self: left;
- margin-bottom: 8px;
- }
+<style scoped>
+.input-array input[type="text"] {
+ max-width: unset;
}
</style>
diff --git a/resources/vue/components/questionnaires/LikertEdit.vue b/resources/vue/components/questionnaires/LikertEdit.vue
index c87f9fe..736be6b 100644
--- a/resources/vue/components/questionnaires/LikertEdit.vue
+++ b/resources/vue/components/questionnaires/LikertEdit.vue
@@ -1,66 +1,27 @@
<template>
<div class="likert_edit">
-
<div class="formpart" tabindex="0" ref="autofocus">
{{ $gettext('Einleitungstext' )}}
- <studip-wysiwyg v-model="val_clone.description"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
- <span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span>
+ <InputArray v-model="val_clone.statements"
+ :label="$gettext('Aussage')"
+ :label-plural="$gettext('Aussagen')"
+ :additional-colspan="val_clone.options.length"
+ >
+ <template #header-cells>
+ <th v-for="(option, index) in val_clone.options" class="option-cell" :key="index">
+ {{ option }}
+ </th>
+ </template>
- <table class="default nohover">
- <thead>
- <tr>
- <th class="dragcolumn"></th>
- <th>{{ $gettext('Aussagen') }}</th>
- <th v-for="(option, index) in val_clone.options" :key="index">{{ option }}</th>
- <th class="actions"></th>
- </tr>
- </thead>
- <draggable v-model="val_clone.statements" handle=".dragarea" tag="tbody" class="statements">
- <tr v-for="(statement, index) in val_clone.statements" :key="index">
- <td class="dragcolumn">
- <a class="dragarea"
- tabindex="0"
- :title="$gettextInterpolate($gettext('Sortierelement für Aussage %{statement}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {statement: statement})"
- @keydown="keyHandler($event, index)"
- :ref="'draghandle_' + index">
- <span class="drag-handle"></span>
- </a>
- </td>
- <td>
- <input type="text"
- :ref="'statement_' + index"
- :placeholder="$gettext('Aussage')"
- @paste="(ev) => onPaste(ev, index)"
- v-model="val_clone.statements[index]">
- </td>
- <td v-for="(option, index2) in val_clone.options" :key="index2">
- <input type="radio" disabled :title="option">
- </td>
- <td class="actions">
- <studip-icon name="delete"
- shape="trash"
- :size="20"
- @click.prevent="deleteStatement(index)"
- :title="$gettext('Aussage löschen')"
- ></studip-icon>
- </td>
- </tr>
- </draggable>
- <tfoot>
- <tr>
- <td :colspan="val_clone.options.length + 3">
- <studip-icon name="add"
- shape="add"
- :size="20"
- @click.prevent="addStatement()"
- :title="$gettext('Aussage hinzufügen')"
- ></studip-icon>
- </td>
- </tr>
- </tfoot>
- </table>
+ <template #body-cells>
+ <td v-for="(option, index) in val_clone.options" class="option-cell" :key="index">
+ <input type="radio" disabled :title="option">
+ </td>
+ </template>
+ </InputArray>
<label>
<input type="checkbox" v-model.number="val_clone.mandatory" true-value="1" false-value="0">
@@ -73,17 +34,18 @@
<div>
<div>{{ $gettext('Antwortmöglichkeiten konfigurieren') }}</div>
- <input-array v-model="val_clone.options"></input-array>
+ <InputArray v-model="val_clone.options" />
</div>
</div>
</template>
<script>
-import draggable from 'vuedraggable';
-import InputArray from "./InputArray.vue";
import { $gettext } from '../../../assets/javascripts/lib/gettext';
+import InputArray from "./InputArray.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-const default_value = () => ({
+// This is necesssar since $gettext does not seem to work in data() or created()
+const default_values = () => ({
description: '',
statements: ['', '', '', ''],
mandatory: 0,
@@ -96,115 +58,16 @@ const default_value = () => ({
$gettext('trifft nicht zu'),
],
});
+
export default {
name: 'likert-edit',
- components: {
- draggable,
- InputArray
- },
- props: {
- value: {
- type: Object,
- required: false,
- default() {
- return {...default_value()};
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data() {
- return {
- val_clone: null,
- assistiveLive: ''
- };
- },
- methods: {
- addStatement(val = '', position = null) {
- if (position === null) {
- this.val_clone.statements.push(val || '');
- } else {
- this.val_clone.statements.splice(position, 0, val || '');
- }
- this.$nextTick(() => {
- this.$refs['statement_' + (this.val_clone.statements.length - 1)][0].focus();
- });
- },
- deleteStatement(index) {
- STUDIP.Dialog.confirm(this.$gettext('Wirklich löschen?')).done(() => {
- this.$delete(this.val_clone.statements, index);
- });
- },
- onPaste(ev, position) {
- let data = ev.clipboardData.getData("text").split("\n");
- for (let i = 0; i < data.length; i++) {
- if (data[i].trim()) {
- this.addStatement(data[i], position + i);
- }
- }
- },
- keyHandler(e, index) {
- switch (e.keyCode) {
- case 38: // up
- e.preventDefault();
- if (index > 0) {
- this.moveUp(index);
- this.$nextTick(() => {
- this.$refs['draghandle_' + (index - 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
- {pos: index, listLength: this.val_clone.statements.length}
- );
- });
- }
- break;
- case 40: // down
- e.preventDefault();
- if (index < this.val_clone.statements.length - 1) {
- this.moveDown(index);
- this.$nextTick(() => {
- this.$refs['draghandle_' + (index + 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
- {pos: index + 2, listLength: this.val_clone.statements.length}
- );
- });
- }
- break;
- }
- },
- moveDown(index) {
- this.val_clone.statements.splice(
- index,
- 2,
- this.val_clone.statements[index + 1],
- this.val_clone.statements[index]
- )
- },
- moveUp(index) {
- this.val_clone.statements.splice(
- index - 1,
- 2,
- this.val_clone.statements[index],
- this.val_clone.statements[index - 1]
- )
- }
- },
+ components: { InputArray },
+ mixins: [ QuestionnaireComponent ],
created() {
- this.val_clone = Object.assign({}, default_value(), this.value ?? {});
+ this.setDefaultValues(default_values());
},
mounted() {
this.$refs.autofocus.focus();
- },
- watch: {
- val_clone: {
- handler(current) {
- this.$emit('input', current);
- },
- deep: true
- }
}
}
</script>
diff --git a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue b/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
index bc5e829..83d5fa2 100644
--- a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
+++ b/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
@@ -8,39 +8,26 @@
<div class="formpart">
{{ $gettext('Hinweistext (optional)') }}
- <studip-wysiwyg v-model="val_clone.description" :key="question_id"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
</div>
</template>
<script>
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
export default {
name: 'questionnaire-info-edit',
- components: {
- StudipWysiwyg
+ mixins: [ QuestionnaireComponent ],
+ created() {
+ this.setDefaultValues({
+ url: '',
+ description: ''
+ });
},
- props: {
- value: {
- type: Object,
- required: false,
- default() {
- return {
- url: '',
- description: ''
- };
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data () {
- return {
- val_clone: this.value,
- };
+ mounted() {
+ this.$refs.infoUrl.focus();
+ this.checkValidity();
},
methods: {
checkValidity() {
@@ -53,15 +40,6 @@ export default {
this.$refs.infoUrl.reportValidity();
}
}
- },
- mounted() {
- this.$refs.infoUrl.focus();
- this.checkValidity();
- },
- watch: {
- value (new_val) {
- this.val_clone = new_val;
- }
}
}
</script>
diff --git a/resources/vue/components/questionnaires/RangescaleEdit.vue b/resources/vue/components/questionnaires/RangescaleEdit.vue
index 91aec1c..cd7ce3b 100644
--- a/resources/vue/components/questionnaires/RangescaleEdit.vue
+++ b/resources/vue/components/questionnaires/RangescaleEdit.vue
@@ -3,68 +3,25 @@
<div class="formpart" tabindex="0" ref="autofocus">
{{ $gettext('Einleitungstext') }}
- <studip-wysiwyg v-model="val_clone.description"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
- <span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span>
-
- <table class="default nohover">
- <thead>
- <tr>
- <th class="dragcolumn"></th>
- <th>{{ $gettext('Aussagen') }}</th>
- <th v-for="i in (val_clone.maximum - val_clone.minimum + 1)" :key="i" class="number">{{ (val_clone.minimum - 1 + i) }}</th>
- <th v-if="val_clone.alternative_answer.trim().length > 0">{{ val_clone.alternative_answer }}</th>
- <th class="actions"></th>
- </tr>
- </thead>
- <draggable v-model="val_clone.statements" handle=".dragarea" tag="tbody" class="statements">
- <tr v-for="(statement, index) in val_clone.statements" :key="index">
- <td class="dragcolumn">
- <a class="dragarea"
- tabindex="0"
- :title="$gettextInterpolate($gettext('Sortierelement für Aussage %{statement}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {statement: statement})"
- @keydown="keyHandler($event, index)"
- :ref="'draghandle_' + index">
- <span class="drag-handle"></span>
- </a>
- </td>
- <td>
- <input type="text"
- :ref="'statement_' + index"
- :placeholder="$gettext('Aussage')"
- @paste="(ev) => onPaste(ev, index)"
- v-model="val_clone.statements[index]">
- </td>
- <td v-for="i in (val_clone.maximum - val_clone.minimum + 1)" :key="i">
- <input type="radio" disabled :title="i + val_clone.minimum - 1">
- </td>
- <td v-if="val_clone.alternative_answer.trim().length > 0">
- <input type="radio" disabled :title="val_clone.alternative_answer">
- </td>
- <td class="actions">
- <studip-icon name="delete"
- shape="trash"
- :size="20"
- @click.prevent="deleteStatement(index)"
- :title="$gettext('Aussage löschen')"
- ></studip-icon>
- </td>
- </tr>
- </draggable>
- <tfoot>
- <tr>
- <td :colspan="val_clone.maximum - val_clone.minimum + 4 + (val_clone.alternative_answer.trim().length > 0 ? 1 : 0)">
- <studip-icon name="add"
- shape="add"
- :size="20"
- @click.prevent="addStatement()"
- :title="$gettext('Aussage hinzufügen')"
- ></studip-icon>
- </td>
- </tr>
- </tfoot>
- </table>
+ <InputArray v-model="val_clone.statements"
+ :label="$gettext('Aussage')"
+ :label-plural="$gettext('Aussagen')"
+ :additional-colspan="options.length"
+ >
+ <template #header-cells>
+ <th v-for="(option, index) in options" class="option-cell" :key="index">
+ {{ option }}
+ </th>
+ </template>
+ <template #body-cells>
+ <td v-for="(option, index) in options" class="option-cell" :key="index">
+ <input type="radio" disabled :title="option">
+ </td>
+ </template>
+ </InputArray>
<label>
<input type="checkbox" v-model.number="val_clone.mandatory" true-value="1" false-value="0">
@@ -82,135 +39,48 @@
<label>
{{ $gettext('Minimum') }}
- <input type="number" v-model.number="val_clone.minimum" min="1">
+ <input type="number" v-model.number="val_clone.minimum" min="1" :max="val_clone.maximum">
</label>
<label>
{{ $gettext('Ausweichantwort (leer lassen für keine)') }}
- <input type="text" v-model="val_clone.alternative_answer">
+ <input type="text" v-model.trim="val_clone.alternative_answer">
</label>
</div>
</template>
<script>
-import draggable from 'vuedraggable';
+import InputArray from './InputArray.vue';
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-const default_value = () => ({
- description: '',
- statements: ['', '', '', ''],
- mandatory: 0,
- randomize: 0,
- minimum: 1,
- maximum: 5,
- alternative_answer: ''
-});
export default {
- name: 'likert-edit',
- components: {
- draggable,
- },
- props: {
- value: {
- type: Object,
- required: false,
- default() {
- return default_value();
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data() {
- return {
- val_clone: null,
- assistiveLive: ''
- };
- },
- methods: {
- addStatement(val = '', position = null) {
- if (position === null) {
- this.val_clone.statements.push(val || '');
- } else {
- this.val_clone.statements.splice(position, 0, val || '');
- }
- this.$nextTick(() => {
- this.$refs['statement_' + (this.value.statements.length - 1)][0].focus();
- });
- },
- deleteStatement(index) {
- STUDIP.Dialog.confirm(this.$gettext('Wirklich löschen?')).done(() => {
- this.$delete(this.value.statements, index);
- });
- },
- onPaste(ev, position) {
- let data = ev.clipboardData.getData('text').split("\n");
- for (let i = 0; i < data.length; i++) {
- if (data[i].trim()) {
- this.addStatement(data[i], position + i);
- }
- }
- },
- keyHandler(e, index) {
- switch (e.keyCode) {
- case 38: // up
- e.preventDefault();
- if (index > 0) {
- this.moveUp(index);
- this.$nextTick(() => {
- this.$refs['draghandle_' + (index - 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
- {pos: index, listLength: this.val_clone.statements.length}
- );
- });
- }
- break;
- case 40: // down
- e.preventDefault();
- if (index < this.val_clone.statements.length - 1) {
- this.moveDown(index);
- this.$nextTick(() => {
- this.$refs['draghandle_' + (index + 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
- {pos: index + 2, listLength: this.val_clone.statements.length}
- );
- });
- }
- break;
- }
- },
- moveDown(index) {
- this.val_clone.statements.splice(
- index,
- 2,
- this.val_clone.statements[index + 1],
- this.val_clone.statements[index]
- );
- },
- moveUp(index) {
- this.val_clone.statements.splice(
- index - 1,
- 2,
- this.val_clone.statements[index],
- this.val_clone.statements[index - 1]
- );
- },
- },
+ name: 'rangescale-edit',
+ components: { InputArray },
+ mixins: [ QuestionnaireComponent ],
created() {
- this.val_clone = Object.assign({}, default_value(), this.value ?? {});
+ this.setDefaultValues({
+ alternative_answer: '',
+ description: '',
+ mandatory: 0,
+ maximum: 5,
+ minimum: 1,
+ randomize: 0,
+ statements: ['', '', '', '']
+ });
},
mounted() {
this.$refs.autofocus.focus();
},
- watch: {
- val_clone: {
- handler(current) {
- this.$emit('input', current);
- },
- deep: true
+ computed: {
+ options() {
+ let result = [];
+ for (let i = this.val_clone.minimum; i <= this.val_clone.maximum; i += 1) {
+ result.push(i);
+ }
+ if (this.val_clone.alternative_answer.length > 0) {
+ result.push(this.val_clone.alternative_answer);
+ }
+ return result;
}
}
}
diff --git a/resources/vue/components/questionnaires/VoteEdit.vue b/resources/vue/components/questionnaires/VoteEdit.vue
index 62de963..1d6d9cf 100644
--- a/resources/vue/components/questionnaires/VoteEdit.vue
+++ b/resources/vue/components/questionnaires/VoteEdit.vue
@@ -2,10 +2,10 @@
<div class="vote_edit">
<div class="formpart" tabindex="0" ref="autofocus">
{{ $gettext('Frage') }}
- <studip-wysiwyg v-model="val_clone.description" :key="question_id"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
- <input-array v-model="val_clone.options"></input-array>
+ <InputArray v-model="val_clone.options" />
<label>
<input type="checkbox" v-model.number="val_clone.multiplechoice" true-value="1" false-value="0">
@@ -24,47 +24,24 @@
</template>
<script>
-import StudipWysiwyg from "../StudipWysiwyg.vue";
import InputArray from "./InputArray.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
export default {
name: 'vote-edit',
- components: {
- StudipWysiwyg,
- InputArray
+ components: { InputArray },
+ mixins: [QuestionnaireComponent],
+ created() {
+ this.setDefaultValues({
+ description: '',
+ mandatory: '0',
+ multiplechoice: '1',
+ options: ['', '', '', ''],
+ randomize: '0',
+ });
},
- props: {
- value: {
- type: Object,
- required: false,
- default: function () {
- return {};
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data: function () {
- return {
- val_clone: {}
- };
- },
- mounted: function () {
- this.val_clone = this.value;
- if (!this.value.description) {
- this.$emit('input', {
- multiplechoice: 1,
- options: ['', '', '', ''],
- });
- }
+ mounted() {
this.$refs.autofocus.focus();
- },
- watch: {
- value (new_val) {
- this.val_clone = new_val;
- }
}
}
</script>
diff --git a/resources/vue/mixins/QuestionnaireComponent.js b/resources/vue/mixins/QuestionnaireComponent.js
new file mode 100644
index 0000000..277f21c
--- /dev/null
+++ b/resources/vue/mixins/QuestionnaireComponent.js
@@ -0,0 +1,24 @@
+export const QuestionnaireComponent = {
+ props: {
+ value: Object
+ },
+ data () {
+ return {val_clone: this.value};
+ },
+ methods: {
+ setDefaultValues(value) {
+ this.val_clone = Object.assign(value, this.value);
+ }
+ },
+ watch: {
+ val_clone: {
+ handler(current) {
+ this.$emit('input', current);
+ },
+ deep: true
+ },
+ value (new_val) {
+ this.val_clone = new_val;
+ }
+ }
+};