diff options
| -rw-r--r-- | app/controllers/admin/plugin.php | 4 | ||||
| -rw-r--r-- | resources/assets/javascripts/bootstrap/forms.js | 191 | ||||
| -rw-r--r-- | resources/vue/components/StudipForm.vue | 249 | ||||
| -rw-r--r-- | templates/forms/form.php | 24 | ||||
| -rw-r--r-- | templates/vue-app.php | 9 |
5 files changed, 289 insertions, 188 deletions
diff --git a/app/controllers/admin/plugin.php b/app/controllers/admin/plugin.php index 0a01083..9d7d589 100644 --- a/app/controllers/admin/plugin.php +++ b/app/controllers/admin/plugin.php @@ -586,13 +586,13 @@ class Admin_PluginController extends AuthenticatedController 'label' => _('Standardbeschreibung des Plugins'), 'type' => 'info', 'value' => $this->metadata['descriptionlong'] ?? $this->metadata['description'], - 'if' => "STUDIPFORM_SELECTEDLANGUAGES.description === 'de_DE'" + 'if' => "i18n.description === 'de_DE'" ], 'manifest_info_en' => [ 'label' => sprintf(_('Standardbeschreibung des Plugins (%s)'), _('Englisch')), 'type' => 'info', 'value' => $this->metadata['descriptionlong_en'] ?? $this->metadata['description_en'] ?? null, - 'if' => "STUDIPFORM_SELECTEDLANGUAGES.description === 'en_GB'" + 'if' => "i18n.description === 'en_GB'" ], 'description_mode' => [ 'label' => _('Modus der neuen Beschreibung'), diff --git a/resources/assets/javascripts/bootstrap/forms.js b/resources/assets/javascripts/bootstrap/forms.js index 7643612..7ee123d 100644 --- a/resources/assets/javascripts/bootstrap/forms.js +++ b/resources/assets/javascripts/bootstrap/forms.js @@ -242,190 +242,13 @@ function createSelect2(element) { } STUDIP.ready(function () { - let forms = window.document.querySelectorAll('form.default.studipform:not(.vueified)'); - if (forms.length > 0) { - STUDIP.Vue.load().then(({createApp}) => { - forms.forEach(f => { - createApp({ - el: f, - data() { - let params = JSON.parse(f.dataset.inputs); - params.STUDIPFORM_REQUIRED = f.dataset.required ? JSON.parse(f.dataset.required) : []; - params.STUDIPFORM_SERVERVALIDATION = f.dataset.server_validation > 0; - params.STUDIPFORM_DISPLAYVALIDATION = false; - params.STUDIPFORM_VALIDATIONNOTES = []; - params.STUDIPFORM_AUTOSAVEURL = f.dataset.autosave; - params.STUDIPFORM_VALIDATION_URL = f.dataset.validation_url; - params.STUDIPFORM_VALIDATED = false; - params.STUDIPFORM_REDIRECTURL = f.dataset.url; - params.STUDIPFORM_INPUTS_ORDER = []; - params.STUDIPFORM_SELECTEDLANGUAGES = {}; - for (let i in JSON.parse(f.dataset.inputs)) { - params.STUDIPFORM_INPUTS_ORDER.push(i); - } - return params; - }, - methods: { - submit: function (e) { - if (this.STUDIPFORM_VALIDATED) { - return; - } - let v = this; - v.STUDIPFORM_VALIDATIONNOTES = []; - this.STUDIPFORM_DISPLAYVALIDATION = true; - - //validation: - let validation_promise = this.validate(); - validation_promise.then(function (validated) { - if (!validated) { - v.$el.scrollIntoView({ - behavior: 'smooth' - }); - return; - } - - if (v.STUDIPFORM_AUTOSAVEURL) { - let params = v.getFormValues(); - params.STUDIPFORM_AUTOSTORE = 1; - - $.ajax({ - url: v.STUDIPFORM_AUTOSAVEURL, - data: params, - type: 'post', - success(output) { - if (output === 'STUDIPFORM_STORE_SUCCESS' && v.STUDIPFORM_REDIRECTURL) { - //The form has been stored successfully: - window.location.href = v.STUDIPFORM_REDIRECTURL; - } else if (output !== 'STUDIPFORM_STORE_SUCCESS') { - Report.error($gettext('Es ist ein Fehler aufgetreten'), output); - } - } - }); - } else { - v.STUDIPFORM_VALIDATED = true; - v.$el.submit(); - } - }); - e.preventDefault(); - }, - getFormValues() { - let v = this; - let params = { - security_token: this.$refs.securityToken.value - }; - Object.keys(v.$data).forEach(function (i) { - if (!i.startsWith('STUDIPFORM_')) { - if (typeof v.$data[i] === 'boolean') { - params[i] = v.$data[i] ? 1 : 0; - } else { - params[i] = v.$data[i]; - } - } - }); - return params; - }, - validate() { - let v = this; - this.STUDIPFORM_VALIDATIONNOTES = []; - - return new Promise((resolve, reject) => { - let validated = v.$el.checkValidity(); - - $(v.$el).find('input, select, textarea').each(function () { - if (!this.validity.valid) { - let note = { - name: this.name, - label: $(this.labels[0]).find('.textlabel').text(), - description: $gettext('Fehler!'), - describedby: this.id - }; - if ($(this).data('validation_requirement')) { - note.description = $(this).data('validation_requirement'); - } - if (this.validity.tooShort) { - note.description = $gettextInterpolate( - $gettext('Geben Sie mindestens %{min} Zeichen ein.'), - {min: this.minLength} - ); - } - if (this.validity.valueMissing) { - if (this.type === 'checkbox') { - note.description = $gettext('Dieses Feld muss ausgewählt sein.'); - } else { - if (this.minLength > 0) { - note.description = $gettextInterpolate( - $gettext('Hier muss ein Wert mit mindestens %{min} Zeichen eingetragen werden.'), - {min: this.minLength} - ); - } else { - note.description = $gettext('Hier muss ein Wert eingetragen werden.'); - } - - } - } - v.STUDIPFORM_VALIDATIONNOTES.push(note); - } - }); - - if (v.STUDIPFORM_SERVERVALIDATION) { - let params = v.getFormValues(); - if (v.STUDIPFORM_AUTOSAVEURL) { - params.STUDIPFORM_AUTOSTORE = 1; - } - params.STUDIPFORM_SERVERVALIDATION = 1; - - $.post(v.STUDIPFORM_VALIDATION_URL, params).done((output) => { - for (let i in output) { - v.STUDIPFORM_VALIDATIONNOTES.push({ - name: output[i].name, - label: output[i].label, - description: output[i].error, - describedby: null - }); - } - validated = v.STUDIPFORM_VALIDATIONNOTES.length < 1; - resolve(validated); - }); - } else { - resolve(validated); - } - }); - }, - setInputs(inputs) { - for (const [key, value] of Object.entries(inputs)) { - if (this[key] !== undefined) { - this[key] = value; - } - } - }, - selectLanguage(input_name, language_id) { - let languages = { - ...this.STUDIPFORM_SELECTEDLANGUAGES - }; - languages[input_name] = language_id; - this.STUDIPFORM_SELECTEDLANGUAGES = languages; - } - }, - computed: { - ordererValidationNotes: function () { - let orderedNotes = []; - for (let i in this.STUDIPFORM_INPUTS_ORDER) { - for (let k in this.STUDIPFORM_VALIDATIONNOTES) { - if (this.STUDIPFORM_VALIDATIONNOTES[k].name === this.STUDIPFORM_INPUTS_ORDER[i]) { - orderedNotes.push(this.STUDIPFORM_VALIDATIONNOTES[k]); - } - } - } - return orderedNotes; - } - }, - mounted () { - $(this.$el).addClass("vueified"); - } - }); - }); - }); - } + // let forms = window.document.querySelectorAll('form.default.studipform:not(.vueified)'); + // if (forms.length > 0) { + // STUDIP.Vue.load().then(({createApp}) => { + // forms.forEach(f => { + // }); + // }); + // } /* * Form elements with the "simplevue" class are meant for forms that just need some vue components diff --git a/resources/vue/components/StudipForm.vue b/resources/vue/components/StudipForm.vue new file mode 100644 index 0000000..d190764 --- /dev/null +++ b/resources/vue/components/StudipForm.vue @@ -0,0 +1,249 @@ +<template> + <form v-cloak + method="post" + :action="form.autostore ? null : form.url" + @submit="submit" + novalidate + :data-secure="this.isSecure" + :id="id" + data-inputs="<?= htmlReady(json_encode($inputs)) ?>" + data-debugmode="<?= htmlReady(json_encode($form->getDebugMode())) ?>" + data-server_validation="<?= $server_validation ? 1 : 0?>" + data-validation_url="<?= htmlReady($_SERVER['REQUEST_URI']) ?>" + class="default studipform" + :class="{collapsable: isCollapsable}"> + + <input type="hidden" :name="csrf.name" :value="csrf.value"> + + <article aria-live="assertive" + class="validation_notes studip" + v-if="form.required.length > 0 || validationNotes.length > 0"> + <header> + <h1> + <studip-icon shape="info-circle" role="info" class="text-bottom validation_notes_icon"></studip-icon> + {{ $gettext('Hinweise zum Ausfüllen des Formulars') }} + </h1> + </header> + <div class="required_note" v-if="form.required.length > 0"> + <div aria-hidden="true"> + {{ $gettext('Pflichtfelder sind mit Sternchen gekennzeichnet.') }} + </div> + <div class="sr-only"> + {{ $gettext('Dieses Formular enthält Pflichtfelder.') }} + </div> + + </div> + <div v-if="displayValidation && validationNotes.length > 0"> + {{ $gettext('Folgende Angaben müssen korrigiert werden, um das Formular abschicken zu können:') }} + <ul> + <li v-for="(note, index) in ordererValidationNotes" + :aria-describedby="note.describedby" + :key="`validation-note-${index}`" + > + {{ note.label.trim() + ": " + note.description }} + </li> + </ul> + </div> + </article> + + <div aria-live="polite"> + <slot v-for="slot in slots" :name="slot"></slot> + </div> + + <footer data-dialog-button> +<!-- <?= \Studip\Button::create($form->getSaveButtonText(), $form->getSaveButtonName(), ['form' => $form_id]) ?>--> +<!-- <? foreach ($form->getButtons() as $button): ?>--> +<!-- <?--> +<!-- $button->attributes['form'] = $form_id;--> +<!-- echo $button;--> +<!-- ?>--> +<!-- <? endforeach ?>--> + </footer> + </form> +</template> +<script> +import StudipIcon from './StudipIcon.vue'; + +let counter = 0; + +export default { + name: 'studip-form', + components: {StudipIcon}, + props: { + form: { + type: Object, + validator(value) { + return 'url' in value + && 'values' in value + && 'autosave' in value + && ('required' in value && Array.isArray(value.required)); + }, + required: true, + }, + isCollapsable: Boolean, + isSecure: Boolean, + requestUrl: String, + inputs: Array, + debugmode: Boolean, + serverValidation: Boolean, + slots: Array, + validationUrl: String, + }, + data() { + return { + ...this.form.values, + + id: `studip-form-${counter++}`, + order: Object.keys(this.form.values), + displayValidation: false, + validationNotes: () => [], + validated: false, + i18n: {}, + }; + }, + methods: { + submit(e) { + if (this.validated) { + return; + } + this.validationNotes = []; + this.displayValidation = true; + + //validation: + (this.validate()).then((validated) => { + if (!validated) { + this.$el.scrollIntoView({ + behavior: 'smooth' + }); + return; + } + + if (this.form.autosave) { + let params = this.getFormValues(); + params.STUDIPFORM_AUTOSTORE = 1; + + $.post(this.requestUrl, params).done((output) => { + if (output !== 'STUDIPFORM_STORE_SUCCESS') { + //The form has not been stored successfully: + Report.error(this.$gettext('Es ist ein Fehler aufgetreten'), output); + } else if (this.form.url) { + window.location.href = this.form.url; + } + }); + } else { + this.validated = true; + this.$el.submit(); + } + }); + e.preventDefault(); + }, + getFormValues() { + let params = { + security_token: this.$refs.securityToken.value + }; + Object.keys(this.$data).forEach((i) => { + if (!i.startsWith('STUDIPFORM_')) { + if (typeof this.$data[i] === 'boolean') { + params[i] = this.$data[i] ? 1 : 0; + } else { + params[i] = this.$data[i]; + } + } + }); + return params; + }, + validate() { + this.validationNotes = []; + + return new Promise((resolve, reject) => { + let validated = this.$el.checkValidity(); + + this.$el.querySelectorAll('input, select, textarea').forEach(input => { + if (!input.validity.valid) { + let note = { + name: input.name, + label: $(input.labels[0]).find('.textlabel').text(), + description: input.$gettext('Fehler!'), + describedby: input.id + }; + if ($(input).data('validation_requirement')) { + note.description = $(input).data('validation_requirement'); + } + if (input.validity.tooShort) { + note.description = this.$gettextInterpolate( + this.$gettext('Geben Sie mindestens %{min} Zeichen ein.'), + {min: this.minLength} + ); + } + if (input.validity.valueMissing) { + if (this.type === 'checkbox') { + note.description = this.$gettext('Dieses Feld muss ausgewählt sein.'); + } else if (input.minLength > 0) { + note.description = this.$gettextInterpolate( + this.$gettext('Hier muss ein Wert mit mindestens %{min} Zeichen eingetragen werden.'), + {min: input.minLength} + ); + } else { + note.description = this.$gettext('Hier muss ein Wert eingetragen werden.'); + } + } + this.validationNotes.push(note); + } + }); + + if (this.form.serverValidation) { + let params = this.getFormValues(); + if (this.form.autosave) { + params.STUDIPFORM_AUTOSTORE = 1; + } + params.STUDIPFORM_SERVERVALIDATION = 1; + + $.post(this.requestUrl, params).done((output) => { + for (let i in output) { + this.validationNotes.push({ + name: output[i].name, + label: output[i].label, + description: output[i].error, + describedby: null + }); + } + validated = this.validationNotes.length < 1; + resolve(validated); + }); + } else { + resolve(validated); + } + }); + }, + setInputs(inputs) { + for (const [key, value] of Object.entries(inputs)) { + if (this[key] !== undefined) { + this[key] = value; + } + } + }, + selectLanguage(input_name, language_id) { + this.i18n = { + ...this.i18n, + [input_name]: language_id, + }; + } + }, + computed: { + csrf() { + return STUDIP.CSRF_TOKEN; + }, + ordererValidationNotes() { + let orderedNotes = []; + for (let i in this.order) { + for (let k in this.validationNotes) { + if (this.validationNotes[k].name === this.order[i]) { + orderedNotes.push(this.validationNotes[k]); + } + } + } + return orderedNotes; + } + }, +} +</script> diff --git a/templates/forms/form.php b/templates/forms/form.php index fe19404..b6406d0 100644 --- a/templates/forms/form.php +++ b/templates/forms/form.php @@ -19,7 +19,29 @@ foreach ($allinputs as $input) { } } $form_id = md5(uniqid()); -?><form v-cloak + +$vueApp = Studip\VueApp::create('StudipForm'); +foreach ($form->getParts() as $index => $part) { + $vueApp->withSlot('part' . $index, $part->renderWithCondition()); +} +echo $vueApp->withProps([ + 'form'=> [ + 'autosave' => $form->isAutoStoring(), + 'values' => $inputs, + 'required' => $required_inputs, + 'serverValidation' => $server_validation, + 'url' => $form->getURL() ?? false, + ], + + 'debug-mode' => $form->getDebugMode(), + 'is-collapsable' => $form->isCollapsable(), + 'is-secure' => $form->getDataSecure(), + 'request-url' => $_SERVER['REQUEST_URI'], + 'slots' => array_keys($vueApp->getSlots()), +]); +?> + +<form v-cloak method="post" <? if (!$form->isAutoStoring()) : ?> action="<?= htmlReady($form->getURL()) ?>" diff --git a/templates/vue-app.php b/templates/vue-app.php index 2c34929..95a228e 100644 --- a/templates/vue-app.php +++ b/templates/vue-app.php @@ -3,6 +3,7 @@ * @var array $attributes * @var string $baseComponent * @var array $props + * @var array $slots * @var array $storeData */ ?> @@ -10,5 +11,11 @@ <script type="application/json" id="vue-store-data-<?= htmlReady($store) ?>"><?= json_encode($data) ?></script> <? endforeach; ?> <div <?= arrayToHtmlAttributes($attributes) ?>> - <<?= strtokebabcase($baseComponent) ?> <?= arrayToHtmlAttributes($props) ?>/> + <<?= strtokebabcase($baseComponent) ?> <?= arrayToHtmlAttributes($props) ?>> + <? foreach ($slots as $name => $slot): ?> + <template #<?= htmlReady($name) ?>> + <?= $slot ?> + </template> + <? endforeach; ?> + </<?= strtokebabcase($baseComponent) ?>> </div> |
