diff --git a/app/views/questionnaire/question_types/likert/likert_answer.php b/app/views/questionnaire/question_types/likert/likert_answer.php index 18475877214d16c8bae78cf4e787aa3592ddeef3..86ccc3eb0c64f5e549f563a931b1d777feba0bb9 100644 --- a/app/views/questionnaire/question_types/likert/likert_answer.php +++ b/app/views/questionnaire/question_types/likert/likert_answer.php @@ -25,7 +25,7 @@ $responseData = isset($response->answerdata['answers']) ? $response->answerdata[ <tr> <th><?= _('Aussage') ?></th> <? foreach ($answers as $answer) : ?> - <th><?= htmlReady($answer) ?></th> + <th class="option-cell"><?= htmlReady($answer) ?></th> <? endforeach ?> </tr> </thead> @@ -35,7 +35,7 @@ $responseData = isset($response->answerdata['answers']) ? $response->answerdata[ <? $html_id = md5(uniqid($index)) ?> <td id="<?= $html_id ?>"><?= htmlReady($statements[$index]) ?></td> <? foreach ($answers as $answer_index => $answer) : ?> - <td> + <td class="option-cell"> <input type="radio" title="<?= htmlReady($answer) ?>" aria-labelledby="<?= $html_id ?>" diff --git a/app/views/questionnaire/question_types/likert/likert_evaluation.php b/app/views/questionnaire/question_types/likert/likert_evaluation.php index fd4eb019eca338adb4b0ff2b0073b16974195797..11ef4b4b98fdfb71c726c566e1db9616976a8e79 100644 --- a/app/views/questionnaire/question_types/likert/likert_evaluation.php +++ b/app/views/questionnaire/question_types/likert/likert_evaluation.php @@ -27,7 +27,7 @@ $options = $vote->questiondata['options']; <tr> <th><?= _('Aussage') ?></th> <? foreach ($options as $option) : ?> - <th><?= htmlReady($option) ?></th> + <th class="option-cell"><?= htmlReady($option) ?></th> <? endforeach ?> </tr> </thead> diff --git a/app/views/questionnaire/question_types/rangescale/rangescale_answer.php b/app/views/questionnaire/question_types/rangescale/rangescale_answer.php index 3525b4cff4856742b4e966f85a4e5bea5e5255d1..43336891f84bfdc97d1c9046e1eba86f3d19b5d6 100644 --- a/app/views/questionnaire/question_types/rangescale/rangescale_answer.php +++ b/app/views/questionnaire/question_types/rangescale/rangescale_answer.php @@ -25,7 +25,7 @@ $responseData = $response['answerdata'] && $response['answerdata']['answers'] ? <tr> <th><?= _('Aussage') ?></th> <? for ($i = $vote->questiondata['minimum'] ?? 1; $i <= $vote->questiondata['maximum']; $i++) : ?> - <th><?= htmlReady($i) ?></th> + <th class="option-cell"><?= htmlReady($i) ?></th> <? endfor ?> </tr> </thead> @@ -35,7 +35,7 @@ $responseData = $response['answerdata'] && $response['answerdata']['answers'] ? <? $html_id = md5(uniqid($index)) ?> <td id="<?= $html_id ?>"><?= htmlReady($statements[$index]) ?></td> <? for ($i = $vote->questiondata['minimum'] ?? 1; $i <= $vote->questiondata['maximum']; $i++) : ?> - <td> + <td class="option-cell"> <input type="radio" title="<?= htmlReady($i) ?>" aria-labelledby="<?= $html_id ?>" diff --git a/app/views/questionnaire/question_types/rangescale/rangescale_evaluation.php b/app/views/questionnaire/question_types/rangescale/rangescale_evaluation.php index 2715934beeadc769310a05133564c9a350168ca1..6b75ea1abe4a1fab0eebfe5fe6a155fbb397346f 100644 --- a/app/views/questionnaire/question_types/rangescale/rangescale_evaluation.php +++ b/app/views/questionnaire/question_types/rangescale/rangescale_evaluation.php @@ -27,7 +27,7 @@ $options = range($vote->questiondata['minimum'], $vote->questiondata['maximum']) <tr> <th><?= _('Aussage') ?></th> <? for ($i = $vote->questiondata['minimum'] ?? 1; $i <= $vote->questiondata['maximum']; $i++) : ?> - <th class="rangescale_center"><?= htmlReady($i) ?></th> + <th class="option-cell"><?= htmlReady($i) ?></th> <? endfor ?> </tr> </thead> diff --git a/resources/assets/stylesheets/scss/questionnaire.scss b/resources/assets/stylesheets/scss/questionnaire.scss index f55b50f36b0d76ae3066cb7593026cbb8ea104b9..fde7d32a2bf1c40ae29adb115cd941046ffec05f 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 29c6f34b81781a832e11a7238ff41db6bc6a5f21..58848edd1d51e58c718734c1b24c41b7d921c06e 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 0fd67db96dbdd8619d8e73918b9f26459ff1ebc8..f57ca58d33acba08253f20d2c611859197e5ef33 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 c87f9fed982ad7d0a4f3264838d2f3b7973e71e1..736be6b93fd586911e4a87661c931e0b9db75513 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 bc5e82970fd72f4909ca2011020c49678214f8de..83d5fa2d5e877b95c9b8d38523086d1c7cd518c8 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 91aec1c24ed16a253d9d84126b981d9634f7be39..cd7ce3b215745987e0e4b1e7123c07a5520996a2 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 62de963e8c36a631a8cba1b660124a8cc1fe5a24..1d6d9cf7c066977535535e4c3b87cfcc52e714ec 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 0000000000000000000000000000000000000000..277f21c87dde745db62eee0063a2896be1d5ce27 --- /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; + } + } +};