From c8d06fae923e69ff015124975813fdc0ba924a08 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms Date: Thu, 4 Jul 2024 15:23:16 +0000 Subject: refactor questionnaire editor to sfc, fixes #4303 Closes #4303 Merge request studip/studip!3155 --- app/views/questionnaire/edit.php | 195 ++---------- lib/models/Freetext.php | 2 +- lib/models/LikertScale.php | 2 +- lib/models/QuestionnaireInfo.php | 2 +- lib/models/RangeScale.php | 2 +- lib/models/Vote.php | 2 +- resources/assets/javascripts/bootstrap/dialog.js | 5 + .../assets/javascripts/bootstrap/questionnaire.js | 5 - resources/assets/javascripts/bootstrap/vue.js | 24 +- resources/assets/javascripts/lib/dialog.js | 5 +- resources/assets/javascripts/lib/questionnaire.js | 204 +----------- resources/assets/stylesheets/scss/forms.scss | 36 ++- .../questionnaires/QuestionnaireEditor.vue | 354 +++++++++++++++++++++ 13 files changed, 440 insertions(+), 398 deletions(-) create mode 100644 resources/vue/components/questionnaires/QuestionnaireEditor.vue diff --git a/app/views/questionnaire/edit.php b/app/views/questionnaire/edit.php index c10857b..e5ef007 100644 --- a/app/views/questionnaire/edit.php +++ b/app/views/questionnaire/edit.php @@ -22,176 +22,31 @@ foreach (get_declared_classes() as $class) { ]; } } + $questionnaire_data = [ - 'id' => $questionnaire->id, - 'title' => $questionnaire['title'], - 'startdate' => $questionnaire->isNew() ? _('sofort') : $questionnaire['startdate'], - 'stopdate' => $questionnaire['stopdate'], - 'copyable' => $questionnaire['copyable'], - 'anonymous' => $questionnaire['anonymous'], - 'editanswers' => $questionnaire['editanswers'], - 'resultvisibility' => $questionnaire['resultvisibility'], + 'anonymous' => $questionnaire->anonymous, + 'copyable' => $questionnaire->copyable, + 'editanswers' => $questionnaire->editanswers, + 'id' => $questionnaire->id, + 'questions' => $questionnaire->questions->map(function ($question) { + return [ + 'id' => $question->id, + 'questiontype' => $question->questiontype, + 'internal_name' => $question->internal_name, + 'questiondata' => $question->questiondata->getArrayCopy(), + ]; + }), + 'resultvisibility' => $questionnaire->resultvisibility, + 'startdate' => $questionnaire->isNew() ? _('sofort') : $questionnaire->startdate, + 'stopdate' => $questionnaire->stopdate, + 'title' => $questionnaire->title, ]; -$questions_data = []; -foreach ($questionnaire->questions as $question) { - $questions_data[] = [ - 'id' => $question->id, - 'questiontype' => $question['questiontype'], - 'internal_name' => $question['internal_name'], - 'questiondata' => $question['questiondata']->getArrayCopy() - ]; -} ?> -
- :data-secure="activateFormSecure"> - -
-
-
- -
-
-

- asImg(['class' => 'text-bottom validation_notes_icon']) ?> - -

-
-
- -
- -
-
-
- -
    -
  • -
-
-
- -
- - -
- -
- - -
- - - - -
-
-
- -
-
-
- - -
-
- -
- - -
- 'STUDIP.Questionnaire.Editor.submit(); return false;']) ?> -
-
+withProps([ + 'as-dialog' => Request::isAjax(), + 'question-data' => $questionnaire_data, + 'question-types' => $questiontypes, + 'range-id' => Request::get('range_id'), + 'range-type' => Request::get('range_type'), + ]) ?> diff --git a/lib/models/Freetext.php b/lib/models/Freetext.php index f241b5a..3e59512 100644 --- a/lib/models/Freetext.php +++ b/lib/models/Freetext.php @@ -52,7 +52,7 @@ class Freetext extends QuestionnaireQuestion implements QuestionType static public function getEditingComponent() { - return ['freetext-edit', '']; + return ['FreetextEdit', '']; } public function beforeStoringQuestiondata($questiondata) diff --git a/lib/models/LikertScale.php b/lib/models/LikertScale.php index 3a055f3..a4c0eb8 100644 --- a/lib/models/LikertScale.php +++ b/lib/models/LikertScale.php @@ -37,7 +37,7 @@ class LikertScale extends QuestionnaireQuestion implements QuestionType static public function getEditingComponent() { - return ['likert-edit', '']; + return ['LikertEdit', '']; } public function beforeStoringQuestiondata($questiondata) diff --git a/lib/models/QuestionnaireInfo.php b/lib/models/QuestionnaireInfo.php index 2bcf25d..e820cee 100644 --- a/lib/models/QuestionnaireInfo.php +++ b/lib/models/QuestionnaireInfo.php @@ -37,7 +37,7 @@ class QuestionnaireInfo extends QuestionnaireQuestion implements QuestionType static public function getEditingComponent() { - return ['questionnaire-info-edit', '']; + return ['QuestionnaireInfoEdit', '']; } public function beforeStoringQuestiondata($questiondata) diff --git a/lib/models/RangeScale.php b/lib/models/RangeScale.php index 20e134a..66ea27d 100644 --- a/lib/models/RangeScale.php +++ b/lib/models/RangeScale.php @@ -46,7 +46,7 @@ class RangeScale extends QuestionnaireQuestion implements QuestionType static public function getEditingComponent() { - return ['rangescale-edit', '']; + return ['RangescaleEdit', '']; } public function getDisplayTemplate() diff --git a/lib/models/Vote.php b/lib/models/Vote.php index b5cd142..21b3359 100644 --- a/lib/models/Vote.php +++ b/lib/models/Vote.php @@ -37,7 +37,7 @@ class Vote extends QuestionnaireQuestion implements QuestionType static public function getEditingComponent() { - return ['vote-edit', '']; + return ['VoteEdit', '']; } public function beforeStoringQuestiondata($questiondata) diff --git a/resources/assets/javascripts/bootstrap/dialog.js b/resources/assets/javascripts/bootstrap/dialog.js index f186307..58d01fd 100644 --- a/resources/assets/javascripts/bootstrap/dialog.js +++ b/resources/assets/javascripts/bootstrap/dialog.js @@ -1,3 +1,8 @@ STUDIP.domReady(function () { STUDIP.Dialog.initialize(); }); + +$(document).on('click', '[data-vue-app] [data-dialog-button] .cancel.button', () => { + STUDIP.Dialog.close(); + return false; +}); diff --git a/resources/assets/javascripts/bootstrap/questionnaire.js b/resources/assets/javascripts/bootstrap/questionnaire.js index 4970b64..3b31764 100644 --- a/resources/assets/javascripts/bootstrap/questionnaire.js +++ b/resources/assets/javascripts/bootstrap/questionnaire.js @@ -1,8 +1,3 @@ -import {dialogReady, ready} from "../lib/ready"; -STUDIP.ready(() => { - STUDIP.Questionnaire.initEditor(); -}); - jQuery(document).on('change', '.show_validation_hints .questionnaire_answer [data-question_type=Vote] input', function() { STUDIP.Questionnaire.Vote.validator.call($(this).closest("article")[0]); }); diff --git a/resources/assets/javascripts/bootstrap/vue.js b/resources/assets/javascripts/bootstrap/vue.js index 64d2492..637241a 100644 --- a/resources/assets/javascripts/bootstrap/vue.js +++ b/resources/assets/javascripts/bootstrap/vue.js @@ -11,7 +11,29 @@ STUDIP.ready(() => { let components = {}; config.components.forEach(component => { const name = component.split('/').reverse()[0]; - components[name] = () => import(`../../../vue/components/${component}.vue`); + components[name] = () => { + // TODO: I wonder if this works with Vue3 + + const temp = import(`../../../vue/components/${component}.vue`); + temp.then(({default: c}) => { + const mounted = c.mounted ?? null; + c.mounted = function (...args) { + if ( + this.$el instanceof Element + && this.$el.querySelector('[data-dialog-button]') + ) { + this.$el.closest('.studip-dialog') + .querySelector('.ui-dialog-buttonpane') + .remove(); + } + if (mounted) { + mounted.call(this, args); + } + }; + return c; + }) + return temp; + }; }); STUDIP.Vue.load().then(async ({createApp, store}) => { diff --git a/resources/assets/javascripts/lib/dialog.js b/resources/assets/javascripts/lib/dialog.js index c602a29..46f9d98 100644 --- a/resources/assets/javascripts/lib/dialog.js +++ b/resources/assets/javascripts/lib/dialog.js @@ -420,7 +420,10 @@ Dialog.show = function(content, options = {}) { }); // Create buttons - if (options.buttons === undefined || (options.buttons && !$.isPlainObject(options.buttons))) { + if ( + options.buttons === undefined + || (options.buttons && !$.isPlainObject(options.buttons)) + ) { dialog_options.buttons = extractButtons.call(this, instance.element); // Create 'close' button if (dialog_options.buttons.cancel === undefined) { diff --git a/resources/assets/javascripts/lib/questionnaire.js b/resources/assets/javascripts/lib/questionnaire.js index 9a89348..5251617 100644 --- a/resources/assets/javascripts/lib/questionnaire.js +++ b/resources/assets/javascripts/lib/questionnaire.js @@ -1,209 +1,7 @@ -import { $gettext } from '../lib/gettext'; -import md5 from 'md5'; -//import html2canvas from "html2canvas"; -//import {jsPDF} from "jspdf"; +import { $gettext } from './gettext'; const Questionnaire = { delayedQueue: [], - Editor: null, - initEditor () { - $('.questionnaire_edit:not(.vueified)').addClass('vueified').each(function () { - STUDIP.Vue.load().then(({createApp}) => { - let form = this; - let components = {}; - let questiontypes = $(form).data('questiontypes'); - for (let i in questiontypes) { - if (questiontypes[i].component[0] && questiontypes[i].component[1]) { - //for plugins to be able to import their vue components: - components[questiontypes[i].component[0]] = () => import(/* webpackIgnore: true */ questiontypes[i].component[1]); - } - } - components.draggable = () => import('vuedraggable'); - components['vote-edit'] = () => import('../../../vue/components/questionnaires/VoteEdit.vue'); - components['freetext-edit'] = () => import('../../../vue/components/questionnaires/FreetextEdit.vue'); - components['likert-edit'] = () => import('../../../vue/components/questionnaires/LikertEdit.vue'); - components['rangescale-edit'] = () => import('../../../vue/components/questionnaires/RangescaleEdit.vue'); - components['questionnaire-info-edit'] = () => import('../../../vue/components/questionnaires/QuestionnaireInfoEdit.vue'); - STUDIP.Questionnaire.Editor = createApp({ - el: form, - components, - data() { - return { - questiontypes, - - questions: $(form).data('questions_data'), - activeTab: 'admin', - hoverTab: null, - data: $(form).data('questionnaire_data'), - form_secured: true, - oldData: { - questions: [], - data: {} - }, - range_type: $(form).data('range_type'), - range_id: $(form).data('range_id'), - editInternalName: null, - tempInternalName: '', - validationNotice: false, - }; - }, - methods: { - addQuestion(questiontype) { - let id = md5(STUDIP.USER_ID + '_QUESTIONTYPE_' + Math.random()); - - this.questions.push({ - id: id, - questiontype: questiontype, - internal_name: '', - questiondata: {}, - }); - - this.activeTab = id; - }, - submit() { - if (!this.data.title) { - this.switchTab('admin'); - this.validationNotice = true; - return; - } - let data = { - title: this.data.title, - copyable: this.data.copyable, - anonymous: this.data.anonymous, - editanswers: this.data.editanswers, - startdate: this.data.startdate, - stopdate: this.data.stopdate, - resultvisibility: this.data.resultvisibility - }; - let questions = []; - for (let i in this.questions) { - questions.push({ - id: this.questions[i].id, - questiontype: this.questions[i].questiontype, - internal_name: this.questions[i].internal_name, - questiondata: Object.assign({}, this.questions[i].questiondata), - }); - } - $.post(STUDIP.URLHelper.getURL('dispatch.php/questionnaire/store/' + (this.data.id || '')), { - questionnaire: data, - questions_data: JSON.stringify(questions), - range_type: this.range_type, - range_id: this.range_id - }).done(() => { - this.form_secured = false; - this.$nextTick(() => { - location.reload(); - }); - }).fail(() => { - STUDIP.Report.error('Could not save questionnaire.'); - }); - }, - getIndexForQuestion: function (question_id) { - for (let i in this.questions) { - if (this.questions[i].id === question_id || this.questions[i].id === question_id.substring(5)) { - return typeof i === "string" ? parseInt(i, 10) : i; - } - } - }, - duplicateQuestion: function (question_id) { - let i = this.getIndexForQuestion(question_id); - let id = md5(STUDIP.USER_ID + '_QUESTIONTYPE_' + Math.random()); - this.questions.push({ - id: id, - questiontype: this.questions[i].questiontype, - internal_name: this.questions[i].internal_name, - questiondata: JSON.parse(JSON.stringify(this.questions[i].questiondata)), - }); - this.activeTab = id; - }, - deleteQuestion(question_id) { - STUDIP.Dialog.confirm(this.$gettext('Wirklich löschen?')).done(() => { - this.$delete(this.questions, this.getIndexForQuestion(question_id)); - this.switchTab('add_question'); - }) - }, - switchTab(tab_id) { - this.activeTab = tab_id; - this.$nextTick(function () { - if (this.$refs.autofocus !== undefined) { - if (Array.isArray(this.$refs.autofocus)) { - if (typeof this.$refs.autofocus[0] !== "undefined") { - this.$refs.autofocus[0].focus(); - } - } else { - this.$refs.autofocus.focus(); - } - } - }); - }, - objectsEqual(obj1, obj2) { - return _.isEqual(obj1, obj2); - }, - renameInternalName(question_id) { - this.editInternalName = question_id; - let index = this.getIndexForQuestion(question_id); - this.tempInternalName = this.questions[index].internal_name; - this.$nextTick(() => { - this.$refs.editInternalName[0].focus(); - }); - }, - saveInternalName(question_id) { - let index = this.getIndexForQuestion(question_id); - this.questions[index].internal_name = this.tempInternalName; - this.editInternalName = null; - }, - moveQuestionDown(question_id) { - let index = this.getIndexForQuestion(question_id); - if (index < this.questions.length - 1) { - let question = this.questions[index]; - this.questions[index] = this.questions[index + 1]; - this.questions[index + 1] = question; - this.$forceUpdate(); - } - }, - moveQuestionUp(question_id) { - let index = this.getIndexForQuestion(question_id); - if (index > 0) { - let question = this.questions[index]; - this.questions[index] = this.questions[index - 1]; - this.questions[index - 1] = question; - this.$forceUpdate(); - } - } - }, - computed: { - activateFormSecure() { - let newData = { - questions: this.questions, - data: this.data - }; - return this.form_secured && !this.objectsEqual(this.oldData, newData); - }, - indexForQuestion() { - for (let i in this.questions) { - if ( - this.questions[i].id === this.activeTab || - this.questions[i].id === this.activeTab.substring(5) - ) { - return typeof i === "string" ? parseInt(i, 10) : i; - } - } - - return null; - }, - }, - mounted() { - this.$refs.autofocus.focus(); - this.oldData = { - questions: [...this.questions], - data: Object.assign({}, this.data) - }; - }, - }); - - }); - }); - }, delayedInterval: null, lastUpdate: null, filtered: {}, diff --git a/resources/assets/stylesheets/scss/forms.scss b/resources/assets/stylesheets/scss/forms.scss index 888cf56..2aa105f 100644 --- a/resources/assets/stylesheets/scss/forms.scss +++ b/resources/assets/stylesheets/scss/forms.scss @@ -622,23 +622,33 @@ form.inline { } .studip-dialog { - form[data-vue-app] { - display: flex; - flex-direction: column; + [data-vue-app] { min-height: 100%; + display: flex; - fieldset { - flex: 0; + > * { + flex: 1; } - footer[data-dialog-button] { - background: var(--white); - border-top-color: var(--base-color-20); - bottom: -0.5em; - margin-top: auto; - padding: 1.3em 0; - position: sticky; - text-align: center; + form { + display: flex; + flex-direction: column; + min-height: 100%; + + > :not(footer[data-dialog-button]) { + flex: 0; + margin-bottom: auto; + } + + footer[data-dialog-button] { + background: var(--white); + border-top-color: var(--base-color-20); + bottom: -0.5em; + margin-top: auto; + padding: 1.3em 0; + position: sticky; + text-align: center; + } } } } diff --git a/resources/vue/components/questionnaires/QuestionnaireEditor.vue b/resources/vue/components/questionnaires/QuestionnaireEditor.vue new file mode 100644 index 0000000..d87305a --- /dev/null +++ b/resources/vue/components/questionnaires/QuestionnaireEditor.vue @@ -0,0 +1,354 @@ + + -- cgit v1.0