diff --git a/app/controllers/news.php b/app/controllers/news.php index d9c371709b5426ae21ddc9fecc3f7db0e5316edc..2571177b047ff83eb4bad6eed59cf44ac341803d 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 0daceb688f614c384638b77279a7942f7f24d48a..7392fa466385cfb4d0b16b214152a4f82b568085 100644 --- a/app/views/blubber/index.php +++ b/app/views/blubber/index.php @@ -2,7 +2,7 @@ data-active_thread="<?= htmlReady($thread->getId()) ?>" data-thread_data="<?= htmlReady(json_encode($thread_data ?: ['thread_posting' => []])) ?>" data-threads_more_down="<?= htmlReady($threads_more_down) ?>" - :class="waiting ? 'waiting' : ''"> + :class="waiting ? 'waiting' : ''" v-cloak> <div id="blubber_stream_container"> <blubber-thread :thread_data="thread_data"></blubber-thread> diff --git a/app/views/news/_actions.php b/app/views/news/_actions.php index 15daf3c15fd73671b0de123da37c4ac67a5dbfe5..343cd102857a91df942cbc925e963e1cae36b934 100644 --- a/app/views/news/_actions.php +++ b/app/views/news/_actions.php @@ -5,8 +5,8 @@ </a> <? endif; ?> -<span class='news_date' title="<?= ($perm ? _("Ablaufdatum") . ': ' . date('d.m.Y', $new['date'] + $new['expire']) : '') ?>"> - <?= date('d.m.Y', $new['date']) ?> +<span class='news_date' title="<?= ($perm ? _('Ablaufdatum') . ': ' . strftime('%x', $new['date'] + $new['expire']) : '') ?>"> + <?= strftime('%x', $new['date']) ?> </span> <? if (Config::get()->NEWS_DISPLAY >= 2 || $new->havePermission('edit')): ?> @@ -24,10 +24,10 @@ if ($new['allow_comments']) : <? if ($num): ?> <? if ($isnew): ?> <span class="news_comments_indicator nowrap" title="<?= sprintf(_('%s neue(r) Kommentar(e)'), $isnew) ?>"> - <?= Icon::create("chat", "new")->asImg() ?> + <?= Icon::create('chat', Icon::ROLE_NEW) ?> <? else: ?> <span class="news_comments_indicator nowrap" title="<?= sprintf(_('%s Kommentare'), $num) ?>"> - <?= Icon::create("chat", "info")->asImg() ?> + <?= Icon::create('chat', Icon::ROLE_INFO) ?> <? endif; ?> <?= $num ?> </span> @@ -37,17 +37,17 @@ if ($new['allow_comments']) : <? if ($new->havePermission('edit')): ?> - <a href="<?= URLHelper::getLink('dispatch.php/news/edit_news/' . $new->id) ?>" rel="get_dialog"> - <?= Icon::create('edit', 'clickable')->asImg(); ?> + <a href="<?= URLHelper::getLink('dispatch.php/news/edit_news/' . $new->id) ?>" data-dialog> + <?= Icon::create('edit') ?> </a> <? if ($new->havePermission('unassign', $range)): ?> <a href=" <?= URLHelper::getLink('', ['remove_news' => $new->id, 'news_range' => $range]) ?>" > - <?= Icon::create('remove', 'clickable')->asImg(); ?> + <?= Icon::create('remove') ?> </a> <? endif; ?> <? if ($new->havePermission('delete')): ?> <a href=" <?= URLHelper::getLink('', ['delete_news' => $new->id]) ?>" > - <?= Icon::create('trash', 'clickable')->asImg(); ?> + <?= Icon::create('trash') ?> </a> <? endif; ?> <? endif; ?> diff --git a/app/views/news/admin_news.php b/app/views/news/admin_news.php index 17e28286154e27125c426429fc27017bb1a9a636..95c819f124c68911ed53d5538e58b283f8744d59 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 873a79c1e0047ae1d7033f83c3f0f186eb01bbfc..22434f9b50093f07ef3f103e481e582a2a8e2d28 100644 --- a/app/views/news/display.php +++ b/app/views/news/display.php @@ -8,18 +8,18 @@ </h1> <nav> <? if ($perm): ?> - <a href="<?= $controller->link_for('news/edit_news/new/' . $range); ?>" rel="get_dialog"> - <?= Icon::create('add', 'clickable')->asImg(); ?> + <a href="<?= $controller->link_for('news/edit_news/new/' . $range); ?>" data-dialog> + <?= Icon::create('add') ?> </a> <? endif; ?> <? if ($perm && Config::get()->NEWS_RSS_EXPORT_ENABLE): ?> <a data-dialog="size=auto;reload-on-close" title="<?=_('RSS-Feed konfigurieren') ?>" href="<?= $controller->link_for('news/rss_config/' . $range); ?>"> - <?= Icon::create('rss+add')->asImg() ?> + <?= Icon::create('rss+add') ?> </a> <? endif; ?> <? if ($rss_id): ?> <a href="<?= URLHelper::getLink('rss.php', ['id' => $rss_id]) ?>"> - <?= Icon::create('rss', 'clickable', ['title' => _('RSS-Feed')])->asImg() ?> + <?= Icon::create('rss')->asImg(['title' => _('RSS-Feed')]) ?> </a> <? endif; ?> </nav> @@ -31,7 +31,7 @@ <header> <h1> <a href="<?= ContentBoxHelper::href($new->id, ['contentbox_type' => 'news']) ?>"> - <?= Icon::create('news', 'clickable')->asImg(); ?> + <?= Icon::create('news') ?> <?= htmlReady($new['topic']); ?> </a> </h1> diff --git a/app/views/news/edit_news.php b/app/views/news/edit_news.php index 2559d065842e0111b591dc09988218424ad0ac0a..45a48d133d2504635b8f3cf84647998abc64bdc5 100644 --- a/app/views/news/edit_news.php +++ b/app/views/news/edit_news.php @@ -1,333 +1 @@ -<? use Studip\Button, Studip\LinkButton; ?> -<? if(!empty($flash['question_text'])) : // TODO: This will certainly break with i18n fields ?> - <? $form_content = ['news_isvisible' => 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) : ''] ?> - <?= (string) QuestionBox::create( - htmlReady($flash['question_text']), - URLHelper::getURL('dispatch.php/'.$route.'#anker', array_merge($flash['question_param'], $form_content)), - URLHelper::getURL('dispatch.php/'.$route.'#anker', $form_content) - );?> -<? endif ?> -<form action="<?=URLHelper::getURL('dispatch.php/'.$route.'#anker')?>" - data-dialog="size=auto" method="POST" class="default collapsable"> - <?=CSRFProtection::tokenTag(); ?> - <input type="hidden" name="news_basic_js" value=""> - <input type="hidden" name="news_comments_js" value=""> - <input type="hidden" name="news_areas_js" value=""> - <input type="hidden" name="news_visibility_js" value=""> - <input type="hidden" name="news_isvisible" value="<?=htmlReady(json_encode($news_isvisible))?>"> - <input type="hidden" name="news_selectable_areas" value="<?=htmlReady(json_encode($area_options_selectable))?>"> - <input type="hidden" name="news_selected_areas" value="<?=htmlReady(json_encode($area_options_selected))?>"> - - <? if (Request::isXhr()) : ?> - <? foreach (PageLayout::getMessages() as $msg) : ?> - <?=$msg?> - <? $anker = ''; ?> - <? endforeach ?> - <? endif ?> - - <fieldset <?= $news_isvisible['news_basic'] ? '' : 'class="collapsed"' ?>> - <legend class="news_category_header" id="news_basic"> - <?=_("Grunddaten")?> - </legend> - - <label> - <span class="required"> - <?= _("Titel") ?> - </span> - <?= I18N::input('news_topic', $news->topic, [ - 'required' => '', - 'class' => 'news_topic news_prevent_submit size-l', - 'aria-label' => _('Titel der Ankündigung'), - ]) ?> - </label> - - <label> - <span class="required"> - <?= _("Inhalt") ?> - </span> - - <?= I18N::textarea('news_body', $news->body, [ - 'required' => '', - 'class' => 'news_body add_toolbar wysiwyg size-l', - 'rows' => 6, - 'wrap' => 'virtual', - 'placeholder' => _('Geben Sie hier den Ankündigungstext ein'), - 'aria-label' => _('Inhalt der Ankündigung'), - ]) ?> - </label> - - <label class="col-2"> - <span class="required"> - <?= _('Veröffentlichungsdatum') ?> - </span> - - <input type="text" class="news_date news_prevent_submit" - name="news_startdate" id="news_startdate" - data-datetime-picker - value="<? if ($news->date) echo strftime('%d.%m.%Y %H:%M', $news->date); ?>" - aria-label="<?= _('Einstelldatum') ?>" required> - </label> - - <label class="col-2"> - <span class="required"> - <?= _('Ablaufdatum') ?> - </span> - - <input type="text" class="news_date news_prevent_submit" - name="news_enddate" id="news_enddate" - data-datetime-picker='{">=":"#news_startdate","offset":"#news_duration"}' - value="<? if ($news->expire) echo strftime('%d.%m.%Y %H:%M', $news->date + $news->expire) ?>" - aria-label="<?= _('Ablaufdatum') ?>" required> - </label> - - <label class="col-2"> - <?= _('Laufzeit in Tagen') ?> - - <input type="number" class="news_date news_prevent_submit" - name="news_duration" id="news_duration" - value="<?= $news->expire ? round($news->expire / (24 * 60 * 60)) : 8 ?>" - aria-label="<?= _('Laufzeit') ?>" - min="1"> - </label> - - <? if ($anker == 'news_comments') : ?> - <a name='anker'></a> - <? endif ?> - <label> - <input type="checkbox" - id="news_allow_comments" name="news_allow_comments" value="1" - <? if ($news->allow_comments) echo 'checked'; ?>> - <?= _('Kommentare zulassen') ?> - </label> - </fieldset> - - <? if (is_array($comments) && count($comments)) : ?> - <fieldset <?= $news_isvisible['news_comments'] ? '' : 'class="collapsed"' ?>> - <legend class="news_category_header" id="news_comments"> - <?=_("Kommentare zu dieser Ankündigung")?> - </legend> - <table class="default nohover"> - <tbody> - <? foreach ($comments as $index => $comment): ?> - <?= $this->render_partial('../../templates/news/comment-box', compact('index', 'comment')) ?> - <? endforeach; ?> - - <? if ($comments_admin): ?> - <tfoot> - <tr> - <td colspan="3"> - <?=Button::create(_('Markierte Kommentare löschen'), 'delete_marked_comments', ['title' => _('Markierte Kommentare löschen')]) ?> - </td> - </tr> - </tfoot> - <? endif ?> - </tbody> - </table> - </fieldset> - <? endif ?> - - <fieldset <?= $news_isvisible['news_areas'] ? '' : 'class="collapsed"' ?>> - <legend class="news_category_header" id="news_areas"> - <?=_('In weiteren Bereichen anzeigen')?> - </legend> - - <? if ($anker == 'news_areas') : ?> - <a name='anker'></a> - <? endif ?> - - <label class="with-action"> - <span> - <?= _('Suchvorlage auswählen') ?> - </span> - - <select name="search_preset" aria-label="<?= _('Vorauswahl bestimmter Bereiche, alternativ zur Suche') ?>" - onchange="jQuery('input[name=area_search_preset]').click()"> - <option><?=_('--- Suchvorlagen ---')?></option> - <? foreach($search_presets as $value => $title) : ?> - <option value="<?=$value?>"<?=($this->current_search_preset == $value) ? ' selected' : '' ?>> - <?=htmlReady($title)?> - </option> - <? endforeach ?> - </select> - - <?= Icon::create('accept')->asInput([ - 'name' => 'area_search_preset', - 'title' => _('Vorauswahl anwenden'), - 'formnovalidate' => '', - ]) ?> - </label> - - <label class="with-action"> - <span> - <?= _('Freitextsuche') ?> - </span> - - <input name="area_search_term" class="news_search_term" type="text" placeholder="<?=_('Suchen')?>" - aria-label="<?= _('Suchbegriff') ?>"> - <?= Icon::create('search')->asInput([ - 'name' => 'area_search', - 'title' => _('Suche starten'), - 'formnovalidate' => '', - ]) ?> - </label> - - - <div class="news_area_selectable"> - <label> - <?=_('Suchergebnis')?> - <select name="area_options_selectable[]" class="news_area_options" size="7" multiple - aria-label="<?= _('Gefundene Bereiche, die der Ankündigung hinzugefügt werden können') ?>" - ondblclick="jQuery('input[name=news_add_areas]').click()"> - <? foreach ($area_structure as $area_key => $area_data) : ?> - <? if (is_array($area_options_selectable[$area_key]) && count($area_options_selectable[$area_key])) : ?> - <optgroup class="news_area_title" - style="background-image: url('<?= Icon::create($area_data['icon'], 'info')->asImagePath() ?>');" label="<?=htmlReady($area_data['title'])?>"> - <? foreach ($area_options_selectable[$area_key] as $area_option_key => $area_option_title) : ?> - <option <?= StudipNews::haveRangePermission('edit', $area_option_key) ? 'value="'.$area_option_key.'"' : 'disabled'?> - <?=tooltip($area_option_title);?>> - <?= htmlReady(mila($area_option_title))?> - </option> - <? endforeach ?> - </optgroup> - <? endif ?> - <? endforeach ?> - </select> - </label> - </div> - <div class="news_area_actions"> - <br> - <br> - <br> - <?= Icon::create('arr_2right')->asInput([ - 'name' => 'news_add_areas', - 'title' => _('In den Suchergebnissen markierte Bereiche der Ankündigung hinzufügen'), - 'formnovalidate' => '', - ]) ?> - <br><br> - <?= Icon::create('arr_2left')->asInput([ - 'name' => 'news_remove_areas', - 'title' => _('Bei den bereits ausgewählten Bereichen die markierten Bereiche entfernen'), - 'formnovalidate' => '', - ]) ?> - </div> - <div class="news_area_selected"> - <? foreach ($area_structure as $area_key => $area_data) : ?> - <? if (isset($area_options_selected[$area_key])) : ?> - <? $area_count += count($area_options_selected[$area_key]) ?> - <? endif ?> - <? endforeach ?> - <label> - <div id="news_area_text"> - <? if ($area_count == 0) : ?> - <?=_('Keine Bereiche ausgewählt')?> - <? elseif ($area_count == 1) : ?> - <?=_('1 Bereich ausgewählt')?> - <? else : ?> - <?=sprintf(_('%s Bereiche ausgewählt'), $area_count)?> - <? endif ?> - </div> - <select name="area_options_selected[]" class="news_area_options" size="7" multiple - aria-label="<?= _('Bereiche, in denen die Ankündigung angezeigt wird') ?>" - ondblclick="jQuery('input[name=news_remove_areas]').click()"> - <? foreach ($area_structure as $area_key => $area_data) : ?> - <? if (isset($area_options_selected[$area_key]) && count($area_options_selected[$area_key])) : ?> - <optgroup class="news_area_title" - style="background-image: url('<?= Icon::create($area_data['icon'], 'info')->asImagePath() ?>');" label="<?=htmlReady($area_data['title'])?>"> - <? foreach ($area_options_selected[$area_key] as $area_option_key => $area_option_title) : ?> - <option <?= (StudipNews::haveRangePermission('edit', $area_option_key) OR $may_delete) ? 'value="'.$area_option_key.'"' : 'disabled'?> - <?=tooltip($area_option_title);?>> - <?= htmlReady(mila($area_option_title))?> - </option> - <? endforeach ?> - </optgroup> - <? endif ?> - <? endforeach ?> - </select> - </label> - </div> - </fieldset> - - <fieldset <?= $news_isvisible['news_visibility'] ? '' : 'class="collapsed"' ?>> - <legend class="news_visibility_header" id="news_visibility"> - <?= _('Sichtbarkeitseinstellungen') ?> - </legend> - - <? if ($anker == 'news_visibility') : ?> - <a name='anker'></a> - <? endif ?> - - <label> - <?= _('Priorität') ?> - - <select name="news_prio"> - <? foreach ($priorities as $key => $label) : ?> - <option value ="<?= $key ?>"<?= $news->prio == $key ? ' selected' : '' ?>><?= $label ?></option> - <? endforeach ?> - </select> - </label> - <? if ($GLOBALS['perm']->have_perm('admin')) : ?> - <label> - <?= _('Sichtbarkeit') ?> - - <select id="assignedroles" name="assignedroles[]" multiple> - <? if ($assigned) : ?> - <? foreach ($assigned as $assignedrole) : ?> - <option value="<?= $assignedrole->getRoleid() ?>" selected> - <?= htmlReady($assignedrole->getRolename()) ?> - <? if ($assignedrole->getSystemtype()) : ?>[<?= _('Systemrolle') ?>]<? endif ?> - (<?= $rolesStats[$assignedrole->getRoleid()]['explicit'] + $rolesStats[$assignedrole->getRoleid()]['implicit'] ?>) - </option> - <? endforeach ?> - <? endif ?> - <? foreach ($roles as $role) : ?> - <option value="<?= $role->getRoleid() ?>"> - <?= htmlReady($role->getRolename()) ?> - <? if ($role->getSystemtype()) : ?>[<?= _('Systemrolle') ?>]<? endif ?> - (<?= $rolesStats[$role->getRoleid()]['explicit'] + $rolesStats[$role->getRoleid()]['implicit'] ?>) - </option> - <? endforeach ?> - </select> - </label> - <? endif ?> - </fieldset> - - <footer data-dialog-button> - <? if ($news->isNew()) : ?> - <?= Button::createAccept(_('Ankündigung erstellen'), 'save_news') ?> - <? else : ?> - <?= Button::createAccept(_('Änderungen speichern'), 'save_news') ?> - <? endif ?> - <? if (Request::isXhr()) : ?> - <?= LinkButton::createCancel(_('Schließen'), URLHelper::getURL(''), ['rel' => 'close_dialog']) ?> - <? endif ?> - </footer> -</form> - -<script> - jQuery('.news_prevent_submit').keydown(function(event) { - if (event.which === 13) { - event.preventDefault(); - } - }); - jQuery('input[name=area_search_term]').keydown(function(event) { - if (event.which === 13) { - jQuery('input[name=area_search]').click(); - event.preventDefault(); - } - }); - <? if ($GLOBALS['perm']->have_perm('admin')) : ?> - $("#assignedroles").select2({ - width: '100%' - }); - <? endif ?> -</script> +<?= $form->render() ?> diff --git a/lib/bootstrap-autoload.php b/lib/bootstrap-autoload.php index 05d424a623833058eac53ab45393ca9d47da9471..fa6ed7b7a1a9cc6047382ddd37adf5a2c5832534 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 4a995927ef6bf9af3ff7dd3c3c23067e9608bba7..440919efe92318f144bf51f0af44dfc8933a8863 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 * <input type="hidden" name="security_token" value="012345678901234567890123456789=="> * \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( - '<input type="hidden" name="%s" value="%s">', - self::TOKEN, - self::token() + '<input type="hidden" %s>', + arrayToHtmlAttributes($attributes) ); } } diff --git a/lib/classes/forms/CalculatorInput.php b/lib/classes/forms/CalculatorInput.php new file mode 100644 index 0000000000000000000000000000000000000000..40c08c18b9798d08b374a8f57f6ad834271f589b --- /dev/null +++ b/lib/classes/forms/CalculatorInput.php @@ -0,0 +1,20 @@ +<?php + +namespace Studip\Forms; + +class CalculatorInput extends Input +{ + public function render() + { + $template = $GLOBALS['template_factory']->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 0000000000000000000000000000000000000000..5616056f4c68e9781cccffd33a01791f4fda65ff --- /dev/null +++ b/lib/classes/forms/CheckboxInput.php @@ -0,0 +1,23 @@ +<?php + +namespace Studip\Forms; + +class CheckboxInput extends Input +{ + public function getValue() + { + return $this->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 0000000000000000000000000000000000000000..9bee82bb344cfe53bb769c84170c9ab764468339 --- /dev/null +++ b/lib/classes/forms/DatetimepickerInput.php @@ -0,0 +1,25 @@ +<?php + +namespace Studip\Forms; + +class DatetimepickerInput extends Input +{ + public function render() + { + $attributes = ""; + foreach ((array) $this->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 0000000000000000000000000000000000000000..e7bced0a1b8d5478a414e42e820e835ad78cbd75 --- /dev/null +++ b/lib/classes/forms/Fieldset.php @@ -0,0 +1,26 @@ +<?php + +namespace Studip\Forms; + +class Fieldset extends Part +{ + protected $legend = null; + + public function __construct($legend = null) + { + $this->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 0000000000000000000000000000000000000000..9aba36ec428d86b95e6b088b6a7811e4e32ed6c3 --- /dev/null +++ b/lib/classes/forms/Form.php @@ -0,0 +1,353 @@ +<?php + +namespace Studip\Forms; + +class Form extends Part +{ + + //models: + protected $afterStore = []; + + //internals + protected $inputs = []; + protected $parts = []; + + //appearance in html-form + protected $url = null; + protected $autoStore = false; + protected $collapsable = false; + + //to identify a form element + protected $id = null; + + /** + * Creates a new Form object from a SORM object so that each field of the db-table becomes + * an input-field of the form. You can modify the form by the params. + * @param \SimpleORMap $object + * @param array $params + * @param string|null $url + * @return Form + */ + public static function fromSORM(\SimpleORMap $object, $params = [], $url = null) + { + $form = static::create(); + $form->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 0000000000000000000000000000000000000000..24c8c740ebc77cbe336bb6855d12773de9ea38b5 --- /dev/null +++ b/lib/classes/forms/HiddenInput.php @@ -0,0 +1,16 @@ +<?php + +namespace Studip\Forms; + +class HiddenInput extends Input +{ + public function render() + { + $template = $GLOBALS['template_factory']->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 0000000000000000000000000000000000000000..842a76750a8f567ad92074b70aff69104fa7eee4 --- /dev/null +++ b/lib/classes/forms/I18n_formattedInput.php @@ -0,0 +1,49 @@ +<?php + +namespace Studip\Forms; + +class I18n_formattedInput extends Input +{ + public function render() + { + if (!isset($this->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 0000000000000000000000000000000000000000..ae563b094276776cdc1e0b8a868dd3c376959d94 --- /dev/null +++ b/lib/classes/forms/I18n_textInput.php @@ -0,0 +1,48 @@ +<?php + +namespace Studip\Forms; + +class I18n_textInput extends Input +{ + public function render() + { + if (!isset($this->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 0000000000000000000000000000000000000000..44ba1f2d5e07e52e4ac32e56c81a86c3c7b8b2a2 --- /dev/null +++ b/lib/classes/forms/I18n_textareaInput.php @@ -0,0 +1,48 @@ +<?php + +namespace Studip\Forms; + +class I18n_textareaInput extends Input +{ + public function render() + { + if (!isset($this->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 0000000000000000000000000000000000000000..3018a5baf391c8d18aed8aa0dc64408f01779259 --- /dev/null +++ b/lib/classes/forms/Input.php @@ -0,0 +1,271 @@ +<?php + +namespace Studip\Forms; + +abstract class Input +{ + protected $title = null; + protected $value = null; + protected $attributes = []; + protected $name = null; + protected $parent = null; + public $mapper = null; + public $store = null; + public $if = null; + public $permission = true; + public $required = false; + + /** + * A static constructor. Returns a new Input-object. + * @param string $name + * @param string $title + * @param mixed $value + * @param array $attributes + * @return static + */ + public static function create($name, $title, $value, array $attributes = []) + { + return new static($name, $title, $value, $attributes); + } + + /** + * This static method returns fielddata as an array from metadata of a database-field. + * This array will be used internally to create the best fitting Input object to the + * database field. + * @param array $meta + * @param $object: most likely a SimpleORMap object. + * @return array + */ + public static function getFielddataFromMeta($meta, $object) : array + { + $bracket_pos = strpos($meta['type'], '('); + if ($bracket_pos !== false) { + $type = substr($meta['type'], 0, $bracket_pos); + } else { + $type = $meta['type']; + } + $fielddata = []; + switch ($type) { + case 'enum': + $fielddata['type'] = 'select'; + preg_match("/\((.*)\)/", $meta['type'], $matches); + $matches = explode(',', $matches[1]); + foreach ($matches as $key => $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 = '<template v-if="' . htmlReady($this->if) . '">' . $html . '</template>'; + } + 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 0000000000000000000000000000000000000000..fc866486a9723f4704703462b89273deb0fb8f8e --- /dev/null +++ b/lib/classes/forms/InputRow.php @@ -0,0 +1,13 @@ +<?php + +namespace Studip\Forms; + +class InputRow extends Part +{ + public function render() + { + $template = $GLOBALS['template_factory']->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 0000000000000000000000000000000000000000..67d9ebffd891fdc5efe0323287a726f8b9ea9426 --- /dev/null +++ b/lib/classes/forms/MultiselectInput.php @@ -0,0 +1,31 @@ +<?php + +namespace Studip\Forms; + +class MultiselectInput extends Input +{ + public function render() + { + $options = $this->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 0000000000000000000000000000000000000000..18d3170cef20106ee9a15af477ddf2d5d8814d67 --- /dev/null +++ b/lib/classes/forms/NewsRangesInput.php @@ -0,0 +1,134 @@ +<?php + +namespace Studip\Forms; + +class NewsRangesInput extends Input +{ + + public function render() + { + $context = $this->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 0000000000000000000000000000000000000000..c4aae60246fbc4a8826e7a490a0453ccdab72cdd --- /dev/null +++ b/lib/classes/forms/NoInput.php @@ -0,0 +1,16 @@ +<?php + +namespace Studip\Forms; + +class NoInput extends Input +{ + public function render() + { + return ''; + } + + public function getAllInputNames() + { + return []; + } +} diff --git a/lib/classes/forms/NumberInput.php b/lib/classes/forms/NumberInput.php new file mode 100644 index 0000000000000000000000000000000000000000..73c233a58f2ea46a7ef2766d49f9070209e09b0b --- /dev/null +++ b/lib/classes/forms/NumberInput.php @@ -0,0 +1,18 @@ +<?php + +namespace Studip\Forms; + +class NumberInput extends Input +{ + public function render() + { + $template = $GLOBALS['template_factory']->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 0000000000000000000000000000000000000000..79c573bf7613cba900a9b63f2192e0a21513019b --- /dev/null +++ b/lib/classes/forms/Part.php @@ -0,0 +1,226 @@ +<?php + +namespace Studip\Forms; + +abstract class Part +{ + protected $parent = null; + protected $contextobject = null; + protected $parts = []; + public $if = null; + + /** + * Constructor of this Part. Can take one or more Part objects or Input objects or arrays representing an Input object. + * @param ...$parts + */ + public function __construct(...$parts) + { + foreach ($parts as $part) { + if (is_subclass_of($part, Part::class)) { + $this->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 = '<template v-if="' . htmlReady($this->if) . '">' . $html . '</template>'; + } + 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 0000000000000000000000000000000000000000..2531a2e19c3ee13134ae6d54bf987fbf789aabd9 --- /dev/null +++ b/lib/classes/forms/QuicksearchInput.php @@ -0,0 +1,18 @@ +<?php + +namespace Studip\Forms; + +class QuicksearchInput extends Input +{ + 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/RangeInput.php b/lib/classes/forms/RangeInput.php new file mode 100644 index 0000000000000000000000000000000000000000..9f99d592929655fc7a0074fcf71179ccf836c4b8 --- /dev/null +++ b/lib/classes/forms/RangeInput.php @@ -0,0 +1,18 @@ +<?php + +namespace Studip\Forms; + +class RangeInput extends Input +{ + public function render() + { + $template = $GLOBALS['template_factory']->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 0000000000000000000000000000000000000000..0ba83c97215e825c19051aa33d1d1390a1c6fbcf --- /dev/null +++ b/lib/classes/forms/SelectInput.php @@ -0,0 +1,21 @@ +<?php + +namespace Studip\Forms; + +class SelectInput extends Input +{ + public function render() + { + $options = $this->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 0000000000000000000000000000000000000000..4ba6eb9dc1056fcd3e2e0db8c3efb2517c477179 --- /dev/null +++ b/lib/classes/forms/TextInput.php @@ -0,0 +1,18 @@ +<?php + +namespace Studip\Forms; + +class TextInput extends Input +{ + public function render() + { + $template = $GLOBALS['template_factory']->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 0000000000000000000000000000000000000000..267c55e1388ecdb38ab7252b56f3dc65cb1c4f57 --- /dev/null +++ b/lib/classes/forms/TextareaInput.php @@ -0,0 +1,18 @@ +<?php + +namespace Studip\Forms; + +class TextareaInput extends Input +{ + public function render() + { + $template = $GLOBALS['template_factory']->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 ab20cf1b7ebf8062ecdf9990a13f49b5e7fe4b3e..b13f0cc19e8448c8ead8dafdafcf8975caa40d05 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 c2df73d7513d17fb6f14eefcb4a36aa8a06f53c0..5d12608b5da7b41a7d8f0d785e1771eeb28cae14 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 503fdda512696bd6c5069f83b5513968a24fdf65..60ae9c3244e644d834049715a602a0b575dbb9f2 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 6329f168ba19a42919fd16bac48edfca0ee47bfc..aa2dc98c8a663fe7d91819cd6af0b0787e0e54bc 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 80575ccf1a172b115fcbe41660d6e14c6eacdf3a..71d221f73fe28e3a8e837868fd96c1fe7a4612ee 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(`<div id="${id}"></div>`); - $(`#${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 37e56150fa7facd6974e9149d3dfbea1d09e85fb..d2b840ff5fcfa72ef0141aaccd3026396844a5bc 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 e4f75bafa4328acd931fabc3d3c1a68af6c2d6da..5ca970fb9158eb071736bbeec9344c34d0294186 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 3af26ee7674aa07faf28996f2ac761b882204b7c..49eee3cb865a1e990fc4e138ed469e56ff37cc87 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 ed867ef91087cc68b4e935c8348608ac23138e58..1a5bb5e5b1fa5cfddcbf4756967a1e943f0bd2c5 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 b57094e559b94bb2ee1532aa700e1205d7f88f97..526687b39763215284e518e8ae748ffc806ca419 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 0000000000000000000000000000000000000000..3ec930c12e01bbc4fa3e8db751ef1ffe26150612 --- /dev/null +++ b/resources/vue/components/Datetimepicker.vue @@ -0,0 +1,81 @@ +<template> + <span> + <input type="hidden" :name="name" :value="value"> + <input type="text" + ref="visibleInput" + class="visible_input" + @change="setUnixTimestamp" + v-bind="$attrs" + v-on="$listeners"> + </span> +</template> + +<script> +export default { + name: 'datetimepicker', + inheritAttrs: false, + props: { + name: { + type: String, + required: false + }, + value: { + required: false + }, + mindate: { + required: false + }, + maxdate: { + required: false + } + }, + methods: { + setUnixTimestamp () { + let formatted_date = this.$refs.visibleInput.value; + let date = formatted_date.match(/(\d+)/g); + date = new Date(`${date[2]}-${date[1]}-${date[0]} ${date[3]}:${date[4]}`); + this.$emit('input', Math.floor(date / 1000)); + } + }, + mounted () { + let value = parseInt(this.value, 10) !== NaN ? parseInt(this.value, 10) : this.value; + if (Number.isInteger(value)) { + let date = new Date(value * 1000); + let formatted_date = + (date.getDate() < 10 ? "0" : "") + date.getDate() + + "." + + (date.getMonth() < 9 ? "0" : "") + (date.getMonth() + 1) + + "." + + date.getFullYear() + + " " + + (date.getHours() < 10 ? "0" : "") + date.getHours() + + ":" + + (date.getMinutes() < 10 ? "0" : "") + date.getMinutes(); + this.$refs.visibleInput.value = formatted_date; + } else { + this.$refs.visibleInput.value = value; + } + let v = this; + let params = { + onSelect () { + v.setUnixTimestamp(); + } + }; + if (this.mindate) { + params.minDate = new Date(this.mindate * 1000) + } + if (this.maxdate) { + params.maxDate = new Date(this.maxdate * 1000) + } + $(this.$refs.visibleInput).datetimepicker(params); + }, + watch: { + mindat (new_data, old_data) { + $(this.$refs.visibleInput).datetimepicker('option', 'minDate', new Date(new_data * 1000)); + }, + maxdate (new_data, old_data) { + $(this.$refs.visibleInput).datetimepicker('option', 'maxDate', new Date(new_data * 1000)); + } + } +} +</script> diff --git a/resources/vue/components/EditableList.vue b/resources/vue/components/EditableList.vue new file mode 100644 index 0000000000000000000000000000000000000000..8b2242272130db993ad376fae334e3c6a936133c --- /dev/null +++ b/resources/vue/components/EditableList.vue @@ -0,0 +1,170 @@ +<template> + <div> + <div class="formpart"> + <ul class="clean editablelist"> + <li v-for="item in sortedItems" :key="item.id" :data-type="item.type"> + <studip-icon v-if="item.icon" :shape="item.icon" role="info" size="20" class="text-bottom" alt=""></studip-icon> + <input v-if="name" type="hidden" :name="name + '[]'" :value="item.value"> + <span>{{item.name}}</span> + <button v-if="item.deletable" @click.prevent="deleteItem(item)" :title="$gettextInterpolate('%{ name } löschen', {name: item.name})" class="undecorated"> + <studip-icon shape="trash" role="clickable" size="20" class="text-bottom"></studip-icon> + </button> + </li> + </ul> + <quicksearch v-if="quicksearch" :searchtype="quicksearch" name="qs" @input="addRange" :placeholder="$gettext('Suchen')"></quicksearch> + </div> + + <label v-if="selectable"> + <translate>Oder aus Liste auswählen:</translate> + <select @change="quickselect" @keydown="navigate_or_select"> + <option value=""><translate>Direkt auswählen ...</translate></option> + <template v-for="opt in selectable"> + <optgroup v-if="opt.label && opt.options" :label="opt.label"> + <option v-for="option in opt.options" :disabled="isSelected(option.value)" :value="JSON.stringify({value: option.value, name: option.name})">{{ option.name + (isSelected(option.value) ? ' ✓' : '') }}</option> + </optgroup> + <option v-else :disabled="isSelected(opt.value)" @click="quicksearch" :value="JSON.stringify({value: opt.value, name: opt.name})">{{ opt.name + (isSelected(option.value) ? ' ✓' : '') }}</option> + </template> + </select> + + </label> + </div> +</template> + +<script> +export default { + name: 'editable-list', + props: { + name: { + type: String, + required: false + }, + items: { + required: false, + type: Array + }, + quicksearch: { + required: false + }, + selectable: { + type: Array, + required: false + }, + category_order: { + type: Array, + required: false, + default: [] + } + }, + data () { + return { + resort: false, //this is just for triggering the computed property sortedItems to be sorted again + preventChangeOfQuickselect: false + }; + }, + methods: { + addRange (id, name) { + let icon = null; + if (id.includes('__')) { + icon = id.split('__')[1]; + id = id.split('__')[0]; + } + let insert = true; + for (let i in this.items) { + if (this.items[i].value === id) { + insert = false; + break; + } + } + + if (insert) { + this.items.push({ + value: id, + name: name, + icon: icon, + deletable: true + }); + this.changed(); + } + }, + changed () { + this.resort = !this.resort; + this.$emit('input', this.items.map(function (item) { + return item.value; + })); + this.$emit('items', this.items.map(function (item) { + return { + value: item.value, + name: item.name, + icon: item.icon, + deletable: item.deletable + }; + })); + }, + quickselect (event) { + if (event.target.value && !this.preventChangeOfQuickselect) { + let new_value = JSON.parse(event.target.value); + this.addRange(new_value.value, new_value.name); + event.target.value = ''; + } + }, + navigate_or_select (event) { + if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(event.key)) { + //don't trigger change for 250 ms + this.preventChangeOfQuickselect = true; + window.setTimeout(() => { + this.preventChangeOfQuickselect = false; + }, 250); + } else if (event.key === 'Enter') { + //select current selection + let new_value = JSON.parse(event.target.value); + this.addRange(new_value.value, new_value.name); + event.target.value = ''; + } + }, + deleteItem (item) { + for (let i in this.items) { + if (this.items[i].value === item.value) { + this.$delete(this.items, i); + } + } + this.changed(); + }, + isSelected (id) { + if (id.includes('__')) { + id = id.split('__')[0]; + } + for (let i in this.items) { + if (this.items[i].value === id) { + return true; + } + } + return false; + } + }, + computed: { + sortedItems () { + let v = this; + let i = this.resort; + let items = this.items.sort(function (a, b) { + if (a.icon === b.icon) { + return a.name.localeCompare(b.name); + } else { + let a_icon = a.icon || ''; + let b_icon = b.icon || ''; + if (v.category_order.indexOf(a_icon) > -1 && v.category_order.indexOf(b_icon) > -1) { + return v.category_order.indexOf(a_icon) < v.category_order.indexOf(b_icon) ? -1 : 1; + } else { + return a_icon.localeCompare(b_icon); + } + } + }); + return items; + } + }, + mounted () { + this.$emit('input', this.items.map(function (item) { + return item.value; + })); + } +} +</script> diff --git a/resources/vue/components/I18nTextarea.vue b/resources/vue/components/I18nTextarea.vue new file mode 100644 index 0000000000000000000000000000000000000000..c4d29768e3d6833e6247ba97fc3bb99b7c58f624 --- /dev/null +++ b/resources/vue/components/I18nTextarea.vue @@ -0,0 +1,190 @@ +<template> + <div class="i18n_group" v-if="languages.length > 1"> + <div class="i18n" + v-for="language in languages" + v-if="(selectedLanguage !== null) && (language.id === selectedLanguage.id)" + :data-lang="language.name" + :data-icon="'url(' + assetsURL + 'images/languages/' + language.picture + ')'"> + <input type=text + :name="nameOfInput(language.id)" + v-model="values[selectedLanguage.id]" + :required="required && defaultLanguage === language.id" + v-bind="$attrs" + v-on="$listeners" + v-if="type === 'text'"> + <textarea :name="nameOfInput(language.id)" + v-bind="$attrs" + v-on="$listeners" + v-model="values[language.id]" + :required="required && defaultLanguage === language.id" + v-else-if="type === 'textarea'">{{ values[language.id] }}</textarea> + <studip-wysiwyg :name="nameOfInput(language.id)" + v-model="values[selectedLanguage.id]" + v-bind="$attrs" + v-on="$listeners" + :required="required && defaultLanguage === language.id" + v-else-if="type === 'wysiwyg' && !wysiwyg_disabled"></studip-wysiwyg> + <textarea-with-toolbar :name="nameOfInput(language.id)" + v-else + v-model="values[selectedLanguage.id]" + v-bind="$attrs" + :required="required && defaultLanguage === language.id" + v-on="$listeners"></textarea-with-toolbar> + </div> + <input type="hidden" + v-for="language in languages" + v-if="(selectedLanguage !== null) && (language.id !== selectedLanguage.id)" + v-model="values[language.id]" + :required="required && defaultLanguage === language.id" + :name="nameOfInput(language.id)"> + <select class="i18n" + tabindex="-1" + @change="selectLanguage" + :style="'background-image: url(' + assetsURL + 'images/languages/' + selectedLanguage.picture + ')'"> + <option v-for="language in languages" :value="language.id">{{language.name}}</option> + </select> + </div> + <div v-else> + <input type=text + :name="name" + v-model="values[selectedLanguage.id]" + v-bind="$attrs" + v-on="$listeners" + :required="required" + v-if="type === 'text'"> + <textarea :name="name" + v-model="values[selectedLanguage.id]" + v-bind="$attrs" + v-on="$listeners" + :required="required" + v-else-if="type === 'textarea'"></textarea> + <studip-wysiwyg :name="name" + v-model="values[selectedLanguage.id]" + v-bind="$attrs" + v-on="$listeners" + :required="required" + v-else-if="type === 'wysiwyg' && !wysiwyg_disabled"></studip-wysiwyg> + <textarea-with-toolbar :name="name" + v-else + v-model="values[selectedLanguage.id]" + v-bind="$attrs" + :required="required" + v-on="$listeners"></textarea-with-toolbar> + </div> +</template> + +<script> +import StudipWysiwyg from './StudipWysiwyg.vue'; +export default { + name: 'i18n-textarea', + components: { + StudipWysiwyg + }, + props: { + name: { + type: String, + required: false + }, + wysiwyg: { + type: Boolean, + required: false, + default: false + }, + type: { + type: String, + required: false, + default: "text" + }, + value: { + required: false, + default: "" + }, + wysiwyg_disabled: { + type: Boolean, + required: false, + default: false + }, + required: { + type: Boolean, + required: false, + default: false + } + }, + data () { + return { + selectedLanguage: {}, + values: {} + }; + }, + mounted () { + for (let i in this.languages) { + this.selectedLanguage = this.languages[i]; + break; + } + let jsonvalue = false; + try { + jsonvalue = JSON.parse(this.value); + } catch (except) { + } + if (jsonvalue !== false) { + this.values = jsonvalue; + } else { + let values = {}; + values[this.selectedLanguage.id] = this.value; + this.values = values; + } + }, + methods: { + selectLanguage (e) { + for (let i in this.languages) { + if (e.target.value === this.languages[i].id) { + this.selectedLanguage = this.languages[i]; + break; + } + } + }, + nameOfInput (language_id) { + return this.name + (this.defaultLanguage === language_id ? '' : '_i18n[' + language_id + ']') + } + }, + computed: { + assetsURL () { + return STUDIP.ASSETS_URL; + }, + defaultLanguage () { + return this.languages[0].id; + }, + languages () { + let languages = []; + let language = {}; + for (let i in STUDIP.CONTENT_LANGUAGES) { + if (STUDIP.INSTALLED_LANGUAGES[STUDIP.CONTENT_LANGUAGES[i]] !== undefined) { + language = STUDIP.INSTALLED_LANGUAGES[STUDIP.CONTENT_LANGUAGES[i]]; + language.id = STUDIP.CONTENT_LANGUAGES[i]; + languages.push(language); + } + } + return languages; + } + }, + inheritAttrs: false, + watch: { + values: { + handler(newValue, oldValue) { + this.$emit('input', newValue[this.defaultLanguage]); + let exportValue = {}; + let input_all = {}; + let name = null; + for (let i in this.languages) { + exportValue[this.languages[i].id] = newValue[this.languages[i].id]; + name = this.nameOfInput(this.languages[i].id); + input_all[name] = newValue[this.languages[i].id]; + } + this.$emit('input_all_languages', exportValue); + this.$emit('allinputs', input_all); + }, + deep: true + } + } +} +</script> diff --git a/resources/vue/components/Multiselect.vue b/resources/vue/components/Multiselect.vue new file mode 100644 index 0000000000000000000000000000000000000000..e1f180347313544098308297efde518183bdd5c2 --- /dev/null +++ b/resources/vue/components/Multiselect.vue @@ -0,0 +1,65 @@ +<template> + <v-select + multiple + v-model="selected" + :options="transformed_options" + :reduce="(option) => option.id" + v-bind="$attrs" + v-on="$listeners" + > + <div slot="no-options"><translate>Keine Auswahlmöglichkeiten</translate></div> + </v-select> +</template> + +<script> +import vSelect from 'vue-select'; +import 'vue-select/dist/vue-select.css' +export default { + name: 'multiselect', + components: { + vSelect, + }, + inheritAttrs: false, + props: { + name: { + type: String, + required: false + }, + value: { + required: false + }, + options: { + type: Object, + required: true + } + }, + data () { + return { + selected: [] + }; + }, + computed: { + transformed_options () { + let output = []; + Object.entries(this.options).forEach(obj => { + output.push({ + id: obj[0], + label: obj[1] + }); + }); + return output; + } + }, + mounted () { + this.selected = this.value; + }, + watch: { + selected: { + handler(newValue, oldValue) { + this.$emit('input', newValue); + }, + deep: true + } + } +} +</script> diff --git a/resources/vue/components/Quicksearch.vue b/resources/vue/components/Quicksearch.vue index 23fd5f4e6af2e105c32d8f7148b43de07c3c9166..94b0a3a89246a93a16ba5658d90f886eb0f727ae 100644 --- a/resources/vue/components/Quicksearch.vue +++ b/resources/vue/components/Quicksearch.vue @@ -3,7 +3,7 @@ <input type="hidden" :name="name" :value="returnValue" - v-if="!autocomplete"> + v-if="!autocomplete && name"> <input type="text" :name="autocomplete ? name : null" v-model="inputValue" @@ -38,7 +38,7 @@ export default { }, name: { type: String, - required: true + required: false }, value: { type: String, @@ -61,6 +61,7 @@ export default { default: '' } }, + inheritAttrs: false, data () { return { searching: false, @@ -74,9 +75,9 @@ export default { }; }, methods: { - initialize (value) { + initialize (value, displayname) { this.initialValue = value; - this.inputValue = value; + this.inputValue = displayname ?? value; this.returnValue = value; }, search (needle) { @@ -117,7 +118,7 @@ export default { } this.results = []; - this.$emit('input', this.returnValue); + this.$emit('input', this.returnValue, this.inputValue); }, selectUp () { if (this.selected > 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 0000000000000000000000000000000000000000..d60d525013a61286af866084b8ce658c05d87b98 --- /dev/null +++ b/resources/vue/components/RangeInput.vue @@ -0,0 +1,67 @@ +<template> + <div class="formpart range_input"> + <input type="range" + :name="name" + :min="min" + :max="max" + :step="step" + :aria-valuemin="min" + :aria-valuemax="max" + :aria-valuenow="myValue" + v-bind="$attrs" + v-on="$listeners" + v-model="myValue"> + <output for="fader"><translate :translate-params="{myValue: myValue ?? '1', max: max}">%{myValue} von %{max}</translate></output> + </div> +</template> + +<script> +export default { + name: 'range-input', + props: { + name: { + type: String, + required: true + }, + value: { + required: false, + default: 1 + }, + min: { + type: Number, + required: false, + default: 1 + }, + max: { + type: Number, + required: false, + default: 10 + }, + step: { + type: Number, + required: false, + default: 1 + } + }, + data () { + return { + myValue: 1 + }; + }, + mounted () { + this.myValue = this.value > this.min ? this.value : this.min; + if (this.myValue > this.max) { + this.myValue = this.max; + } + }, + inheritAttrs: false, + watch: { + myValue: { + handler(newValue, oldValue) { + this.$emit('input', newValue); + }, + deep: true + } + } +} +</script> diff --git a/resources/vue/components/StudipWysiwyg.vue b/resources/vue/components/StudipWysiwyg.vue index 707fb55d3eb7a6df2e583b782f4f15f9a533f542..bdeacfa5c04dab85cce1deb41dcee025f01e00fd 100755 --- a/resources/vue/components/StudipWysiwyg.vue +++ b/resources/vue/components/StudipWysiwyg.vue @@ -1,8 +1,9 @@ <template> - <textarea + <textarea :value="value" ref="studip_wysiwyg" class="studip-wysiwyg" + :required="required" @input="updateValue($event.target.value)"/> </template> @@ -11,7 +12,12 @@ export default { name: 'studip-wysiwyg', props: { - value: String + value: String, + required: { + type: Boolean, + required: false, + default: false + } }, data() { return { @@ -49,7 +55,3 @@ export default { } </script> - -<style> - -</style> \ No newline at end of file diff --git a/resources/vue/components/TextareaWithToolbar.vue b/resources/vue/components/TextareaWithToolbar.vue new file mode 100644 index 0000000000000000000000000000000000000000..1f19864a54ab82dd1f6f5fa749c016b6ea45462f --- /dev/null +++ b/resources/vue/components/TextareaWithToolbar.vue @@ -0,0 +1,19 @@ +<template> + <textarea :name="name" ref="textarea" + v-bind="$attrs" v-on="$listeners"></textarea> +</template> + +<script> +export default { + name: 'textarea-with-toolbar', + props: { + name: { + type: String, + required: false + } + }, + mounted () { + $(this.$refs.textarea).addToolbar(); + } +} +</script> diff --git a/templates/forms/calculator_input.php b/templates/forms/calculator_input.php new file mode 100644 index 0000000000000000000000000000000000000000..95e33c09295e729b396a37c15b1c80802b33007d --- /dev/null +++ b/templates/forms/calculator_input.php @@ -0,0 +1,4 @@ +<div class="formpart"> + <?= htmlReady($title) ?> + <output class="calculator_result" <?= $attributes ?>>{{ <?= htmlReady($value) ?> }}</output> +</div> diff --git a/templates/forms/checkbox_input.php b/templates/forms/checkbox_input.php new file mode 100644 index 0000000000000000000000000000000000000000..b689d696f8e32b50b91cc8f6f78b531b3f303b6f --- /dev/null +++ b/templates/forms/checkbox_input.php @@ -0,0 +1,16 @@ +<input type="hidden" name="<?= htmlReady($name) ?>" value="0"> + <label<?= ($required ? ' class="studiprequired"' : '') ?>> + <input type="checkbox" + v-model="<?= htmlReady($this->name) ?>" + name="<?= htmlReady($this->name) ?>" + value="1" + id="<?= $id ?>" + <?= ($this->required ? 'required aria-required="true"' : '') ?> + <?= $attributes ?>> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> +</label> diff --git a/templates/forms/datetimepicker_input.php b/templates/forms/datetimepicker_input.php new file mode 100644 index 0000000000000000000000000000000000000000..f2d6b9271b9a450ee40074c33465b6824165b670 --- /dev/null +++ b/templates/forms/datetimepicker_input.php @@ -0,0 +1,15 @@ +<div class="formpart"> + <label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>"> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> + </label> + <datetimepicker v-model="<?= htmlReady($name) ?>" + name="<?= htmlReady($name) ?>" + id="<?= $id ?>" + <?= ($this->required ? 'required aria-required="true"' : '')?> + <?= $attributes ?>> +</div> diff --git a/templates/forms/fieldset.php b/templates/forms/fieldset.php new file mode 100644 index 0000000000000000000000000000000000000000..491f7260ea121df88b6e47befb72f3c1fc696c1b --- /dev/null +++ b/templates/forms/fieldset.php @@ -0,0 +1,8 @@ +<fieldset> + <? if ($legend) : ?> + <legend><?= htmlReady($this->legend) ?></legend> + <? endif ?> + <? foreach ($parts as $part) : ?> + <?= $part->renderWithCondition() ?> + <? endforeach ?> +</fieldset> diff --git a/templates/forms/form.php b/templates/forms/form.php new file mode 100644 index 0000000000000000000000000000000000000000..72a1b25838e445ed550ff90ca58fbcaf96959711 --- /dev/null +++ b/templates/forms/form.php @@ -0,0 +1,67 @@ +<? +$inputs = []; +$allinputs = $form->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()); +?><form v-cloak + method="post" + <? if (!$form->isAutoStoring()) : ?> + action="<?= htmlReady($form->getURL()) ?>" + <? else : ?> + data-autosave="<?= htmlReady($_SERVER['REQUEST_URI']) ?>" + data-url="<?= htmlReady($form->getURL()) ?>" + <? endif ?> + @submit="submit" + novalidate + id="<?= htmlReady($form_id) ?>" + data-inputs="<?= htmlReady(json_encode($inputs)) ?>" + data-required="<?= htmlReady(json_encode($required_inputs)) ?>" + class="default studipform<?= $form->isCollapsable() ? ' collapsable' : '' ?>"> + + <?= CSRFProtection::tokenTag(['ref' => 'securityToken']) ?> + + <article aria-live="assertive" + class="validation_notes studip" + v-if="(STUDIPFORM_REQUIRED.length > 0) || STUDIPFORM_DISPLAYVALIDATION"> + <header> + <h1> + <?= Icon::create('info-circle', Icon::ROLE_INFO)->asImg(17, ['class' => "text-bottom validation_notes_icon"]) ?> + <?= _('Hinweise zum Ausfüllen des Formulars') ?> + </h1> + </header> + <div class="required_note" v-if="STUDIPFORM_REQUIRED.length > 0"> + <div aria-hidden> + <?= _('Pflichtfelder sind mit Sternchen gekennzeichnet.') ?> + </div> + <div class="sr-only"> + <?= _('Dieses Formular enthält Pflichtfelder.') ?> + </div> + + </div> + <div v-if="STUDIPFORM_DISPLAYVALIDATION && (STUDIPFORM_VALIDATIONNOTES.length > 0)"> + <?= _('Folgende Angaben müssen korrigiert werden, um das Formular abschicken zu können:') ?> + <ul> + <li v-for="note in STUDIPFORM_VALIDATIONNOTES" :aria-describedby="note.describedby">{{ note.name + ": " + note.description }}</li> + </ul> + </div> + </article> + + <div aria-live="polite"> + <? foreach ($form->getParts() as $part) : ?> + <?= $part->renderWithCondition() ?> + <? endforeach ?> + </div> +</form> + +<div data-dialog-button> + <?= \Studip\Button::create(_('Speichern'), null, ['form' => $form_id]) ?> +</div> diff --git a/templates/forms/hidden_input.php b/templates/forms/hidden_input.php new file mode 100644 index 0000000000000000000000000000000000000000..27824cd016bb0912cf3bdef2ffceb4e9fffcf421 --- /dev/null +++ b/templates/forms/hidden_input.php @@ -0,0 +1,4 @@ +<input type="hidden" + name="<?= htmlReady($this->name) ?>" + value="<?= htmlReady($this->value) ?>" + id="<?= $id ?>" <?= $attributes ?>> diff --git a/templates/forms/i18n_formatted_input.php b/templates/forms/i18n_formatted_input.php new file mode 100644 index 0000000000000000000000000000000000000000..4e667f5f2bf8f518157827ad867feddfae790ffd --- /dev/null +++ b/templates/forms/i18n_formatted_input.php @@ -0,0 +1,17 @@ +<div class="formpart"> + <label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>"> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> + </label> + <i18n-textarea type="wysiwyg" + id="<?= $id ?>" + name="<?= htmlReady($name) ?>" + value="<?= htmlReady($value) ?>" + @allinputs="setInputs" + :wysiwyg_disabled="<?= \Config::get()->WYSIWYG ? 'false' : 'true' ?>" <?= $required ? 'required' : '' ?>> + </i18n-textarea> + </div> diff --git a/templates/forms/i18n_text_input.php b/templates/forms/i18n_text_input.php new file mode 100644 index 0000000000000000000000000000000000000000..5e99cd1cb517204fd6b832e845530f80653b4bc4 --- /dev/null +++ b/templates/forms/i18n_text_input.php @@ -0,0 +1,17 @@ +<div class="formpart"> + <label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>"> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> + </label> + <i18n-textarea type="text" + id="<?= $id ?>" + name="<?= htmlReady($this->name) ?>" + value="<?= htmlReady($value) ?>" + <?= $required ? 'required' : '' ?> + @allinputs="setInputs"> + </i18n-textarea> +</div> diff --git a/templates/forms/i18n_textarea_input.php b/templates/forms/i18n_textarea_input.php new file mode 100644 index 0000000000000000000000000000000000000000..3209b6827e5e723e43d5e22ebeadcdb17dbb8f88 --- /dev/null +++ b/templates/forms/i18n_textarea_input.php @@ -0,0 +1,17 @@ +<div class="formpart"> + <label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>"> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> + </label> + <i18n-textarea type="textarea" + id="<?= $id ?>" + name="<?= htmlReady($this->name) ?>" + value="<?= htmlReady($value) ?>" + <?= $required ? 'required' : '' ?> + @allinputs="setInputs"> + </i18n-textarea> +</div> diff --git a/templates/forms/input_row.php b/templates/forms/input_row.php new file mode 100644 index 0000000000000000000000000000000000000000..6bf522340b9eb86e629cad0d6599dca0e996f25a --- /dev/null +++ b/templates/forms/input_row.php @@ -0,0 +1,5 @@ +<div class="hgroup"> + <? foreach ($parts as $part) : ?> + <?= $part->renderWithCondition() ?> + <? endforeach ?> +</div> diff --git a/templates/forms/multiselect_input.php b/templates/forms/multiselect_input.php new file mode 100644 index 0000000000000000000000000000000000000000..cd9aec6391dd90532d910972a475f71e3bf2bf2a --- /dev/null +++ b/templates/forms/multiselect_input.php @@ -0,0 +1,17 @@ +<div class="formpart"> + <label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>"> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> + </label> + <multiselect name="<?= htmlReady($name) ?>[]" <?= ($required ? 'required aria-required="true"' : '') ?> + :options="<?= htmlReady(json_encode($options)) ?>" + :value="<?= htmlReady(json_encode($value)) ?>" + v-model="<?= htmlReady($name) ?>" + id="<?= $id ?>" + <?= $attributes ?>> + </multiselect> +</div> diff --git a/templates/forms/news_ranges_input.php b/templates/forms/news_ranges_input.php new file mode 100644 index 0000000000000000000000000000000000000000..ac1fe51f8a55b8f3f4a300efde407cc6a1c97a7c --- /dev/null +++ b/templates/forms/news_ranges_input.php @@ -0,0 +1,7 @@ +<editable-list name="<?= htmlReady($this->name) ?>" + quicksearch="<?= htmlReady((string) $searchtype) ?>" + :items="<?= htmlReady(json_encode($items)) ?>" + :selectable="<?= htmlReady(json_encode($selectable)) ?>" + :category_order="<?= htmlReady(json_encode($category_order)) ?>" + @input="output => <?= htmlReady($this->name) ?> = output"> +</editable-list> diff --git a/templates/forms/number_input.php b/templates/forms/number_input.php new file mode 100644 index 0000000000000000000000000000000000000000..622553a6fac10ae15142a7f2628045c47ec31db4 --- /dev/null +++ b/templates/forms/number_input.php @@ -0,0 +1,14 @@ +<label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>"> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> +</label> +<input type="number" + name="<?= htmlReady($name) ?>" + value="<?= htmlReady($value) ?>" + id="<?= $id ?>" + <?= ($required ? 'required aria-required="true"' : '') ?> + <?= $attributes ?>> diff --git a/templates/forms/quicksearch_input.php b/templates/forms/quicksearch_input.php new file mode 100644 index 0000000000000000000000000000000000000000..5a8fadd218eb23307a1bab40c419f98dfcd430d9 --- /dev/null +++ b/templates/forms/quicksearch_input.php @@ -0,0 +1,17 @@ +<div class="formpart"> + <label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>"> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> + </label> + <span> + <quicksearch value="<?= htmlReady($value) ?>" + name="<?= htmlReady($name) ?>" + id="<?= $id ?>" + <?= ($this->required ? 'required aria-required="true"' : '') ?> + <?= $attributes ?>> + </span>' +</div> diff --git a/templates/forms/range_input.php b/templates/forms/range_input.php new file mode 100644 index 0000000000000000000000000000000000000000..c85b02d3a398d3bd3f76e2a25a5f23d0a64eb1f5 --- /dev/null +++ b/templates/forms/range_input.php @@ -0,0 +1,13 @@ +<label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>"> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> +</label> +<range-input v-model="<?= htmlReady($name) ?>" + name="<?= htmlReady($name) ?>" + value="<?= htmlReady($value) ?>" + id="<?= $id ?>" + <?= $attributes ?>></range-input> diff --git a/templates/forms/select_input.php b/templates/forms/select_input.php new file mode 100644 index 0000000000000000000000000000000000000000..52d67768193ecebf1781bee2c50205db37271d7b --- /dev/null +++ b/templates/forms/select_input.php @@ -0,0 +1,17 @@ +<div class="formpart"> + <label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>"> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> + </label> + <select class="select2" name="<?= htmlReady($this->name) ?>" <?= ($this->required ? 'required aria-required="true"' : '') ?> id="<?= $id ?>" <?= $attributes ?>> + <? foreach ($options as $key => $option) : ?> + <option value="<?= htmlReady($key) ?>"<?= ($key == $value ? " selected" : "") ?>> + <?= htmlReady($option) ?> + </option> + <? endforeach ?> + </select> +</div> diff --git a/templates/forms/text_input.php b/templates/forms/text_input.php new file mode 100644 index 0000000000000000000000000000000000000000..546a125766ad5f0604db6def06d5d9ce1010c759 --- /dev/null +++ b/templates/forms/text_input.php @@ -0,0 +1,16 @@ +<div class="formpart"> + <label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>"> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> + </label> + <input type="text" + v-model="<?= htmlReady($this->name) ?>" + name="<?= htmlReady($this->name) ?>" + value="<?= htmlReady($this->value) ?>" + id="<?= $id ?>" <?= ($this->required ? 'required aria-required="true"' : '') ?> + <?= $attributes ?>> +</div> diff --git a/templates/forms/textarea_input.php b/templates/forms/textarea_input.php new file mode 100644 index 0000000000000000000000000000000000000000..2afa96c381027ba880a10acbb088ed1021cdd73d --- /dev/null +++ b/templates/forms/textarea_input.php @@ -0,0 +1,13 @@ +<label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>"> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> +</label> +<textarea name="<?= htmlReady($name) ?>" + v-model="<?= htmlReady($name) ?>" + id="<?= $id ?>" + <?= ($required ? 'required aria-required="true"' : '') ?> + <?= $attributes ?>><?= htmlReady($value) ?></textarea> diff --git a/templates/layouts/base.php b/templates/layouts/base.php index 2528a642bfa86aa267c5a7a80a3a07429f6a3868..a0dc7136ff8abaaeb4fd342edc47a217e80c17d5 100644 --- a/templates/layouts/base.php +++ b/templates/layouts/base.php @@ -72,6 +72,7 @@ $getInstalledLanguages = function () { value: '<? try {echo CSRFProtection::token();} catch (SessionRequiredException $e){}?>' }, INSTALLED_LANGUAGES: <?= json_encode($getInstalledLanguages()) ?>, + CONTENT_LANGUAGES: <?= json_encode(array_keys($GLOBALS['CONTENT_LANGUAGES'])) ?>, STUDIP_SHORT_NAME: "<?= htmlReady(Config::get()->STUDIP_SHORT_NAME) ?>", URLHelper: { base_url: "<?= $GLOBALS['ABSOLUTE_URI_STUDIP'] ?>",