From 3fb174cb7d12d3b5c354683ce808937fd5493381 Mon Sep 17 00:00:00 2001 From: Rasmus Fuhse Date: Mon, 13 Jun 2022 08:55:14 +0000 Subject: =?UTF-8?q?Resolve=20"Formularbaukasten=20und=20Ank=C3=BCndigungsb?= =?UTF-8?q?earbeitung"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #837 Merge request studip/studip!455 --- app/controllers/news.php | 329 +++++++-------------- app/views/blubber/index.php | 2 +- app/views/news/_actions.php | 16 +- app/views/news/admin_news.php | 12 +- app/views/news/display.php | 10 +- app/views/news/edit_news.php | 334 +-------------------- lib/bootstrap-autoload.php | 1 + lib/classes/CSRFProtection.php | 15 +- lib/classes/forms/CalculatorInput.php | 20 ++ lib/classes/forms/CheckboxInput.php | 23 ++ lib/classes/forms/DatetimepickerInput.php | 25 ++ lib/classes/forms/Fieldset.php | 26 ++ lib/classes/forms/Form.php | 353 +++++++++++++++++++++++ lib/classes/forms/HiddenInput.php | 16 + lib/classes/forms/I18n_formattedInput.php | 49 ++++ lib/classes/forms/I18n_textInput.php | 48 +++ lib/classes/forms/I18n_textareaInput.php | 48 +++ lib/classes/forms/Input.php | 271 +++++++++++++++++ lib/classes/forms/InputRow.php | 13 + lib/classes/forms/MultiselectInput.php | 31 ++ lib/classes/forms/NewsRangesInput.php | 134 +++++++++ lib/classes/forms/NoInput.php | 16 + lib/classes/forms/NumberInput.php | 18 ++ lib/classes/forms/Part.php | 226 +++++++++++++++ lib/classes/forms/QuicksearchInput.php | 18 ++ lib/classes/forms/RangeInput.php | 18 ++ lib/classes/forms/SelectInput.php | 21 ++ lib/classes/forms/TextInput.php | 18 ++ lib/classes/forms/TextareaInput.php | 18 ++ lib/models/NewsRange.class.php | 2 +- lib/modules/NewsWidget.php | 2 +- resources/assets/javascripts/bootstrap/forms.js | 108 +++++++ resources/assets/javascripts/bootstrap/news.js | 10 - resources/assets/javascripts/lib/news.js | 27 -- resources/assets/stylesheets/less/buttons.less | 2 +- resources/assets/stylesheets/less/forms.less | 70 +++++ resources/assets/stylesheets/scss/blubber.scss | 5 - resources/assets/stylesheets/studip.scss | 4 + resources/vue/base-components.js | 12 + resources/vue/components/Datetimepicker.vue | 81 ++++++ resources/vue/components/EditableList.vue | 170 +++++++++++ resources/vue/components/I18nTextarea.vue | 190 ++++++++++++ resources/vue/components/Multiselect.vue | 65 +++++ resources/vue/components/Quicksearch.vue | 20 +- resources/vue/components/RangeInput.vue | 67 +++++ resources/vue/components/StudipWysiwyg.vue | 14 +- resources/vue/components/TextareaWithToolbar.vue | 19 ++ templates/forms/calculator_input.php | 4 + templates/forms/checkbox_input.php | 16 + templates/forms/datetimepicker_input.php | 15 + templates/forms/fieldset.php | 8 + templates/forms/form.php | 67 +++++ templates/forms/hidden_input.php | 4 + templates/forms/i18n_formatted_input.php | 17 ++ templates/forms/i18n_text_input.php | 17 ++ templates/forms/i18n_textarea_input.php | 17 ++ templates/forms/input_row.php | 5 + templates/forms/multiselect_input.php | 17 ++ templates/forms/news_ranges_input.php | 7 + templates/forms/number_input.php | 14 + templates/forms/quicksearch_input.php | 17 ++ templates/forms/range_input.php | 13 + templates/forms/select_input.php | 17 ++ templates/forms/text_input.php | 16 + templates/forms/textarea_input.php | 13 + templates/layouts/base.php | 1 + 66 files changed, 2643 insertions(+), 639 deletions(-) create mode 100644 lib/classes/forms/CalculatorInput.php create mode 100644 lib/classes/forms/CheckboxInput.php create mode 100644 lib/classes/forms/DatetimepickerInput.php create mode 100644 lib/classes/forms/Fieldset.php create mode 100644 lib/classes/forms/Form.php create mode 100644 lib/classes/forms/HiddenInput.php create mode 100644 lib/classes/forms/I18n_formattedInput.php create mode 100644 lib/classes/forms/I18n_textInput.php create mode 100644 lib/classes/forms/I18n_textareaInput.php create mode 100644 lib/classes/forms/Input.php create mode 100644 lib/classes/forms/InputRow.php create mode 100644 lib/classes/forms/MultiselectInput.php create mode 100644 lib/classes/forms/NewsRangesInput.php create mode 100644 lib/classes/forms/NoInput.php create mode 100644 lib/classes/forms/NumberInput.php create mode 100644 lib/classes/forms/Part.php create mode 100644 lib/classes/forms/QuicksearchInput.php create mode 100644 lib/classes/forms/RangeInput.php create mode 100644 lib/classes/forms/SelectInput.php create mode 100644 lib/classes/forms/TextInput.php create mode 100644 lib/classes/forms/TextareaInput.php create mode 100644 resources/vue/components/Datetimepicker.vue create mode 100644 resources/vue/components/EditableList.vue create mode 100644 resources/vue/components/I18nTextarea.vue create mode 100644 resources/vue/components/Multiselect.vue create mode 100644 resources/vue/components/RangeInput.vue create mode 100644 resources/vue/components/TextareaWithToolbar.vue create mode 100644 templates/forms/calculator_input.php create mode 100644 templates/forms/checkbox_input.php create mode 100644 templates/forms/datetimepicker_input.php create mode 100644 templates/forms/fieldset.php create mode 100644 templates/forms/form.php create mode 100644 templates/forms/hidden_input.php create mode 100644 templates/forms/i18n_formatted_input.php create mode 100644 templates/forms/i18n_text_input.php create mode 100644 templates/forms/i18n_textarea_input.php create mode 100644 templates/forms/input_row.php create mode 100644 templates/forms/multiselect_input.php create mode 100644 templates/forms/news_ranges_input.php create mode 100644 templates/forms/number_input.php create mode 100644 templates/forms/quicksearch_input.php create mode 100644 templates/forms/range_input.php create mode 100644 templates/forms/select_input.php create mode 100644 templates/forms/text_input.php create mode 100644 templates/forms/textarea_input.php diff --git a/app/controllers/news.php b/app/controllers/news.php index d9c3717..2571177 100644 --- a/app/controllers/news.php +++ b/app/controllers/news.php @@ -220,8 +220,6 @@ class NewsController extends StudipController $this->route .= "/{$template_id}"; } - $msg_object = new messaging(); - if ($id === 'new') { unset($id); PageLayout::setTitle(_('Ankündigung erstellen')); @@ -236,15 +234,14 @@ class NewsController extends StudipController // load news and comment data and check if user has permission to edit $news = new StudipNews($id); - if (!$news->isNew()) { - $this->comments = StudipComment::GetCommentsForObject($id); - } if (!$news->havePermission('edit') && !$news->isNew()) { throw new AccessDeniedException(); } - if(!$news->isNew()){ + if (!$news->isNew()) { + $this->comments = StudipComment::GetCommentsForObject($id); + $this->assigned = NewsRoles::getRoles($id); if ($this->assigned){ $this->news_isvisible['news_visibility'] = true; @@ -253,38 +250,12 @@ class NewsController extends StudipController } // if form sent, get news data by post vars - if (Request::get('news_isvisible')) { - // visible categories, selected areas, topic, and body are utf8 encoded when sent via ajax - $this->news_isvisible = json_decode(Request::get('news_isvisible'), true); - $this->area_options_selected = json_decode(Request::get('news_selected_areas'), true); - $this->area_options_selectable = json_decode(Request::get('news_selectable_areas'), true); - - $news->topic = Request::i18n('news_topic'); - $news->body = Request::i18n('news_body', null, function ($string) { - if (!$string) { - return $string; - } - return transformBeforeSave(Studip\Markup::purifyHtml($string)); - }); - $news->date = $this->getTimeStamp(Request::get('news_startdate'), 'start'); - $news->expire = $this->getTimeStamp(Request::get('news_enddate'), 'end') - ? $this->getTimeStamp(Request::get('news_enddate'), 'end') - $news->date - : ''; - $news->allow_comments = Request::bool('news_allow_comments', false); - $news->prio = Request::int('news_prio', 0); - $assignedroles = Request::intArray('assignedroles',false); - - $this->assigned = NewsRoles::load($assignedroles); - if ($this->assigned){ - $this->news_isvisible['news_visibility'] = true; - } - } elseif ($id) { + if ($id) { // if news id given check for valid id and load ranges if ($news->isNew()) { PageLayout::postError(_('Die Ankündigung existiert nicht!')); return $this->render_nothing(); } - $ranges = $news->news_ranges->toArray(); } elseif ($template_id) { // otherwise, load data from template $news_template = new StudipNews($template_id); @@ -298,6 +269,7 @@ class NewsController extends StudipController return $this->render_nothing(); } $ranges = $news_template->news_ranges->toArray(); + // remove those ranges for which user doesn't have permission foreach ($ranges as $key => $news_range) if (!$news->haveRangePermission('edit', $news_range['range_id'])) { @@ -324,197 +296,110 @@ class NewsController extends StudipController $ranges[] = $add_range->toArray(); } } - // build news var for template - $this->news = $news; - - // treat faculties and institutes as one area group (inst) - foreach ($ranges as $range) { - switch ($range['type']) { - case 'fak' : - $this->area_options_selected['inst'][$range['range_id']] = $range['name']; - break; - default: - $this->area_options_selected[$range['type']][$range['range_id']] = (string) $range['name']; - } - } - - // define search presets - $this->search_presets['user'] = _('Meine Profilseite'); - if ($GLOBALS['perm']->have_perm('autor') && !$GLOBALS['perm']->have_perm('admin')) { - $my_sem = $this->search_area('__THIS_SEMESTER__'); - if (is_array($my_sem['sem']) && count($my_sem['sem'])) - $this->search_presets['sem'] = _('Meine Veranstaltungen im aktuellen Semester') . ' (' . count($my_sem['sem']) . ')'; - } - if ($GLOBALS['perm']->have_perm('autor') && !$GLOBALS['perm']->have_perm('admin')) { - $my_nextsem = $this->search_area('__NEXT_SEMESTER__'); - if (is_array($my_nextsem['sem']) && count($my_nextsem['sem'])) - $this->search_presets['nextsem'] = _('Meine Veranstaltungen im nächsten Semester') . ' (' . count($my_nextsem['sem']) . ')'; - } - if ($GLOBALS['perm']->have_perm('dozent') && !$GLOBALS['perm']->have_perm('root')) { - $my_inst = $this->search_area('__MY_INSTITUTES__'); - if (count($my_inst)) - $this->search_presets['inst'] = _('Meine Einrichtungen') . ' (' . count($my_inst['inst']) . ')'; - } - if ($GLOBALS['perm']->have_perm('root')) { - $this->search_presets['global'] = $this->area_structure['global']['title']; - } - - // perform search - if (Request::submitted('area_search') || Request::submitted('area_search_preset')) { - $this->news_isvisible['news_areas'] = true; - $this->anker = 'news_areas'; - $this->search_term = Request::get('area_search_term'); - if (Request::submitted('area_search')) { - $this->area_options_selectable = $this->search_area($this->search_term); - } else { - $this->current_search_preset = Request::option('search_preset'); - if ($this->current_search_preset === 'inst') { - $this->area_options_selectable = $my_inst; - } elseif ($this->current_search_preset === 'sem') { - $this->area_options_selectable = $my_sem; - } elseif ($this->current_search_preset === 'nextsem') { - $this->area_options_selectable = $my_nextsem; - } elseif ($this->current_search_preset === 'user') { - $this->area_options_selectable = ['user' => [$GLOBALS['user']->id => get_fullname()]]; - } elseif ($this->current_search_preset === 'global') { - $this->area_options_selectable = ['global' => ['studip' => _('Stud.IP')]]; - } - } - if (!count($this->area_options_selectable)) { - unset($this->search_term); - } else { - // already assigned areas won't be selectable - foreach($this->area_options_selected as $type => $data) { - foreach ($data as $id => $title) { - unset($this->area_options_selectable[$type][$id]); - } - } - } - } - // delete comment(s) - if (Request::submitted('delete_marked_comments')) { - $this->anker = 'news_comments'; - $this->flash['question_text'] = delete_comments(Request::optionArray('mark_comments')); - $this->flash['question_param'] = ['mark_comments' => Request::optionArray('mark_comments'), - 'delete_marked_comments' => 1]; - // reload comments - if (!$this->flash['question_text']) { - $this->comments = StudipComment::GetCommentsForObject($id); - } - } - if ($news->havePermission('delete')) { - $this->comments_admin = true; - } - if (is_array($this->comments)) { - foreach ($this->comments as $key => $comment) { - if (Request::submitted('news_delete_comment_'.$comment['comment_id'])) { - $this->anker = 'news_comments'; - $this->flash['question_text'] = delete_comments($comment['comment_id']); - $this->flash['question_param'] = ['mark_comments' => [$comment['comment_id']], - 'delete_marked_comments' => 1]; - } - } - } - // open / close category - foreach($this->news_isvisible as $category => $value) { - if (Request::get($category . '_js') == 'toggle') { - $this->news_isvisible[$category] = !$this->news_isvisible[$category]; - $this->anker = $category; - } - } - // add / remove areas - if (Request::submitted('news_add_areas') && is_array($this->area_options_selectable)) { - $this->news_isvisible['news_areas'] = true; - - $this->anker = 'news_areas'; - foreach (Request::optionArray('area_options_selectable') as $range_id) { - foreach ($this->area_options_selectable as $type => $data) { - if (isset($data[$range_id])) { - $this->area_options_selected[$type][$range_id] = $data[$range_id]; - unset($this->area_options_selectable[$type][$range_id]); - } - } - } - } - if (Request::submitted('news_remove_areas') && is_array($this->area_options_selected)) { - $this->news_isvisible['news_areas'] = true; - - $this->anker = 'news_areas'; - foreach (Request::optionArray('area_options_selected') as $range_id) { - foreach ($this->area_options_selected as $type => $data) { - if (isset($data[$range_id])) { - $this->area_options_selectable[$type][$range_id] = $data[$range_id]; - unset($this->area_options_selected[$type][$range_id]); - } - } - } - } - // prepare to save news - if (Request::submitted('save_news') && Request::isPost()) { - CSRFProtection::verifySecurityToken(); - //prepare ranges array for already assigned news_ranges - foreach($news->getRanges() as $range_id) { - $this->ranges[$range_id] = get_object_type($range_id, ['global', 'fak', 'inst', 'sem', 'user']); - } - - // check if new ranges must be added - foreach ($this->area_options_selected as $type => $area_group) { - foreach ($area_group as $range_id => $area_title) { - if (!isset($this->ranges[$range_id])) { - if ($news->haveRangePermission('edit', $range_id)) { - $news->addRange($range_id); - } else { - PageLayout::postError(sprintf(_('Sie haben keine Berechtigung zum Ändern der Bereichsverknüpfung für "%s".'), htmlReady($area_title))); - $error++; + foreach ($ranges as $range_array) { + $range = new NewsRange(); + $range['range_id'] = $range_array['range_id']; + $news['news_ranges'][] = $range; + } + + + $this->form = \Studip\Forms\Form::fromSORM( + $news, + [ + 'legend' => _('Grunddaten'), + 'fields' => [ + 'topic' => [ + 'label' => _('Titel'), + 'required' => true + ], + 'body' => [ + 'label' => _('Ankündigungstext'), + 'required' => true, + 'type' => 'i18n_formatted' + ], + 'hgroup1' => new \Studip\Forms\InputRow( + [ + 'name' => 'date', + 'label' => _('Beginn'), + 'type' => 'datetimepicker', + 'required' => true + ], + [ + 'name' => 'expire', + 'label' => _('Ende'), + 'type' => 'datetimepicker', + 'value' => $news['date'] + $news['expire'], + 'mindate' => 'date', + 'mapper' => function ($value, $obj) { //hier müssen wir vom UnixTimestamp noch den Beginn abziehen: + return $value - $obj['date']; + }, + 'required' => true + ], + [ + 'name' => 'days', + 'label' => _('Laufzeit in Tagen'), + 'type' => 'calculator', + 'value' => "Math.floor((expire - date) / 86400)" + ] + ), + 'allow_comments' => [ + 'label' => _('Kommentare zulassen'), + 'type' => 'checkbox' + ], + 'user_id' => [ + 'type' => 'no', + 'mapper' => function () { + return User::findCurrent()->id; } - } - } - } - - // check if assigned ranges must be removed - foreach ($this->ranges as $range_id => $range_type) { - if (($range_type === 'fak' && !isset($this->area_options_selected['inst'][$range_id])) || - ($range_type !== 'fak' && !isset($this->area_options_selected[$range_type][$range_id]))) - { - if ($news->havePermission('unassign', $range_id)) { - $news->deleteRange($range_id); - } else { - PageLayout::postError(_('Sie haben keine Berechtigung zum Ändern der Bereichsverknüpfung.')); - $error++; - } - } - } - - // save news - if ($news->validate() && !$error) { - if ($news->user_id !== $GLOBALS['user']->id) { - $news->chdate_uid = $GLOBALS['user']->id; - setTempLanguage($news->user_id); - $msg = sprintf(_('Ihre Ankündigung "%s" wurde von %s verändert.'), $news->topic, get_fullname() . ' ('.get_username().')'). "\n"; - $msg_object->insert_message($msg, get_username($news->user_id) , "____%system%____", FALSE, FALSE, "1", FALSE, _("Systemnachricht:")." "._("Ankündigung geändert")); - restoreLanguage(); - } else { - $news->chdate_uid = ''; - } - - $news->store(); - - if ($GLOBALS['perm']->have_perm('admin')) { - NewsRoles::update($news->id, $assignedroles); - } + ], + 'author' => [ + 'type' => 'no', + 'mapper' => function () { + return get_fullname(); + } + ] + ] + ], + URLHelper::getURL('?') + )->addSORM( + $news, + [ + 'legend' => _('In weiteren Bereichen anzeigen'), + 'fields' => [ + 'news_ranges' => [ + 'label' => _('Bereich auswählen'), + 'type' => 'NewsRanges', + 'required' => true + ] + ] + ] + )->addSORM( + $news, + [ + 'legend' => _('Sichtbarkeitseinstellungen'), + 'fields' => [ + 'prio' => [ + 'label' => _('Priorität'), + 'type' => 'range' + ], + 'newsroles' => [ + 'permission' => $GLOBALS['perm']->have_perm('admin'), + 'label' => _('Sichtbarkeit'), + 'value' => $news->news_roles->pluck('roleid'), + 'type' => 'multiselect', + 'options' => array_map(function ($r) { return $r->getRolename(); }, RolePersistence::getAllRoles()), + 'store' => function ($value, $input) { + $news = $input->getContextObject(); + NewsRoles::update($news->id, $value); + } + ] + ] + ] + )->setCollapsable() + ->autoStore(); - PageLayout::postSuccess(_('Die Ankündigung wurde gespeichert.')); - if (!Request::isXhr() && !$id) { - // in fallback mode redirect to edit page with proper news id - $this->redirect('news/edit_news/' . $news->id); - } elseif (Request::isXhr()) { - // if in dialog mode send empty result (STUDIP.News closes dialog and initiates reload) - $this->render_nothing(); - } - } - } // check if user has full permission on news object if ($news->havePermission('delete')) { $this->may_delete = true; @@ -692,8 +577,8 @@ class NewsController extends StudipController _('Ankündigung erstellen'), $this->url_for('news/edit_news/new'), Icon::create('news+add'), - ['rel' => 'get_dialog', 'target' => '_blank'] - ); + ['target' => '_blank'] + )->asDialog(); $this->sidebar->addWidget($widget); } diff --git a/app/views/blubber/index.php b/app/views/blubber/index.php index 0daceb6..7392fa4 100644 --- a/app/views/blubber/index.php +++ b/app/views/blubber/index.php @@ -2,7 +2,7 @@ data-active_thread="getId()) ?>" data-thread_data=" []])) ?>" data-threads_more_down="" - :class="waiting ? 'waiting' : ''"> + :class="waiting ? 'waiting' : ''" v-cloak>
diff --git a/app/views/news/_actions.php b/app/views/news/_actions.php index 15daf3c..343cd10 100644 --- a/app/views/news/_actions.php +++ b/app/views/news/_actions.php @@ -5,8 +5,8 @@ -"> - + + NEWS_DISPLAY >= 2 || $new->havePermission('edit')): ?> @@ -24,10 +24,10 @@ if ($new['allow_comments']) : - asImg() ?> + - asImg() ?> + @@ -37,17 +37,17 @@ if ($new['allow_comments']) : havePermission('edit')): ?> - - asImg(); ?> + + havePermission('unassign', $range)): ?> - asImg(); ?> + havePermission('delete')): ?> - asImg(); ?> + diff --git a/app/views/news/admin_news.php b/app/views/news/admin_news.php index 17e2828..95c819f 100644 --- a/app/views/news/admin_news.php +++ b/app/views/news/admin_news.php @@ -124,26 +124,26 @@ $menu->addLink( $controller->url_for('news/edit_news/' . $news['object']->news_id), _('Ankündigung bearbeiten'), - Icon::create('edit', 'clickable'), - ['rel' => 'get_dialog', 'target' => '_blank'] + Icon::create('edit'), + ['data-dialog' => '', 'target' => '_blank'] ); $menu->addLink( $controller->url_for('news/edit_news/new/template/' . $news['object']->news_id), _('Kopieren, um neue Ankündigung zu erstellen'), - Icon::create('news+export', 'clickable'), - ['rel' => 'get_dialog', 'target' => '_blank'] + Icon::create('news+export'), + ['data-dialog' => '1', 'target' => '_blank'] ); if ($news['object']->havePermission('unassign', $news['range_id'])) { $menu->addButton( 'news_remove_' . $news['object']->news_id . '_' . $news['range_id'], _('Ankündigung aus diesem Bereich entfernen'), - Icon::create('remove', 'clickable') + Icon::create('remove') ); } else { $menu->addButton( 'news_remove_' . $news['object']->news_id . '_' . $news['range_id'], _('Ankündigung löschen'), - Icon::create('trash', 'clickable') + Icon::create('trash') ); } echo $menu->render(); diff --git a/app/views/news/display.php b/app/views/news/display.php index 873a79c..22434f9 100644 --- a/app/views/news/display.php +++ b/app/views/news/display.php @@ -8,18 +8,18 @@ @@ -31,7 +31,7 @@

- asImg(); ?> +

diff --git a/app/views/news/edit_news.php b/app/views/news/edit_news.php index 2559d06..45a48d1 100644 --- a/app/views/news/edit_news.php +++ b/app/views/news/edit_news.php @@ -1,333 +1 @@ - - - htmlReady(json_encode($news_isvisible)), - 'news_selectable_areas' => htmlReady(json_encode($area_options_selectable)), - 'news_selected_areas' => htmlReady(json_encode($area_options_selected)), - 'news_basic_js' => '', - 'news_comments_js' => '', - 'news_areas_js' => '', - 'news_allow_comments' => $news->allow_comments, - 'news_topic' => $news->topic, - 'news_body' => $news->body, - 'news_startdate' => $news->date ? date('d.m.Y H:i', $news->date) : '', - 'news_enddate' => $news->expire ? date('d.m.Y H:i', $news->date + $news->expire) : ''] ?> - - -
- - - - - - - - - - - - - - - - -
> - - - - - - - - - - - - - - - - - - -
- - -
> - - - - - - $comment): ?> - render_partial('../../templates/news/comment-box', compact('index', 'comment')) ?> - - - - - - - - - - -
- _('Markierte Kommentare löschen')]) ?> -
-
- - -
> - - - - - - - - - - - - - -
- -
-
-
-
-
- asInput([ - 'name' => 'news_add_areas', - 'title' => _('In den Suchergebnissen markierte Bereiche der Ankündigung hinzufügen'), - 'formnovalidate' => '', - ]) ?> -

- asInput([ - 'name' => 'news_remove_areas', - 'title' => _('Bei den bereits ausgewählten Bereichen die markierten Bereiche entfernen'), - 'formnovalidate' => '', - ]) ?> -
-
- $area_data) : ?> - - - - - -
-
- -
> - - - - - - - - - - have_perm('admin')) : ?> - - -
- -
- isNew()) : ?> - - - - - - 'close_dialog']) ?> - -
-
- - +render() ?> diff --git a/lib/bootstrap-autoload.php b/lib/bootstrap-autoload.php index 05d424a..fa6ed7b 100644 --- a/lib/bootstrap-autoload.php +++ b/lib/bootstrap-autoload.php @@ -23,6 +23,7 @@ StudipAutoloader::addAutoloadPath('lib/classes/admission/userfilter'); StudipAutoloader::addAutoloadPath('lib/classes/auth_plugins'); StudipAutoloader::addAutoloadPath('lib/classes/calendar'); StudipAutoloader::addAutoloadPath('lib/classes/exportdocument'); +StudipAutoloader::addAutoloadPath('lib/classes/forms'); StudipAutoloader::addAutoloadPath('lib/classes/globalsearch'); StudipAutoloader::addAutoloadPath('lib/classes/helpbar'); StudipAutoloader::addAutoloadPath('lib/classes/librarysearch/resultparsers'); diff --git a/lib/classes/CSRFProtection.php b/lib/classes/CSRFProtection.php index 4a99592..440919e 100644 --- a/lib/classes/CSRFProtection.php +++ b/lib/classes/CSRFProtection.php @@ -49,7 +49,7 @@ class CSRFProtection * This checks the request and throws an InvalidSecurityTokenException if * fails to verify its authenticity. * - * @throws MethodNotAllowed The request has to be unsafe + * @throws MethodNotAllowedException The request has to be unsafe * in terms of RFC 2616. * @throws InvalidSecurityTokenException The request is invalid as the * security token does not match. @@ -139,14 +139,19 @@ class CSRFProtection * * \endcode * + * @param array $attributes Additional attributes to be added to the input * @return string the HTML snippet containing the input element */ - public static function tokenTag() + public static function tokenTag(array $attributes = []) { + $attributes = array_merge($attributes, [ + 'name' => self::TOKEN, + 'value' => self::token(), + ]); + return sprintf( - '', - self::TOKEN, - self::token() + '', + arrayToHtmlAttributes($attributes) ); } } diff --git a/lib/classes/forms/CalculatorInput.php b/lib/classes/forms/CalculatorInput.php new file mode 100644 index 0000000..40c08c1 --- /dev/null +++ b/lib/classes/forms/CalculatorInput.php @@ -0,0 +1,20 @@ +open('forms/calculator_input'); + $template->title = $this->title; + $template->value = $this->value; + $template->attributes = arrayToHtmlAttributes($this->attributes); + return $template->render(); + } + + public function getAllInputNames() + { + return []; + } +} diff --git a/lib/classes/forms/CheckboxInput.php b/lib/classes/forms/CheckboxInput.php new file mode 100644 index 0000000..5616056 --- /dev/null +++ b/lib/classes/forms/CheckboxInput.php @@ -0,0 +1,23 @@ +value ? true : false; + } + + public function render() + { + $template = $GLOBALS['template_factory']->open('forms/checkbox_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $this->value; + $template->id = md5(uniqid()); + $template->required = $this->required; + $template->attributes = arrayToHtmlAttributes($this->attributes); + return $template->render(); + } +} diff --git a/lib/classes/forms/DatetimepickerInput.php b/lib/classes/forms/DatetimepickerInput.php new file mode 100644 index 0000000..9bee82b --- /dev/null +++ b/lib/classes/forms/DatetimepickerInput.php @@ -0,0 +1,25 @@ +attributes as $key => $value) { + if (in_array($key, ['mindate', 'maxdate'])) { + $key = ":".$key; + } + $attributes .= " ".$key.'="'.htmlReady($value).'"'; + } + $template = $GLOBALS['template_factory']->open('forms/datetimepicker_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $this->value; + $template->id = md5(uniqid()); + $template->required = $this->required; + $template->attributes = $attributes; + return $template->render(); + } +} diff --git a/lib/classes/forms/Fieldset.php b/lib/classes/forms/Fieldset.php new file mode 100644 index 0000000..e7bced0 --- /dev/null +++ b/lib/classes/forms/Fieldset.php @@ -0,0 +1,26 @@ +legend = $legend; + } + + public function setLegend($legend) + { + $this->legend = $legend; + } + + public function render() + { + $template = $GLOBALS['template_factory']->open('forms/fieldset'); + $template->legend = $this->legend; + $template->parts = $this->parts; + return $template->render(); + } +} diff --git a/lib/classes/forms/Form.php b/lib/classes/forms/Form.php new file mode 100644 index 0000000..9aba36e --- /dev/null +++ b/lib/classes/forms/Form.php @@ -0,0 +1,353 @@ +addSORM($object, $params); + if ($url) { + $form->setURL($url); + } + return $form; + } + + + /** + * A static constructor for an empty Form object. + * @return Form + */ + public static function create() : Form + { + $form = new static(); + return $form; + } + + + /** + * Adds a new Fieldset to the Form object with the SORM object's fields as + * input fields. These fields can be modified or specified by the $params array. + * @param \SimpleORMap $object + * @param array $params + * @return Form $this + */ + public function addSORM(\SimpleORMap $object, array $params = []) + { + $metadata = $object->getTableMetadata(); + + if ($params['fields']) { + //Setting the label + foreach ($params['fields'] as $fieldname => $fielddata) { + if (is_string($fielddata)) { + $params['fields'][$fieldname] = [ + 'label' => $fielddata + ]; + } + } + //Setting the type and name + foreach ($params['fields'] as $fieldname => $fielddata) { + if (is_array($fielddata)) { + $meta = $metadata['fields'][$fieldname]; + if (!isset($fielddata['type'])) { + if ($meta) { + $fielddata = array_merge(Input::getFielddataFromMeta($meta, $object), $fielddata); + } else { + $fielddata['type'] = 'text'; + } + + $params['fields'][$fieldname] = $fielddata; + } + $params['fields'][$fieldname]['name'] = $fieldname; + } + } + } else { + foreach ($metadata['fields'] as $attribute => $meta) { + if (!in_array($attribute, (array) $params['without'])) { + $fielddata = [ + 'label' => $attribute + ]; + $fielddata = array_merge(Input::getFielddataFromMeta($meta, $object), $fielddata); + + $params['fields'][$attribute] = $fielddata; + } + } + } + foreach ($params['fields'] as $fieldname => $fielddata) { + if (is_array($fielddata) && !array_key_exists('value', $fielddata)) { + if ($object->isField($fieldname)) { + $params['fields'][$fieldname]['value'] = $object[$fieldname]; + } + } + } + foreach ((array) $params['types'] as $fieldname => $type) { + $params['fields'][$fieldname]['type'] = $type; + } + //respect the without param: + foreach ((array) $params['without'] as $fieldname) { + unset($params['fields'][$fieldname]); + } + $fields = $params['fields']; + + //Now initializing the fieldset: + $fieldset = new Fieldset($params['legend'] ?: _("Daten")); + $fieldset->setContextObject($object); + $this->addPart($fieldset); + + foreach ($fields as $fieldname => $fielddata) { + if (is_array($fielddata)) { + $fieldset->addInput($fieldset->getInputFromArray($fielddata)); + } elseif(is_subclass_of($fielddata, Part::class)) { + $fieldset->addPart($fielddata); + } elseif(is_subclass_of($fielddata, Input::class)) { + $fieldset->addInput($fielddata); + } + } + return $this; + } + + /** + * Sets the URL where the Form should be leading after submitting. + * @param $url + * @return Form $this + */ + public function setURL($url) + { + $this->url = $url; + return $this; + } + + /** + * Returns the URL where the Form is leading to after the submit. + * @return string|null + */ + public function getURL() + { + return $this->url; + } + + public function setCollapsable($collapsing = true) + { + $this->collapsable = $collapsing; + return $this; + } + + public function isCollapsable() + { + return $this->collapsable; + } + + /** + * Stores the Form object if this is a POST-request. This also erases the URL so that the auto-save URL + * will be set automatically to the current $_SERVER['REQUEST_URI']. + * @return $this + * @throws \AccessDeniedException + */ + public function autoStore() + { + $this->autoStore = true; + if (\Request::isPost() && \Request::isAjax() && !\Request::isDialog()) { + $this->store(); + \PageLayout::postSuccess(_('Daten wurden gespeichert.')); + die(); + } + return $this; + } + + public function isAutoStoring() + { + return $this->autoStore; + } + + /** + * Adds a callback function that is executed right after the store-method. That callback receives this + * Form object as the only parameter. + * @param callable $c + * @return Form $this + */ + public function addAfterStoreCallback(Callable $c) + { + $this->afterStore[] = $c; + return $this; + } + + /** + * Sets the ID if this form. This ID is only relevant for plugins to identify this Form object. + * @param string|null $id + * @return Form $this + */ + public function setId($id) + { + $this->id = $id; + return $this; + } + + /** + * Returns the ID if this form. This ID is only relevant for plugins to identify this Form object. + * @return string|null + */ + public function getId() + { + return $this->id; + } + + /** + * Returns the number of storing processes + * @return: a number of storing processes. 0 if nothing was stored. + */ + public function store() + { + if (!\CSRFProtection::verifyRequest()) { + throw new \AccessDeniedException(); + } + \NotificationCenter::postNotification('FormWillStore', $this); + + $stored = 0; + + //store by each input + foreach ($this->getAllInputs() as $input) { + $value = $this->getStorableValueFromRequest($input); + if ($value !== null) { + $callback = $this->getStoringCallback($input); + $stored += $callback($value, $input); + } + } + + foreach ($this->parts as $part) { + $context = $part->getContextObject(); + if ($context && method_exists($context, 'store')) { + $stored += $context->store(); + } + } + + foreach ($this->afterStore as $callback) { + if (is_callable($callback)) { + $stored += call_user_func($callback, $this); + } else { + //throw warning if callback is not available: + if ($callback === null) { + $callback = 'NULL'; + } + trigger_error(sprintf('Could not execute callback %s in Form object.', $callback), E_USER_WARNING); + } + } + return $stored; + } + + /** + * Adds a Part object to this form like a fieldset + * @param Part $part + * @return Form|void + */ + public function addPart(Part $part) + { + $part->setParent($this); + $this->parts[] = $part; + } + + /** + * Returns all the Part objects like Fieldsets as an array. + * @return array + */ + public function getParts() : array + { + return $this->parts; + } + + /** + * Returns the last part of the form. If there is none yet, it will create a fieldset and return that. + * @return Part + */ + public function getLastPart() : Part + { + if (count($this->parts) === 0) { + $this->parts[] = new Fieldset(); + } + return $this->parts[count($this->parts) - 1]; + } + + /** + * Renders the whole form as a string. + * @return string + * @throws \Flexi_TemplateNotFoundException + */ + public function render() + { + \NotificationCenter::postNotification('FormWillRender', $this); + $template = $GLOBALS['template_factory']->open('forms/form'); + $template->form = $this; + return $template->render(); + } + + /** + * Returns the function to be used to store the value into the input. If the given Input has no storing + * function it will generate a Closuer to set the value to the SimpleORMap context object. + * @param $input + * @return \Closure|void + */ + protected function getStoringCallback(Input $input) + { + if ($input->store) { + return $input->store; + } + $context = $input->getParent()->getContextObject(); + if ($context && is_subclass_of($context, \SimpleORMap::class)) { + return function ($value) use ($context, $input) { + $context[$input->getName()] = $value; + }; + } + } + + /** + * Returns the value for the Input object from the $_REQUEST. This value will also be mapped by + * the Input's dataMapper function and after that by a special mapper-callback the Input + * probably has. + * @param Input $input + * @return mixed + */ + protected function getStorableValueFromRequest(Input $input) + { + $requestparam = $input->getName(); + $bracket_pos = strpos($requestparam, "["); + if ($bracket_pos !== false) { + $requestparam = substr($requestparam, 0, $bracket_pos); + $value = Request::getArray($requestparam); + foreach ($value as $i => $v) { + $value[$i] = $input->dataMapper($v); + } + } else { + $value = $input->getRequestValue(); + $value = $input->dataMapper($value); + } + if ($input->mapper && is_callable($input->mapper)) { + $mapper = $input->mapper; + $value = $mapper($value, $input->getContextObject()); + } + return $value; + } +} diff --git a/lib/classes/forms/HiddenInput.php b/lib/classes/forms/HiddenInput.php new file mode 100644 index 0000000..24c8c74 --- /dev/null +++ b/lib/classes/forms/HiddenInput.php @@ -0,0 +1,16 @@ +open('forms/hidden_input'); + $template->name = $this->name; + $template->value = $this->value; + $template->id = md5(uniqid()); + $template->attributes = arrayToHtmlAttributes($this->attributes); + return $template->render(); + } +} diff --git a/lib/classes/forms/I18n_formattedInput.php b/lib/classes/forms/I18n_formattedInput.php new file mode 100644 index 0000000..842a767 --- /dev/null +++ b/lib/classes/forms/I18n_formattedInput.php @@ -0,0 +1,49 @@ +attributes['id'])) { + $id = md5(uniqid()); + $this->attributes['id'] = $id; + } else { + $id = $this->attributes['id']; + } + if (!is_object($this->value)) { + $value = $this->value; + } else { + $value = [\I18NString::getDefaultLanguage() => $this->value->original()]; + $value = json_encode(array_merge($value, $this->value->toArray())); + } + + $template = $GLOBALS['template_factory']->open('forms/i18n_formatted_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $value; + $template->id = $id; + $template->required = $this->required; + $template->attributes = $this->attributes; + return $template->render(); + } + + public function getAllInputNames() + { + $all_names = [$this->getName()]; + if (is_object($this->value)) { + foreach (\Config::get()->CONTENT_LANGUAGES as $lang_id => $language) { + if (\I18NString::getDefaultLanguage() !== $lang_id) { + $all_names[] = $this->getName() . '_i18n[' . $lang_id . ']'; + } + } + } + return $all_names; + } + + public function getRequestValue() + { + return \Request::i18n($this->name); + } +} diff --git a/lib/classes/forms/I18n_textInput.php b/lib/classes/forms/I18n_textInput.php new file mode 100644 index 0000000..ae563b0 --- /dev/null +++ b/lib/classes/forms/I18n_textInput.php @@ -0,0 +1,48 @@ +attributes['id'])) { + $id = md5(uniqid()); + $this->attributes['id'] = $id; + } else { + $id = $this->attributes['id']; + } + if (!is_object($this->value)) { + $value = $this->value; + } else { + $value = [\I18NString::getDefaultLanguage() => $this->value->original()]; + $value = json_encode(array_merge($value, $this->value->toArray())); + } + $template = $GLOBALS['template_factory']->open('forms/i18n_text_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $value; + $template->id = $id; + $template->required = $this->required; + $template->attributes = $this->attributes; + return $template->render(); + } + + public function getAllInputNames() + { + $all_names = [$this->getName()]; + if (is_object($this->value)) { + foreach (\Config::get()->CONTENT_LANGUAGES as $lang_id => $language) { + if (\I18NString::getDefaultLanguage() !== $lang_id) { + $all_names[] = $this->getName() . '_i18n[' . $lang_id . ']'; + } + } + } + return $all_names; + } + + public function getRequestValue() + { + return \Request::i18n($this->name); + } +} diff --git a/lib/classes/forms/I18n_textareaInput.php b/lib/classes/forms/I18n_textareaInput.php new file mode 100644 index 0000000..44ba1f2 --- /dev/null +++ b/lib/classes/forms/I18n_textareaInput.php @@ -0,0 +1,48 @@ +attributes['id'])) { + $id = md5(uniqid()); + $this->attributes['id'] = $id; + } else { + $id = $this->attributes['id']; + } + if (!is_object($this->value)) { + $value = $this->value; + } else { + $value = [\I18NString::getDefaultLanguage() => $this->value->original()]; + $value = json_encode(array_merge($value, $this->value->toArray())); + } + $template = $GLOBALS['template_factory']->open('forms/i18n_textarea_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $value; + $template->id = $id; + $template->required = $this->required; + $template->attributes = $this->attributes; + return $template->render(); + } + + public function getAllInputNames() + { + $all_names = [$this->getName()]; + if (is_object($this->value)) { + foreach (\Config::get()->CONTENT_LANGUAGES as $lang_id => $language) { + if (\I18NString::getDefaultLanguage() !== $lang_id) { + $all_names[] = $this->getName() . '_i18n[' . $lang_id . ']'; + } + } + } + return $all_names; + } + + public function getRequestValue() + { + return \Request::i18n($this->name); + } +} diff --git a/lib/classes/forms/Input.php b/lib/classes/forms/Input.php new file mode 100644 index 0000000..3018a5b --- /dev/null +++ b/lib/classes/forms/Input.php @@ -0,0 +1,271 @@ + $status) { + $matches[$key] = substr($status, 1, strlen($status) - 2); + } + $fielddata['attributes']['options'] = $matches; + break; + case 'tinyint': + preg_match("/\((.*)\)/", $meta['type'], $matches); + if ($matches[1] == 1) { + $fielddata['type'] = 'checkbox'; + break; + } + case 'integer': + $fielddata['type'] = 'number'; + break; + case 'text': + if ($object->isI18nField($meta['name'])) { + $fielddata['type'] = 'i18n_textarea'; + } else { + $fielddata['type'] = 'textarea'; + } + break; + default: + if ($object->isI18nField($meta['name'])) { + $fielddata['type'] = 'i18n_text'; + } else { + $fielddata['type'] = 'text'; + } + } + return $fielddata; + } + + /** + * Constructor of the Input class. + * @param $name + * @param $title + * @param $value + * @param $attributes + */ + public function __construct($name, $title, $value, array $attributes = []) + { + $this->name = $name; + $this->title = $title; + $this->value = $value; + $this->attributes = $attributes; + } + + /** + * Sets the parent of this Input object. Usually this is done automatically by the framework in the moment that + * the input is initialized in the Form object. So you usually don't need to call this method on your own. + * @param Part $parent + * @return $this + */ + public function setParent(Part $parent) + { + $this->parent = $parent; + return $this; + } + + /** + * Returns the parent of this Input if there is already one. + * @return null|Part + */ + public function getParent() + { + return $this->parent; + } + + public function dataMapper($value) + { + return $value; + } + + /** + * Returns the name of the given input. Also have a look at the method getAllInputNames if you want to + * provide multiple input elements (like in i18n input fields) within one virtual input. In that case + * this getName method returns the main-input name like the attribute in the SORM class. + * @return null + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the value of this input. + * @return null + */ + public function getValue() + { + return $this->value; + } + + /** + * Returns an array with all names of all inputs that this Input-object has. Normally this is just one + * name because there is only one input. But if you think of i18n-inputs there are possibly more + * textareas - one for each language. In that case this function would return all names of all inputs that + * are present. + * @return string[] + */ + public function getAllInputNames() + { + return [$this->getName()]; + } + + /** + * Renders the Input but maybe encapsulated in a template that is displayed only if a condition is true. + * This is helpful for the if-attribute on the Input like in setIfCondition. + * @return string + */ + public function renderWithCondition() + { + if (!$this->permission) { + return ''; + } + $html = $this->render(); + if (!trim($html)) { + return ''; + } + if ($this->if !== null) { + $html = ''; + } + return $html; + } + + /** + * This renders the Input. + * @return string + */ + abstract public function render(); + + /** + * Returns the context-object which is usually a SimpleORMap object. + * @return null|\SimpleORMap + */ + public function getContextObject() + { + if ($this->getParent()) { + return $this->getParent()->getContextObject(); + } + return null; + } + + /** + * Sets a special mapper-function to turn the request-values into the real values for the database. + * @param callable $callback + * @return $this + */ + public function setMapper(Callable $callback) + { + $this->mapper = $callback; + return $this; + } + + /** + * Sets the storing function. This would override the normal storing-function that just sets the value + * of a given context object like a SORM object. + * @param callable $store + * @return $this + */ + public function setStoringFunction(Callable $store) + { + $this->store = $store; + return $this; + } + + /** + * Sets a condition to display this input. The condition is a javascript condition which is used by vue to + * hide the input if the condition is not satisfies. + * @param string $if + * @return $this + */ + public function setIfCondition($if) + { + $this->if = $if; + return $this; + } + + /** + * Set if the user is able to see and edit this input + * @param boolean $if + * @return $this + */ + public function setPermission(bool $permission) + { + $this->permission = $permission; + return $this; + } + + /** + * Marks the input as a required field. + * @param $required + * @return $this + */ + public function setRequired($required = true) + { + $this->required = $required; + return $this; + } + + /** + * Returns the values from the request. Normally this is \Request::get, but special Input-classes could also + * return arrays or objects. + * @return string|null + */ + public function getRequestValue() + { + return \Request::get($this->name); + } + + protected function extractOptionsFromAttributes(array &$attributes) + { + $options = null; + if (isset($attributes['options'])) { + $options = $attributes['options']; + unset($attributes['options']); + } + return $options; + } +} diff --git a/lib/classes/forms/InputRow.php b/lib/classes/forms/InputRow.php new file mode 100644 index 0000000..fc86648 --- /dev/null +++ b/lib/classes/forms/InputRow.php @@ -0,0 +1,13 @@ +open('forms/input_row'); + $template->parts = $this->parts; + return $template->render(); + } +} diff --git a/lib/classes/forms/MultiselectInput.php b/lib/classes/forms/MultiselectInput.php new file mode 100644 index 0000000..67d9ebf --- /dev/null +++ b/lib/classes/forms/MultiselectInput.php @@ -0,0 +1,31 @@ +extractOptionsFromAttributes($this->attributes); + + $name = $this->name; + if (substr($name, -2) === '[]') { + $name .= substr($name, 0, -2); + } + + $template = $GLOBALS['template_factory']->open('forms/multiselect_input'); + $template->title = $this->title; + $template->name = $name; + $template->value = $this->value; + $template->id = md5(uniqid()); + $template->required = $this->required; + $template->attributes = arrayToHtmlAttributes($this->attributes); + $template->options = $options; + return $template->render(); + } + + public function getRequestValue() + { + return \Request::getArray($this->name); + } +} diff --git a/lib/classes/forms/NewsRangesInput.php b/lib/classes/forms/NewsRangesInput.php new file mode 100644 index 0000000..18d3170 --- /dev/null +++ b/lib/classes/forms/NewsRangesInput.php @@ -0,0 +1,134 @@ +getContextObject(); + $sql = "SELECT CONCAT(`Seminar_id`, '__seminar') AS `range_id`, `name` FROM `seminare` WHERE `name` LIKE :input "; + if ($GLOBALS['perm']->have_perm('admin')) { + $sql .= "UNION SELECT CONCAT(`Institut_id`, '__institute') AS `range_id`, `Name` AS `name` FROM Institute WHERE `name` LIKE :input "; + if (!$GLOBALS['perm']->have_perm('root')) { + $sql .= "AND "; + } + } + if ($GLOBALS['perm']->have_perm('root')) { + $sql .= "UNION SELECT * FROM (SELECT BINARY 'studip__home' AS `range_id`, '"._('Stud.IP-Startseite')."' AS `name`) as tmp_global_table WHERE `name` LIKE :input "; + $sql .= "UNION SELECT CONCAT(`user_id`, '__person') AS `range_id`, CONCAT(`Vorname`, ' ', `Nachname`) AS `name` FROM `auth_user_md5` WHERE CONCAT(`Vorname`, ' ', `Nachname`) LIKE :input "; + } else { + $sql .= "UNION SELECT * FROM (SELECT '".\User::findCurrent()->id."__person' AS `range_id`, '".\addslashes(\User::findCurrent()->getFullName()." - "._('Profilseite'))."' AS `name`) as tmp_user_table WHERE `name` LIKE :input "; + } + $searchtype = new \SQLSearch($sql, _('Bereich suchen')); + $items = []; + $icons = [ + 'global' => 'home', + 'sem' => 'seminar', + 'inst' => 'institute', + 'user' => 'person' + ]; + foreach ($context->{$this->name} as $newsrange) { + $items[] = [ + 'value' => $newsrange->range_id, + 'name' => (string) $newsrange->name, + 'icon' => $icons[$newsrange->type], + 'deletable' => \StudipNews::haveRangePermission('edit', $newsrange->range_id) + ]; + } + + $selectable = []; + $studip_options = []; + if ($GLOBALS['perm']->have_perm('root')) { + $studip_options[] = [ + 'value' => 'studip__home', + 'name' => _('Stud.IP-Startseite'), + ]; + } + $studip_options[] = [ + 'value' => \User::findCurrent()->id . '__person', + 'name' => _('Meine Profilseite') + ]; + $selectable[] = [ + 'label' => _('Stud.IP'), + 'options' => $studip_options + ]; + if ($GLOBALS['perm']->have_perm('admin')) { + $inst_options = []; + foreach (\Institute::getMyInstitutes() as $institut) { + $inst_options[] = [ + 'value' => $institut['Institut_id'] . '__institute', + 'name' => $institut['Name'], + ]; + } + if (count($inst_options)) { + $selectable[] = [ + 'label' => _('Einrichtungen'), + 'options' => $inst_options + ]; + } + } else { + $course_options = []; + foreach (\Course::findByUser(\User::findCurrent()->id) as $course) { + $course_options[] = [ + 'value' => $course->getId()."__seminar", + 'name' => $course['name'] + ]; + } + if (count($course_options)) { + $selectable[] = [ + 'label' => _('Veranstaltungen'), + 'options' => $course_options + ]; + } + } + + $template = $GLOBALS['template_factory']->open('forms/news_ranges_input'); + $template->name = $this->name; + $template->items = $items; + $template->searchtype = $searchtype; + $template->selectable = $selectable; + $template->category_order = ['home', 'institute', 'seminar', 'person']; + return $template->render(); + } + + public function getRequestValue() + { + $new_ranges = \Request::getArray($this->name); + $context = $this->getContextObject(); + if ($context) { + $options = $context->getRelationOptions($this->name); + $old_ranges = array_map(function ($r) { + return $r['range_id']; + }, $context[$this->name]->getArrayCopy()); + + foreach ($new_ranges as $index => $range_id) { + if (!in_array($range_id, $old_ranges)) { + if (!\StudipNews::haveRangePermission('edit', $range_id)) { + unset($new_ranges[$index]); + } + } + } + foreach ($old_ranges as $index => $range_id) { + if (!in_array($range_id, $new_ranges)) { + if (!\StudipNews::haveRangePermission('edit', $range_id)) { + $new_ranges[] = $range_id; + } + } + } + + $class = $options['class_name']; + return array_map(function ($id) use ($class, $context) { + $range = new $class(); + $range['range_id'] = $id; + if (!$context->id) { + $context->setId($context->getNewId()); + } + $range['news_id'] = $context->id; + return $range; + }, $new_ranges); + } + return []; + } +} diff --git a/lib/classes/forms/NoInput.php b/lib/classes/forms/NoInput.php new file mode 100644 index 0000000..c4aae60 --- /dev/null +++ b/lib/classes/forms/NoInput.php @@ -0,0 +1,16 @@ +open('forms/number_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $this->value; + $template->id = md5(uniqid()); + $template->required = $this->required; + $template->attributes = arrayToHtmlAttributes($this->attributes); + return $template->render(); + } +} diff --git a/lib/classes/forms/Part.php b/lib/classes/forms/Part.php new file mode 100644 index 0000000..79c573b --- /dev/null +++ b/lib/classes/forms/Part.php @@ -0,0 +1,226 @@ +addPart($part); + } else { + if (!is_array($part)) { + $part->setParent($this); + } + $this->parts[] = $part; + } + } + } + + /** + * Sets the context-object which is most likely a SimpleORMap object + * @param $object + * @return $this + */ + public function setContextObject($object) + { + $this->contextobject = $object; + return $this; + } + + /** + * Returns the context object of this Part if there is any. If there is none it tries to return the context-object + * of a parent object. + * @return void|null + */ + public function getContextObject() + { + if ($this->contextobject) { + return $this->contextobject; + } elseif ($this->parent) { + return $this->parent->getContextObject(); + } + } + + /** + * Adds a Part object on the next layer. + * @param Part $part + * @return $this + */ + public function addPart(Part $part) + { + $part->setParent($this); + $this->parts[] = $part; + return $this; + } + + /** + * Adds an Input to this Part. + * @param Input $input + * @return Input + */ + public function addInput(Input $input) + { + $input->setParent($this); + $this->parts[] = $input; + return $input; + } + + /** + * Renders this Part object. This could be a section or any other HTML element with child-elements. + * @return string + */ + public function render() + { + return ''; + } + + /** + * Renders the Part element with a condition. + * @return string + */ + public function renderWithCondition() + { + $html = $this->render(); + if (!trim($html)) { + return ''; + } + if ($this->if !== null) { + $html = ''; + } + return $html; + } + + /** + * Recursively returns all Input elements attached to this Part object or any child Parts. + * @return array + */ + public function getAllInputs() + { + $inputs = []; + foreach ($this->parts as $part) { + if (is_subclass_of($part, Input::class) && $part->permission) { + $inputs[] = $part; + } elseif(is_subclass_of($part, Part::class)) { + $inputs = array_merge($inputs, $part->getAllInputs()); + } + } + return $inputs; + } + + /** + * Sets the parent object of this Part. Usually this is done automatically. + * @param Part $parent + * @return $this + * @throws \Exception + */ + public function setParent(Part $parent) + { + $this->parent = $parent; + //Inputs aktualisieren? + foreach ($this->parts as $i => $part) { + if (is_array($part)) { + $input = $this->getInputFromArray($part); + $input->setParent($this); + $this->parts[$i] = $input; + } + } + return $this; + } + + /** + * Sets a condition to display this Part. The condition is a javascript condition which is used by vue to + * hide the input if the condition is not satisfies. + * @param string $if + * @return $this + */ + public function setIfCondition($if) + { + $this->if = $if; + return $this; + } + + /** + * Returns an Input element from an array. + * @param array $data + * @return array|mixed + * @throws \Exception + */ + public function getInputFromArray(array $data) + { + $context = $this->getContextObject(); + if ($context && method_exists($context, 'getTableMetadata')) { + $metadata = $context->getTableMetadata(); + $meta = $metadata['fields'][$data['name']]; + if (!isset($data['type'])) { + if ($meta) { + $data = array_merge(Input::getFielddataFromMeta($meta, $context), $data); + } else { + $data['type'] = 'text'; + } + } + } + if (!isset($data['label'])) { + $data['label'] = $data['name']; + } + + if (!isset($data['value']) && $context && method_exists($context, 'isField')) { + if ($context->isField($data['name'])) { + $data['value'] = $context[$data['name']]; + } + } + if (!$data['type']) { + return $data; + } + + $classname = "\\Studip\\Forms\\".ucfirst($data['type'])."Input"; + $attributes = $data; + unset($attributes['name']); + unset($attributes['label']); + unset($attributes['value']); + unset($attributes['type']); + unset($attributes['mapper']); + unset($attributes['store']); + unset($attributes['if']); + unset($attributes['permission']); + unset($attributes['required']); + unset($attributes['attributes']); + $attributes = array_merge($attributes, (array) $data['attributes']); + if (class_exists($classname)) { + $input = new $classname($data['name'], $data['label'], $data['value'], $attributes); + } elseif (class_exists($data['type'])) { + $classname = $data['type']; + $input = new $classname($data['name'], $data['label'], $data['value'], $attributes); + } else { + //this should not happen: + throw new \Exception(sprintf(_("Klasse %s oder %s existiert nicht."), $classname, $data['type'])); + } + + if ($data['mapper'] && is_callable($data['mapper'])) { + $input->mapper = $data['mapper']; + } + if ($data['store'] && is_callable($data['store'])) { + $input->store = $data['store']; + } + if ($data['if']) { + $input->if = $data['if']; + } + if (isset($data['permission'])) { + $input->permission = $data['permission']; + } + if ($data['required']) { + $input->required = true; + } + return $input; + } +} diff --git a/lib/classes/forms/QuicksearchInput.php b/lib/classes/forms/QuicksearchInput.php new file mode 100644 index 0000000..2531a2e --- /dev/null +++ b/lib/classes/forms/QuicksearchInput.php @@ -0,0 +1,18 @@ +open('forms/checkbox_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $this->value; + $template->id = md5(uniqid()); + $template->required = $this->required; + $template->attributes = arrayToHtmlAttributes($this->attributes); + return $template->render(); + } +} diff --git a/lib/classes/forms/RangeInput.php b/lib/classes/forms/RangeInput.php new file mode 100644 index 0000000..9f99d59 --- /dev/null +++ b/lib/classes/forms/RangeInput.php @@ -0,0 +1,18 @@ +open('forms/range_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $this->value; + $template->id = md5(uniqid()); + $template->required = $this->required; + $template->attributes = arrayToHtmlAttributes($this->attributes); + return $template->render(); + } +} diff --git a/lib/classes/forms/SelectInput.php b/lib/classes/forms/SelectInput.php new file mode 100644 index 0000000..0ba83c9 --- /dev/null +++ b/lib/classes/forms/SelectInput.php @@ -0,0 +1,21 @@ +extractOptionsFromAttributes($this->attributes); + + $template = $GLOBALS['template_factory']->open('forms/select_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $this->value; + $template->id = md5(uniqid()); + $template->required = $this->required; + $template->attributes = arrayToHtmlAttributes($this->attributes); + $template->options = $options; + return $template->render(); + } +} diff --git a/lib/classes/forms/TextInput.php b/lib/classes/forms/TextInput.php new file mode 100644 index 0000000..4ba6eb9 --- /dev/null +++ b/lib/classes/forms/TextInput.php @@ -0,0 +1,18 @@ +open('forms/text_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $this->value; + $template->id = md5(uniqid()); + $template->required = $this->required; + $template->attributes = arrayToHtmlAttributes($this->attributes); + return $template->render(); + } +} diff --git a/lib/classes/forms/TextareaInput.php b/lib/classes/forms/TextareaInput.php new file mode 100644 index 0000000..267c55e --- /dev/null +++ b/lib/classes/forms/TextareaInput.php @@ -0,0 +1,18 @@ +open('forms/textarea_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $this->value; + $template->id = md5(uniqid()); + $template->required = $this->required; + $template->attributes = arrayToHtmlAttributes($this->attributes); + return $template->render(); + } +} diff --git a/lib/models/NewsRange.class.php b/lib/models/NewsRange.class.php index ab20cf1..b13f0cc 100644 --- a/lib/models/NewsRange.class.php +++ b/lib/models/NewsRange.class.php @@ -52,7 +52,7 @@ class NewsRange extends SimpleORMap { switch ($this->type) { case 'global': - return 'Stud.IP'; + return _('Stud.IP-Startseite'); break; case 'sem': return $this->course->name; diff --git a/lib/modules/NewsWidget.php b/lib/modules/NewsWidget.php index c2df73d..5d12608 100644 --- a/lib/modules/NewsWidget.php +++ b/lib/modules/NewsWidget.php @@ -49,7 +49,7 @@ class NewsWidget extends CorePlugin implements PortalPlugin if ($GLOBALS['perm']->have_perm('root')) { $navigation = new Navigation('', 'dispatch.php/news/edit_news/new/studip'); - $navigation->setImage(Icon::create('add', 'clickable', ["title" => _('Ankündigungen bearbeiten')]), ['rel' => 'get_dialog']); + $navigation->setImage(Icon::create('add', Icon::ROLE_CLICKABLE, ['title' => _('Ankündigungen bearbeiten')]), ['data-dialog' => '1']); $icons[] = $navigation; if (Config::get()->NEWS_RSS_EXPORT_ENABLE) { $navigation = new Navigation('', 'dispatch.php/news/rss_config/studip'); diff --git a/resources/assets/javascripts/bootstrap/forms.js b/resources/assets/javascripts/bootstrap/forms.js index 503fdda..60ae9c3 100644 --- a/resources/assets/javascripts/bootstrap/forms.js +++ b/resources/assets/javascripts/bootstrap/forms.js @@ -245,6 +245,114 @@ 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_DISPLAYVALIDATION = false; + params.STUDIPFORM_VALIDATIONNOTES = []; + params.STUDIPFORM_AUTOSAVEURL = f.dataset.autosave; + params.STUDIPFORM_REDIRECTURL = f.dataset.url; + return params; + }, + methods: { + submit: function (e) { + let v = this; + v.STUDIPFORM_VALIDATIONNOTES = []; + this.STUDIPFORM_DISPLAYVALIDATION = true; + + //validation: + let validated = this.validate(); + + if (!validated) { + e.preventDefault(); + v.$el.scrollIntoView({ + "behavior": "smooth" + }); + return; + } + + if (this.STUDIPFORM_AUTOSAVEURL) { + let params = this.getFormValues(); + + $.ajax({ + url: this.STUDIPFORM_AUTOSAVEURL, + data: params, + type: 'post', + success() { + if (v.STUDIPFORM_REDIRECTURL) { + window.location.href = v.STUDIPFORM_REDIRECTURL + } + } + }); + 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 = []; + + let validated = this.$el.checkValidity(); + + $(this.$el).find('input, select, textarea').each(function () { + if (!this.validity.valid) { + let note = { + 'name': $(this.labels[0]).find('.textlabel').text(), + 'description': "Fehler!".toLocaleString(), + 'describedby': this.id + }; + if (this.validity.tooShort) { + note.description = "Geben Sie mindestens %s Zeichen ein.".toLocaleString().replace("%s", this.minLength); + } + if (this.validity.valueMissing) { + if (this.type === 'checkbox') { + note.description = "Dieses Feld muss ausgewählt sein.".toLocaleString(); + } else { + note.description = "Hier muss ein Wert eingetragen werden.".toLocaleString(); + } + } + v.STUDIPFORM_VALIDATIONNOTES.push(note); + } + }); + return validated; + }, + setInputs(inputs) { + for (const [key, value] of Object.entries(inputs)) { + if (this[key] !== undefined) { + this[key] = value; + } + } + } + }, + mounted () { + $(this.$el).addClass("vueified"); + } + }); + }); + }); + } + // Well, this is really nasty: Select2 can't determine the select // element's width if it is hidden (by itself or by it's parent). // This is due to the fact that elements are not rendered when hidden diff --git a/resources/assets/javascripts/bootstrap/news.js b/resources/assets/javascripts/bootstrap/news.js index 6329f16..aa2dc98 100644 --- a/resources/assets/javascripts/bootstrap/news.js +++ b/resources/assets/javascripts/bootstrap/news.js @@ -9,16 +9,6 @@ STUDIP.domReady(() => { } STUDIP.News.pending_ajax_request = false; - $(document).on('click', 'a[rel~="get_dialog"]', function(event) { - event.preventDefault(); - STUDIP.News.get_dialog('news_dialog', $(this).attr('href')); - }); - - $(document).on('click', 'a[rel~="close_dialog"]', function(event) { - event.preventDefault(); - $('#news_dialog').dialog('close'); - }); - // open/close categories without ajax-request $(document).on('click', '.news_category_header', function(event) { event.preventDefault(); diff --git a/resources/assets/javascripts/lib/news.js b/resources/assets/javascripts/lib/news.js index 80575cc..71d221f 100644 --- a/resources/assets/javascripts/lib/news.js +++ b/resources/assets/javascripts/lib/news.js @@ -59,33 +59,6 @@ const News = { }); }, - get_dialog (id, route) { - // initialize dialog - $('body').append(`
`); - $(`#${id}`).dialog({ - modal: true, - height: News.dialog_height, - title: $gettext('Dialog wird geladen...'), - width: News.dialog_width, - close () { - $(`#${id}`).remove(); - } - }); - - // load actual dialog content - $.get(route, 'html').done(function (html, status, xhr) { - $(`#${id}`).dialog('option', 'title', decodeURIComponent(xhr.getResponseHeader('X-Title'))); - $(`#${id}`).html(html); - $(`#${id}_content`).css({ - height : (News.dialog_height - 120) + 'px', - maxHeight: (News.dialog_height - 120) + 'px' - }); - - News.init(id); - }).fail(function () { - window.alert($gettext('Fehler beim Aufruf des News-Controllers')); - }); - }, update_dialog (id, route, form_data) { if (!News.pending_ajax_request) { diff --git a/resources/assets/stylesheets/less/buttons.less b/resources/assets/stylesheets/less/buttons.less index 37e5615..d2b840f 100644 --- a/resources/assets/stylesheets/less/buttons.less +++ b/resources/assets/stylesheets/less/buttons.less @@ -152,9 +152,9 @@ button, border: 0; margin: 0; padding: 0; + cursor: pointer; &[formaction] { - cursor: pointer; color: @base-color; transition: color 0.3s; diff --git a/resources/assets/stylesheets/less/forms.less b/resources/assets/stylesheets/less/forms.less index e4f75ba..5ca970f 100644 --- a/resources/assets/stylesheets/less/forms.less +++ b/resources/assets/stylesheets/less/forms.less @@ -96,6 +96,25 @@ form.default { } } + .formpart { + margin-bottom: @gap; + + output.calculator_result { + display: block; + margin-top: 2.3ex; + } + } + .editablelist { + margin-bottom: @gap; + > li { + margin-bottom: 10px; + &:last-child { + margin-bottom: 0px; + } + } + } + + .label-text { display: inline-block; text-indent: 0.25ex; @@ -190,6 +209,12 @@ form.default { color: red; } } + .studiprequired { + font-weight: bold; + .asterisk { + color: red; + } + } input[type=checkbox], input[type=radio] { vertical-align: text-bottom; @@ -281,6 +306,7 @@ form.default { display: flex; align-items: center; flex-wrap: wrap; + align-items: flex-start; max-width: @max-width-m; &.size-s { @@ -314,6 +340,9 @@ form.default { margin-top: 0; width: auto; } + .quicksearch_container input { + width: 100%; + } } .button { @@ -443,6 +472,11 @@ form.default { } } + .validation_notes_icon { + position: relative; + top: -2px; + } + &.show_validation_hints { :invalid, .invalid { .icon('before', 'exclaim-circle', 'attention', 16, 5px); @@ -452,6 +486,42 @@ form.default { border-left: 4px solid @red; } } + + //designing vue-select in studipform: + .vs__dropdown-toggle { + border-radius: 0px; + } + .vs__selected { + border-radius: 0px; + padding: 5px; + } + + .range_input { + display: flex; + align-items: center; + input[type=range] { + &::-moz-range-track { + height: 11px; + border: 1px solid @content-color; + background-color: transparent; + } + &::-moz-range-progress { + background-color: @base-color; + height: 11px; + } + &::-moz-range-thumb { + border-radius: 0px; + width: 1.2em; + height: 1.2em; + } + &::-moz-range-thumb:hover { + background-color: @content-color; + } + } + output { + margin-left: 10px; + } + } } form.narrow { diff --git a/resources/assets/stylesheets/scss/blubber.scss b/resources/assets/stylesheets/scss/blubber.scss index 3af26ee..49eee3c 100644 --- a/resources/assets/stylesheets/scss/blubber.scss +++ b/resources/assets/stylesheets/scss/blubber.scss @@ -7,11 +7,6 @@ filter: blur(1px); opacity: 0.5; } - [v-if], - [v-for], - [v-show] { - display: none; - } .context_info { .followunfollow { &.loading { diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index ed867ef..1a5bb5e 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -54,3 +54,7 @@ html { overflow: auto; scroll-padding-top: calc(#{$bar-bottom-container-height} + 1em); } + +[v-cloak] { + display: none; +} diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js index b57094e..526687b 100644 --- a/resources/vue/base-components.js +++ b/resources/vue/base-components.js @@ -1,3 +1,5 @@ +import Multiselect from './components/Multiselect.vue'; +import EditableList from "./components/EditableList.vue"; import Quicksearch from './components/Quicksearch.vue'; import StudipActionMenu from './components/StudipActionMenu.vue'; import StudipAssetImg from './components/StudipAssetImg.vue'; @@ -5,6 +7,10 @@ import StudipDateTime from './components/StudipDateTime.vue'; import StudipDialog from './components/StudipDialog.vue'; import StudipFileSize from './components/StudipFileSize.vue'; import StudipIcon from './components/StudipIcon.vue'; +import RangeInput from './components/RangeInput.vue'; +import Datetimepicker from './components/Datetimepicker.vue'; +import TextareaWithToolbar from './components/TextareaWithToolbar.vue'; +import I18nTextarea from "./components/I18nTextarea.vue"; // import StudipLoadingIndicator from './StudipLoadingIndicator.vue'; import StudipMessageBox from './components/StudipMessageBox.vue'; import StudipProxyCheckbox from './components/StudipProxyCheckbox.vue'; @@ -13,19 +19,25 @@ import StudipTooltipIcon from './components/StudipTooltipIcon.vue'; import StudipSelect from './components/StudipSelect.vue'; const BaseComponents = { + Multiselect, + EditableList, Quicksearch, + RangeInput, StudipActionMenu, StudipAssetImg, StudipDateTime, + Datetimepicker, StudipDialog, StudipFileSize, StudipIcon, + I18nTextarea, // StudipLoadingIndicator, StudipMessageBox, StudipProxyCheckbox, StudipProxiedCheckbox, StudipTooltipIcon, StudipSelect, + TextareaWithToolbar }; export default BaseComponents; diff --git a/resources/vue/components/Datetimepicker.vue b/resources/vue/components/Datetimepicker.vue new file mode 100644 index 0000000..3ec930c --- /dev/null +++ b/resources/vue/components/Datetimepicker.vue @@ -0,0 +1,81 @@ + + + diff --git a/resources/vue/components/EditableList.vue b/resources/vue/components/EditableList.vue new file mode 100644 index 0000000..8b22422 --- /dev/null +++ b/resources/vue/components/EditableList.vue @@ -0,0 +1,170 @@ + + + diff --git a/resources/vue/components/I18nTextarea.vue b/resources/vue/components/I18nTextarea.vue new file mode 100644 index 0000000..c4d2976 --- /dev/null +++ b/resources/vue/components/I18nTextarea.vue @@ -0,0 +1,190 @@ + + + diff --git a/resources/vue/components/Multiselect.vue b/resources/vue/components/Multiselect.vue new file mode 100644 index 0000000..e1f1803 --- /dev/null +++ b/resources/vue/components/Multiselect.vue @@ -0,0 +1,65 @@ + + + diff --git a/resources/vue/components/Quicksearch.vue b/resources/vue/components/Quicksearch.vue index 23fd5f4..94b0a3a 100644 --- a/resources/vue/components/Quicksearch.vue +++ b/resources/vue/components/Quicksearch.vue @@ -3,7 +3,7 @@ + v-if="!autocomplete && name"> 0) { @@ -156,7 +157,10 @@ export default { } }, created () { - this.initialize(this.autocomplete ? this.value : this.needle); + this.initialize( + this.value, + this.autocomplete ? this.value : this.needle + ); }, computed: { isVisible() { @@ -168,8 +172,8 @@ export default { this.reset(true); this.initialize(val); }, - inputValue (needle) { - if (this.initialValue !== needle && needle.length > 2) { + inputValue (needle, oldneedle) { + if (oldneedle !== null && (oldneedle !== needle) && needle.length > 2) { this.search(needle); } } diff --git a/resources/vue/components/RangeInput.vue b/resources/vue/components/RangeInput.vue new file mode 100644 index 0000000..d60d525 --- /dev/null +++ b/resources/vue/components/RangeInput.vue @@ -0,0 +1,67 @@ + + + diff --git a/resources/vue/components/StudipWysiwyg.vue b/resources/vue/components/StudipWysiwyg.vue index 707fb55..bdeacfa 100755 --- a/resources/vue/components/StudipWysiwyg.vue +++ b/resources/vue/components/StudipWysiwyg.vue @@ -1,8 +1,9 @@ + + diff --git a/templates/forms/calculator_input.php b/templates/forms/calculator_input.php new file mode 100644 index 0000000..95e33c0 --- /dev/null +++ b/templates/forms/calculator_input.php @@ -0,0 +1,4 @@ +
+ + >{{ }} +
diff --git a/templates/forms/checkbox_input.php b/templates/forms/checkbox_input.php new file mode 100644 index 0000000..b689d69 --- /dev/null +++ b/templates/forms/checkbox_input.php @@ -0,0 +1,16 @@ + + > + required ? 'required aria-required="true"' : '') ?> + > + + title) ?> + + required) : ?> + + + diff --git a/templates/forms/datetimepicker_input.php b/templates/forms/datetimepicker_input.php new file mode 100644 index 0000000..f2d6b92 --- /dev/null +++ b/templates/forms/datetimepicker_input.php @@ -0,0 +1,15 @@ +
+ required ? ' class="studiprequired"' : '') ?> for=""> + + title) ?> + + required) : ?> + + + + required ? 'required aria-required="true"' : '')?> + > +
diff --git a/templates/forms/fieldset.php b/templates/forms/fieldset.php new file mode 100644 index 0000000..491f726 --- /dev/null +++ b/templates/forms/fieldset.php @@ -0,0 +1,8 @@ +
+ + legend) ?> + + + renderWithCondition() ?> + +
diff --git a/templates/forms/form.php b/templates/forms/form.php new file mode 100644 index 0000000..72a1b25 --- /dev/null +++ b/templates/forms/form.php @@ -0,0 +1,67 @@ +getAllInputs(); +$required_inputs = []; +foreach ($allinputs as $input) { + foreach ($input->getAllInputNames() as $name) { + $inputs[$name] = $input->getValue(); + } + + if ($input->required) { + $required_inputs[] = $input->getName(); + } +} +$form_id = md5(uniqid()); +?>
isAutoStoring()) : ?> + action="getURL()) ?>" + + data-autosave="" + data-url="getURL()) ?>" + + @submit="submit" + novalidate + id="" + data-inputs="" + data-required="" + class="default studipformisCollapsable() ? ' collapsable' : '' ?>"> + + 'securityToken']) ?> + +
+
+

+ asImg(17, ['class' => "text-bottom validation_notes_icon"]) ?> + +

+
+
+
+ +
+
+ +
+ +
+
+ +
    +
  • {{ note.name + ": " + note.description }}
  • +
+
+
+ +
+ getParts() as $part) : ?> + renderWithCondition() ?> + +
+
+ +
+ $form_id]) ?> +
diff --git a/templates/forms/hidden_input.php b/templates/forms/hidden_input.php new file mode 100644 index 0000000..27824cd --- /dev/null +++ b/templates/forms/hidden_input.php @@ -0,0 +1,4 @@ +> diff --git a/templates/forms/i18n_formatted_input.php b/templates/forms/i18n_formatted_input.php new file mode 100644 index 0000000..4e667f5 --- /dev/null +++ b/templates/forms/i18n_formatted_input.php @@ -0,0 +1,17 @@ +
+ required ? ' class="studiprequired"' : '') ?> for=""> + + title) ?> + + required) : ?> + + + + > + +
diff --git a/templates/forms/i18n_text_input.php b/templates/forms/i18n_text_input.php new file mode 100644 index 0000000..5e99cd1 --- /dev/null +++ b/templates/forms/i18n_text_input.php @@ -0,0 +1,17 @@ +
+ required ? ' class="studiprequired"' : '') ?> for=""> + + title) ?> + + required) : ?> + + + + + @allinputs="setInputs"> + +
diff --git a/templates/forms/i18n_textarea_input.php b/templates/forms/i18n_textarea_input.php new file mode 100644 index 0000000..3209b68 --- /dev/null +++ b/templates/forms/i18n_textarea_input.php @@ -0,0 +1,17 @@ +
+ required ? ' class="studiprequired"' : '') ?> for=""> + + title) ?> + + required) : ?> + + + + + @allinputs="setInputs"> + +
diff --git a/templates/forms/input_row.php b/templates/forms/input_row.php new file mode 100644 index 0000000..6bf5223 --- /dev/null +++ b/templates/forms/input_row.php @@ -0,0 +1,5 @@ +
+ + renderWithCondition() ?> + +
diff --git a/templates/forms/multiselect_input.php b/templates/forms/multiselect_input.php new file mode 100644 index 0000000..cd9aec6 --- /dev/null +++ b/templates/forms/multiselect_input.php @@ -0,0 +1,17 @@ +
+ required ? ' class="studiprequired"' : '') ?> for=""> + + title) ?> + + required) : ?> + + + + + :options="" + :value="" + v-model="" + id="" + > + +
diff --git a/templates/forms/news_ranges_input.php b/templates/forms/news_ranges_input.php new file mode 100644 index 0000000..ac1fe51 --- /dev/null +++ b/templates/forms/news_ranges_input.php @@ -0,0 +1,7 @@ + + diff --git a/templates/forms/number_input.php b/templates/forms/number_input.php new file mode 100644 index 0000000..622553a --- /dev/null +++ b/templates/forms/number_input.php @@ -0,0 +1,14 @@ +required ? ' class="studiprequired"' : '') ?> for=""> + + title) ?> + + required) : ?> + + + + + > diff --git a/templates/forms/quicksearch_input.php b/templates/forms/quicksearch_input.php new file mode 100644 index 0000000..5a8fadd --- /dev/null +++ b/templates/forms/quicksearch_input.php @@ -0,0 +1,17 @@ +
+ required ? ' class="studiprequired"' : '') ?> for=""> + + title) ?> + + required) : ?> + + + + + required ? 'required aria-required="true"' : '') ?> + > + ' +
diff --git a/templates/forms/range_input.php b/templates/forms/range_input.php new file mode 100644 index 0000000..c85b02d --- /dev/null +++ b/templates/forms/range_input.php @@ -0,0 +1,13 @@ +required ? ' class="studiprequired"' : '') ?> for=""> + + title) ?> + + required) : ?> + + + +> diff --git a/templates/forms/select_input.php b/templates/forms/select_input.php new file mode 100644 index 0000000..52d6776 --- /dev/null +++ b/templates/forms/select_input.php @@ -0,0 +1,17 @@ +
+ required ? ' class="studiprequired"' : '') ?> for=""> + + title) ?> + + required) : ?> + + + + +
diff --git a/templates/forms/text_input.php b/templates/forms/text_input.php new file mode 100644 index 0000000..546a125 --- /dev/null +++ b/templates/forms/text_input.php @@ -0,0 +1,16 @@ +
+ required ? ' class="studiprequired"' : '') ?> for=""> + + title) ?> + + required) : ?> + + + + required ? 'required aria-required="true"' : '') ?> + > +
diff --git a/templates/forms/textarea_input.php b/templates/forms/textarea_input.php new file mode 100644 index 0000000..2afa96c --- /dev/null +++ b/templates/forms/textarea_input.php @@ -0,0 +1,13 @@ +required ? ' class="studiprequired"' : '') ?> for=""> + + title) ?> + + required) : ?> + + + + diff --git a/templates/layouts/base.php b/templates/layouts/base.php index 2528a64..a0dc713 100644 --- a/templates/layouts/base.php +++ b/templates/layouts/base.php @@ -72,6 +72,7 @@ $getInstalledLanguages = function () { value: '' }, INSTALLED_LANGUAGES: , + CONTENT_LANGUAGES: , STUDIP_SHORT_NAME: "STUDIP_SHORT_NAME) ?>", URLHelper: { base_url: "", -- cgit v1.0