aboutsummaryrefslogtreecommitdiff
path: root/resources
diff options
context:
space:
mode:
authorPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
committerPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
commit4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch)
tree5c07151ae61276d334e88f6309c30d439a85c12e /resources
parentda0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff)
parent97a188592c679890a25c37ab78463add76a52ff7 (diff)
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'resources')
-rw-r--r--resources/assets/javascripts/bootstrap/application.js1
-rw-r--r--resources/assets/javascripts/bootstrap/article.js1
-rw-r--r--resources/assets/javascripts/bootstrap/clipboard.js13
-rw-r--r--resources/assets/javascripts/bootstrap/consultations.js4
-rw-r--r--resources/assets/javascripts/bootstrap/contentbox.js1
-rw-r--r--resources/assets/javascripts/bootstrap/copyable_links.js24
-rw-r--r--resources/assets/javascripts/bootstrap/forms.js10
-rw-r--r--resources/assets/javascripts/bootstrap/global_search.js42
-rw-r--r--resources/assets/javascripts/bootstrap/resources.js67
-rw-r--r--resources/assets/javascripts/bootstrap/responsive-navigation.js2
-rw-r--r--resources/assets/javascripts/bootstrap/search.js2
-rw-r--r--resources/assets/javascripts/bootstrap/studip_helper_attributes.js9
-rw-r--r--resources/assets/javascripts/bootstrap/system-notifications.js11
-rw-r--r--resources/assets/javascripts/bootstrap/tooltip.js67
-rw-r--r--resources/assets/javascripts/bootstrap/treeview.js3
-rw-r--r--resources/assets/javascripts/bootstrap/vue.js12
-rw-r--r--resources/assets/javascripts/bootstrap/wysiwyg.js7
-rw-r--r--resources/assets/javascripts/chunk-loader.js153
-rw-r--r--resources/assets/javascripts/chunks/vue.js3
-rw-r--r--resources/assets/javascripts/entry-base.js3
-rw-r--r--resources/assets/javascripts/init.js7
-rw-r--r--resources/assets/javascripts/lib/RestrictedDatesHelper.ts89
-rw-r--r--resources/assets/javascripts/lib/abstract-api.js69
-rw-r--r--resources/assets/javascripts/lib/activityfeed.js53
-rw-r--r--resources/assets/javascripts/lib/blubber.js1
-rw-r--r--resources/assets/javascripts/lib/clipboard.js145
-rw-r--r--resources/assets/javascripts/lib/dialog.js7
-rw-r--r--resources/assets/javascripts/lib/extract_callback.js5
-rw-r--r--resources/assets/javascripts/lib/fullcalendar.js32
-rw-r--r--resources/assets/javascripts/lib/global_search.js16
-rw-r--r--resources/assets/javascripts/lib/header_magic.js2
-rw-r--r--resources/assets/javascripts/lib/jsonapi.ts (renamed from resources/assets/javascripts/lib/jsonapi.js)8
-rw-r--r--resources/assets/javascripts/lib/messages.js2
-rw-r--r--resources/assets/javascripts/lib/personal_notifications.js5
-rw-r--r--resources/assets/javascripts/lib/questionnaire.js4
-rw-r--r--resources/assets/javascripts/lib/resources.js45
-rw-r--r--resources/assets/javascripts/lib/restapi.js12
-rw-r--r--resources/assets/javascripts/lib/scroll.js59
-rw-r--r--resources/assets/javascripts/lib/scroll_to_top.js22
-rw-r--r--resources/assets/javascripts/lib/search.js12
-rw-r--r--resources/assets/javascripts/lib/tooltip.js227
-rw-r--r--resources/assets/javascripts/lib/wysiwyg.js6
-rw-r--r--resources/assets/javascripts/mvv.js229
-rw-r--r--resources/assets/javascripts/studip-ui.js84
-rw-r--r--resources/assets/stylesheets/highcontrast.scss15
-rw-r--r--resources/assets/stylesheets/mixins/misc.scss4
-rw-r--r--resources/assets/stylesheets/mixins/studip.scss16
-rw-r--r--resources/assets/stylesheets/print.scss1
-rw-r--r--resources/assets/stylesheets/scss/admin-courses.scss4
-rw-r--r--resources/assets/stylesheets/scss/blubber.scss16
-rw-r--r--resources/assets/stylesheets/scss/buttons.scss3
-rw-r--r--resources/assets/stylesheets/scss/contents.scss4
-rw-r--r--resources/assets/stylesheets/scss/copyable-links.scss24
-rw-r--r--resources/assets/stylesheets/scss/courseware/blockadder.scss4
-rw-r--r--resources/assets/stylesheets/scss/courseware/blocks/document.scss1
-rw-r--r--resources/assets/stylesheets/scss/courseware/toolbar.scss17
-rw-r--r--resources/assets/stylesheets/scss/dashboard.scss2
-rw-r--r--resources/assets/stylesheets/scss/evaluation.scss40
-rw-r--r--resources/assets/stylesheets/scss/files.scss9
-rw-r--r--resources/assets/stylesheets/scss/forms.scss22
-rw-r--r--resources/assets/stylesheets/scss/globalsearch.scss4
-rw-r--r--resources/assets/stylesheets/scss/lists.scss40
-rw-r--r--resources/assets/stylesheets/scss/messagebox.scss2
-rw-r--r--resources/assets/stylesheets/scss/mvv.scss24
-rw-r--r--resources/assets/stylesheets/scss/my_courses.scss39
-rw-r--r--resources/assets/stylesheets/scss/profile.scss2
-rw-r--r--resources/assets/stylesheets/scss/questionnaire.scss198
-rw-r--r--resources/assets/stylesheets/scss/responsive.scss20
-rw-r--r--resources/assets/stylesheets/scss/schedule.scss48
-rw-r--r--resources/assets/stylesheets/scss/sidebar.scss18
-rw-r--r--resources/assets/stylesheets/scss/system-notifications.scss178
-rw-r--r--resources/assets/stylesheets/scss/tables.scss8
-rw-r--r--resources/assets/stylesheets/scss/tabs.scss2
-rw-r--r--resources/assets/stylesheets/scss/talk-bubble.scss9
-rw-r--r--resources/assets/stylesheets/scss/tooltip.scss10
-rw-r--r--resources/assets/stylesheets/scss/wiki.scss2
-rw-r--r--resources/assets/stylesheets/studip.scss5
-rw-r--r--resources/vue/base-components.js88
-rw-r--r--resources/vue/components/ActiveFilter.vue2
-rw-r--r--resources/vue/components/AdminCourses.vue45
-rw-r--r--resources/vue/components/CacheAdministration.vue34
-rw-r--r--resources/vue/components/ConsultationCreator.vue496
-rw-r--r--resources/vue/components/ContentModulesControl.vue2
-rw-r--r--resources/vue/components/ContentModulesEditTiles.vue9
-rw-r--r--resources/vue/components/ContentmodulesEditTable.vue8
-rw-r--r--resources/vue/components/Datepicker.vue146
-rw-r--r--resources/vue/components/EditableList.vue2
-rw-r--r--resources/vue/components/FilesTable.vue120
-rw-r--r--resources/vue/components/MyCoursesTables.vue15
-rw-r--r--resources/vue/components/MyCoursesTiles.vue6
-rw-r--r--resources/vue/components/Quicksearch.vue1
-rw-r--r--resources/vue/components/StudipAssetImg.vue2
-rw-r--r--resources/vue/components/StudipDialog.vue16
-rw-r--r--resources/vue/components/StudipFileChooser.vue4
-rw-r--r--resources/vue/components/StudipIcon.vue2
-rw-r--r--resources/vue/components/StudipTooltipIcon.vue3
-rw-r--r--resources/vue/components/StudipWysiwyg.vue20
-rw-r--r--resources/vue/components/SystemNotification.vue173
-rw-r--r--resources/vue/components/SystemNotificationManager.vue73
-rw-r--r--resources/vue/components/Timepicker.vue37
-rw-r--r--resources/vue/components/blubber/Composer.vue4
-rw-r--r--resources/vue/components/courseware/CoursewareContentPermissions.vue6
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue4
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareBlockActions.vue1
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue12
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue15
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue40
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue19
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue4
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue4
-rw-r--r--resources/vue/components/courseware/containers/CoursewareContainerActions.vue1
-rw-r--r--resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue49
-rw-r--r--resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue58
-rw-r--r--resources/vue/components/courseware/structural-element/CoursewareRootContent.vue171
-rw-r--r--resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue4
-rw-r--r--resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue4
-rw-r--r--resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue4
-rw-r--r--resources/vue/components/courseware/toolbar/CoursewareToolbar.vue48
-rw-r--r--resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue170
-rw-r--r--resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue14
-rw-r--r--resources/vue/components/courseware/unit/CoursewareUnitItem.vue2
-rw-r--r--resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue119
-rw-r--r--resources/vue/components/courseware/unit/CoursewareUnitProgress.vue2
-rw-r--r--resources/vue/components/file-chooser/FileChooserDialog.vue4
-rw-r--r--resources/vue/components/form_inputs/CalendarPermissionsTable.vue6
-rw-r--r--resources/vue/components/form_inputs/CaptchaInput.vue70
-rw-r--r--resources/vue/components/form_inputs/DateListInput.vue20
-rw-r--r--resources/vue/components/form_inputs/MyCoursesColouredTable.vue2
-rw-r--r--resources/vue/components/questionnaires/FreetextEdit.vue39
-rw-r--r--resources/vue/components/questionnaires/InputArray.vue279
-rw-r--r--resources/vue/components/questionnaires/LikertEdit.vue189
-rw-r--r--resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue44
-rw-r--r--resources/vue/components/questionnaires/RangescaleEdit.vue216
-rw-r--r--resources/vue/components/questionnaires/VoteEdit.vue51
-rw-r--r--resources/vue/components/responsive/ResponsiveContentBar.vue8
-rw-r--r--resources/vue/components/responsive/ResponsiveNavigation.vue22
-rw-r--r--resources/vue/components/tree/StudipTree.vue37
-rw-r--r--resources/vue/components/tree/StudipTreeList.vue13
-rw-r--r--resources/vue/components/tree/StudipTreeTable.vue11
-rw-r--r--resources/vue/components/tree/StudipTreeViewWidget.vue56
-rw-r--r--resources/vue/components/tree/TreeBreadcrumb.vue2
-rw-r--r--resources/vue/components/tree/TreeCourseDetails.vue8
-rw-r--r--resources/vue/components/tree/TreeNodeCoursePath.vue8
-rw-r--r--resources/vue/components/tree/TreeNodeTile.vue2
-rw-r--r--resources/vue/components/tree/TreeSearchResult.vue5
-rw-r--r--resources/vue/mixins/QuestionnaireComponent.js24
-rw-r--r--resources/vue/mixins/courseware/import.js32
-rw-r--r--resources/vue/store/ContentModulesStore.js2
-rw-r--r--resources/vue/store/MyCoursesStore.js2
-rw-r--r--resources/vue/store/blubber.js2
-rw-r--r--resources/vue/store/courseware/courseware-shelf.module.js24
-rw-r--r--resources/vue/store/courseware/courseware.module.js20
152 files changed, 3288 insertions, 2336 deletions
diff --git a/resources/assets/javascripts/bootstrap/application.js b/resources/assets/javascripts/bootstrap/application.js
index a9f53df..4ad248f 100644
--- a/resources/assets/javascripts/bootstrap/application.js
+++ b/resources/assets/javascripts/bootstrap/application.js
@@ -353,6 +353,7 @@ jQuery(document).on('click', 'a[data-behaviour~="ajax-toggle"]', function (event
(function ($) {
$(document).on('click', 'form[name=course-details] fieldset legend', function () {
$('#open_variable').attr('value', $(this).parent('fieldset').data('open'));
+ $(this).parent('fieldset').attr('aria-expanded', $(this).parent('fieldset').attr('aria-expanded') == 'true' ? 'false' : 'true');
});
}(jQuery));
diff --git a/resources/assets/javascripts/bootstrap/article.js b/resources/assets/javascripts/bootstrap/article.js
index fbfc131..04316f5 100644
--- a/resources/assets/javascripts/bootstrap/article.js
+++ b/resources/assets/javascripts/bootstrap/article.js
@@ -13,6 +13,7 @@
// Open the contentbox
article.toggleClass('open').removeClass('new');
+ article.attr('aria-expanded', article.attr('aria-expanded') === 'true' ? 'false' : 'true');
});
// Open closed article contents when location hash matches
diff --git a/resources/assets/javascripts/bootstrap/clipboard.js b/resources/assets/javascripts/bootstrap/clipboard.js
index e525b35..a64a605 100644
--- a/resources/assets/javascripts/bootstrap/clipboard.js
+++ b/resources/assets/javascripts/bootstrap/clipboard.js
@@ -25,7 +25,9 @@ STUDIP.domReady(function () {
jQuery(document).on('click', '.clipboard-remove-button', function (event) {
event.preventDefault();
- STUDIP.Dialog.confirm($(this).data('confirm-message'), function() {
+
+ const message = $(this).data('confirm-message');
+ STUDIP.Dialog.confirm(message).done(() => {
STUDIP.Clipboard.handleRemoveClick(event.target);
});
});
@@ -62,10 +64,11 @@ STUDIP.domReady(function () {
});
});
- jQuery(document).on('submit', '.clipboard-widget .new-clipboard-form', function (event) {
- event.preventDefault();
- STUDIP.Clipboard.handleAddForm(event);
- });
+ jQuery(document).on(
+ 'submit',
+ '.clipboard-widget .new-clipboard-form',
+ STUDIP.Clipboard.handleAddForm
+ );
jQuery(document).on('click', '.clipboard-add-item-button', function (event) {
event.preventDefault();
diff --git a/resources/assets/javascripts/bootstrap/consultations.js b/resources/assets/javascripts/bootstrap/consultations.js
index ef79d9c..51ffa85 100644
--- a/resources/assets/javascripts/bootstrap/consultations.js
+++ b/resources/assets/javascripts/bootstrap/consultations.js
@@ -10,9 +10,9 @@ $(document).on('click', '.consultation-delete-check:not(.ignore)', event => {
}
let requests = ids.map(id => {
- return STUDIP.jsonapi.GET(`consultation-slots/${id}/bookings`).then(result => result.data.length);
+ return STUDIP.jsonapi.withPromises().get(`consultation-slots/${id}/bookings`).then(response => response.data.length);
});
- $.when(...requests).done((...results) => {
+ Promise.all(requests).then((...results) => {
if (results.some(result => result > 0)) {
$(event.target).addClass('ignore').click().removeClass('ignore');
} else {
diff --git a/resources/assets/javascripts/bootstrap/contentbox.js b/resources/assets/javascripts/bootstrap/contentbox.js
index 42c5df1..3f05331 100644
--- a/resources/assets/javascripts/bootstrap/contentbox.js
+++ b/resources/assets/javascripts/bootstrap/contentbox.js
@@ -13,5 +13,6 @@ $(document).on('click', 'section.contentbox article header h1 a', function(e) {
// Open the contentbox
article.toggleClass('open').removeClass('new');
+ article.attr('aria-expanded', article.attr('aria-expanded') === 'true' ? 'false' : 'true');
}
});
diff --git a/resources/assets/javascripts/bootstrap/copyable_links.js b/resources/assets/javascripts/bootstrap/copyable_links.js
index 521eae4..b4f6fc6 100644
--- a/resources/assets/javascripts/bootstrap/copyable_links.js
+++ b/resources/assets/javascripts/bootstrap/copyable_links.js
@@ -16,24 +16,8 @@ $(document).on('click', 'a.copyable-link', function (event) {
document.execCommand('Copy');
dummy.remove();
- // Show visual hint using a deferred (this way we don't need to
- // duplicate the functionality in the done() handler)
- (new Promise((resolve, reject) => {
- let confirmation = $('<div class="copyable-link-confirmation copyable-link-success">');
- confirmation.text($gettext('Link wurde kopiert'));
- confirmation.insertBefore('#content');
-
- // Resolve deferred when animation has ended or after 2 seconds as a
- // fail safe
- let timeout = setTimeout(() => {
- $(this).parent().off('animationend');
- resolve(confirmation);
- }, 1500);
- $(this).parent().one('animationend', () => {
- clearTimeout(timeout);
- resolve(confirmation);
- });
- })).then((confirmation, parent) => {
- confirmation.remove();
- });
+ STUDIP.eventBus.emit(
+ 'push-system-notification',
+ { type: 'success', message: $gettext('Link wurde kopiert') }
+ );
});
diff --git a/resources/assets/javascripts/bootstrap/forms.js b/resources/assets/javascripts/bootstrap/forms.js
index c34a0c1..7643612 100644
--- a/resources/assets/javascripts/bootstrap/forms.js
+++ b/resources/assets/javascripts/bootstrap/forms.js
@@ -1,4 +1,5 @@
import { $gettext, $gettextInterpolate } from '../lib/gettext';
+import Report from '../lib/report.js';
// Allow fieldsets to collapse
$(document).on(
@@ -291,9 +292,12 @@ STUDIP.ready(function () {
url: v.STUDIPFORM_AUTOSAVEURL,
data: params,
type: 'post',
- success() {
- if (v.STUDIPFORM_REDIRECTURL) {
- window.location.href = v.STUDIPFORM_REDIRECTURL
+ success(output) {
+ if (output === 'STUDIPFORM_STORE_SUCCESS' && v.STUDIPFORM_REDIRECTURL) {
+ //The form has been stored successfully:
+ window.location.href = v.STUDIPFORM_REDIRECTURL;
+ } else if (output !== 'STUDIPFORM_STORE_SUCCESS') {
+ Report.error($gettext('Es ist ein Fehler aufgetreten'), output);
}
}
});
diff --git a/resources/assets/javascripts/bootstrap/global_search.js b/resources/assets/javascripts/bootstrap/global_search.js
index 4d0738e..0e179b7 100644
--- a/resources/assets/javascripts/bootstrap/global_search.js
+++ b/resources/assets/javascripts/bootstrap/global_search.js
@@ -27,15 +27,47 @@ STUDIP.domReady(() => {
// Enlarge search input on focus and show hints.
$('#globalsearch-input').on('focus', function() {
STUDIP.GlobalSearch.toggleSearchBar(true, false);
- });
-
- // Start search on Enter
- $('#globalsearch-input').on('keypress', function(e) {
- if (e.which === 13) {
+ }).on('keypress', (e) => {
+ // Start search on Enter
+ if (e.key === 'Enter') {
STUDIP.GlobalSearch.doSearch();
return false;
}
});
+ $('#globalsearch-searchbar').on('keydown', function(e) {
+ if (!['ArrowDown', 'ArrowUp'].includes(e.key)) {
+ return;
+ }
+
+ e.preventDefault();
+
+ // Get all possible items
+ const items = $('#globalsearch-list [role=listitem]:visible');
+
+ // Find focussed element
+ const focussed = items.filter(':focus');
+
+ // Get index of focussed element in all items
+ let index = focussed.length > 0 ? items.index(focussed[0]) : null;
+
+ // Move focussed element up or down in items
+ if (e.key === 'ArrowDown') {
+ index = (index ?? -1) + 1;
+ } else {
+ index = (index ?? items.length) - 1;
+ }
+
+ // Clamp index to sane boundaries
+ if (index < 0) {
+ index = 0;
+ } else if (index > items.length - 1) {
+ index = items.length - 1;
+ }
+
+ // Focus new element by index
+ items.get(index).focus();
+ });
+
// Close search on click on page.
$('#navigation-level-1, #current-page-structure, #main-footer').on('click', function() {
diff --git a/resources/assets/javascripts/bootstrap/resources.js b/resources/assets/javascripts/bootstrap/resources.js
index 8c89b7f..7eb6a68 100644
--- a/resources/assets/javascripts/bootstrap/resources.js
+++ b/resources/assets/javascripts/bootstrap/resources.js
@@ -416,7 +416,7 @@ STUDIP.ready(function () {
$("#BookingEndDateInput").prop('defaultValue', $(this).val());
$("#BookingEndDateInput").val($(this).val()).trigger('change');
}
- updateRepeatEndSemesterByTimestamp(Math.floor(d / 1000));
+ updateRepeatEndSemesterByTimestamp(d);
} else if ($(this).attr('id') == 'BookingEndDateInput') {
$("#end_date-weekdays span").addClass('invisible');
$("#end_date-weekdays #" + day_numer).removeClass('invisible');
@@ -545,38 +545,41 @@ STUDIP.ready(function () {
}
);
- function updateRepeatEndSemesterByTimestamp(timestamp, api_url = 'api.php/semesters') {
- var semester = null;
- jQuery.ajax(
- STUDIP.URLHelper.getURL(api_url),
- {
- method: 'get',
- dataType: 'json',
- success: function (data) {
- if (data) {
- Object.values(data.collection).forEach(item => {
- if (timestamp >= item.begin && timestamp < item.end) {
- semester = item;
- }
- });
- if (semester) {
- $("#semester_course_name").text(semester.title);
- $(".semester-time-option").prop('disabled', false);
- } else {
- if (data.pagination && data.pagination.links.next != api_url) {
- semester = updateRepeatEndSemesterByTimestamp(timestamp, data.pagination.links.next);
- } else {
- $("#semester_course_name").text('außerhalb definierter Zeiten');
- $(".semester-time-option").prop('checked', false);
- $(".semester-time-option").prop('disabled', true);
- $(".manual-time-option").prop('checked', true);
- $(".manual-time-option").trigger('change');
- }
- }
- }
- }
+ function updateRepeatEndSemesterByTimestamp(timestamp) {
+ (new Promise((resolve, reject) => {
+ const cache = STUDIP.Cache.getInstance('jsonapi');
+ if (cache.has('semesters')) {
+ resolve(cache.get('semesters'));
+ } else {
+ STUDIP.jsonapi.GET('semesters', { data: { page: { limit: 100000 }}})
+ .done(({data}) => {
+ cache.set('semesters', data);
+ resolve(data)
+ })
+ .fail(() => {
+ reject(new Error('Could not load semesters'));
+ });
+ }
+ })).then(semesters => {
+ const semester = semesters.find(({attributes}) => {
+ return new Date(attributes.start) <= timestamp
+ && timestamp <= new Date(attributes.end);
+ });
+
+ if (semester) {
+ $('#semester_course_name').text(semester.attributes.title);
+ $('.semester-time-option').prop('disabled', false);
+ } else {
+ $('#semester_course_name').text('außerhalb definierter Zeiten');
+ $('.semester-time-option').prop({
+ checked: false,
+ disabled: true
+ });
+ $('.manual-time-option')
+ .prop('checked', true)
+ .trigger('change');
}
- );
+ });
}
function updateViewURL(defaultView) {
diff --git a/resources/assets/javascripts/bootstrap/responsive-navigation.js b/resources/assets/javascripts/bootstrap/responsive-navigation.js
index aa81107..ad39d2b 100644
--- a/resources/assets/javascripts/bootstrap/responsive-navigation.js
+++ b/resources/assets/javascripts/bootstrap/responsive-navigation.js
@@ -1,6 +1,6 @@
import ResponsiveNavigation from '../../../vue/components/responsive/ResponsiveNavigation.vue';
-STUDIP.ready(() => {
+STUDIP.domReady(() => {
STUDIP.Vue.load().then(({ createApp }) => {
createApp({
el: '#responsive-menu',
diff --git a/resources/assets/javascripts/bootstrap/search.js b/resources/assets/javascripts/bootstrap/search.js
index 6a0559b..7d4fb76 100644
--- a/resources/assets/javascripts/bootstrap/search.js
+++ b/resources/assets/javascripts/bootstrap/search.js
@@ -2,8 +2,6 @@ STUDIP.domReady(() => {
var cache = STUDIP.Search.getCache();
// initially hide all filters except for the semester filter
$('#reset-search').hide();
- STUDIP.Search.hideAllFilters();
- $('div#semester_filter').show();
STUDIP.Search.setActiveCategory('show_all_categories');
STUDIP.Search.showActiveFilters(STUDIP.Search.getFilter());
diff --git a/resources/assets/javascripts/bootstrap/studip_helper_attributes.js b/resources/assets/javascripts/bootstrap/studip_helper_attributes.js
index c106de3..aea1823 100644
--- a/resources/assets/javascripts/bootstrap/studip_helper_attributes.js
+++ b/resources/assets/javascripts/bootstrap/studip_helper_attributes.js
@@ -276,6 +276,15 @@ $(document).on('click keydown', '[data-toggles]', function (event) {
$(target).toggle();
}
+ const controls = $(event.currentTarget).attr('aria-controls');
+ if (controls) {
+ // Find elements which control the expanded status of the same element.
+ const elements = $('[aria-controls="' + controls + '"]');
+ const expanded = $(event.currentTarget).attr('aria-expanded') === 'true';
+ // Set the aria-expanded status accordingly.
+ elements.attr('aria-expanded', !expanded);
+ }
+
event.preventDefault();
}
});
diff --git a/resources/assets/javascripts/bootstrap/system-notifications.js b/resources/assets/javascripts/bootstrap/system-notifications.js
new file mode 100644
index 0000000..7a85fcd
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/system-notifications.js
@@ -0,0 +1,11 @@
+import SystemNotificationManager from '../../../vue/components/SystemNotificationManager.vue';
+
+STUDIP.domReady(() => {
+ document.getElementById('system-notifications')?.classList.add('vueified');
+ STUDIP.Vue.load().then(({ createApp }) => {
+ createApp({
+ el: '#system-notifications',
+ components: { SystemNotificationManager }
+ });
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/tooltip.js b/resources/assets/javascripts/bootstrap/tooltip.js
deleted file mode 100644
index c84042b..0000000
--- a/resources/assets/javascripts/bootstrap/tooltip.js
+++ /dev/null
@@ -1,67 +0,0 @@
-// Attach global hover handler for tooltips.
-// Applies to all elements having a "data-tooltip" attribute.
-// Tooltip may be provided in the data-attribute itself or by
-// defining a title attribute. The latter is prefered due to
-// the obvious accessibility issues.
-
-var timeout = null;
-
-STUDIP.Tooltip.threshold = 6;
-
-$(document).on('mouseenter mouseleave focusin focusout', '[data-tooltip],.tooltip:has(.tooltip-content)', function(event) {
- let data = $(this).data();
-
- const visible = event.type === 'mouseenter' || event.type === 'focusin';
- const offset = $(this).offset();
- const x = offset.left + $(this).outerWidth(true) / 2;
- const y = offset.top;
- const delay = data.tooltipDelay ?? 300;
-
- let content;
- let tooltip;
-
- if (!data.tooltipObject) {
- // If tooltip has not yet been created (first hover), obtain it's
- // contents and create the actual tooltip object.
- if (!data.tooltip || !$.isPlainObject(data.tooltip)) {
- content = $('<div/>').text(data.tooltip || $(this).attr('title')).html();
- } else if (data.tooltip.html !== undefined) {
- content = data.tooltip.html;
- } else if (data.tooltip.text !== undefined) {
- content = data.tooltip.text;
- } else {
- throw "Invalid content for tooltip via data";
- }
- if (!content) {
- content = $(this).find('.tooltip-content').remove().html();
- }
- $(this).attr('title', null);
- $(this).attr('data-tooltip', content);
-
- tooltip = new STUDIP.Tooltip(x, y, content);
-
- data.tooltipObject = tooltip;
- $(this).attr('aria-describedby', tooltip.id);
-
- $(this).on('remove', function() {
- tooltip.remove();
- });
- } else if (visible) {
- // If tooltip has already been created, update it's position.
- // This is neccessary if the surrounding content is scrollable AND has
- // been scrolled. Otherwise the tooltip would appear at it's previous
- // and now wrong location.
- data.tooltipObject.position(x, y);
- }
-
- if (visible) {
- $('.studip-tooltip').not(data.tooltipObject).hide();
- data.tooltipObject.show();
- } else {
- timeout = setTimeout(() => data.tooltipObject.hide(), delay);
- }
-}).on('mouseenter focusin', '.studip-tooltip', () => {
- clearTimeout(timeout);
-}).on('mouseleave focusout', '.studip-tooltip', function() {
- $(this).hide();
-});
diff --git a/resources/assets/javascripts/bootstrap/treeview.js b/resources/assets/javascripts/bootstrap/treeview.js
index 998a70e..d132775 100644
--- a/resources/assets/javascripts/bootstrap/treeview.js
+++ b/resources/assets/javascripts/bootstrap/treeview.js
@@ -1,7 +1,8 @@
import StudipTree from '../../../vue/components/tree/StudipTree.vue'
STUDIP.ready(() => {
- document.querySelectorAll('[data-studip-tree]').forEach(element => {
+ document.querySelectorAll('[data-studip-tree]:not(.vueified)').forEach(element => {
+ element.classList.add('vueified');
STUDIP.Vue.load().then(({ createApp }) => {
createApp({
el: element,
diff --git a/resources/assets/javascripts/bootstrap/vue.js b/resources/assets/javascripts/bootstrap/vue.js
index b8c938d..c6816a2 100644
--- a/resources/assets/javascripts/bootstrap/vue.js
+++ b/resources/assets/javascripts/bootstrap/vue.js
@@ -28,27 +28,17 @@ STUDIP.ready(() => {
});
STUDIP.Vue.load().then(async ({createApp, store}) => {
- let vm;
if (config.store) {
const storeConfig = await import(`../../../vue/store/${config.store}.js`);
- console.log('store', storeConfig.default);
store.registerModule(config.id, storeConfig.default, {root: true});
Object.keys(data).forEach(command => {
store.commit(`${config.id}/${command}`, data[command]);
});
- vm = createApp({components});
- } else {
- vm = createApp({data, components});
}
- // import myCoursesStore from '../stores/MyCoursesStore.js';
- //
- // myCoursesStore.namespaced = true;
- //
- // store.registerModule('my-courses', myCoursesStore);
- vm.$mount(this);
+ createApp({components, data}).$mount(this);
});
$(this).attr('data-vue-app-created', '');
diff --git a/resources/assets/javascripts/bootstrap/wysiwyg.js b/resources/assets/javascripts/bootstrap/wysiwyg.js
index fb158bb..9e18cf8 100644
--- a/resources/assets/javascripts/bootstrap/wysiwyg.js
+++ b/resources/assets/javascripts/bootstrap/wysiwyg.js
@@ -6,8 +6,11 @@ STUDIP.domReady(() => {
$(document).on('focus blur', '.studip-dialog .ck-editor__editable_inline', function(event) {
let height = this.clientHeight;
let editor = this.ckeditorInstance;
- editor.editing.view.change(writer => {
- writer.setStyle('height', height + 'px', editor.editing.view.document.getRoot());
+ // this is needed on Chrome, see https://gitlab.studip.de/studip/studip/-/issues/3510
+ setTimeout(() => {
+ editor.editing.view.change(writer => {
+ writer.setStyle('height', height + 'px', editor.editing.view.document.getRoot());
+ });
});
});
diff --git a/resources/assets/javascripts/chunk-loader.js b/resources/assets/javascripts/chunk-loader.js
index dcd95cc..f372286 100644
--- a/resources/assets/javascripts/chunk-loader.js
+++ b/resources/assets/javascripts/chunk-loader.js
@@ -8,95 +8,100 @@ export const loadScript = function (script_name) {
});
};
-export const loadChunk = (function () {
- let mathjax_promise = null;
+let mathjax_promise = null;
- return function (chunk) {
- let promise = null;
- switch (chunk) {
+/** This function dynamically loads JS features organized in chunks.
+ *
+ * @param {string} chunk The name of the chunk to load.
+ * @param {{ silent: boolean }} options Options for loading the chunk.
+ * Pass `{ silent: true }` to supress
+ * error messages.
+ * @return {Promise}
+ */
+export const loadChunk = function (chunk, { silent = false } = {}) {
+ let promise = null;
+ switch (chunk) {
+ case 'code-highlight':
+ promise = import(
+ /* webpackChunkName: "code-highlight" */
+ './chunks/code-highlight'
+ ).then(({ default: hljs }) => {
+ return hljs;
+ });
+ break;
- case 'code-highlight':
- promise = import(
- /* webpackChunkName: "code-highlight" */
- './chunks/code-highlight'
- ).then(({default: hljs}) => {
- return hljs;
- });
- break;
+ case 'courseware':
+ promise = Promise.all([
+ STUDIP.loadChunk('vue'),
+ import(
+ /* webpackChunkName: "courseware" */
+ './chunks/courseware'
+ ),
+ ]).then(([Vue]) => Vue);
+ break;
- case 'courseware':
- promise = Promise.all([
- STUDIP.loadChunk('vue'),
- import(
- /* webpackChunkName: "courseware" */
- './chunks/courseware'
- ),
- ]).then(([Vue]) => Vue);
- break;
+ case 'chartist':
+ promise = import(
+ /* webpackChunkName: "chartist" */
+ './chunks/chartist'
+ ).then(({ default: Chartist }) => Chartist);
+ break;
- case 'chartist':
- promise = import(
- /* webpackChunkName: "chartist" */
- './chunks/chartist'
- ).then(({ default: Chartist }) => Chartist);
- break;
+ case 'fullcalendar':
+ promise = import(
+ /* webpackChunkName: "fullcalendar" */
+ './chunks/fullcalendar'
+ );
+ break;
- case 'fullcalendar':
- promise = import(
- /* webpackChunkName: "fullcalendar" */
- './chunks/fullcalendar'
- );
- break;
+ case 'tablesorter':
+ promise = import(
+ /* webpackChunkName: "tablesorter" */
+ './chunks/tablesorter'
+ );
+ break;
- case 'tablesorter':
- promise = import(
- /* webpackChunkName: "tablesorter" */
- './chunks/tablesorter'
- );
- break;
-
- case 'mathjax':
- if (mathjax_promise === null) {
- mathjax_promise = STUDIP.loadScript(
- 'javascripts/mathjax/MathJax.js?config=TeX-AMS_HTML,default'
- ).then(() => {
+ case 'mathjax':
+ if (mathjax_promise === null) {
+ mathjax_promise = STUDIP.loadScript('javascripts/mathjax/MathJax.js?config=TeX-AMS_HTML,default')
+ .then(() => {
(function (origPrint) {
window.print = function () {
- window.MathJax.Hub.Queue(
- ['Delay', window.MathJax.Callback, 700],
- origPrint
- );
+ window.MathJax.Hub.Queue(['Delay', window.MathJax.Callback, 700], origPrint);
};
})(window.print);
return window.MathJax;
- }).catch(() => {
- console.log('Could not load mathjax')
+ })
+ .catch(() => {
+ throw new Error('Could not load mathjax');
});
- }
- promise = mathjax_promise;
- break;
+ }
+ promise = mathjax_promise;
+ break;
- case 'vue':
- promise = import(
- /* webpackChunkName: "vue.js" */
- './chunks/vue'
- );
- break;
+ case 'vue':
+ promise = import(
+ /* webpackChunkName: "vue.js" */
+ './chunks/vue'
+ );
+ break;
- case 'wysiwyg':
- promise = import(
- /* webpackChunkName: "wysiwyg.js" */
- './chunks/wysiwyg'
- );
- break;
+ case 'wysiwyg':
+ promise = import(
+ /* webpackChunkName: "wysiwyg.js" */
+ './chunks/wysiwyg'
+ );
+ break;
- default:
- promise = Promise.reject(new Error(`Unknown chunk: ${chunk}`));
- }
+ default:
+ promise = Promise.reject(new Error(`Unknown chunk: ${chunk}`));
+ }
- return promise.catch((error) => {
+ return promise.catch((error) => {
+ if (!silent) {
console.error(`Could not load chunk ${chunk}`, error);
- });
- };
-}());
+ }
+ throw error;
+ });
+};
diff --git a/resources/assets/javascripts/chunks/vue.js b/resources/assets/javascripts/chunks/vue.js
index b98cc27..8d506a1 100644
--- a/resources/assets/javascripts/chunks/vue.js
+++ b/resources/assets/javascripts/chunks/vue.js
@@ -39,6 +39,9 @@ Vue.mixin({
globalOn(...args) {
eventBus.on(...args);
},
+ globalOff(...args) {
+ eventBus.off(...args);
+ },
getStudipConfig: store.getters['studip/getConfig']
},
});
diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js
index 73b1aaa..5f88c30 100644
--- a/resources/assets/javascripts/entry-base.js
+++ b/resources/assets/javascripts/entry-base.js
@@ -16,6 +16,8 @@ import "./init.js"
import "./bootstrap/responsive.js"
import "./bootstrap/vue.js"
+import "./bootstrap/system-notifications.js"
+
import "./bootstrap/my-courses.js";
import "./studip-ui.js"
@@ -55,7 +57,6 @@ import "./bootstrap/article.js"
import "./bootstrap/copyable_links.js"
import "./bootstrap/selection.js"
import "./bootstrap/data_secure.js"
-import "./bootstrap/tooltip.js"
import "./bootstrap/lightbox.js"
import "./bootstrap/application.js"
import "./bootstrap/global_search.js"
diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js
index 2d592be..1d7d5ac 100644
--- a/resources/assets/javascripts/init.js
+++ b/resources/assets/javascripts/init.js
@@ -38,7 +38,7 @@ import HeaderMagic from './lib/header_magic.js';
import i18n from './lib/i18n.js';
import Instschedule from './lib/instschedule.js';
import InlineEditing from './lib/inline-editing.js';
-import JSONAPI, { jsonapi } from './lib/jsonapi.js';
+import JSONAPI, { jsonapi } from './lib/jsonapi.ts';
import JSUpdater from './lib/jsupdater.js';
import Lightbox from './lib/lightbox.js';
import Markup from './lib/markup.js';
@@ -64,7 +64,6 @@ import register from './lib/register.js';
import Report from './lib/report.js';
import Resources from './lib/resources.js';
import Responsive from './lib/responsive.js';
-import RESTAPI, { api } from './lib/restapi.js';
import Schedule from './lib/schedule.js';
import Screenreader from './lib/screenreader.js';
import Scroll from './lib/scroll.js';
@@ -76,7 +75,6 @@ import Statusgroups from './lib/statusgroups.js';
import study_area_selection from './lib/study_area_selection.js';
import Table from './lib/table.js';
import TableOfContents from './lib/table-of-contents.js';
-import Tooltip from './lib/tooltip.js';
import Tour from './lib/tour.js';
import * as Gettext from './lib/gettext';
import UserFilter from './lib/user_filter.js';
@@ -93,7 +91,6 @@ window.STUDIP = _.assign(window.STUDIP || {}, {
admin_sem_class,
AdminCourses,
Admission,
- api,
Arbeitsgruppen,
Archive,
Avatar,
@@ -152,7 +149,6 @@ window.STUDIP = _.assign(window.STUDIP || {}, {
register,
Report,
Responsive,
- RESTAPI,
Schedule,
Scroll,
Screenreader,
@@ -164,7 +160,6 @@ window.STUDIP = _.assign(window.STUDIP || {}, {
study_area_selection,
Table,
TableOfContents,
- Tooltip,
Tour,
URLHelper,
UserFilter,
diff --git a/resources/assets/javascripts/lib/RestrictedDatesHelper.ts b/resources/assets/javascripts/lib/RestrictedDatesHelper.ts
new file mode 100644
index 0000000..bcc0af2
--- /dev/null
+++ b/resources/assets/javascripts/lib/RestrictedDatesHelper.ts
@@ -0,0 +1,89 @@
+import { jsonapi } from "./jsonapi";
+
+type RestrictedDate = {
+ year: Number,
+ month: Number,
+ day: Number,
+
+ reason: string | null,
+ lock: boolean
+}
+
+class RestrictedDatesHelper
+{
+ static #loadedYears : Number[] = [];
+ static #restrictedDates: RestrictedDate[] = [];
+
+ static isDateRestricted(date: Date, returnBoolean: Boolean = false): RestrictedDate | Boolean {
+ const restrictedDate : RestrictedDate | undefined = this.#restrictedDates.find(item => {
+ return item.year === date.getFullYear()
+ && item.month === date.getMonth() + 1
+ && item.day === date.getDate();
+ });
+
+ if (returnBoolean) {
+ return !!restrictedDate;
+ }
+
+ return restrictedDate ?? this.#convertDate(date, null, false);
+ }
+
+ static async loadRestrictedDatesByYear(year: Number): Promise<void> {
+ if (this.#loadedYears.includes(year)) {
+ return Promise.reject();
+ }
+
+ this.#loadedYears.push(year);
+
+ jsonapi.withPromises().request('holidays', {data: {
+ 'filter[year]': year
+ }}).then((response: [] | Object) => {
+ // Since PHP will return an empty object as an array,
+ // we need to check
+ if (Array.isArray(response)) {
+ return;
+ }
+
+ for (const [date, data] of Object.entries(response)) {
+ this.#addRestrictedDate(
+ new Date(date),
+ data.holiday,
+ data.mandatory
+ );
+ }
+ });
+ }
+
+ static #addRestrictedDate(date: Date, reason: string, lock: boolean = true): void {
+ const restricted = this.#convertDate(date, reason, lock);
+
+ this.#restrictedDates = this.#restrictedDates.filter(item => {
+ return item.year !== restricted.year
+ || item.month !== restricted.month
+ || item.day !== restricted.day;
+ });
+
+ this.#restrictedDates.push(restricted);
+ }
+
+ static removeRestrictedDate(date: Date): void {
+ this.#restrictedDates = this.#restrictedDates.filter(item => {
+ return item.year !== date.getFullYear()
+ || item.month !== date.getMonth() + 1
+ || item.day !== date.getDate();
+ });
+ }
+
+ static #convertDate(date: Date, reason: string | null, lock: boolean): RestrictedDate {
+ return {
+ year: date.getFullYear(),
+ month: date.getMonth() + 1,
+ day: date.getDate(),
+
+ reason,
+ lock
+ };
+ }
+}
+
+export default RestrictedDatesHelper;
diff --git a/resources/assets/javascripts/lib/abstract-api.js b/resources/assets/javascripts/lib/abstract-api.js
index eafca85..95ae015 100644
--- a/resources/assets/javascripts/lib/abstract-api.js
+++ b/resources/assets/javascripts/lib/abstract-api.js
@@ -1,5 +1,20 @@
import Overlay from './overlay.js';
+class APIError extends Error
+{
+ static createWithJqXhr(message, jqXhr) {
+ const error = new APIError(message);
+ error.setJqXhr(jqXhr);
+ return error;
+ }
+
+ jqXhr = null;
+
+ setJqXhr(jqXhr) {
+ this.jqXhr = jqXhr;
+ }
+}
+
class AbstractAPI
{
static get supportedMethods() {
@@ -52,6 +67,8 @@ class AbstractAPI
var deferred;
+ const request = this.#createRequest(url, options);
+
if (options.async && this.request_count > 0) {
// Request should be sent asynchronous after every other request
// is finished. The configuration for this particular request is
@@ -73,10 +90,10 @@ class AbstractAPI
this.total_requests += 1;
// Actual request
- deferred = $.ajax(STUDIP.URLHelper.getURL(`${this.base_url}/${url}`, {}, true), {
+ deferred = $.ajax(request.url, {
contentType: options.contentType || 'application/x-www-form-urlencoded; charset=UTF-8',
method: options.method.toUpperCase(),
- data: this.encodeData(options.data, options.method.toUpperCase()),
+ data: this.encodeData(request.data, options.method.toUpperCase()),
headers: options.headers
}).always(() => {
// Decrease request counter, remove overlay if neccessary
@@ -93,6 +110,54 @@ class AbstractAPI
}
}).promise();
}
+
+ #createRequest(url, options) {
+ const hasBody = ['post', 'put', 'patch'].includes(options.method.toLowerCase());
+ const query = hasBody ? '' : `?${this.convertDataToRequestParameters(options.data)}`;
+
+ return {
+ url: STUDIP.URLHelper.getURL(`${this.base_url}/${url}${query}`, {}, true),
+ data: hasBody ? options.data : {},
+ };
+ }
+
+ convertDataToRequestParameters(data, prefix = '') {
+ return Object.entries(data).filter(([key, value]) => {
+ return value !== null;
+ }).map(([key, value]) => {
+ const name = prefix ? `${prefix}[${key}]` : `${key}`;
+ if (value.constructor?.name === 'Object') {
+ return this.convertDataToRequestParameters(value, name);
+ } else {
+ return `${name}=${value}`;
+ }
+ }).join('&');
+ }
+
+ withPromises() {
+ return new Proxy(this, {
+ get(target, prop, receiver) {
+ // This will allow http methods to be written as lowercase when called as methods
+ // (e.g. api.patch() instead of api.PATCH())
+ if (target[prop] === undefined && AbstractAPI.supportedMethods.includes(prop.toUpperCase())) {
+ prop = prop.toUpperCase();
+ }
+
+ // Only handle calls to request methods
+ if (prop !== 'request') {
+ return Reflect.get(target, prop, receiver);
+ }
+
+ // Return a wrapped promise that handles the deferred
+ return (url, options = {}) => new Promise((resolve, reject) => {
+ target[prop].apply(target, [url, options]).then(
+ (response) => resolve(response),
+ (jqXhr, textStatus, errorThrown) => reject(APIError.createWithJqXhr(errorThrown || textStatus, jqXhr))
+ );
+ });
+ }
+ })
+ }
}
// Create shortcut methods for easier access by method
diff --git a/resources/assets/javascripts/lib/activityfeed.js b/resources/assets/javascripts/lib/activityfeed.js
index 74c27f9..12f0bac 100644
--- a/resources/assets/javascripts/lib/activityfeed.js
+++ b/resources/assets/javascripts/lib/activityfeed.js
@@ -6,13 +6,13 @@ const ActivityFeed = {
maxheight: null,
filter: null,
- init: function() {
+ init() {
STUDIP.ActivityFeed.maxheight = parseInt($('#stream-container').css('max-height').replace(/[^-\d.]/g, ''));
STUDIP.ActivityFeed.loadFeed(STUDIP.ActivityFeed.filter);
- $('#stream-container').scroll(function () {
- var scrollBottom = $('#stream-container').scrollTop() + $('#stream-container').height() + 250;
+ $('#stream-container').scroll(() => {
+ const scrollBottom = $('#stream-container').scrollTop() + $('#stream-container').height() + 250;
if ($('#stream-container').prop('scrollHeight') < scrollBottom) {
STUDIP.ActivityFeed.loadFeed(STUDIP.ActivityFeed.filter);
@@ -23,7 +23,7 @@ const ActivityFeed = {
$(document).on('click', '.provider_circle', function () {
$(this).parent().parent().children('.activity-content').toggle();
}).on('click', '#toggle-all-activities,#toggle-user-activities', function () {
- var toggled = $(this).is(':not(.toggled)');
+ const toggled = $(this).is(':not(.toggled)');
$(this).toggleClass('toggled', toggled);
STUDIP.ActivityFeed.setToggleStatus();
@@ -32,11 +32,11 @@ const ActivityFeed = {
});
},
- getTemplate: _.memoize(function(name) {
- return _.template($("script." + name).html());
+ getTemplate: _.memoize(name => {
+ return _.template($(`script.${name}`).html());
}),
- loadFeed: function(filtertype) {
+ loadFeed(filtertype) {
if (STUDIP.ActivityFeed.user_id === null) {
console.log('Could not retrieve activities, no valid user id found!');
return false;
@@ -48,17 +48,18 @@ const ActivityFeed = {
STUDIP.ActivityFeed.polling = true;
- STUDIP.api.GET(['user', STUDIP.ActivityFeed.user_id, 'activitystream'], {
- data: {
- filtertype: JSON.stringify(filtertype),
- scrollfrom: STUDIP.ActivityFeed.scrolledfrom
- }
- }).done(function (activities) {
- var stream = STUDIP.ActivityFeed.getTemplate('activity_stream');
- var activity = STUDIP.ActivityFeed.getTemplate('activity');
- var activity_urls = STUDIP.ActivityFeed.getTemplate('activity-urls');
- var num_entries = Object.keys(activities).length;
- var lastelem = $(activities).last();
+ const url = STUDIP.URLHelper.getURL('dispatch.php/activityfeed/load', {
+ filtertype: JSON.stringify(filtertype),
+ scrollfrom: STUDIP.ActivityFeed.scrolledfrom,
+ });
+ fetch(url).then(
+ response => response.json(),
+ ).then(activities => {
+ const stream = STUDIP.ActivityFeed.getTemplate('activity_stream');
+ const activity = STUDIP.ActivityFeed.getTemplate('activity');
+ const activity_urls = STUDIP.ActivityFeed.getTemplate('activity-urls');
+ const num_entries = Object.keys(activities).length;
+ const lastelem = $(activities).last();
if (lastelem[0]) {
STUDIP.ActivityFeed.scrolledfrom = lastelem[0].mkdate;
@@ -79,15 +80,15 @@ const ActivityFeed = {
if ($('#stream-container').height() < STUDIP.ActivityFeed.maxheight) {
STUDIP.ActivityFeed.loadFeed('');
}
- }).fail(function () {
- var template = STUDIP.ActivityFeed.getTemplate('activity-load-error');
+ }).catch(() => {
+ const template = STUDIP.ActivityFeed.getTemplate('activity-load-error');
STUDIP.ActivityFeed.writeToStream(template());
- }).always(function () {
+ }).finally(() => {
STUDIP.ActivityFeed.polling = false;
});
},
- writeToStream: function (html) {
+ writeToStream(html) {
if (STUDIP.ActivityFeed.initial) {
// replace data in DOM
$('#stream-container').html('');
@@ -98,9 +99,9 @@ const ActivityFeed = {
$('#stream-container').append(html);
},
- setToggleStatus: function() {
- var show_details = $('#toggle-all-activities').is('.toggled'),
- show_own = $('#toggle-user-activities').is('.toggled');
+ setToggleStatus() {
+ const show_details = $('#toggle-all-activities').is('.toggled');
+ const show_own = $('#toggle-user-activities').is('.toggled');
// update toggle status fir activity contents
$('.activity-content').toggle(show_details);
@@ -109,7 +110,7 @@ const ActivityFeed = {
$('.activity:has(.provider_circle.right)').toggle(show_own);
},
- updateFilter: function(filter) {
+ updateFilter(filter) {
STUDIP.ActivityFeed.filter = filter;
STUDIP.ActivityFeed.initial = true;
STUDIP.ActivityFeed.scrolledfrom = Math.floor(Date.now() / 1000);
diff --git a/resources/assets/javascripts/lib/blubber.js b/resources/assets/javascripts/lib/blubber.js
index 3e93985..4e724b4 100644
--- a/resources/assets/javascripts/lib/blubber.js
+++ b/resources/assets/javascripts/lib/blubber.js
@@ -47,6 +47,7 @@ const Blubber = {
subscribe: follow,
});
}).then(() => {
+ elements.attr('aria-pressed', follow ? 'true' : 'false');
elements.toggleClass('unfollowed', !follow);
}).finally(() => {
elements.removeClass('loading');
diff --git a/resources/assets/javascripts/lib/clipboard.js b/resources/assets/javascripts/lib/clipboard.js
index e5890ab..6fd1489 100644
--- a/resources/assets/javascripts/lib/clipboard.js
+++ b/resources/assets/javascripts/lib/clipboard.js
@@ -1,4 +1,14 @@
-import {$gettext} from './gettext';
+function extractAttribute(node, attribute) {
+ return node.querySelector(`input[name="${attribute}"]`)?.value.trim();
+}
+
+function extractAttributes(node, attributes) {
+ const result = {};
+ for (const key of attributes) {
+ result[key] = extractAttribute(node, key);
+ }
+ return result;
+}
const Clipboard = {
switchClipboard: function(event) {
@@ -32,32 +42,30 @@ const Clipboard = {
}
},
- handleAddForm: function(event) {
- if (!event) {
- return false;
- }
-
+ handleAddForm(event) {
+ event.preventDefault();
+ const attributes = extractAttributes(event.target, ['name', 'allowed_item_class']);
//Check if a name is entered in the form:
- let name_input = jQuery(event.target).find('input[type="text"][name="name"]');
+ const name_input = event.target.querySelector('input[name="name"]');
if (!name_input) {
//Something is wrong with the HTML:
return false;
}
- let name = jQuery(name_input).val().trim();
- if (!name) {
+ if (!attributes.name) {
//The name field is empty. Why send an empty field?
return false;
}
- //Submit the form via AJAX:
- STUDIP.api.POST(
- 'clipboard/add',
- {
- data: jQuery(event.target).serialize()
- }
- ).done(STUDIP.Clipboard.add);
+ // Submit the form via AJAX:
+ STUDIP.jsonapi.POST('clipboards', {data: {data: {attributes}}}).done(({data}) => {
+ STUDIP.Clipboard.add({
+ id: data.id,
+ name: data.attributes.name,
+ widget_id: extractAttribute(event.target, 'widget_id')
+ });
+ });
},
add: function(data) {
@@ -134,11 +142,9 @@ const Clipboard = {
jQuery(widget_node).find('#clipboard-group-container').removeClass('invisible');
//Call the droppable jQuery method on the new clipboard area:
- jQuery(clipboard_node).droppable(
- {
- drop: STUDIP.Clipboard.handleItemDrop
- }
- );
+ jQuery(clipboard_node).droppable({
+ drop: STUDIP.Clipboard.handleItemDrop
+ });
//Clear the text input in the "add clipboard" form:
jQuery(widget_node).find(
@@ -238,17 +244,19 @@ const Clipboard = {
}
//Add the item to the clipboard via AJAX:
- STUDIP.api.POST(
- 'clipboard/' + clipboard_id + '/item',
- {
+ STUDIP.jsonapi.POST(`clipboards/${clipboard_id}/items`, {
+ data: {
data: {
- 'range_id': range_id,
- 'range_type': range_type,
- 'widget_id': widget_id
+ attributes: { range_id, range_type }
}
}
- ).done(function(data) {
- STUDIP.Clipboard.addDroppedItem(data);
+ }).done(({data}) => {
+ STUDIP.Clipboard.addDroppedItem({
+ id: data.id,
+ name: data.attributes.name,
+ range_id: data.attributes.range_id,
+ widget_id
+ });
});
},
@@ -263,6 +271,7 @@ const Clipboard = {
let widget = jQuery('#ClipboardWidget_' + response_data['widget_id']);
let clipboard_id = jQuery(widget).find(".clipboard-selector").val();
+
if (!widget) {
//The widget with the speicified widget-ID
//is not present on the current page.
@@ -302,7 +311,6 @@ const Clipboard = {
jQuery(new_item_node).removeClass('invisible');
let name_column = jQuery(new_item_node).find('td.item-name');
- console.log(name_column);
jQuery('<span/>').text(response_data['name']).appendTo(name_column)
let id_field = jQuery(new_item_node).find("input[name='selected_clipboard_items[]']");
jQuery(id_field).val(checkbox_id);
@@ -325,25 +333,16 @@ const Clipboard = {
);
},
- rename: function(widget_id) {
- if (!widget_id) {
- //Required data are missing!
- return;
- }
+ rename(widget_id) {
+ const widget = jQuery('#ClipboardWidget_' + widget_id);
+ const clipboard_id = widget.find('.clipboard-selector').val();
+ const name = widget.find('input.clipboard-name').val();
- let widget = jQuery('#ClipboardWidget_' + widget_id);
- let clipboard_id = jQuery(widget).find(".clipboard-selector").val();
- let namer = jQuery(widget).find("input.clipboard-name");
-
- STUDIP.api.PUT(
- 'clipboard/' + clipboard_id,
- {
- data: {
- name: namer.val()
- }
- }
- ).done(function(data) {
- STUDIP.Clipboard.update(data, widget_id)
+ STUDIP.jsonapi.PATCH(`clipboards/${clipboard_id}`, {data: {data: {attributes: {name}}}}).done(({data}) => {
+ STUDIP.Clipboard.update({
+ id: data.id,
+ name: data.attributes.name,
+ }, widget_id)
});
},
@@ -358,7 +357,7 @@ const Clipboard = {
STUDIP.Clipboard.toggleEditButtons(widget_id);
},
- remove: function(clipboard_id, widget_id) {
+ remove(clipboard_id, widget_id) {
if (!clipboard_id || !widget_id) {
//Required data are missing!
return;
@@ -427,10 +426,6 @@ const Clipboard = {
},
handleRemoveClick: function(delete_icon) {
- if (!delete_icon) {
- return;
- }
-
//Get the data of the clipboard:
let clipboard_select = jQuery(delete_icon).siblings('.clipboard-selector')[0];
if (!clipboard_select) {
@@ -444,52 +439,42 @@ const Clipboard = {
//Another case where something is wrong with the HTML.
return;
}
- let widget_id = jQuery(widget).data('widget_id');
- STUDIP.api.DELETE(
- 'clipboard/' + clipboard_id,
- {
- data: {
- widget_id: widget_id
- }
- }
- ).done(function() {
+ const widget_id = jQuery(widget).data('widget_id');
+
+ STUDIP.jsonapi.DELETE(`clipboards/${clipboard_id}`).done(() => {
STUDIP.Clipboard.remove(clipboard_id, widget_id);
});
},
- removeItem: function(delete_icon) {
- if (!delete_icon) {
- return;
- }
-
- //Get the item-ID:
- let item_html = jQuery(delete_icon).parents('tr');
- let range_id = jQuery(item_html).data('range_id');
- let clipboard_element = jQuery(item_html).parents('table');
- let clipboard_id = jQuery(clipboard_element).data('id');
+ removeItem(delete_icon) {
+ // Get the item-ID:
+ const item_element = jQuery(delete_icon).parents('tr');
+ const range_id = jQuery(item_element).data('range_id');
+ const clipboard_element = jQuery(item_element).parents('table');
+ const clipboard_id = jQuery(clipboard_element).data('id');
if (!range_id || !clipboard_id) {
//We cannot proceed without the item-ID and the clipboard-ID!
return;
}
- STUDIP.api.DELETE(
- 'clipboard/' + clipboard_id + '/item/' + range_id
- ).done(function() {
+ STUDIP.jsonapi.DELETE(`clipboards/${clipboard_id}/items`, {
+ data: {
+ filter: { range_id }
+ }
+ }).done(() => {
//Check if the item has siblings:
- let siblings = jQuery(item_html).siblings();
+ let siblings = item_element.siblings();
if (siblings.length < 3) {
//Only the "no items" element and the template
//are siblings of the item.
//We must display the "no items" element:
- jQuery(item_html).siblings(
- '.empty-clipboard-message'
- ).removeClass('invisible');
+ item_element.siblings('.empty-clipboard-message').removeClass('invisible');
jQuery("#clipboard-group-container").find('.widget-links').addClass('invisible');
}
//Finally remove the item:
- jQuery(item_html).remove();
+ item_element.remove();
});
},
diff --git a/resources/assets/javascripts/lib/dialog.js b/resources/assets/javascripts/lib/dialog.js
index b5cab54..8f6e50a 100644
--- a/resources/assets/javascripts/lib/dialog.js
+++ b/resources/assets/javascripts/lib/dialog.js
@@ -386,7 +386,12 @@ Dialog.show = function(content, options = {}) {
.before(element);
}
- $(this).parent().find('.ui-dialog-title').attr('title', options.title);
+ $(this).parent().find('.ui-dialog-title').attr({
+ title: options.title,
+ role: 'heading',
+ 'aria-level': 2
+ });
+ $(this).parents('.studip-dialog').attr('aria-modal', 'true');
instance.open = true;
// Execute scripts
diff --git a/resources/assets/javascripts/lib/extract_callback.js b/resources/assets/javascripts/lib/extract_callback.js
index bf7ac79..a630275 100644
--- a/resources/assets/javascripts/lib/extract_callback.js
+++ b/resources/assets/javascripts/lib/extract_callback.js
@@ -56,8 +56,9 @@ export default function extractCallback(cmd, payload, root = window) {
}
}
- if (callback[chunk] === undefined) {
- throw 'Error: Undefined callback ' + cmd;
+ if (callback === null || callback[chunk] === undefined) {
+ console.log('Error: Undefined callback ' + cmd);
+ return;
}
if (typeof callback[chunk] === 'function' && parameters !== null) {
diff --git a/resources/assets/javascripts/lib/fullcalendar.js b/resources/assets/javascripts/lib/fullcalendar.js
index 49274f4..5b7d032 100644
--- a/resources/assets/javascripts/lib/fullcalendar.js
+++ b/resources/assets/javascripts/lib/fullcalendar.js
@@ -622,8 +622,13 @@ class Fullcalendar
$('.fc-slats tr:odd .fc-widget-content:not(.fc-axis)').remove();
}
- STUDIP.api.GET(`semester/${timestamp}/week`).done((data) => {
+ if (document.getElementById('booking-plan-header-semname') === null) {
+ return;
+ }
+ $.getJSON(
+ STUDIP.URLHelper.getURL(`dispatch.php/resources/ajax/semester_week/${timestamp}`)
+ ).done((data) => {
if (data) {
$('#booking-plan-header-semname').text(data.semester_name);
if (data.sem_week) {
@@ -640,7 +645,7 @@ class Fullcalendar
$('#booking-plan-header-semrow').hide();
$('#booking-plan-header-semweek-part').hide();
}
- })
+ });
},
resourceRender (renderInfo) {
if ($(renderInfo.view.context.calendar.el).hasClass('room-group-booking-plan')) {
@@ -731,12 +736,31 @@ class Fullcalendar
//Get the timestamp:
let timestamp = changedMoment.getTime() / 1000;
- jQuery('a.resource-bookings-actions').each(function () {
+ jQuery('a.resource-bookings-actions, a.calendar-action').each(function () {
const url = new URL(this.href);
- url.searchParams.set('timestamp', timestamp)
+ url.searchParams.set('timestamp', timestamp.toString())
url.searchParams.set('defaultDate', changed_date)
this.href = url.toString();
});
+ jQuery('.sidebar-widget.calendar-action').each(function() {
+ //Each sidebar widget is different. The placement of the defaultDate URL parameter
+ //has to reflect that.
+ jQuery(this).find('button[formaction]').each(function() {
+ //Modify the formaction attribute:
+ let url = new URL(jQuery(this).attr('formaction'));
+ url.searchParams.set('defaultDate', changed_date);
+ jQuery(this).attr('formaction', url.toString());
+ });
+ jQuery(this).find('form[action]').each(function() {
+ //Add a hidden input with the defaultDate:
+ let hidden_input = jQuery(this).find('input[name="defaultDate"]')[0];
+ if (!hidden_input) {
+ hidden_input = jQuery('<input type="hidden" name="defaultDate">');
+ jQuery(this).append(hidden_input);
+ }
+ jQuery(hidden_input).val(changed_date);
+ });
+ });
// Now change the URL of the window.
const url = new URL(window.location.href);
diff --git a/resources/assets/javascripts/lib/global_search.js b/resources/assets/javascripts/lib/global_search.js
index dbd045b..394b7e3 100644
--- a/resources/assets/javascripts/lib/global_search.js
+++ b/resources/assets/javascripts/lib/global_search.js
@@ -9,6 +9,7 @@ const GlobalSearch = {
*/
toggleSearchBar: function(visible, cleanup) {
$('#globalsearch-searchbar').toggleClass('is-visible', visible);
+ $('#globalsearch-input').attr('aria-expanded', visible ? 'true' : 'false');
$('#globalsearch-input').toggleClass('hidden-small-down', !visible);
$('#globalsearch-icon').toggleClass('hidden-small-down', visible);
$('#globalsearch-clear').toggleClass('hidden-small-down', !visible);
@@ -70,7 +71,7 @@ const GlobalSearch = {
// Iterate over each result category.
$.each(json, function(name, value) {
// Create an <article> for category.
- var category = $(`<article id="globalsearch-${name}">`),
+ var category = $(`<article id="globalsearch-${name}" role="list">`),
header = $('<header>').appendTo(category),
counter = 0;
@@ -96,7 +97,7 @@ const GlobalSearch = {
// Process results and create corresponding entries.
$.each(value.content, function(index, result) {
// Create single result entry.
- var single = $('<section>'),
+ var single = $(`<a href="${result.url}" role="listitem" ${dataDialog}>`),
data = $('<div class="globalsearch-result-data">'),
details = $('<div class="globalsearch-result-details">');
@@ -107,17 +108,17 @@ const GlobalSearch = {
// Which result types should be opened via dialog?
const openInDialog = ['GlobalSearchFiles', 'GlobalSearchMessages'];
var dataDialog = (openInDialog.indexOf(name) >= 0 ? dataDialog = 'data-dialog' : dataDialog = '');
- var link = $(`<a href="${result.url}" ${dataDialog}>`).appendTo(single);
+ //var link = $(`<a href="${result.url}" ${dataDialog}>`).appendTo(single);
// Optional image...
if (result.img !== null) {
- $(`<img src="${result.img}">`)
+ $(`<img src="${result.img}" alt="">`)
.wrap('<div class="globalsearch-result-img">')
.parent() // Element is now the wrapper
- .appendTo(link);
+ .appendTo(single);
}
- link.append(data);
+ single.append(data);
// Name/title
$('<div class="globalsearch-result-title">')
@@ -144,7 +145,7 @@ const GlobalSearch = {
if (result.date !== null) {
$('<div class="globalsearch-result-time">')
.html(result.date)
- .appendTo(link);
+ .appendTo(single);
}
// "Expand" attribute for further, result-related search
@@ -178,6 +179,7 @@ const GlobalSearch = {
GlobalSearch.lastSearch = null;
$('#globalsearch-searchbar').removeClass('is-visible has-value');
+ $('#globalsearch-input').attr('aria-expanded', 'false');
$('#globalsearch-input').val('');
$('#globalsearch-results').html('');
$('#globalsearch-input').focus();
diff --git a/resources/assets/javascripts/lib/header_magic.js b/resources/assets/javascripts/lib/header_magic.js
index f465e7e..a581107 100644
--- a/resources/assets/javascripts/lib/header_magic.js
+++ b/resources/assets/javascripts/lib/header_magic.js
@@ -17,7 +17,7 @@ const scroll = function(scrolltop) {
const HeaderMagic = {
enable() {
fold = $('#navigation-level-1').height();
- Scroll.addHandler('header', scroll);
+ Scroll.addHandler('header', scroll, true);
},
disable() {
Scroll.removeHandler('header');
diff --git a/resources/assets/javascripts/lib/jsonapi.js b/resources/assets/javascripts/lib/jsonapi.ts
index f3217bc..80176cc 100644
--- a/resources/assets/javascripts/lib/jsonapi.js
+++ b/resources/assets/javascripts/lib/jsonapi.ts
@@ -3,11 +3,11 @@ import AbstractAPI from './abstract-api.js';
// Actual JSONAPI object
class JSONAPI extends AbstractAPI
{
- constructor(version = 1) {
+ constructor(version: number = 1) {
super(`jsonapi.php/v${version}`);
}
- encodeData (data, method) {
+ encodeData (data: any, method: string): any {
data = super.encodeData(data);
if (['DELETE', 'GET', 'HEAD'].includes(method)) {
@@ -21,11 +21,11 @@ class JSONAPI extends AbstractAPI
return JSON.stringify(data);
}
- request (url, options = {}) {
+ request (url: string, options: any = {}) {
options.contentType = 'application/vnd.api+json';
return super.request(url, options);
}
}
export default JSONAPI;
-export const jsonapi = new JSONAPI();
+export const jsonapi: JSONAPI = new JSONAPI();
diff --git a/resources/assets/javascripts/lib/messages.js b/resources/assets/javascripts/lib/messages.js
index 7ce5328..8e27f8f 100644
--- a/resources/assets/javascripts/lib/messages.js
+++ b/resources/assets/javascripts/lib/messages.js
@@ -252,6 +252,8 @@ const Messages = {
if (jQuery('#' + name).is(':visible')) {
jQuery('#' + name)[0].scrollIntoView(false);
}
+ jQuery('#toggle-' + name)
+ .attr('aria-expanded', jQuery('#toggle-' + name).attr('aria-expanded') !== 'true');
}
};
diff --git a/resources/assets/javascripts/lib/personal_notifications.js b/resources/assets/javascripts/lib/personal_notifications.js
index 90f1053..392e8b0 100644
--- a/resources/assets/javascripts/lib/personal_notifications.js
+++ b/resources/assets/javascripts/lib/personal_notifications.js
@@ -116,6 +116,11 @@ const PersonalNotifications = {
.click(STUDIP.PersonalNotifications.activate);
}
}
+
+ // Special handling for personal notifications:
+ $('#notification-container').on('mouseover mouseout', function (event) {
+ $(this).attr('aria-expanded', $(this).attr('aria-expanded') === 'true' ? 'false' : 'true');
+ });
},
activate () {
Promise.resolve(Notification.requestPermission()).then(permission => {
diff --git a/resources/assets/javascripts/lib/questionnaire.js b/resources/assets/javascripts/lib/questionnaire.js
index 2bca8c6..9a89348 100644
--- a/resources/assets/javascripts/lib/questionnaire.js
+++ b/resources/assets/javascripts/lib/questionnaire.js
@@ -86,7 +86,7 @@ const Questionnaire = {
}
$.post(STUDIP.URLHelper.getURL('dispatch.php/questionnaire/store/' + (this.data.id || '')), {
questionnaire: data,
- questions_data: questions,
+ questions_data: JSON.stringify(questions),
range_type: this.range_type,
range_id: this.range_id
}).done(() => {
@@ -112,7 +112,7 @@ const Questionnaire = {
id: id,
questiontype: this.questions[i].questiontype,
internal_name: this.questions[i].internal_name,
- questiondata: Object.assign({}, this.questions[i].questiondata)
+ questiondata: JSON.parse(JSON.stringify(this.questions[i].questiondata)),
});
this.activeTab = id;
},
diff --git a/resources/assets/javascripts/lib/resources.js b/resources/assets/javascripts/lib/resources.js
index 3287b42..6ff4156 100644
--- a/resources/assets/javascripts/lib/resources.js
+++ b/resources/assets/javascripts/lib/resources.js
@@ -50,7 +50,7 @@ class Resources
jQuery(row_tds[user_td_index]).children('input').removeAttr('disabled');
if (username) {
- jQuery(row_tds[user_td_index]).append(username);
+ jQuery('<span>').text(username).appendTo(row_tds[user_td_index]);
} else {
jQuery(row_tds[user_td_index]).append('ID ' + user_id);
}
@@ -60,8 +60,6 @@ class Resources
}
jQuery(user_id_input).val(user_id);
- var perm_select = jQuery(row_tds[user_td_index + 1]).children()[0];
-
if (temp_perms_row) {
//Set the time input fields to useful values:
@@ -134,22 +132,19 @@ class Resources
jQuery(table_element).trigger('update');
};
- STUDIP.api.GET(
- `user/${user_id}`
- ).done(function (data) {
- var username = data.name.family
- + ', '
- + data.name.given;
- if (data.name.prefix) {
- username += ', ' + data.name.prefix;
+ STUDIP.jsonapi.GET(`users/${user_id}`).done(data => {
+ const attributes = data.data.attributes;
+
+ let username = `${attributes['family-name']}, ${attributes['given-name']}`;
+ if (attributes['name-prefix']) {
+ username += `, ${attributes['name-prefix']}`;
}
- if (data.name.suffix) {
- username += ' ' + data.name.suffix;
+ if (attributes['name-suffix']) {
+ username += ` ${attributes['name-suffix']}`;
}
- username += ' (' + data.name.username + ')'
- + ' (' + data.perms + ')';
+ username += ` (${attributes.username}) (${attributes.permission})`;
insert_function(user_id, username);
- }).fail(function () {
+ }).fail(() => {
insert_function(user_id);
});
}
@@ -160,23 +155,13 @@ class Resources
return;
}
- STUDIP.api.GET(
- `course/${course_id}/members`,
- {
- data: {
- //The limit '0' results in a division by zero.
- //Hopefully, the limit is set to a value high enough:
- limit: 1000000
- }
- }
- ).done(function (data) {
- for (var attribute in data.collection) {
- var user_id = data.collection[attribute].member.id;
+ STUDIP.jsonapi.GET(`courses/${course_id}/memberships`, {data: {page: {limit: 1000000}}}).done(data => {
+ data.data.forEach(membership => {
STUDIP.Resources.addUserToPermissionList(
- user_id,
+ membership.relationships.user.data.id,
table_element
);
- }
+ });
});
}
diff --git a/resources/assets/javascripts/lib/restapi.js b/resources/assets/javascripts/lib/restapi.js
deleted file mode 100644
index b6e31df..0000000
--- a/resources/assets/javascripts/lib/restapi.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import AbstractAPI from './abstract-api.js';
-
-// Actual RESTAPI object
-class RESTAPI extends AbstractAPI
-{
- constructor() {
- super('api.php');
- }
-}
-
-export default RESTAPI;
-export const api = new RESTAPI();
diff --git a/resources/assets/javascripts/lib/scroll.js b/resources/assets/javascripts/lib/scroll.js
index a4d24d5..f4ffc66 100644
--- a/resources/assets/javascripts/lib/scroll.js
+++ b/resources/assets/javascripts/lib/scroll.js
@@ -6,49 +6,54 @@
* Updates/calls to the callback are synchronized to screen refresh by using
* the animation frame method (which will fallback to a timer based solution).
*/
-var handlers = {};
-var animId = false;
+const handlers = {};
+let animId = false;
-var lastTop = null;
-var lastLeft = null;
-
-function scrollHandler() {
- var scrollTop = $(document).scrollTop();
- var scrollLeft = $(document).scrollLeft();
-
- if (scrollTop !== lastTop || scrollLeft !== lastLeft) {
- $.each(handlers, function(index, handler) {
- handler(scrollTop, scrollLeft);
- });
-
- lastTop = scrollTop;
- lastLeft = scrollLeft;
- }
-
- animId = false;
-
- engageScrollTrigger();
-}
+let lastTop = null;
+let lastLeft = null;
function refresh() {
- var hasHandlers = !$.isEmptyObject(handlers);
+ const hasHandlers = Object.keys(handlers).length > 0;
if (!hasHandlers && animId !== false) {
window.cancelAnimationFrame(animId);
animId = false;
} else if (hasHandlers && animId === false) {
- animId = window.requestAnimationFrame(scrollHandler);
+ animId = window.requestAnimationFrame(() => Scroll.executeHandlers());
}
}
function engageScrollTrigger() {
- $(window).off('scroll.studip-handler');
- $(window).one('scroll.studip-handler', refresh);
+ window.removeEventListener('scroll', refresh);
+ window.addEventListener('scroll', refresh, {once: true});
}
const Scroll = {
- addHandler(index, handler) {
+ executeHandlers(only_these = []) {
+ const scrollTop = document.scrollingElement.scrollTop;
+ const scrollLeft = document.scrollingElement.scrollLeft;
+
+ if (scrollTop !== lastTop || scrollLeft !== lastLeft) {
+ for (const [index, handler] of Object.entries(handlers)) {
+ if (only_these.length === 0 || only_these.includes(index)) {
+ handler(scrollTop, scrollLeft);
+ }
+ }
+
+ lastTop = scrollTop;
+ lastLeft = scrollLeft;
+ }
+
+ animId = false;
+
+ engageScrollTrigger();
+ },
+ addHandler(index, handler, immediate = false) {
handlers[index] = handler;
engageScrollTrigger();
+
+ if (immediate) {
+ Scroll.executeHandlers([index]);
+ }
},
removeHandler(index) {
delete handlers[index];
diff --git a/resources/assets/javascripts/lib/scroll_to_top.js b/resources/assets/javascripts/lib/scroll_to_top.js
index 2a75402..9b0d3ee 100644
--- a/resources/assets/javascripts/lib/scroll_to_top.js
+++ b/resources/assets/javascripts/lib/scroll_to_top.js
@@ -4,9 +4,9 @@ let fold;
let was_below_the_fold = false;
const back_to_top = function(scrolltop) {
- var is_below_the_fold = scrolltop > fold;
+ let is_below_the_fold = scrolltop > fold;
if (is_below_the_fold !== was_below_the_fold) {
- $('#scroll-to-top').toggleClass('hide', !is_below_the_fold);
+ document.getElementById('scroll-to-top').classList.toggle('hide', !is_below_the_fold);
was_below_the_fold = is_below_the_fold;
}
};
@@ -23,15 +23,21 @@ const ScrollToTop = {
},
disable() {
Scroll.removeHandler('header');
- $('#scroll-to-top').addClass('hide');
+ document.getElementById('scroll-to-top').classList.add('hide');
},
moveBack() {
- $('#scroll-to-top').on('click', function(e) {
- $('html, body').stop().animate({
- scrollTop: (0)
- }, 1000, 'easeInOutExpo');
- e.preventDefault();
+ document.getElementById('scroll-to-top').addEventListener('click', (evt) => {
+ evt.preventDefault();
+ this.toTop();
});
+ document.getElementById('scroll-to-top').addEventListener('keypress', (evt) => {
+ if (evt.code === 'Space') {
+ this.toTop();
+ }
+ });
+ },
+ toTop() {
+ window.scroll({top: 0, left: 0, behavior: 'smooth'});
}
};
diff --git a/resources/assets/javascripts/lib/search.js b/resources/assets/javascripts/lib/search.js
index 5d39f43..f8108cd 100644
--- a/resources/assets/javascripts/lib/search.js
+++ b/resources/assets/javascripts/lib/search.js
@@ -200,7 +200,7 @@ const Search = {
// Optional image...
if (result.img !== null) {
$('<div class="search-result-img hidden-tiny-down">')
- .append(`<img src="${result.img}">`)
+ .append(`<img src="${result.img}" alt="">`)
.appendTo(link);
}
@@ -339,7 +339,8 @@ const Search = {
* Hide all select filters in the sidebar.
*/
hideAllFilters: function () {
- $('div[id$="_filter"]').hide();
+ $('#filter_widget').hide();
+ $('#filter_widget *[id$="_filter"]').hide();
},
/**
@@ -350,12 +351,11 @@ const Search = {
showFilter: function (category) {
var filters = $('#search-results').data('filters');
STUDIP.Search.hideAllFilters();
- if (filters && filters[category] !== undefined && category != 'show_all_categories') {
+ if (filters && filters[category] !== undefined && filters[category].length > 0) {
+ $('#filter_widget').show();
for (let i = 0; i < filters[category].length; i++) {
$(`#${filters[category][i]}_filter`).show();
}
- } else if (category === 'show_all_categories') {
- $('#semester_filter').show();
}
},
@@ -547,7 +547,7 @@ const Search = {
if (item != 'category') {
var value = filter[item];
if (value.trim()) {
- var name = $(`#${item}_filter .sidebar-widget-header`).text().trim();
+ var name = $(`#${item}_filter .label-text`).text().trim();
var value_text = $(`#${item}_select option:selected`).text().trim();
var filterItem = $('<button></button>').addClass('button remove-filter').text(name + ': ' + value_text).attr('data-filter-name', item);
filterItem.on('click', function () {
diff --git a/resources/assets/javascripts/lib/tooltip.js b/resources/assets/javascripts/lib/tooltip.js
deleted file mode 100644
index 2cdac27..0000000
--- a/resources/assets/javascripts/lib/tooltip.js
+++ /dev/null
@@ -1,227 +0,0 @@
-import CSS from './css.js';
-
-/**
- * Tooltip library for Stud.IP
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @copyright Stud.IP Core Group 2014
- * @license GPL2 or any later version
- * @since Stud.IP 3.1
- */
-
-let count = 0;
-let threshold = 0;
-
-class Tooltip {
- static get count() {
- return count;
- }
-
- static set count(value) {
- count = value;
- }
-
- // Threshold used for "edge detection" (imagine a padding along the edges)
- static get threshold() {
- return threshold;
- }
-
- static set threshold(value) {
- threshold = value;
- }
-
- /**
- * Returns a new unique id of a tooltip.
- *
- * @return {string} Unique id
- * @static
- */
- static getId() {
- const id = `studip-tooltip-${Tooltip.count}`;
- Tooltip.count += 1;
- return id;
- }
-
- /**
- * Constructs a new tooltip at given location with given content.
- * The applied css class may be changed by the fourth parameter.
- *
- * @class
- * @classdesc Stud.IP tooltips provide an improved layout and handling
- * of contents (including html) than the browser's default
- * tooltip through title attribute would
- *
- * @param {int} x - Horizontal position of the tooltip
- * @param {int} y - Vertical position of the tooltip
- * @param {string} content - Content of the tooltip (may be html)
- * @param {string} css_class - Optional name of the applied css class /
- * defaults to 'studip-tooltip'
- */
- constructor(x, y, content, css_class) {
- // Obtain unique id of the tooltip
- this.id = Tooltip.getId();
-
- // Create dom element of the tooltip, apply id and class and attach
- // to dom
- this.element = $('<div>');
- this.element.addClass(css_class || 'studip-tooltip');
- this.element.attr('id', this.id);
- this.element.attr('role', 'tooltip');
- this.element.appendTo('body');
-
- // Set position and content and paint the tooltip
- this.position(x, y);
- this.update(content);
- this.paint();
- }
-
- /**
- * Translates the arrow(s) under a tooltip using css3 translate
- * transforms. This is needed at the edges of the screen.
- * This implies that a current browser is used. The translation could
- * also be achieved by adjusting margins but that way we would need
- * to hardcode values into this function since it's a struggle to
- * obtain the neccessary values from the CSS pseudo selectors in JS.
- *
- * Internal, css rules are dynamically created and applied to the current
- * document by using the methods provided in the file studip-css.js.
- *
- * @param {int} x - Horizontal offset
- * @param {int} y - Vertical offset
- */
- translateArrows(x, y, left_arrow = false) {
- CSS.removeRule(`#${this.id}::before`);
- CSS.removeRule(`#${this.id}::after`);
-
- if (x !== 0 || y !== 0) {
- let before_rule = {
- transform: `translate(${x}px, ${y}px);`
- };
- if (left_arrow) {
- before_rule.transform = `translate(${x}px, ${y}px) rotate(90deg);`;
- }
- let after_rule = before_rule;
- if (left_arrow) {
- after_rule['border-width'] = '9px';
- }
- CSS.addRule(`#${this.id}::before`, before_rule, ['-ms-', '-webkit-']);
- CSS.addRule(`#${this.id}::after`, after_rule, ['-ms-', '-webkit-']);
- }
- }
-
- /**
- * Updates the position of the tooltip.
- *
- * @param {int} x - Horizontal position of the tooltip
- * @param {int} y - Vertical position of the tooltip
- */
- position(x, y) {
- this.x = x;
- this.y = y;
- }
-
- /**
- * Updates the contents of the tooltip.
- *
- * @param {string} content - Content of the tooltip (may be html)
- */
- update(content) {
- this.element.html(content);
- }
-
- /**
- * "Paints" the tooltip. This method actually computes the dimensions of
- * the tooltips, checks for screen edges and calculates the actual offset
- * in the current document.
- * This method is neccessary due to the fact that position and content
- * can be changed apart from each other.
- * Thus: Don't forget to repaint after adjusting any of the two.
- */
- paint() {
- const width = this.element.outerWidth(true);
- const height = this.element.outerHeight(true);
- const maxWidth = $(document).width();
- const maxHeight = $(document).height();
- let x = this.x - width / 2;
- let y = this.y - height;
- //The arrow offset is the offset from the bottom right corner of
- //the tooltip "frame".
- let arrow_offset_x = 0;
- let arrow_offset_y = 0;
- let left_arrow = false;
-
- if (y < 0) {
- y = 0;
- x = this.x + 20;
- //Put the arrow on the left side and move the tooltip,
- //if there is still enough place left on the right.
- left_arrow = true;
- arrow_offset_y = -height + this.y + 10;
- if (arrow_offset_y > -20) {
- y+= arrow_offset_y + 20;
- arrow_offset_y = -20;
- }
- arrow_offset_x = -width / 2 - 8;
- } else if (y + height > maxHeight) {
- y = maxHeight - height;
- }
-
- if (x < 0) {
- arrow_offset_x = 0;
- x = 0;
- } else if (x + width > maxWidth) {
- arrow_offset_x = x + width - maxWidth;
- x = maxWidth - width;
- }
- this.translateArrows(arrow_offset_x, arrow_offset_y, left_arrow);
-
- this.element.css({
- left: x,
- top: y
- });
- }
-
- /**
- * Toggles the visibility of the tooltip. If no state is provided,
- * the tooltip will be hidden if visible and vice versa. Pretty straight
- * forward and no surprises here.
- * This method implicitely calls paint before a tooltip is shown (in case
- * it was forgotten).
- *
- * @param {bool} visible - Optional visibility parameter to set the
- * tooltip to a certain state
- */
- toggle(visible) {
- if (visible) {
- this.paint();
- }
- this.element.toggle(visible);
- }
-
- /**
- * Reveals the tooltip.
- *
- * @see Tooltip.toggle
- */
- show() {
- this.toggle(true);
- }
-
- /**
- * Hides the tooltip.
- *
- * @see Tooltip.toggle
- */
- hide() {
- this.toggle(false);
- }
-
- /**
- * Removes the tooltip
- */
- remove() {
- this.element.remove();
- }
-}
-
-export default Tooltip;
diff --git a/resources/assets/javascripts/lib/wysiwyg.js b/resources/assets/javascripts/lib/wysiwyg.js
index f9acb81..47c64d0 100644
--- a/resources/assets/javascripts/lib/wysiwyg.js
+++ b/resources/assets/javascripts/lib/wysiwyg.js
@@ -13,17 +13,17 @@ const wysiwyg = {
isHtml: function isHtml(text) {
// NOTE keep this function in sync with
- // Markup::isHtml in Markup.class.php
+ // Markup::isHtml in Markup.php
return this.hasHtmlMarker(text);
},
hasHtmlMarker: function hasHtmlMarker(text) {
// NOTE keep this function in sync with
- // Markup::hasHtmlMarker in Markup.class.php
+ // Markup::hasHtmlMarker in Markup.php
return this.htmlMarkerRegExp.test(text);
},
markAsHtml: function markAsHtml(text) {
// NOTE keep this function in sync with
- // Markup::markAsHtml in Markup.class.php
+ // Markup::markAsHtml in Markup.php
if (this.hasHtmlMarker(text) || text.trim() == '') {
return text; // marker already set, don't set twice
}
diff --git a/resources/assets/javascripts/mvv.js b/resources/assets/javascripts/mvv.js
index a339624..92dcd68 100644
--- a/resources/assets/javascripts/mvv.js
+++ b/resources/assets/javascripts/mvv.js
@@ -67,10 +67,17 @@ jQuery(function ($) {
});
$(document).on('click', '.stgfile .remove_attachment', function($event) {
- STUDIP.MVV.Document.remove_attachment($(this));
+ STUDIP.Dialog.confirm($gettext('Soll die Datei wirklich gelöscht werden?')).done(() => {
+ STUDIP.MVV.Document.remove_attachment(this);
+ });
return false;
});
+ $(document).on('click', '.stgfile .refresh_attachment', (event) => {
+ STUDIP.MVV.Document.refresh_attachment(event.target);
+ event.preventDefault();
+ });
+
STUDIP.dialogReady(
function() {
@@ -663,27 +670,55 @@ STUDIP.MVV.Document = {
})
}, 100);
},
- remove_attachment: function(item) {
- jQuery.ajax({
- url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/materialien/files/delete_attachment',
- data: {
- mvvfile_id: jQuery('#mvvfile_id').val(),
- fileref_id: item.closest('li')
- .find('input[name=document_id]')
- .val()
- },
- type: 'POST'
+ refresh_attachment(item) {
+ const language = item.closest('button').dataset.language;
+ const document_id = item.closest('.stgfile').querySelector('[name="document_id"]').value;
+
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.hidden = true;
+
+ input.addEventListener('cancel', () => {
+ input.remove();
});
- item.parents('td').find('.attachments').toggle();
- item.closest('li')
- .fadeOut(300, function() {
- jQuery(this).remove();
+ input.addEventListener('change', () => {
+ const fd = new FormData();
+ fd.append('file', input.files[0], input.files[0].name);
+ fd.append('mvvfile_id', jQuery('#mvvfile_id').val());
+ fd.append('range_id', jQuery('#range_id').val());
+ fd.append('document_id', document_id);
+ fd.append('file_language', language);
+
+ const statusbar = $('#statusbar_container .statusbar')
+ .first()
+ .clone()
+ .show();
+ statusbar.appendTo('#statusbar_container');
+
+ STUDIP.MVV.Document.upload_file(fd, statusbar, true).then(() => {
+ input.remove();
+ });
+ });
+
+ item.parentNode.after(input);
+ input.click();
+ },
+ remove_attachment(item) {
+ const url = STUDIP.URLHelper.getURL('dispatch.php/materialien/files/delete_attachment', {
+ mvvfile_id: document.getElementById('mvvfile_id').value,
+ fileref_id: item.closest('li').querySelector('input[name=document_id]').value,
+ });
+ $.post(url).done(() => {
+ $(item).closest('td').find('.attachments').toggle();
+ $(item).closest('li').fadeOut(300, function () {
+ this.remove();
jQuery('#upload_chooser').show();
});
+ });
},
- upload_from_input: function(input, file_language) {
+ upload_from_input(input, file_language) {
STUDIP.MVV.Document.upload_files(input.files, file_language);
- jQuery(input).val('');
+ input.value = '';
},
fileIDQueue: 1,
upload_files: function(files, file_language) {
@@ -701,82 +736,94 @@ STUDIP.MVV.Document = {
STUDIP.MVV.Document.upload_file(fd, statusbar);
}
},
- upload_file: function(formdata, statusbar) {
- $.ajax({
- xhr: function() {
- var xhrobj = $.ajaxSettings.xhr();
- if (xhrobj.upload) {
- xhrobj.upload.addEventListener(
- 'progress',
- function(event) {
- var percent = 0;
- var position = event.loaded || event.position;
- var total = event.total;
- if (event.lengthComputable) {
- percent = Math.ceil((position / total) * 100);
- }
- //Set progress
- statusbar.find('.progress').css({ 'min-width': percent + '%', 'max-width': percent + '%' });
- statusbar
- .find('.progresstext')
- .text(percent === 100 ? jQuery('#upload_finished').text() : percent + '%');
- },
- false
- );
+ upload_file(formdata, statusbar, update = false) {
+ return new Promise((resolve, reject) => {
+ $.ajax({
+ xhr() {
+ var xhrobj = $.ajaxSettings.xhr();
+ if (xhrobj.upload) {
+ xhrobj.upload.addEventListener(
+ 'progress',
+ function(event) {
+ var percent = 0;
+ var position = event.loaded || event.position;
+ var total = event.total;
+ if (event.lengthComputable) {
+ percent = Math.ceil((position / total) * 100);
+ }
+ //Set progress
+ statusbar.find('.progress').css({ 'min-width': percent + '%', 'max-width': percent + '%' });
+ statusbar
+ .find('.progresstext')
+ .text(percent === 100 ? jQuery('#upload_finished').text() : percent + '%');
+ },
+ false
+ );
+ }
+ return xhrobj;
+ },
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/materialien/files/upload_attachment',
+ type: 'POST',
+ contentType: false,
+ processData: false,
+ cache: false,
+ data: formdata,
+ dataType: 'json'
+ }).done((data) => {
+ const language = formdata.get('file_language');
+
+ statusbar.find('.progress').css({ 'min-width': '100%', 'max-width': '100%' });
+ var file = jQuery('#fileselector_'+formdata.get('file_language')).find('.stgfiles > .stgfile')
+ .first()
+ .clone();
+ file.find('.name').text(data.name);
+ if (data.size < 1024) {
+ file.find('.size').text(data.size + 'B');
}
- return xhrobj;
- },
- url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/materialien/files/upload_attachment',
- type: 'POST',
- contentType: false,
- processData: false,
- cache: false,
- data: formdata,
- dataType: 'json'
- })
- .done(function(data) {
- statusbar.find('.progress').css({ 'min-width': '100%', 'max-width': '100%' });
- var file = jQuery('#fileselector_'+formdata.get('file_language')).find('.stgfiles > .stgfile')
- .first()
- .clone();
- file.find('.name').text(data.name);
- if (data.size < 1024) {
- file.find('.size').text(data.size + 'B');
- }
- if (data.size > 1024 && data.size < 1024 * 1024) {
- file.find('.size').text(Math.floor(data.size / 1024) + 'KB');
- }
- if (data.size > 1024 * 1024 && data.size < 1024 * 1024 * 1024) {
- file.find('.size').text(Math.floor(data.size / 1024 / 1024) + 'MB');
- }
- if (data.size > 1024 * 1024 * 1024) {
- file.find('.size').text(Math.floor(data.size / 1024 / 1024 / 1024) + 'GB');
- }
- file.find('.icon').html(data.icon);
- file.find('input[name=document_id]').attr('value', data.document_id);
- jQuery('#fileviewer_'+formdata.get('file_language')).find('.stgfiles').append(file);
- jQuery('#fileselector_'+formdata.get('file_language')).toggle();
- jQuery('#fileselector_'+formdata.get('file_language')).parents('.attachments').toggle();
- jQuery('#fileselector_'+formdata.get('file_language')).parents('.attachments').find('span').toggle();
- file.fadeIn(300);
- statusbar.find('.progresstext').text(jQuery('#upload_received_data').text());
- statusbar.delay(1000).fadeOut(300, function() {
- jQuery('#upload_chooser').hide();
- jQuery(this).remove();
- });
- })
- .fail(function(jqxhr, status, errorThrown) {
- var error = jqxhr.responseJSON.error;
-
- statusbar
- .find('.progress')
- .addClass('progress-error')
- .attr('title', error);
- statusbar.find('.progresstext').html(error);
- statusbar.on('click', function() {
- jQuery(this).fadeOut(300, function() {
- jQuery(this).remove();
+ if (data.size > 1024 && data.size < 1024 * 1024) {
+ file.find('.size').text(Math.floor(data.size / 1024) + 'KB');
+ }
+ if (data.size > 1024 * 1024 && data.size < 1024 * 1024 * 1024) {
+ file.find('.size').text(Math.floor(data.size / 1024 / 1024) + 'MB');
+ }
+ if (data.size > 1024 * 1024 * 1024) {
+ file.find('.size').text(Math.floor(data.size / 1024 / 1024 / 1024) + 'GB');
+ }
+ file.find('.icon').html(data.icon);
+ file.find('input[name=document_id]').attr('value', data.document_id);
+ if (update) {
+ $(`#fileviewer_${language} .stgfiles`).empty().append(file);
+ file.show();
+ } else {
+ $(`#fileviewer_${language}`).find('.stgfiles').append(file);
+ $(`#fileselector_${language}`)
+ .toggle()
+ .parents('.attachments').toggle()
+ .find('span').toggle();
+ file.fadeIn(300);
+ }
+ statusbar.find('.progresstext').text(jQuery('#upload_received_data').text());
+ statusbar.delay(1000).fadeOut(300, function() {
+ $('#upload_chooser').hide();
+ this.remove();
});
+
+ resolve();
+ }).fail(function(jqxhr, status, errorThrown) {
+ var error = jqxhr.responseJSON.error;
+
+ statusbar
+ .find('.progress')
+ .addClass('progress-error')
+ .attr('title', error);
+ statusbar.find('.progresstext').html(error);
+ statusbar.on('click', function() {
+ jQuery(this).fadeOut(300, function() {
+ jQuery(this).remove();
+ });
+ });
+
+ reject(new Error(error));
});
});
}
diff --git a/resources/assets/javascripts/studip-ui.js b/resources/assets/javascripts/studip-ui.js
index f581295..f98ba94 100644
--- a/resources/assets/javascripts/studip-ui.js
+++ b/resources/assets/javascripts/studip-ui.js
@@ -1,5 +1,6 @@
import { $gettext } from './lib/gettext';
import eventBus from "./lib/event-bus.ts";
+import RestrictedDatesHelper from './lib/RestrictedDatesHelper';
/**
* This file contains extensions/adjustments for jQuery UI.
@@ -28,33 +29,11 @@ import eventBus from "./lib/event-bus.ts";
}
function disableHolidaysBeforeShow(date) {
- const year = date.getFullYear();
-
- if (STUDIP.UI.restrictedDates[year] === undefined) {
- STUDIP.UI.restrictedDates[year] = {};
-
- STUDIP.jsonapi.GET('holidays', {data: {
- 'filter[year]': year
- }}).done(response => {
- // Since PHP will return an empty object as an array,
- // we need to check
- if (Array.isArray(response)) {
- return;
- }
-
- for (const [date, data] of Object.entries(response)) {
- STUDIP.UI.addRestrictedDate(
- new Date(date),
- data.holiday,
- data.mandatory
- );
- }
-
- $(this).datepicker('refresh');
- });
- }
-
- const {reason, lock} = STUDIP.UI.isDateRestricted(date, false);
+ RestrictedDatesHelper.loadRestrictedDatesByYear(date.getFullYear()).then(
+ () => $(this).datepicker('refresh'),
+ () => null
+ );
+ const {reason, lock} = RestrictedDatesHelper.isDateRestricted(date);
return [!lock, lock ? 'ui-datepicker-is-locked' : null, reason];
}
@@ -83,57 +62,8 @@ import eventBus from "./lib/event-bus.ts";
return;
}
+ STUDIP.UI = {};
// Setup Stud.IP's own datepicker extensions
- STUDIP.UI = Object.assign(STUDIP.UI || {}, {
- restrictedDates: {},
- addRestrictedDate(date, reason = '', lock = true) {
- if (this.isDateRestricted(date)) {
- return;
- }
-
- const [year, month, day] = this.convertDateForRestriction(date);
- if (this.restrictedDates[year] === undefined) {
- this.restrictedDates[year] = {};
- }
- if (this.restrictedDates[year][month] === undefined) {
- this.restrictedDates[year][month] = {};
- }
-
- this.restrictedDates[year][month][day] = {reason, lock};
- },
- removeRestrictedDate(date) {
- if (!this.isDateRestricted(date)) {
- return false;
- }
- const [year, month, day] = this.convertDateForRestriction(date);
-
- delete this.restrictedDates[year][month][day];
-
- if (Object.keys(this.restrictedDates[year][month]).length === 0) {
- delete this.restrictedDates[year][month];
- }
-
- return true;
- },
- isDateRestricted(date, return_bool = true) {
- const [year, month, day] = this.convertDateForRestriction(date);
- if (
- this.restrictedDates[year] === undefined
- || this.restrictedDates[year][month] === undefined
- || this.restrictedDates[year][month][day] === undefined
- ) {
- return return_bool ? false : {
- reason: null,
- lock: false,
- };
- }
-
- return return_bool ? true : this.restrictedDates[year][month][day];
- },
- convertDateForRestriction(date) {
- return [date.getFullYear(), date.getMonth() + 1, date.getDate()];
- }
- });
STUDIP.UI.Datepicker = {
selector: '.has-date-picker,[data-date-picker]',
// Initialize all datepickers that not yet been initialized (e.g. in dialogs)
diff --git a/resources/assets/stylesheets/highcontrast.scss b/resources/assets/stylesheets/highcontrast.scss
index 4e1e4ef..277e383 100644
--- a/resources/assets/stylesheets/highcontrast.scss
+++ b/resources/assets/stylesheets/highcontrast.scss
@@ -1309,3 +1309,18 @@ form.default {
border: 1px solid var(--black);
}
}
+
+.studip-tree-course {
+ .course-dates {
+ color: var(--black) !important;
+
+ }
+}
+
+div.avatar-widget {
+ .profile-avatar {
+ .avatar-overlay {
+ background-color: fade-out($base-color, 0.1);
+ }
+ }
+}
diff --git a/resources/assets/stylesheets/mixins/misc.scss b/resources/assets/stylesheets/mixins/misc.scss
index 90d61c5..1fe9d08 100644
--- a/resources/assets/stylesheets/mixins/misc.scss
+++ b/resources/assets/stylesheets/mixins/misc.scss
@@ -15,10 +15,6 @@
clear: both;
}
}
-@mixin list-unstyled {
- padding-left: 0;
- list-style: none;
-}
@mixin size($height, $width) {
diff --git a/resources/assets/stylesheets/mixins/studip.scss b/resources/assets/stylesheets/mixins/studip.scss
index 344f802..1bbd7d5 100644
--- a/resources/assets/stylesheets/mixins/studip.scss
+++ b/resources/assets/stylesheets/mixins/studip.scss
@@ -259,19 +259,3 @@
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
-
-@mixin list-unstyled {
- padding-left: 0;
- list-style: none;
-}
-
-@mixin list-inline {
- @include list-unstyled();
- margin-left: -5px;
-
- > li {
- display: inline-block;
- padding-left: 5px;
- padding-right: 5px;
- }
-}
diff --git a/resources/assets/stylesheets/print.scss b/resources/assets/stylesheets/print.scss
index bace5fb..7f6c043 100644
--- a/resources/assets/stylesheets/print.scss
+++ b/resources/assets/stylesheets/print.scss
@@ -3,7 +3,6 @@
@import "scss/visibility";
@import "scss/fullcalendar-print";
@import "scss/resources-print";
-@import "scss/schedule" ;
/*******************************************************************************
diff --git a/resources/assets/stylesheets/scss/admin-courses.scss b/resources/assets/stylesheets/scss/admin-courses.scss
index 753e56b..09ec6ca 100644
--- a/resources/assets/stylesheets/scss/admin-courses.scss
+++ b/resources/assets/stylesheets/scss/admin-courses.scss
@@ -29,10 +29,6 @@
}
}
-#admin-filter-widget .label-text {
- display: block;
-}
-
.action-menu.filter {
margin-left: 1em;
}
diff --git a/resources/assets/stylesheets/scss/blubber.scss b/resources/assets/stylesheets/scss/blubber.scss
index 1893fd5..76bafe4 100644
--- a/resources/assets/stylesheets/scss/blubber.scss
+++ b/resources/assets/stylesheets/scss/blubber.scss
@@ -1,7 +1,7 @@
.blubber_panel {
display: flex;
align-items: stretch;
- height: calc(100vh - 174px);
+ height: calc(100vh - 130px);
transition: opacity 100ms, filter 100ms;
&.waiting {
filter: blur(1px);
@@ -447,15 +447,12 @@
}
-form.default {
- .blubber_composer_select_container {
- input, select, .container {
- width: calc(100% - 50px);
- display: inline-block;
- }
+.blubber_composer_select_container {
+ input, select, .container {
+ width: 90%;
+ display: inline-block;
}
}
-
.float_right {
float: right;
}
@@ -523,4 +520,7 @@ ol.tagcloud {
.blubber_thread {
margin-right: 0;
}
+ .blubber_threads_widget .sidebar-widget-content {
+ max-height: calc(100vh - 230px);
+ }
}
diff --git a/resources/assets/stylesheets/scss/buttons.scss b/resources/assets/stylesheets/scss/buttons.scss
index 6c5d070..c148b72 100644
--- a/resources/assets/stylesheets/scss/buttons.scss
+++ b/resources/assets/stylesheets/scss/buttons.scss
@@ -140,7 +140,8 @@ button,
.button {
&.as-link,
&.styleless,
- &.undecorated {
+ &.undecorated,
+ &.icon-button {
background-color: unset;
border: 0;
}
diff --git a/resources/assets/stylesheets/scss/contents.scss b/resources/assets/stylesheets/scss/contents.scss
index fc8a6c9..8e02284 100644
--- a/resources/assets/stylesheets/scss/contents.scss
+++ b/resources/assets/stylesheets/scss/contents.scss
@@ -8,7 +8,7 @@
width: 100%;
.content-item {
- height: 100px;
+ min-height: 100px;
.content-item-link {
padding: 5px;
@@ -46,7 +46,7 @@
background-color: var(--dark-gray-color-5);
border: solid thin var(--light-gray-color-40);
display: flex;
- height: 150px;
+ min-height: 150px;
justify-content: stretch;
.content-item-link {
diff --git a/resources/assets/stylesheets/scss/copyable-links.scss b/resources/assets/stylesheets/scss/copyable-links.scss
deleted file mode 100644
index f5ff73a..0000000
--- a/resources/assets/stylesheets/scss/copyable-links.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-.copyable-link-confirmation {
- position: fixed;
- bottom: 60px;
- right: 12px;
- height: 60px;
- line-height: 60px;
- max-width: calc(100% - 140px);
- z-index: 42000;
- border: solid thin var(--content-color-40);
- background-color: var(--white);
- background-repeat: no-repeat;
- background-position: 1em center;
- background-size: 100px;
- box-shadow: 5px 5px var(--dark-gray-color-10);
- padding: 5px 100px;
- transition: transform .5s ease;
-
- &.copyable-link-success {
- @include background-icon(check-circle, status-green, 24);
- }
- &.copyable-link-error {
- @include background-icon(decline-circle, status-red, 24);
- }
-}
diff --git a/resources/assets/stylesheets/scss/courseware/blockadder.scss b/resources/assets/stylesheets/scss/courseware/blockadder.scss
index 93325dd..f0e993a 100644
--- a/resources/assets/stylesheets/scss/courseware/blockadder.scss
+++ b/resources/assets/stylesheets/scss/courseware/blockadder.scss
@@ -7,6 +7,7 @@
.cw-blockadder-item-wrapper {
display: flex;
+ position: relative;
border: solid thin var(--content-color-40);
max-width: 268px;
@@ -54,6 +55,7 @@
}
.cw-containeradder-item-wrapper {
+ position: relative;
border: solid thin var(--content-color-40);
margin-bottom: 4px;
.cw-sortable-handle {
@@ -136,7 +138,7 @@
}
.cw-element-inserter-wrapper {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(225px, 1fr));
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
grid-auto-rows: auto;
grid-gap: 4px;
margin-bottom: 8px;
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/document.scss b/resources/assets/stylesheets/scss/courseware/blocks/document.scss
index fb230b2..0c69cda 100644
--- a/resources/assets/stylesheets/scss/courseware/blocks/document.scss
+++ b/resources/assets/stylesheets/scss/courseware/blocks/document.scss
@@ -109,6 +109,7 @@
.cw-pdf-outer-container {
position: relative;
width: 100%;
+ overflow: hidden;
.cw-pdf-content {
display: flex;
diff --git a/resources/assets/stylesheets/scss/courseware/toolbar.scss b/resources/assets/stylesheets/scss/courseware/toolbar.scss
index fdabbf2..0670486 100644
--- a/resources/assets/stylesheets/scss/courseware/toolbar.scss
+++ b/resources/assets/stylesheets/scss/courseware/toolbar.scss
@@ -14,8 +14,7 @@
min-height: 100%;
border: solid thin var(--content-color-40);
background-color: var(--white);
- overflow-y: auto;
- overflow-x: hidden;
+ overflow: hidden;
position: relative;
padding: 0 4px;
top: 0;
@@ -33,6 +32,11 @@
right: 0;
}
+ .cw-toolbar-tool-content {
+ overflow-y: auto;
+ padding-right: 8px;
+ }
+
.cw-toolbar-blocks {
.input-group.files-search {
&.search {
@@ -96,6 +100,12 @@
}
}
+
+ .cw-toolbar-clipboard {
+ .cw-collapsible {
+ margin-bottom: 4px;
+ }
+ }
}
.cw-toolbar-folded-wrapper {
display: flex;
@@ -124,6 +134,7 @@
&.cw-toolbar-button-toggle {
text-align: end;
+ flex-grow: 1;
}
&.active {
@@ -142,7 +153,7 @@
.cw-toolbar-tools.hd {
.cw-toolbar-button-wrapper {
.cw-toolbar-button {
- width: 128px;
+ min-width: 110px;
padding: 2px 16px 0 16px;
&.cw-toolbar-button-toggle {
text-align: end;
diff --git a/resources/assets/stylesheets/scss/dashboard.scss b/resources/assets/stylesheets/scss/dashboard.scss
index 6020d6e..e353f72 100644
--- a/resources/assets/stylesheets/scss/dashboard.scss
+++ b/resources/assets/stylesheets/scss/dashboard.scss
@@ -77,7 +77,7 @@
padding-bottom: 1em;
ul {
- @include list-inline();
+ @extend .list-inline;
img {
margin-left: 0.25em;
diff --git a/resources/assets/stylesheets/scss/evaluation.scss b/resources/assets/stylesheets/scss/evaluation.scss
deleted file mode 100644
index 63fcef6..0000000
--- a/resources/assets/stylesheets/scss/evaluation.scss
+++ /dev/null
@@ -1,40 +0,0 @@
-/* classes for the evaluation modules in Stud.IP ---------------------------- */
-.eval_title {
- font-size: 1.2em;
- font-weight: bold;
- color: var(--base-color);
-}
-
-.eval_error {
- color: var(--red);
-}
-
-.eval_success {
- color: var(--green);
-}
-
-.eval_info {
- color: var(--base-gray);
-}
-
-.eval_metainfo {
- font-size: 0.8em;
-}
-
-.eval_highlight {
- background-color: var(--content-color-60);
-}
-
-.eval_gray {
- background: var(--dark-gray-color-20) none;
-}
-.evaluation_item {
- box-sizing: border-box;
- margin: 3px;
-}
-
-h3.eval {
- font-size: 1.3em;
- color: var(--black);
- font-weight: bold;
-}
diff --git a/resources/assets/stylesheets/scss/files.scss b/resources/assets/stylesheets/scss/files.scss
index 7808f15..a75f8f3 100644
--- a/resources/assets/stylesheets/scss/files.scss
+++ b/resources/assets/stylesheets/scss/files.scss
@@ -406,19 +406,20 @@ form.default {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 6px 6px 2px;
+ padding: 0 10px 0;
margin-bottom: 0;
border-top: none;
> .text {
width: 100%;
margin-left: 10px;
}
- > .arrow {
- margin-right: 5px;
- }
> .check {
display: none;
}
+
+ > .icon {
+ margin-top: 6px;
+ }
}
> label:first-of-type {
border-top: 1px solid var(--content-color-40);
diff --git a/resources/assets/stylesheets/scss/forms.scss b/resources/assets/stylesheets/scss/forms.scss
index c647ee8..888cf56 100644
--- a/resources/assets/stylesheets/scss/forms.scss
+++ b/resources/assets/stylesheets/scss/forms.scss
@@ -621,6 +621,28 @@ form.inline {
}
}
+.studip-dialog {
+ form[data-vue-app] {
+ display: flex;
+ flex-direction: column;
+ min-height: 100%;
+
+ fieldset {
+ flex: 0;
+ }
+
+ footer[data-dialog-button] {
+ background: var(--white);
+ border-top-color: var(--base-color-20);
+ bottom: -0.5em;
+ margin-top: auto;
+ padding: 1.3em 0;
+ position: sticky;
+ text-align: center;
+ }
+ }
+}
+
@media (min-width: 800px) {
form.default .form-columns {
display: flex;
diff --git a/resources/assets/stylesheets/scss/globalsearch.scss b/resources/assets/stylesheets/scss/globalsearch.scss
index 4faee5f..ef7c6e6 100644
--- a/resources/assets/stylesheets/scss/globalsearch.scss
+++ b/resources/assets/stylesheets/scss/globalsearch.scss
@@ -171,7 +171,7 @@
}
}
- section {
+ a[role=listitem] {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
@@ -189,7 +189,7 @@
display: none;
}
- & > a {
+ & > span.detail {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
diff --git a/resources/assets/stylesheets/scss/lists.scss b/resources/assets/stylesheets/scss/lists.scss
index 8ef1eb7..80796b7 100644
--- a/resources/assets/stylesheets/scss/lists.scss
+++ b/resources/assets/stylesheets/scss/lists.scss
@@ -10,9 +10,25 @@ ol {
}
}
+.list-unstyled {
+ padding-left: 0;
+ list-style: none;
+}
+
+.list-inline {
+ @extend .list-unstyled;
+ margin-left: -5px;
+
+ > li {
+ display: inline-block;
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+}
+
//comma separated
.list-csv {
- @include list-inline();
+ @extend .list-inline;
margin-left: 0;
> li {
@@ -22,8 +38,11 @@ ol {
content: ",";
}
- &:last-child::after {
- content: unset;
+ &:last-child {
+ padding-right: 0;
+ &::after {
+ content: unset;
+ }
}
}
@@ -35,7 +54,7 @@ ol {
}
.list-pipe-separated {
- @include list-inline();
+ @extend .list-inline;
display: flex; // Prevents the mystery gap between elements
> li {
@@ -47,6 +66,19 @@ ol {
}
}
+.list-slash-separated-small {
+ @extend .list-csv;
+
+ > li {
+ padding-right: 0;
+ font-size: small;
+
+ &::after {
+ content: "/";
+ }
+ }
+}
+
dl {
dt {
font-weight: bold;
diff --git a/resources/assets/stylesheets/scss/messagebox.scss b/resources/assets/stylesheets/scss/messagebox.scss
index a928bb6..7f77f0f 100644
--- a/resources/assets/stylesheets/scss/messagebox.scss
+++ b/resources/assets/stylesheets/scss/messagebox.scss
@@ -72,7 +72,7 @@ div.messagebox_details {
// Define modal messagebox
.modaloverlay {
- background: fadeout($base-color, 50%);
+ background: fade-out($base-color, 0.5);
position: fixed;
top: 0;
left: 0;
diff --git a/resources/assets/stylesheets/scss/mvv.scss b/resources/assets/stylesheets/scss/mvv.scss
index c93b214..ce85701 100644
--- a/resources/assets/stylesheets/scss/mvv.scss
+++ b/resources/assets/stylesheets/scss/mvv.scss
@@ -253,22 +253,6 @@ ul {
dd {
margin: 0;
}
-
- &.even {
- background-color: var(--dark-gray-color-10);
-
- &:hover {
- background-color: var(--content-color-60);
- }
- }
-
- &.odd {
- background-color: var(--dark-gray-color-5);
-
- &:hover {
- background-color: var(--content-color-40);
- }
- }
}
&.mvv-modul li {
@@ -559,14 +543,6 @@ form.default .mvv-inst-chooser select {
h3 {
margin-top: 1em;
}
-
- &.odd {
- background-color: var(--dark-gray-color-5);
- }
-
- &.even {
- background-color: var(--content-color-20);
- }
}
#lvgruppe_selection_chosen {
diff --git a/resources/assets/stylesheets/scss/my_courses.scss b/resources/assets/stylesheets/scss/my_courses.scss
index b82ec2b..769f3d2 100644
--- a/resources/assets/stylesheets/scss/my_courses.scss
+++ b/resources/assets/stylesheets/scss/my_courses.scss
@@ -9,7 +9,7 @@
background: var(--white);
}
-.mycourses-group-selector {
+form.default .mycourses-group-selector {
position: relative;
background-clip: padding-box;
@@ -19,26 +19,41 @@
@extend .sr-only;
&:checked + label {
- @include background-icon(accept, info);
+ .group-number {
+ display: none;
+ }
+ .checked-icon {
+ display: inline;
+ }
}
}
&:hover label {
- @include background-icon(accept, info);
+ .group-number {
+ display: none;
+ }
+ .checked-icon {
+ display: inline;
+ }
}
label {
- @include hide-text();
+ text-align: center;
+ font-size: large;
+ font-weight: bold;
+ cursor: pointer;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
+ background-color: var(--white);
+ margin-bottom: 0;
+ text-indent: 0;
- background-position: center;
- background-repeat: no-repeat;
+ height: 1.2em;
- cursor: pointer;
+ .group-number {
+ display: inline;
+ }
+ .checked-icon {
+ display: none;
+ }
}
}
diff --git a/resources/assets/stylesheets/scss/profile.scss b/resources/assets/stylesheets/scss/profile.scss
index ef7e75a..c180e03 100644
--- a/resources/assets/stylesheets/scss/profile.scss
+++ b/resources/assets/stylesheets/scss/profile.scss
@@ -13,7 +13,7 @@
padding: 0 1em;
}
.profile-view-actions {
- @include list-unstyled();
+ @extend .list-unstyled;
img {
vertical-align: text-top;
}
diff --git a/resources/assets/stylesheets/scss/questionnaire.scss b/resources/assets/stylesheets/scss/questionnaire.scss
index f5e726b..fde7d32 100644
--- a/resources/assets/stylesheets/scss/questionnaire.scss
+++ b/resources/assets/stylesheets/scss/questionnaire.scss
@@ -1,8 +1,6 @@
$width: 270px;
.questionnaire_edit {
-
-
.editor {
display: flex;
flex-direction: row-reverse;
@@ -14,14 +12,16 @@ $width: 270px;
min-width: $width;
width: $width;
.questions_container {
- padding: 0px;
+ padding: 0;
.questions {
display: flex;
flex-direction: column;
}
}
- > .admin, > .add_question, .questions > * {
+ > .admin,
+ > .add_question,
+ .questions > * {
width: calc(100% - 8px);
padding: 4px;
border-bottom: 1px solid var(--content-color-40);
@@ -42,8 +42,8 @@ $width: 270px;
&::before {
content: '';
position: absolute;
- height: 0px;
- width: 0px;
+ height: 0;
+ width: 0;
border-top: 25px transparent solid;
border-bottom: 25px transparent solid;
border-left: 7px var(--content-color-40) solid;
@@ -52,8 +52,8 @@ $width: 270px;
&::after {
content: '';
position: absolute;
- height: 0px;
- width: 0px;
+ height: 0;
+ width: 0;
border-top: 25px transparent solid;
border-bottom: 25px transparent solid;
border-left: 7px var(--yellow-40) solid;
@@ -93,42 +93,11 @@ $width: 270px;
border: 1px solid var(--content-color-40);
border-left: none;
flex-grow: 1;
- padding: 10px;
- padding-left: 15px;
+ padding: 10px 10px 10px 15px;
min-height: 150px;
min-width: 0;
}
- .vote_edit {
- .options {
- > li {
- display: flex;
- align-items: center;
- > * {
- margin-right: 10px;
- }
- }
- }
- }
- .rangescale_edit table.default > thead > tr > th.number {
- padding-left: 12px;
- }
-
- .dragcolumn {
- max-width: 1px;
- padding-bottom: 0px;
- > .dragarea {
- display: inline-block;
- height: 27px;
- }
- }
-
- .input-array {
- margin-left: 4px;
- }
- .likert_edit .input-array {
- margin-left: 7px;
- }
.inline_editing {
width: 100%;
display: flex;
@@ -150,114 +119,25 @@ $width: 270px;
justify-items: center;
}
}
- .drag-handle {
- display: inline-block;
- height: 24px;
- }
- }
- /* ab hier der alte kram */
-
- section {
- border: thin solid var(--black);
- margin: 3px;
- }
-
- .options {
- padding: 0;
- list-style-type: none;
-
- > li {
- margin-top: 5px;
- margin-bottom: 5px;
-
- > .move {
- cursor: move;
- display: inline-block;
- vertical-align: middle;
- }
-
- > input {
- display: inline-block;
- vertical-align: middle;
- }
-
- > input[type=text] {
- width: calc(100% - 70px);
- }
-
- .delete {
+ .dragcolumn {
+ max-width: 1px;
+ padding-bottom: 0;
+ > .dragarea {
display: inline-block;
- vertical-align: middle;
- cursor: pointer;
- }
-
- .add {
- display: none;
- vertical-align: middle;
- cursor: pointer;
+ height: 27px;
}
}
- > li:last-child .delete {
- display: none;
- }
-
- > li:last-child .add {
+ .drag-handle {
display: inline-block;
+ height: 24px;
}
- > li:only-child .move {
- display: none;
- }
-
- }
-
- .all_questions {
- .question:first-child .move_up {
- display: none;
- }
-
- .question:last-child .move_down {
- display: none;
- }
- }
-
- .add_questions {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- align-items: stretch;
- border: thin dashed var(--content-color-40);
-
- > a {
- background-color: transparent;
- margin: 10px;
- border: thin solid var(--content-color-20);
- padding: 5px;
- width: 100px;
- min-width: 100px;
- max-width: 100px;
- height: 100px;
- min-height: 100px;
- max-height: 100px;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- justify-content: space-around;
- align-items: center;
+ .option-cell {
text-align: center;
-
- > img {
- margin-left: auto;
- margin-right: auto;
- }
}
}
-
- .questionnaire_metadata {
- margin-top: 10px;
- }
}
.questionnaire_results {
@@ -308,7 +188,8 @@ $width: 270px;
}
-.questionnaire_answer, .questionnaire_results {
+.questionnaire_answer,
+.questionnaire_results {
.description_container {
display: flex;
> .icon_container {
@@ -335,7 +216,7 @@ $width: 270px;
border: none;
> :first-child {
- margin-top: 0px;
+ margin-top: 0;
}
.invalidation_notice {
@@ -351,9 +232,6 @@ $width: 270px;
font-size: 0.7em;
padding-left: 5px;
}
- .rangescale_center {
- text-align: center;
- }
.centerline {
border-top: 1px solid var(--base-color);
position: relative;
@@ -387,39 +265,13 @@ $width: 270px;
}
}
}
+}
- .centerline {
- border-top: 1px solid var(--base-color);
- position: relative;
- top: 35px;
- margin-left: -5px;
- margin-right: -5px;
- z-index: 2;
- }
- .questionnaire-evaluation-circle-container {
+.questionnaire_edit,
+.questionnaire_answer,
+.questionnaire_results {
+ .option-cell {
text-align: center;
- display: block;
- .questionnaire-evaluation-circle {
- width: 70px;
- height: 70px;
- display: flex;
- justify-content: center;
- align-items: center;
- margin-left: auto;
- margin-right: auto;
- z-index: 3;
- position: relative;
- > .value {
- border-radius: 100px;
- color: white;
- display: flex;
- justify-content: center;
- align-items: center;
- background-color: var(--base-color);
- width: 100%;
- height: 100%;
- }
- }
}
}
diff --git a/resources/assets/stylesheets/scss/responsive.scss b/resources/assets/stylesheets/scss/responsive.scss
index c4317e9..e84a8a8 100644
--- a/resources/assets/stylesheets/scss/responsive.scss
+++ b/resources/assets/stylesheets/scss/responsive.scss
@@ -537,6 +537,26 @@ $sidebarOut: -330px;
}
}
}
+
+ #system-notifications {
+ top: 0;
+ position: fixed;
+ left: 0;
+ width: 100%;
+ z-index: 1001;
+ }
+
+ .system-notification {
+ &.system-notification-slide-enter,
+ &.system-notification-slide-leave-to {
+ transform: translateY(-100%);
+ }
+
+ &.system-notification-slide-leave,
+ &.system-notification-slide-enter-to {
+ transform: translateY(0);
+ }
+ }
}
/* Settings especially for fullscreen mode */
diff --git a/resources/assets/stylesheets/scss/schedule.scss b/resources/assets/stylesheets/scss/schedule.scss
index c8c8f5a..a09e3ad 100644
--- a/resources/assets/stylesheets/scss/schedule.scss
+++ b/resources/assets/stylesheets/scss/schedule.scss
@@ -185,10 +185,6 @@ td.schedule-adminbind {
}
#color_picker {
- div {
- display: flex;
- flex-wrap: wrap;
- }
span {
flex: 0 0 auto;
@@ -198,7 +194,7 @@ td.schedule-adminbind {
}
input[type="radio"] {
- display: none;
+ @extend .sr-only;
&:checked + label {
outline: 1px solid var(--black);
@@ -266,6 +262,15 @@ td.schedule-adminbind {
&.schedule-category15 {
background-color: $calendar-category-15;
}
+ &.schedule-category16 {
+ background-color: $calendar-category-16;
+ }
+ &.schedule-category17 {
+ background-color: $calendar-category-17;
+ }
+ &.schedule-category18 {
+ background-color: $calendar-category-18;
+ }
&.schedule-category255 {
background-color: $calendar-category-255;
}
@@ -440,5 +445,38 @@ div.schedule_entry {
color: contrast($calendar-category-15-aux, $black, $white);
}
}
+ &.schedule-category16 {
+ background-color: $calendar-category-16-aux;
+ border: 1px solid $calendar-category-16;
+ dt {
+ background-color: $calendar-category-16;
+ color: contrast($calendar-category-16, $black, $white);
+ }
+ dd {
+ color: contrast($calendar-category-16-aux, $black, $white);
+ }
+ }
+ &.schedule-category17 {
+ background-color: $calendar-category-17-aux;
+ border: 1px solid $calendar-category-17;
+ dt {
+ background-color: $calendar-category-17;
+ color: contrast($calendar-category-17, $black, $white);
+ }
+ dd {
+ color: contrast($calendar-category-17-aux, $black, $white);
+ }
+ }
+ &.schedule-category18 {
+ background-color: $calendar-category-18-aux;
+ border: 1px solid $calendar-category-18;
+ dt {
+ background-color: $calendar-category-18;
+ color: contrast($calendar-category-18, $black, $white);
+ }
+ dd {
+ color: contrast($calendar-category-18-aux, $black, $white);
+ }
+ }
}
}
diff --git a/resources/assets/stylesheets/scss/sidebar.scss b/resources/assets/stylesheets/scss/sidebar.scss
index 9a97976..c161778 100644
--- a/resources/assets/stylesheets/scss/sidebar.scss
+++ b/resources/assets/stylesheets/scss/sidebar.scss
@@ -313,6 +313,7 @@ select.sidebar-selectlist {
flex: 1;
padding: .25em .5em;
width: 100%;
+ order: 1;
}
.submit-search {
@@ -324,17 +325,22 @@ select.sidebar-selectlist {
cursor: pointer;
font: 0/0 a;
text-shadow: none;
+ order: 3;
}
.reset-search {
- background-color: transparent;
- border: 1px solid var(--dark-gray-color-30);
- border-left: 0;
- border-right: 0;
+ background: unset;
display: inline-block;
- padding-right: 5px;
- padding-top: 4px;
cursor: pointer;
+ order: 2;
+ height: 100%;
+ box-sizing: border-box;
+ margin-right: 2px;
+ margin-left: -22px;
+
+ img {
+ padding-top: 4px;
+ }
}
}
diff --git a/resources/assets/stylesheets/scss/system-notifications.scss b/resources/assets/stylesheets/scss/system-notifications.scss
new file mode 100644
index 0000000..34c341b
--- /dev/null
+++ b/resources/assets/stylesheets/scss/system-notifications.scss
@@ -0,0 +1,178 @@
+#system-notifications {
+ &.bottom-right {
+ bottom: 50px;
+ right: 15px;
+
+ .system-notification {
+ box-shadow: 5px 5px var(--dark-gray-color-10);
+
+ &.system-notification-slide-enter,
+ &.system-notification-slide-leave-to {
+ opacity: 0;
+ transform: translateX(100%);
+ }
+
+ &.system-notification-slide-leave,
+ &.system-notification-slide-enter-to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
+ }
+
+ &.top-center {
+ left: calc(50% - 300px);
+ top: 0;
+
+ .system-notification {
+ box-shadow: 0 0 0 3px var(--dark-gray-color-10);
+
+ &.system-notification-slide-enter,
+ &.system-notification-slide-leave-to {
+ opacity: 0;
+ transform: translateY(-100%);
+ }
+
+ &.system-notification-slide-leave,
+ &.system-notification-slide-enter-to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+ }
+
+ &:not(.system-notifications-login) {
+ position: fixed;
+ width: 600px;
+ }
+
+ &.system-notifications-login {
+ margin-bottom: 15px;
+ }
+
+ overflow: hidden;
+ z-index: 1001;
+}
+
+.system-notification {
+ background-color: var(--content-color-20);
+ border: thin solid var(--content-color-40);
+ color: var(--black);
+ cursor: pointer;
+ display: flex;
+ padding: 10px;
+ position: relative;
+
+ &.system-notification-slide-enter-active,
+ &.system-notification-slide-leave-active {
+ transition: all var(--transition-duration-slow) ease-in-out;
+ }
+
+ .system-notification-icon {
+ flex: 0;
+ padding: 10px;
+ }
+
+ .system-notification-content {
+ flex: 1;
+ height: auto;
+ margin: auto 25px auto 10px;
+ word-wrap: break-word;
+
+ .system-notification-details {
+ .system-notification-details-content {
+ padding-left: 10px;
+ }
+ }
+ }
+
+ .system-notification-close {
+ align-self: normal;
+ flex: 0;
+ height: 20px;
+ width: 20px;
+
+ img, svg {
+ cursor: pointer;
+ position: absolute;
+ right: 10px;
+ top: 10px;
+ }
+ }
+
+ .system-notification-timeout {
+ &.system-notification-timeout-enter-active,
+ &.system-notification-timeout-leave-active {
+ transition: width 5s linear;
+ }
+
+ &.system-notification-timeout-enter {
+ width: 100%;
+ }
+
+ &.system-notification-timeout-leave {
+ width: 0;
+ }
+
+ background-color: var(--base-color-40);
+ bottom: 0;
+ height: 5px;
+ left: 0;
+ position: absolute;
+ width: 0;
+ }
+
+ &.system-notification-disrupted .system-notification-timeout {
+ display: none;
+ }
+
+ a:not(.system-notification-message) {
+ color: var(--black);
+ text-decoration-line: underline;
+ }
+
+ a.system-notification-message {
+ color: var(--text-color);
+ text-decoration: unset;
+ }
+}
+
+.system-notification-exception {
+ background-color: var(--red-40);
+
+ .system-notification-timeout {
+ background-color: var(--red);
+ }
+}
+
+.system-notification-error {
+ background-color: var(--red-20);
+
+ .system-notification-timeout {
+ background-color: var(--red-80);
+ }
+}
+
+.system-notification-warning {
+ background-color: var(--yellow-20);
+
+ .system-notification-timeout {
+ background-color: var(--yellow-80);
+ }
+}
+
+.system-notification-success {
+ background-color: var(--green-20);
+
+ .system-notification-timeout {
+ background-color: var(--green-80);
+ }
+}
+
+.system-notification-info {
+ background-color: var(--dark-violet-20);
+
+ .system-notification-timeout {
+ background-color: var(--dark-violet-60);
+ }
+}
diff --git a/resources/assets/stylesheets/scss/tables.scss b/resources/assets/stylesheets/scss/tables.scss
index 9e05f01..665a6f3 100644
--- a/resources/assets/stylesheets/scss/tables.scss
+++ b/resources/assets/stylesheets/scss/tables.scss
@@ -504,7 +504,7 @@ table.default {
> thead {
> tr > th {
background-color: var(--content-color-20);
- border-bottom: 1px solid fadeout($brand-color-lighter, 80%);
+ border-bottom: 1px solid fade-out($brand-color-lighter, 0.8);
border-top: 1px solid var(--brand-color-darker);
font-size: 1.0em;
}
@@ -515,7 +515,7 @@ table.default {
> th {
background-color: var(--content-color-20);
border-top: 1px solid var(--brand-color-darker);
- border-bottom: 1px solid fadeout($brand-color-lighter, 80%);
+ border-bottom: 1px solid fade-out($brand-color-lighter, 0.8);
text-align: left;
}
@@ -580,7 +580,7 @@ table.default {
// Hover effect
&:not(.nohover) > tbody:not(.nohover) > tr:not(.nohover):hover > td:not(.nohover) {
- background-color: fadeout($light-gray-color, 80%);
+ background-color: fade-out($light-gray-color, 0.8);
}
&:not(.nohover) > tbody:not(.nohover) > tr.selected:not(.nohover):hover > td:not(.nohover) {
@@ -742,7 +742,7 @@ table.withdetails {
}
> tbody > tr.open > td {
- background-color: fadeout($light-gray-color, 80%);
+ background-color: fade-out($light-gray-color, 0.8);
}
> tbody > tr.open > td:first-child {
diff --git a/resources/assets/stylesheets/scss/tabs.scss b/resources/assets/stylesheets/scss/tabs.scss
index b37005e..63bf1a0 100644
--- a/resources/assets/stylesheets/scss/tabs.scss
+++ b/resources/assets/stylesheets/scss/tabs.scss
@@ -31,8 +31,6 @@ div.clear
.quiet img { opacity: 0.25; }
li {
- background-color: var(--dark-gray-color-10);
-
&:last-child {
border-right: none;
}
diff --git a/resources/assets/stylesheets/scss/talk-bubble.scss b/resources/assets/stylesheets/scss/talk-bubble.scss
index bc44600..380fb6d 100644
--- a/resources/assets/stylesheets/scss/talk-bubble.scss
+++ b/resources/assets/stylesheets/scss/talk-bubble.scss
@@ -8,6 +8,13 @@ $ownColor: var(--petrol-40);
.talk-bubble-avatar {
padding: 8px;
+ width: 40px;
+ height: 40px;
+
+ img {
+ width: 100%;
+ height: 100%;
+ }
}
.talk-bubble {
@@ -136,4 +143,4 @@ $ownColor: var(--petrol-40);
flex-direction: row-reverse;
}
}
-} \ No newline at end of file
+}
diff --git a/resources/assets/stylesheets/scss/tooltip.scss b/resources/assets/stylesheets/scss/tooltip.scss
index 4b7b36b..ac570a8 100644
--- a/resources/assets/stylesheets/scss/tooltip.scss
+++ b/resources/assets/stylesheets/scss/tooltip.scss
@@ -7,7 +7,7 @@
box-shadow: 0 1px 0 fade-out($white, 0.5) inset;
font-size: var(--font-size-base);
margin-bottom: 8px;
- max-width: 230px;
+ max-width: $grid-element-width;
padding: 10px;
position: absolute;
text-align: left;
@@ -38,11 +38,13 @@
@extend %tooltip;
display: none;
}
- &:hover .tooltip-content {
+
+ &:hover .tooltip-content,
+ &:focus .tooltip-content {
bottom: 100%;
display: inline-block;
left: 50%;
- margin-left: -129px;
- width: 230px;
+ margin-left: - calc($grid-element-width / 2) - 10px;
+ width: $grid-element-width;
}
}
diff --git a/resources/assets/stylesheets/scss/wiki.scss b/resources/assets/stylesheets/scss/wiki.scss
index e19b9b1..323fbb5 100644
--- a/resources/assets/stylesheets/scss/wiki.scss
+++ b/resources/assets/stylesheets/scss/wiki.scss
@@ -190,12 +190,12 @@ article.studip.wiki {
padding: 0;
li {
margin-bottom: 5px;
+ display: flex;
}
a {
background-position: left top;
background-repeat: no-repeat;
background-size: var(--avatar-small);
- display: block;
min-height: var(--avatar-small);
padding-left: calc(var(--avatar-small) + 1ex);
}
diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss
index d19621a..bf4022a 100644
--- a/resources/assets/stylesheets/studip.scss
+++ b/resources/assets/stylesheets/studip.scss
@@ -32,7 +32,6 @@
@import "scss/contents";
@import "scss/content";
@import "scss/comments";
-@import "scss/copyable-links";
@import "scss/cronjobs";
@import "scss/coursewizard";
@import "scss/css_tree";
@@ -42,7 +41,6 @@
@import "scss/documents";
@import "scss/drag-handle";
@import "scss/enrolment";
-@import "scss/evaluation";
@import "scss/files";
@import "scss/feedback";
@import "scss/forms";
@@ -97,6 +95,7 @@
@import "scss/studygroup";
@import "scss/studip-overlay";
@import "scss/studip-selection";
+@import "scss/system-notifications";
@import "scss/table_of_contents";
@import "scss/tables";
@import "scss/tabs";
@@ -244,8 +243,6 @@ ol.clean {
padding: 5px;
}
-
-
.minor {
color: var(--black);
font-size: 0.75em;
diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js
index 2390bb9..5d11daa 100644
--- a/resources/vue/base-components.js
+++ b/resources/vue/base-components.js
@@ -1,63 +1,33 @@
-import CalendarPermissionsTable from "./components/form_inputs/CalendarPermissionsTable.vue";
-import DayOfWeekSelect from './components/form_inputs/DayOfWeekSelect.vue';
-import DateListInput from './components/form_inputs/DateListInput.vue';
-import Multiselect from './components/Multiselect.vue';
-import MyCoursesColouredTable from './components/form_inputs/MyCoursesColouredTable.vue';
-import EditableList from "./components/EditableList.vue";
-import Quicksearch from './components/Quicksearch.vue';
-import RepetitionInput from "./components/form_inputs/RepetitionInput.vue";
-import SidebarWidget from './components/SidebarWidget.vue';
-import StudipActionMenu from './components/StudipActionMenu.vue';
-import StudipAssetImg from './components/StudipAssetImg.vue';
-import StudipDateTime from './components/StudipDateTime.vue';
-import StudipDialog from './components/StudipDialog.vue';
-import StudipFileSize from './components/StudipFileSize.vue';
-import StudipFolderSize from './components/StudipFolderSize.vue';
-import StudipIcon from './components/StudipIcon.vue';
-import RangeInput from './components/RangeInput.vue';
-import Datepicker from './components/Datepicker.vue';
-import Datetimepicker from './components/Datetimepicker.vue';
-import TextareaWithToolbar from './components/TextareaWithToolbar.vue';
-import I18nTextarea from "./components/I18nTextarea.vue";
-import StudipWysiwyg from "./components/StudipWysiwyg.vue";
-// import StudipLoadingIndicator from './StudipLoadingIndicator.vue';
-import StudipMessageBox from './components/StudipMessageBox.vue';
-import StudipProxyCheckbox from './components/StudipProxyCheckbox.vue';
-import StudipProxiedCheckbox from './components/StudipProxiedCheckbox.vue';
-import StudipTooltipIcon from './components/StudipTooltipIcon.vue';
-import StudipSelect from './components/StudipSelect.vue';
-import StudipMultiPersonSearch from './components/StudipMultiPersonSearch.vue';
-
const BaseComponents = {
- CalendarPermissionsTable,
- DayOfWeekSelect,
- DateListInput,
- Multiselect,
- MyCoursesColouredTable,
- EditableList,
- Quicksearch,
- RangeInput,
- RepetitionInput,
- SidebarWidget,
- StudipActionMenu,
- StudipAssetImg,
- StudipDateTime,
- Datepicker,
- Datetimepicker,
- StudipDialog,
- StudipFileSize,
- StudipFolderSize,
- StudipIcon,
- I18nTextarea,
- StudipWysiwyg,
-// StudipLoadingIndicator,
- StudipMessageBox,
- StudipProxyCheckbox,
- StudipProxiedCheckbox,
- StudipTooltipIcon,
- StudipSelect,
- TextareaWithToolbar,
- StudipMultiPersonSearch
+ CaptchaInput: () => import('./components/form_inputs/CaptchaInput.vue'),
+ CalendarPermissionsTable: () => import("./components/form_inputs/CalendarPermissionsTable.vue"),
+ DateListInput: () => import('./components/form_inputs/DateListInput.vue'),
+ Datepicker: () => import('./components/Datepicker.vue'),
+ Datetimepicker: () => import('./components/Datetimepicker.vue'),
+ DayOfWeekSelect: () => import('./components/form_inputs/DayOfWeekSelect.vue'),
+ EditableList: () => import("./components/EditableList.vue"),
+ I18nTextarea: () => import("./components/I18nTextarea.vue"),
+ Multiselect: () => import('./components/Multiselect.vue'),
+ MyCoursesColouredTable: () => import('./components/form_inputs/MyCoursesColouredTable.vue'),
+ Quicksearch: () => import('./components/Quicksearch.vue'),
+ RangeInput: () => import('./components/RangeInput.vue'),
+ RepetitionInput: () => import("./components/form_inputs/RepetitionInput.vue"),
+ SidebarWidget: () => import('./components/SidebarWidget.vue'),
+ StudipActionMenu: () => import('./components/StudipActionMenu.vue'),
+ StudipAssetImg: () => import('./components/StudipAssetImg.vue'),
+ StudipDateTime: () => import('./components/StudipDateTime.vue'),
+ StudipDialog: () => import('./components/StudipDialog.vue'),
+ StudipFileSize: () => import('./components/StudipFileSize.vue'),
+ StudipFolderSize: () => import('./components/StudipFolderSize.vue'),
+ StudipIcon: () => import('./components/StudipIcon.vue'),
+ StudipMessageBox: () => import('./components/StudipMessageBox.vue'),
+ StudipMultiPersonSearch: () => import('./components/StudipMultiPersonSearch.vue'),
+ StudipProxiedCheckbox: () => import('./components/StudipProxiedCheckbox.vue'),
+ StudipProxyCheckbox: () => import('./components/StudipProxyCheckbox.vue'),
+ StudipSelect: () => import('./components/StudipSelect.vue'),
+ StudipTooltipIcon: () => import('./components/StudipTooltipIcon.vue'),
+ StudipWysiwyg: () => import("./components/StudipWysiwyg.vue"),
+ TextareaWithToolbar: () => import('./components/TextareaWithToolbar.vue'),
};
export default BaseComponents;
diff --git a/resources/vue/components/ActiveFilter.vue b/resources/vue/components/ActiveFilter.vue
index d4654fa..5394bce 100644
--- a/resources/vue/components/ActiveFilter.vue
+++ b/resources/vue/components/ActiveFilter.vue
@@ -4,7 +4,7 @@
<button
@click="onRemoveActiveFilter"
type="button"
- :title="$gettextInterpolate($gettext('Filter \'%{name}\' entfernen'), { name })"
+ :title="$gettextInterpolate($gettext('Filter \'%{name}\' entfernen'), { name }, true)"
>
<StudipIcon class="text-bottom" shape="decline" role="presentation" alt="" />
</button>
diff --git a/resources/vue/components/AdminCourses.vue b/resources/vue/components/AdminCourses.vue
index a2f61d7..d24a900 100644
--- a/resources/vue/components/AdminCourses.vue
+++ b/resources/vue/components/AdminCourses.vue
@@ -23,7 +23,7 @@
<th v-for="activeField in sortedActivatedFields" :key="`field-${activeField}`" :class="sort.by === activeField ? 'sort' + sort.direction.toLowerCase() : ''">
<a href="#"
@click.prevent="changeSort(activeField)"
- :title="sort.by === activeField && sort.direction === 'ASC' ? $gettextInterpolate('Sortiert aufsteigend nach %{field}', {field: fields[activeField]}) : (sort.by === activeField && sort.direction === 'DESC' ? $gettextInterpolate('Sortiert absteigend nach %{ field } ', { field: fields[activeField]}) : $gettextInterpolate('Sortieren nach %{ field }', { field: fields[activeField]}))"
+ :title="sort.by === activeField && sort.direction === 'ASC' ? $gettextInterpolate('Sortiert aufsteigend nach %{field}', {field: fields[activeField]}, true) : (sort.by === activeField && sort.direction === 'DESC' ? $gettextInterpolate('Sortiert absteigend nach %{ field } ', { field: fields[activeField]}, true) : $gettextInterpolate('Sortieren nach %{ field }', { field: fields[activeField]}, true))"
v-if="!unsortableFields.includes(activeField)"
>
{{ fields[activeField] }}
@@ -221,22 +221,28 @@ export default {
});
},
sortArray (array) {
+ const mappedFields = {
+ last_activity: 'last_activity_raw',
+ semester: 'semester_sort',
+ };
+
if (!array.length) {
return [];
}
- let sortby = this.sort.by;
- if (!this.activatedFields.includes(sortby) && sortby !== 'completion') {
+ if (!this.activatedFields.includes(this.sort.by) && this.sort.by !== 'completion') {
return array;
}
const striptags = function (text) {
- if (typeof text === "string") {
+ if (typeof text === 'string') {
return text.replace(/(<([^>]+)>)/gi, "");
} else {
return text;
}
};
+ let sortby = mappedFields[this.sort.by] ?? this.sort.by;
+
// Define sort direction by this factor
const directionFactor = this.sort.direction === 'ASC' ? 1 : -1;
@@ -246,33 +252,28 @@ export default {
sensitivity: 'base'
});
let sortFunction = function (a, b) {
- return collator.compare(striptags(a[sortby]), striptags(b[sortby]));
+ return collator.compare(striptags(a[sortby]), striptags(b[sortby]))
+ || collator.compare(striptags(a.number), striptags(b.number));
};
- if (sortby === 'last_activity') {
- sortFunction = (a, b) => a.last_activity_raw - b.last_activity_raw;
- } else if (sortby === 'name') {
- sortFunction = (a, b) => {
- return collator.compare(striptags(a.name), striptags(b.name))
- || collator.compare(striptags(a.number), striptags(b.number));
- };
- } else if (sortby === 'number') {
+ if (sortby === 'number') {
sortFunction = (a, b) => {
return collator.compare(striptags(a.number), striptags(b.number))
|| collator.compare(striptags(a.name), striptags(b.name));
};
} else {
- let is_numeric = true;
- for (let i in array) {
- if (striptags(array[i][sortby]) && isNaN(striptags(array[i][sortby]))) {
- is_numeric = false;
- break;
- }
- }
+ let is_numeric = !array.some(i => {
+ const value = striptags(i[sortby]);
+ return value && isNaN(parseInt(value, 10));
+ });
+
if (is_numeric) {
sortFunction = function (a, b) {
- return (striptags(a[sortby]) ? parseInt(striptags(a[sortby]), 10) : 0)
- - (striptags(b[sortby]) ? parseInt(striptags(b[sortby]), 10) : 0);
+ const aValue = (striptags(a[sortby]) ? parseInt(striptags(a[sortby]), 10) : 0);
+ const bValue = (striptags(b[sortby]) ? parseInt(striptags(b[sortby]), 10) : 0);
+
+ return aValue - bValue
+ || collator.compare(striptags(a.number), striptags(b.number));
};
}
}
diff --git a/resources/vue/components/CacheAdministration.vue b/resources/vue/components/CacheAdministration.vue
index af3d461..5da9568 100644
--- a/resources/vue/components/CacheAdministration.vue
+++ b/resources/vue/components/CacheAdministration.vue
@@ -82,24 +82,24 @@ export default {
* @param event
*/
getCacheConfig (event) {
- fetch(STUDIP.URLHelper.getURL(`dispatch.php/admin/cache/get_config/${this.selectedCacheType}`))
- .then((response) => {
- if (!response.ok) {
- throw response
- }
+ const url = STUDIP.URLHelper.getURL(
+ 'dispatch.php/admin/cache/get_config',
+ {cache: this.selectedCacheType},
+ true
+ );
+ fetch(url).then((response) => {
+ if (!response.ok) {
+ throw response
+ }
- response.json()
- .then((json) => {
- this.configComponent = json.component
- this.configProps = json.props
- }).catch((error) => {
- console.error(error)
- console.error(error.status + ': ', error.statusText)
- })
- }).catch((error) => {
- console.error(error)
- console.error(error.status + ': ', error.statusText)
- })
+ response.json().then((json) => {
+ this.configComponent = json.component
+ this.configProps = json.props
+ });
+ }).catch((error) => {
+ console.error(error)
+ console.error(error.status + ': ', error.statusText)
+ })
},
validateConfig () {
if (this.configComponent == null || this.isValid) {
diff --git a/resources/vue/components/ConsultationCreator.vue b/resources/vue/components/ConsultationCreator.vue
new file mode 100644
index 0000000..375e98a
--- /dev/null
+++ b/resources/vue/components/ConsultationCreator.vue
@@ -0,0 +1,496 @@
+<template>
+ <form :action="storeUrl" method="post" class="default" :data-dialog="asDialog ? '' : null" @submit="validateInputs">
+ <input type="hidden" :name="csrf.name" :value="csrf.value">
+ <input v-for="id in responsibleGroups" type="hidden" name="responsibilities[statusgroup][]" :value="id" :key="`group-${id}`">
+ <input v-for="id in responsibleInstitutes" type="hidden" name="responsibilities[institute][]" :value="id" :key="`institute-${id}`">
+ <input v-for="id in responsibleUsers" type="hidden" name="responsibilities[user][]" :value="id" :key="`user-${id}`">
+
+ <StudipMessageBox type="info" v-if="errors.length > 0">
+ {{ $gettext('Folgende Angaben müssen korrigiert werden, um das Formular abschicken zu können:') }}
+
+ <template #details>
+ <ul>
+ <li v-for="(error, index) in errors" :key="`error-${index}`">
+ {{ error }}
+ </li>
+ </ul>
+ </template>
+ </StudipMessageBox>
+
+ <fieldset>
+ <legend>{{ $gettext('Ort und Zeit') }}</legend>
+
+ <label>
+ <span class="required">{{ $gettext('Ort') }}</span>
+
+ <input required type="text" name="room"
+ v-model="room"
+ :placeholder="$gettext('Ort')">
+ </label>
+
+ <label :class="{'col-3': !isSingleDay}">
+ <span class="required">{{ $gettext('Intervall') }}</span>
+ <select required name="interval" v-model="interval">
+ <option v-for="(label, value) in intervals" :key="value" :value="value">
+ {{ label }}
+ </option>
+ </select>
+ </label>
+
+ <label class="col-3" v-if="!isSingleDay">
+ <span class="required">{{ $gettext('Am Wochentag') }}</span>
+
+ <select required name="day-of-week" v-model="dayOfWeek">
+ <option v-for="(label, value) in daysOfTheWeek" :value="value" :key="value">
+ {{ label }}
+ </option>
+ </select>
+ </label>
+
+ <label :class="{'col-3': !isSingleDay}">
+ <span class="required">{{ isSingleDay ? $gettext('Datum') : $gettext('Beginn') }}</span>
+
+ <Datepicker v-model="startDate"
+ name="start-date"
+ :disable-holidays="true"
+ :placeholder="$gettext('tt.mm.jjjj')"
+ mindate="today"
+ :emit-date="true"
+ ></Datepicker>
+ </label>
+
+ <label class="col-3" v-if="!isSingleDay">
+ <span class="required">{{ $gettext('Ende') }}</span>
+
+ <Datepicker v-model="endDate"
+ name="end-date"
+ :disable-holidays="true"
+ :placeholder="$gettext('tt.mm.jjjj')"
+ :mindate="startDate"
+ :emit-date="true"
+ ></Datepicker>
+ </label>
+
+ <label for="start-time" class="col-3">
+ <span class="required">{{ $gettext('Von') }}</span>
+
+ <Timepicker name="start-time"
+ v-model="startTime"
+ :maxtime="endTime"
+ ></Timepicker>
+ </label>
+
+ <label for="ende_hour" class="col-3">
+ <span class="required">{{ $gettext('Bis') }}</span>
+
+ <Timepicker name="end-time"
+ v-model="endTime"
+ :mintime="startTime"
+ ></Timepicker>
+ </label>
+
+ <label class="col-3">
+ <span class="required">{{ $gettext('Dauer eines Termins in Minuten') }}</span>
+ <input required type="number" name="duration" min="1"
+ v-model="duration">
+ </label>
+
+ <label class="col-3">
+ {{ $gettext('Maximale Teilnehmerzahl') }}
+ <StudipTooltipIcon :text="$gettext('Falls Sie mehrere Personen zulassen wollen (wie z.B. zu einer Klausureinsicht), so geben Sie hier die maximale Anzahl an Personen an, die sich anmelden dürfen.')"></StudipTooltipIcon>
+ <input required type="text" name="size" id="size" min="1" max="50"
+ v-model="size">
+ </label>
+
+ <label>
+ <input type="checkbox" name="pause" value="1"
+ v-model="pause">
+ {{ $gettext('Pausen zwischen den Terminen einfügen?') }}
+ </label>
+
+ <label class="col-3" v-if="pause">
+ {{ $gettext('Eine Pause nach wie vielen Minuten einfügen?') }}
+ <input type="number" name="pause_time" min="1"
+ v-model="pauseTime">
+ </label>
+
+ <label class="col-3" v-if="pause">
+ {{ $gettext('Dauer der Pause in Minuten') }}
+ <input type="number" name="pause_duration" min="1"
+ v-model="pauseDuration">
+ </label>
+
+ <label>
+ <input type="checkbox" name="lock" value="1"
+ v-model="lock">
+ {{ $gettext('Termine für Buchungen sperren?') }}
+ </label>
+
+ <label v-if="lock">
+ {{ $gettext('Wieviele Stunden vor Beginn des Blocks sollen die Termine für Buchungen gesperrt werden?') }}
+ <input type="number" name="lock_time" min="1"
+ v-model="lockTime">
+ </label>
+
+ <label>
+ <input type="checkbox" name="consecutive" value="1"
+ v-model="consecutive">
+ {{ $gettext('Termine innerhalb der Blöcke nur fortlaufend vergeben') }}
+ </label>
+
+ <slot name="extension-point-1"></slot>
+ </fieldset>
+
+ <fieldset v-if="withResponsible">
+ <legend>{{ $gettext('Durchführende Personen, Gruppen oder Einrichtungen') }}</legend>
+
+ <template v-if="isInstitute">
+ <p>
+ {{ $gettext('Bei Einrichtungen muss mindestens eine durchführende Person, Gruppe oder Einrichtung zugewiesen werden.') }}
+ </p>
+ <p>
+ {{ $gettext('Bitte beachten Sie, dass bei Zuweisungen von Statusgruppen alle Personen der Gruppe mit dem Status '
+ + '"tutor" und "dozent" als durchführende Personen zugewiesen werden und über alle Buchungen '
+ + 'informiert werden.') }}
+ {{ $gettext('Gleiches gilt für eine zugewiesene Einrichtung. Bitte achten Sie darauf, dass Sie Ihre hier '
+ + ' getroffene Auswahl in Absprache tätigen.') }}
+ </p>
+ </template>
+
+ <label v-if="withResponsible.users">
+ {{ $gettext('Durchführende Personen') }}
+ <StudipSelect v-model="responsibleUsers"
+ :options="withResponsible.users"
+ :reduce="option => option.id"
+ multiple
+ :clearable="true"
+ >
+ <template #open-indicator>
+ <span><studip-icon shape="arr_1down" :size="10" /></span>
+ </template>
+ </StudipSelect>
+ </label>
+
+ <label v-if="withResponsible.groups">
+ {{ $gettext('Durchführende Gruppen') }}
+ <StudipSelect v-model="responsibleGroups"
+ :options="withResponsible.groups"
+ :reduce="option => option.id"
+ multiple
+ :clearable="true"
+ >
+ <template #open-indicator>
+ <span><studip-icon shape="arr_1down" :size="10" /></span>
+ </template>
+ </StudipSelect>
+ </label>
+
+ <label v-if="withResponsible.institutes">
+ {{ $gettext('Durchführende Einrichtungen') }}
+ <StudipSelect v-model="responsibleInstitutes"
+ :options="withResponsible.institutes"
+ :reduce="option => option.id"
+ multiple
+ :clearable="true"
+ >
+ <template #open-indicator>
+ <span><studip-icon shape="arr_1down" :size="10" /></span>
+ </template>
+ </StudipSelect>
+ </label>
+ </fieldset>
+
+ <fieldset>
+ <legend>{{ $gettext('Weitere Einstellungen') }}</legend>
+
+ <label>
+ {{ $gettext('Information zu den Terminen in diesem Block') }}
+ <textarea name="note" v-model="note"></textarea>
+ </label>
+
+ <label>
+ <input type="checkbox" name="calender-events" value="1"
+ v-model="calendarEvents">
+ {{ $gettext('Die freien Termine auch im Kalender markieren') }}
+ </label>
+
+ <label v-if="isCourse">
+ <input type="checkbox" name="mail-to-tutors" value="1"
+ v-model="mailToTutors">
+ {{ $gettext('Tutor/innen beim Versand von Buchungsbenachrichtigungen berücksichtigen?') }}
+ </label>
+
+ <label>
+ <input type="checkbox" name="show-participants" value="1"
+ v-model="showParticipants">
+ {{ $gettext('Namen der buchenden Personen sind öffentlich sichtbar') }}
+ </label>
+
+ <label>{{ $gettext('Grund der Buchung abfragen') }}</label>
+ <div class="hgroup">
+ <label>
+ <input type="radio" name="require-reason" value="yes"
+ v-model="requireReason">
+ {{ $gettext('Ja, zwingend erforderlich') }}
+ </label>
+
+ <label>
+ <input type="radio" name="require-reason" value="optional"
+ v-model="requireReason">
+ {{ $gettext('Ja, optional') }}
+ </label>
+
+ <label>
+ <input type="radio" name="require-reason" value="no"
+ v-model="requireReason">
+ {{ $gettext('Nein') }}
+ </label>
+ </div>
+
+ <label>
+ {{ $gettext('Bestätigung für folgenden Text einholen') }}
+ ({{ $gettext('optional') }})
+ <StudipTooltipIcon :text="$gettext('Wird hier ein Text eingegeben, so müssen Buchende bestätigen, dass sie diesen Text gelesen haben.')"></StudipTooltipIcon>
+ <textarea name="confirmation-text" v-model="confirmationText"></textarea>
+ </label>
+
+ <slot name="extension-point-2"></slot>
+ </fieldset>
+
+ <fieldset v-if="needsConfirmation">
+ <legend>{{ $gettext('Bestätigung der Erstellung vieler Termine') }}</legend>
+
+ <p>
+ {{ $gettext('Sie erstellen eine sehr große Anzahl an Terminen.') }}
+ {{ $gettext('Bitte bestätigen Sie diese Aktion.') }}
+ </p>
+
+ <label>
+ <input type="checkbox" v-model="confirmed">
+ {{ $gettextInterpolate(
+ $gettext('Ja, ich möchte wirklich %{ n } Termine erstellen.'),
+ { n: slotCount }
+ ) }}
+ </label>
+ </fieldset>
+
+ <footer data-dialog-button>
+ <button class="accept button" :disabled="!confirmed">
+ {{ $gettext('Termin speichern') }}
+ </button>
+ <a :href="cancelUrl" class="cancel button" @click="evt => closeCreator(evt)">
+ {{ $gettext('Abbrechen') }}
+ </a>
+ </footer>
+ </form>
+</template>
+<script>
+import StudipTooltipIcon from './StudipTooltipIcon.vue';
+import Datepicker from './Datepicker.vue';
+
+import moment from 'moment';
+import StudipSelect from './StudipSelect.vue';
+import Timepicker from './Timepicker.vue';
+
+export default {
+ name: 'ConsultationCreator',
+ components: {Datepicker, StudipSelect, StudipTooltipIcon, Timepicker},
+ props: {
+ asDialog: {
+ type: Boolean,
+ default: false,
+ },
+ cancelUrl: {
+ type: String,
+ required: true
+ },
+ defaultRoom: String,
+ rangeType: {
+ type: String,
+ required: true,
+ },
+ slotCountThreshold: {
+ type: Number,
+ required: true,
+ },
+ storeUrl: {
+ type: String,
+ required: true
+ },
+ withResponsible: {
+ type: [Boolean, Object],
+ default: false,
+ },
+ },
+ data() {
+ return {
+ calendarEvents: false,
+ confirmationText: '',
+ confirmed: false,
+ consecutive: false,
+ dayOfWeek: (new Date()).getDay(),
+ duration: 15,
+ endDate: moment().add(4, 'weeks').toDate(),
+ endTime: '09:00',
+ errors: [],
+ interval: 1,
+ lock: false,
+ lockTime: 24,
+ mailToTutors: true,
+ note: '',
+ pause: false,
+ pauseDuration: 15,
+ pauseTime: 45,
+ requireReason: 'optional',
+ responsibleGroups: [],
+ responsibleInstitutes: [],
+ responsibleUsers: [],
+ room: this.defaultRoom,
+ showParticipants: false,
+ size: 1,
+
+ slotCount: null,
+ startDate: moment().add(1, 'weeks').toDate(),
+ startTime: '08:00',
+ }
+ },
+ computed: {
+ csrf() {
+ return STUDIP.CSRF_TOKEN;
+ },
+ daysOfTheWeek() {
+ return {
+ 1: this.$gettext('Montag'),
+ 2: this.$gettext('Dienstag'),
+ 3: this.$gettext('Mittwoch'),
+ 4: this.$gettext('Donnerstag'),
+ 5: this.$gettext('Freitag'),
+ 6: this.$gettext('Samstag'),
+ 0: this.$gettext('Sonntag'),
+ };
+ },
+ intervals() {
+ return {
+ 0: this.$gettext('einmalig (ohne Wiederholung)'),
+ 1: this.$gettext('wöchentlich'),
+ 2: this.$gettext('zweiwöchentlich'),
+ 3: this.$gettext('dreiwöchentlich'),
+ 4: this.$gettext('monatlich'),
+ };
+ },
+ isCourse() {
+ return this.rangeType === 'Course';
+ },
+ isInstitute() {
+ return this.rangeType === 'Institute';
+ },
+ isSingleDay() {
+ return this.interval === '0';
+ },
+ needsConfirmation() {
+ return this.slotCount > this.slotCountThreshold;
+ },
+ recalculationProperty() {
+ return [
+ this.startDate,
+ this.startTime,
+ this.endDate,
+ this.endTime,
+ this.dayOfWeek,
+ this.interval,
+ this.duration,
+ this.pause,
+ this.pauseTime,
+ this.pauseDuration,
+ ].join();
+ },
+ },
+ methods: {
+ closeCreator(event) {
+ if (this.$el.closest('.studip-dialog')) {
+ STUDIP.Dialog.close();
+ event.preventDefault();
+ }
+ },
+ validateInputs(event) {
+ const errors = [];
+
+ if (this.startTime > this.endTime) {
+ errors.push(this.$gettext('Die Endzeit liegt vor der Startzeit!'));
+ }
+
+ if (this.startDate > this.endDate) {
+ errors.push(this.$gettext('Das Enddatum liegt vor dem Startdatum!'));
+ }
+
+ if (this.pauseTime && this.pauseTime < this.duration) {
+ errors.push(this.$gettext('Die definierte Zeit bis zur Pause ist kleiner als die Dauer eines Termins.'));
+ }
+
+ if (
+ this.isInstitute
+ && this.responsibleGroups.length === 0
+ && this.responsibleInstitutes.length === 0
+ && this.responsibleUsers.length === 0
+ ) {
+ errors.push(this.$gettext('Es muss mindestens eine durchführende Person, Statusgruppe oder Einrichtung ausgewählt werden.'));
+ }
+
+ if (this.needsConfirmation && !this.confirmed) {
+ errors.push(this.$gettext('Sie müssen bestätigen, dass sie eine große Anzahl von Terminen erstellen möchten.'));
+
+ }
+
+ if (errors.length > 0) {
+ this.errors = errors;
+ event.preventDefault();
+ }
+ },
+ combineDateAndTime(date, time) {
+ const [hour, minute] = time.split(':').map(item => parseInt(item, 10));
+ const result = new Date(date);
+ result.setHours(hour);
+ result.setMinutes(minute);
+ result.setSeconds(0);
+ return result;
+ }
+ },
+ watch: {
+ interval(current) {
+ if (current === '0') {
+ this.endDate = new Date(this.startDate);
+ }
+ },
+ recalculationProperty: {
+ handler() {
+ STUDIP.jsonapi.withPromises().GET('consultation-slots/count', {
+ data: {
+ start: this.combineDateAndTime(this.startDate, this.startTime).toISOString(),
+ end: this.combineDateAndTime(this.endDate, this.endTime).toISOString(),
+ dow: this.dayOfWeek,
+ interval: this.interval,
+ duration: this.duration,
+ pause_time: this.pause ? this.pauseTime : null,
+ pause_duration: this.pause ? this.pauseDuration : null,
+ }
+ }).then((count) => {
+ this.slotCount = count;
+ this.confirmed = count <= this.slotCountThreshold;
+ });
+ },
+ immediate: true
+ },
+ startDate(current) {
+ this.dayOfWeek = current.getDay();
+ },
+ },
+ beforeCreate() {
+ STUDIP.Vue.emit('ConsultationCreatorWillCreate', this);
+ }
+}
+</script>
+<style scoped>
+form.default label input[type="time"] {
+ max-width: 48em;
+}
+</style>
diff --git a/resources/vue/components/ContentModulesControl.vue b/resources/vue/components/ContentModulesControl.vue
index f403bdd..34f4125 100644
--- a/resources/vue/components/ContentModulesControl.vue
+++ b/resources/vue/components/ContentModulesControl.vue
@@ -15,7 +15,7 @@
@click.prevent="toggleModuleVisibility(module)">
<studip-icon :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'"
class="text-bottom"
- :title="$gettextInterpolate($gettext('Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname})"></studip-icon>
+ :title="$gettextInterpolate($gettext('Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname}, true)"></studip-icon>
</a>
</div>
</div>
diff --git a/resources/vue/components/ContentModulesEditTiles.vue b/resources/vue/components/ContentModulesEditTiles.vue
index 383c6ab..7407632 100644
--- a/resources/vue/components/ContentModulesEditTiles.vue
+++ b/resources/vue/components/ContentModulesEditTiles.vue
@@ -35,7 +35,8 @@
$gettext(
'Sortierelement für Werkzeug %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'
),
- { module: module.displayname }
+ { module: module.displayname },
+ true
)
"
@keydown="keyboardHandler($event, module)"
@@ -76,7 +77,8 @@
$gettext(
'Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'
),
- { name: module.displayname }
+ { name: module.displayname },
+ true
)
"
></studip-icon>
@@ -90,7 +92,8 @@
$gettext(
'Umbenennen des Inhaltsmoduls %{ name }'
),
- { name: module.displayname }
+ { name: module.displayname },
+ true
)
"
></studip-icon>
diff --git a/resources/vue/components/ContentmodulesEditTable.vue b/resources/vue/components/ContentmodulesEditTable.vue
index 8724a24..0a8a0aa 100644
--- a/resources/vue/components/ContentmodulesEditTable.vue
+++ b/resources/vue/components/ContentmodulesEditTable.vue
@@ -25,7 +25,8 @@
$gettext(
'Sortierelement für Module %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'
),
- { module: module.displayname }
+ { module: module.displayname },
+ true
)
"
@keydown="keyboardHandler($event, module)"
@@ -71,7 +72,8 @@
$gettext(
'Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'
),
- { name: module.displayname }
+ { name: module.displayname },
+ true
)
"
></studip-icon>
@@ -83,7 +85,7 @@
:title="
$gettextInterpolate($gettext('Umbenennen des Inhaltsmoduls %{ name }'), {
name: module.displayname,
- })
+ }, true)
"
></studip-icon>
</a>
diff --git a/resources/vue/components/Datepicker.vue b/resources/vue/components/Datepicker.vue
index 3db44ce..5c2c0f7 100644
--- a/resources/vue/components/Datepicker.vue
+++ b/resources/vue/components/Datepicker.vue
@@ -1,75 +1,143 @@
<template>
<span>
- <input type="hidden" :name="name" :value="value">
+ <input type="hidden" :name="name" :value="returnValue">
<input type="text"
ref="visibleInput"
class="visible_input"
- @change="setUnixTimestamp"
v-bind="$attrs"
- v-on="$listeners">
+ v-on="$listeners"
+ :placeholder="placeholder">
</span>
</template>
<script>
+import RestrictedDatesHelper from '../../assets/javascripts/lib/RestrictedDatesHelper';
+
export default {
- name: "datepicker",
+ name: 'Datepicker',
inheritAttrs: false,
props: {
name: {
type: String,
required: false
},
- value: {
- required: false
+ value: [Date, String, Number],
+ mindate: [Date, Number, String],
+ maxdate: [Date, Number, String],
+ placeholder: String,
+ disableHolidays: {
+ type: Boolean,
+ default: false,
},
- mindate: {
- required: false
+ emitDate: {
+ type: Boolean,
+ default: false,
},
- maxdate: {
- required: false
+ returnAs: {
+ type: String,
+ default: 'localized',
+ validator(value) {
+ return ['localized', 'unix', 'iso'].includes(value);
+ }
+ }
+ },
+ computed: {
+ input() {
+ return $(this.$refs.visibleInput);
+ },
+ parameters() {
+ let params = {
+ onSelect: () => {
+ this.setUnixTimestamp();
+ },
+ maxDate: this.convertInputToNativeDate(this.maxdate),
+ minDate: this.convertInputToNativeDate(this.mindate),
+ };
+ if (this.disableHolidays) {
+ params.beforeShowDay = (date) => {
+ RestrictedDatesHelper.loadRestrictedDatesByYear(date.getFullYear()).then(
+ () => this.input.datepicker('refresh'),
+ () => null
+ );
+
+ const {reason, lock} = RestrictedDatesHelper.isDateRestricted(date);
+ return [!lock, lock ? 'ui-datepicker-is-locked' : null, reason];
+ };
+ }
+
+ return params;
+ },
+ returnValue() {
+ if (this.returnAs === 'unix') {
+ return this.convertInputToUnixTimestamp(this.value);
+ }
+
+ if (this.returnAs === 'iso') {
+ return this.convertInputToNativeDate(this.value).toISOString();
+ }
+
+ return this.convertInputToNativeDate(this.value).toLocaleDateString();
}
},
methods: {
+ convertInputToNativeDate(input) {
+ if (input instanceof Date) {
+ return input;
+ }
+
+ if (input === 'today') {
+ return new Date();
+ }
+
+ return input ? new Date(input * 1000) : null;
+ },
+ convertInputToUnixTimestamp(input) {
+ if (input instanceof Date) {
+ return Math.floor(input.getTime() / 1000);
+ }
+
+ if (!isNaN(parseInt(input, 10))) {
+ return parseInt(input, 10);
+ }
+
+ return input;
+ },
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));
+ let date = this.input.datepicker('getDate');
+ this.$emit('input', this.emitDate ? date : Math.floor(date.getTime() / 1000));
}
},
mounted () {
- let value = !isNaN(parseInt(this.value, 10)) ? parseInt(this.value, 10) : this.value;
+ let value = this.convertInputToUnixTimestamp(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();
- this.$refs.visibleInput.value = formatted_date;
+ this.input.val(date.toLocaleDateString());
} else {
- this.$refs.visibleInput.value = value;
- }
- let params = {
- onSelect: () => {
- this.setUnixTimestamp();
- }
- };
- if (this.mindate) {
- params.minDate = new Date(this.mindate * 1000)
- }
- if (this.maxdate) {
- params.maxDate = new Date(this.maxdate * 1000)
+ this.input.val(value);
}
- $(this.$refs.visibleInput).datetimepicker(params);
+ this.input.datepicker(this.parameters);
},
watch: {
- mindate (new_data, old_data) {
- $(this.$refs.visibleInput).datetimepicker('option', 'minDate', new Date(new_data * 1000));
+ maxdate(current) {
+ this.input.datepicker(
+ 'option',
+ 'maxDate',
+ this.convertInputToNativeDate(current)
+ );
},
- maxdate (new_data, old_data) {
- $(this.$refs.visibleInput).datetimepicker('option', 'maxDate', new Date(new_data * 1000));
+ mindate(current) {
+ this.input.datepicker(
+ 'option',
+ 'minDate',
+ this.convertInputToNativeDate(current)
+ );
+ },
+ value(current, previous) {
+ if (current.toISOString() !== previous.toISOString()) {
+ this.input.datepicker('setDate', current);
+ this.input.datepicker('refresh');
+ }
}
}
}
diff --git a/resources/vue/components/EditableList.vue b/resources/vue/components/EditableList.vue
index cf1716b..39c32ac 100644
--- a/resources/vue/components/EditableList.vue
+++ b/resources/vue/components/EditableList.vue
@@ -12,7 +12,7 @@
<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($gettext('%{ name } löschen'), {name: item.name})" class="undecorated">
+ <button v-if="item.deletable" @click.prevent="deleteItem(item)" :title="$gettextInterpolate($gettext('%{ name } löschen'), {name: item.name}, true)" class="undecorated">
<studip-icon shape="trash" role="clickable" :size="20" class="text-bottom"></studip-icon>
</button>
</li>
diff --git a/resources/vue/components/FilesTable.vue b/resources/vue/components/FilesTable.vue
index 646e914..1e19cc9 100644
--- a/resources/vue/components/FilesTable.vue
+++ b/resources/vue/components/FilesTable.vue
@@ -42,40 +42,83 @@
</colgroup>
<thead>
<tr class="sortable">
- <th v-if="show_bulk_actions" data-sort="false" :aria-label="$gettext('Ordner und Dateien auswählen')">
+ <th v-if="show_bulk_actions"
+ data-sort="false"
+ :aria-label="$gettext('Ordner und Dateien auswählen')">
<studip-proxy-checkbox
v-model="selectedIds"
:total="allIds"
:title="$gettext('Alle Ordner und Dateien auswählen')"
></studip-proxy-checkbox>
</th>
- <th @click="sort('mime_type')" :class="sortClasses('mime_type')">
- <a href="#" @click.prevent>
+ <th @click="sort('mime_type')"
+ :class="sortClasses('mime_type')"
+ :aria-sort="getAriaSortString('mime_type')"
+ :aria-label="getAriaSortLabel('mime_type', $gettext('Typ'))"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettext('Nach Typ sortieren')">
{{ $gettext('Typ') }}
</a>
</th>
- <th @click="sort('name')" :class="sortClasses('name')">
- <a href="#" @click.prevent>
+ <th @click="sort('name')"
+ :class="sortClasses('name')"
+ :aria-sort="getAriaSortString('name')"
+ :aria-label="getAriaSortLabel('name', $gettext('Name'))"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettext('Nach Name sortieren')">
{{ $gettext('Name') }}
</a>
</th>
- <th @click="sort('size')" class="responsive-hidden" :class="sortClasses('size')">
- <a href="#" @click.prevent>
+ <th @click="sort('size')"
+ class="responsive-hidden"
+ :class="sortClasses('size')"
+ :aria-sort="getAriaSortString('size')"
+ :aria-label="getAriaSortLabel('size', $gettext('Größe'))"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettext('Nach Größe sortieren')">
{{ $gettext('Größe') }}
</a>
</th>
- <th v-if="showdownloads" @click="sort('downloads')" class="responsive-hidden" :class="sortClasses('downloads')">
- <a href="#" @click.prevent>
+ <th v-if="showdownloads"
+ @click="sort('downloads')"
+ class="responsive-hidden"
+ :class="sortClasses('downloads')"
+ :aria-sort="getAriaSortString('downloads')"
+ :aria-label="getAriaSortLabel('downloads', $gettext('Downloads'))"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettext('Nach Downloads sortieren')">
{{ $gettext('Downloads') }}
</a>
</th>
- <th class="responsive-hidden" @click="sort('author_name')" :class="sortClasses('author_name')">
- <a href="#" @click.prevent>
+ <th class="responsive-hidden"
+ @click="sort('author_name')"
+ :class="sortClasses('author_name')"
+ :aria-sort="getAriaSortString('author_name')"
+ :aria-label="getAriaSortLabel('author_name', $gettext('Autor/-in'))"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettext('Nach Autor/-in sortieren')">
{{ $gettext('Autor/-in') }}
</a>
</th>
- <th class="responsive-hidden" @click="sort('chdate')" :class="sortClasses('chdate')">
- <a href="#" @click.prevent>
+ <th class="responsive-hidden"
+ @click="sort('chdate')"
+ :class="sortClasses('chdate')"
+ :aria-sort="getAriaSortString('chdate')"
+ :aria-label="getAriaSortLabel('chdate', $gettext('Datum'))"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettext('Nach Datum sortieren')">
{{ $gettext('Datum') }}
</a>
</th>
@@ -83,11 +126,15 @@
:key="index"
@click="sort(index)"
class="responsive-hidden"
- :class="sortClasses(index)">
- <a href="#" @click.prevent>
+ :class="sortClasses(index)"
+ :aria-sort="getAriaSortString(name)"
+ :aria-label="getAriaSortLabel(name, name)"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettextInterpolate($gettext('Nach %{ colName } sortieren'), {colName: name}, true)">
{{name}}
</a>
-
</th>
<th class="actions" data-sort="false">{{ $gettext('Aktionen') }}</th>
</tr>
@@ -120,12 +167,17 @@
></studip-proxied-checkbox>
</td>
<td class="document-icon">
- <a :href="folder.url" :id="`folder-${folder.id}`">
- <studip-icon :shape="folder.icon" :size="26" class="text-bottom"></studip-icon>
+ <a :href="folder.url"
+ :id="`folder-${folder.id}`"
+ :title="$gettextInterpolate($gettext('Ordner %{foldername} öffnen'),
+ { foldername: folder.name}, true)">
+ <studip-icon :shape="folder.icon" :size="26" class="text-bottom" alt=""></studip-icon>
</a>
</td>
<td :class="{'filter-match': valueMatchesFilter(folder.name)}">
- <a :href="folder.url">
+ <a :href="folder.url"
+ :title="$gettextInterpolate($gettext('Ordner %{foldername} öffnen'),
+ { foldername: folder.name}, true)">
<span v-html="highlightString(folder.name)"></span>
</a>
</td>
@@ -172,7 +224,11 @@
></studip-proxied-checkbox>
</td>
<td class="document-icon">
- <a v-if="file.download_url" :href="file.download_url" target="_blank" rel="noopener noreferrer">
+ <a v-if="file.download_url"
+ :href="file.download_url"
+ target="_blank" rel="noopener noreferrer"
+ :title="$gettextInterpolate($gettext('Datei %{filename} herunterladen'),
+ { filename: file.name }, true)">
<studip-icon :shape="file.icon" :size="24" class="text-bottom"></studip-icon>
</a>
<studip-icon v-else :shape="file.icon" :size="24"></studip-icon>
@@ -180,10 +236,16 @@
<a :href="file.download_url"
v-if="file.download_url && file.mime_type.indexOf('image/') === 0"
class="lightbox-image"
- data-lightbox="gallery"></a>
+ data-lightbox="gallery"
+ :title="$gettextInterpolate($gettext('Datei %{filename} anzeigen'),
+ { filename: file.name }, true)"></a>
</td>
<td :class="{'filter-match': valueMatchesFilter(file.name)}">
- <a :href="file.details_url" data-dialog :id="`file-${file.id}`">
+ <a :href="file.details_url"
+ data-dialog
+ :id="`file-${file.id}`"
+ :title="$gettextInterpolate($gettext('Details zur Datei %{filename} anzeigen'),
+ { filename: file.name }, true)">
<span v-html="highlightString(file.name)"></span>
<studip-icon v-if="file.isAccessible"
shape="accessibility"
@@ -405,6 +467,20 @@ export default {
this.$gettext('Datei %{name} auswählen'),
{name: file.name}
);
+ },
+ getAriaSortString(column) {
+ return column === this.sortedBy
+ ? (this.sortDirection === 'asc' ? 'ascending' : 'descending')
+ : null;
+ },
+ getAriaSortLabel(column, label) {
+ if (column !== this.sortedBy) {
+ return null;
+ }
+ const template = this.sortDirection === 'asc'
+ ? this.$gettext('Es wird aufsteigend nach der Spalte %{ label } sortiert.')
+ : this.$gettext('Es wird absteigend nach der Spalte %{ label } sortiert.');
+ return this.$gettextInterpolate(template, { label });
}
},
computed: {
diff --git a/resources/vue/components/MyCoursesTables.vue b/resources/vue/components/MyCoursesTables.vue
index 80d04dd..1c39d95 100644
--- a/resources/vue/components/MyCoursesTables.vue
+++ b/resources/vue/components/MyCoursesTables.vue
@@ -12,7 +12,11 @@
</colgroup>
<thead>
<tr class="sortable">
- <th></th>
+ <th>
+ <span class="sr-only">
+ {{ $gettext('Zugeordnete Farbgruppe') }}
+ </span>
+ </th>
<th></th>
<th v-if="getConfig('sem_number') && !responsiveDisplay" :class="getOrderClasses('number')">
<a href="#" @click.prevent="changeOrder('number')">
@@ -37,7 +41,14 @@
<th v-if="!responsiveDisplay" class="dont-hide" colspan="2"></th>
</tr>
<tr v-for="course in getOrderedCourses(subgroup.ids)" :data-course-id="course.id" :class="getCourseClasses(course)" :key="course.id">
- <td :class="`gruppe${course.group}`"></td>
+ <td :class="`gruppe${course.group}`">
+ <span class="sr-only">
+ {{ $gettextInterpolate(
+ $gettext('Diese Veranstaltung gehört zur Farbgruppe %{group}'),
+ course
+ ) }}
+ </span>
+ </td>
<td :class="{'subcourse-indented': isChild(course)}">
<span :style="{backgroundImage: `url(${course.avatar}`}" class="my-courses-avatar course-avatar-small" :title="course.name" alt=""></span>
</td>
diff --git a/resources/vue/components/MyCoursesTiles.vue b/resources/vue/components/MyCoursesTiles.vue
index 73f8aad..12b2627 100644
--- a/resources/vue/components/MyCoursesTiles.vue
+++ b/resources/vue/components/MyCoursesTiles.vue
@@ -136,7 +136,7 @@ export default {
return this.shownColorPicker === course.id;
},
changeColor(course, index) {
- STUDIP.jsonapi.PATCH(`course-memberships/${course.id}_${this.userid}`, {
+ STUDIP.jsonapi.withPromises().patch(`course-memberships/${course.id}_${this.userid}`, {
data: {
data: {
type: 'course-memberships',
@@ -145,9 +145,9 @@ export default {
}
}
}
- }).done(() => {
+ }).then(() => {
course.group = index;
- }).always(() => {
+ }).finally(() => {
this.shownColorPicker = null;
});
},
diff --git a/resources/vue/components/Quicksearch.vue b/resources/vue/components/Quicksearch.vue
index 94b0a3a..0f37ae0 100644
--- a/resources/vue/components/Quicksearch.vue
+++ b/resources/vue/components/Quicksearch.vue
@@ -119,6 +119,7 @@ export default {
this.results = [];
this.$emit('input', this.returnValue, this.inputValue);
+ this.inputValue = '';
},
selectUp () {
if (this.selected > 0) {
diff --git a/resources/vue/components/StudipAssetImg.vue b/resources/vue/components/StudipAssetImg.vue
index 6a250a9..b60915b 100644
--- a/resources/vue/components/StudipAssetImg.vue
+++ b/resources/vue/components/StudipAssetImg.vue
@@ -1,6 +1,6 @@
<template>
<img :src="url"
- :width="width">
+ :width="width" alt="">
</template>
<script>
diff --git a/resources/vue/components/StudipDialog.vue b/resources/vue/components/StudipDialog.vue
index f14dd37..79337f8 100644
--- a/resources/vue/components/StudipDialog.vue
+++ b/resources/vue/components/StudipDialog.vue
@@ -1,6 +1,6 @@
<template>
<MountingPortal mountTo="body" append>
- <focus-trap v-model="trap" :initial-focus="() => defaultFocus ? $refs.buttonB : null">
+ <focus-trap v-model="trap">
<div class="studip-dialog" @keydown.esc="closeDialog">
<transition name="dialog-fade">
<div class="studip-dialog-backdrop">
@@ -38,7 +38,11 @@
<header
class="studip-dialog-header"
>
- <span :id="dialogTitleId" class="studip-dialog-title" :title="dialogTitle">
+ <span :id="dialogTitleId"
+ class="studip-dialog-title"
+ :title="dialogTitle"
+ role="heading"
+ aria-level="2">
{{ dialogTitle }}
</span>
<slot name="dialogHeader"></slot>
@@ -262,5 +266,13 @@ export default {
return typeof value !== "number" ? 0 : value;
}
},
+ mounted() {
+ if (this.defaultFocus) {
+ this.$nextTick()
+ .then(() => {
+ this.$refs.buttonB.focus();
+ });
+ }
+ }
};
</script>
diff --git a/resources/vue/components/StudipFileChooser.vue b/resources/vue/components/StudipFileChooser.vue
index e021aec..798d646 100644
--- a/resources/vue/components/StudipFileChooser.vue
+++ b/resources/vue/components/StudipFileChooser.vue
@@ -128,7 +128,7 @@ export default {
max-width: 48em;
button {
margin: 0.5ex 0 0.5ex 0;
- min-width: 140px;
+ width: 150px;
}
span {
box-sizing: border-box;
@@ -138,7 +138,7 @@ export default {
font-size: 14px;
line-height: 130%;
min-width: 100px;
- width: calc(100% - 140px);
+ width: calc(100% - 150px);
overflow: hidden;
text-overflow: ellipsis;
padding: 5px 15px;
diff --git a/resources/vue/components/StudipIcon.vue b/resources/vue/components/StudipIcon.vue
index 9ca1c7b..5e27372 100644
--- a/resources/vue/components/StudipIcon.vue
+++ b/resources/vue/components/StudipIcon.vue
@@ -9,6 +9,7 @@
:role="ariaRole"
v-bind="$attrs"
v-on="$listeners"
+ :alt="$attrs.alt ?? ''"
/>
<img v-else
:src="url"
@@ -17,6 +18,7 @@
:role="ariaRole"
v-bind="$attrs"
v-on="$listeners"
+ :alt="$attrs.alt ?? ''"
/>
</template>
diff --git a/resources/vue/components/StudipTooltipIcon.vue b/resources/vue/components/StudipTooltipIcon.vue
index 39856bb..30d2033 100644
--- a/resources/vue/components/StudipTooltipIcon.vue
+++ b/resources/vue/components/StudipTooltipIcon.vue
@@ -37,6 +37,9 @@
</script>
<style lang="scss" scoped>
+.tooltip img {
+ vertical-align: text-bottom;
+}
.tooltip.tooltip-icon::before {
display: none;
}
diff --git a/resources/vue/components/StudipWysiwyg.vue b/resources/vue/components/StudipWysiwyg.vue
index e343e80..799c5f1 100644
--- a/resources/vue/components/StudipWysiwyg.vue
+++ b/resources/vue/components/StudipWysiwyg.vue
@@ -2,7 +2,7 @@
<ckeditor
:editor="editor"
:config="editorConfig"
- @ready="prefill"
+ @ready="onReady"
v-model="currentText"
@input="onInput"
/>
@@ -29,11 +29,15 @@ export default {
},
default: 'classic',
},
+ autofocus: Boolean,
},
data() {
return {
currentText: '',
editorConfig: {},
+
+ createdEditor: null,
+ shouldFocus: this.autofocus,
};
},
computed: {
@@ -48,13 +52,25 @@ export default {
},
},
methods: {
- prefill(editor) {
+ onReady(editor) {
+ this.createdEditor = editor;
this.currentText = this.text;
+
+ if (this.shouldFocus) {
+ this.focus();
+ }
},
onInput(value) {
this.currentText = value;
this.$emit('input', value);
},
+ focus() {
+ if (this.createdEditor) {
+ this.createdEditor.focus();
+ } else {
+ this.shouldFocus = true;
+ }
+ }
},
created() {
STUDIP.loadChunk('mathjax');
diff --git a/resources/vue/components/SystemNotification.vue b/resources/vue/components/SystemNotification.vue
new file mode 100644
index 0000000..60cefd7
--- /dev/null
+++ b/resources/vue/components/SystemNotification.vue
@@ -0,0 +1,173 @@
+<template>
+ <div v-cloak
+ class="system-notification"
+ :class="cssClasses"
+ @mouseover="disruptTimeout"
+ @mouseout="initTimeout"
+ @focus="disruptTimeout"
+ @blur="initTimeout"
+ >
+ <div class="system-notification-icon">
+ <studip-icon :shape="icon.shape"
+ :size="48"
+ :role="icon.color"
+ alt=""
+ title=""></studip-icon>
+ </div>
+ <div class="system-notification-content">
+ <p v-html="notification.message"></p>
+ <p class="sr-only" v-if="hasTimeout">
+ {{ $gettext('Strg+Alt+T hält das automatische Ausblenden der Meldung an bzw. setzt es wieder fort.') }}
+ </p>
+ <details v-if="notification.details?.length > 0"
+ class="system-notification-details">
+ <summary>
+ {{ $gettext('Details') }}
+ </summary>
+ <template v-if="Array.isArray(notification.details)">
+ <p v-for="(detail, index) in notification.details"
+ :key="index"
+ v-html="detail"></p>
+ </template>
+ <p v-else v-html="notification.details"></p>
+ </details>
+ </div>
+ <button v-if="allowClosing"
+ class="system-notification-close undecorated"
+ :title="$gettext('Diese Meldung schließen')"
+ @click.prevent="destroyMe"
+ @keydown.space="destroyMe"
+ tabindex="0">
+ <studip-icon shape="decline"
+ :size="20"
+ class="close-system-notification"/>
+ </button>
+ <transition v-if="hasTimeout"
+ name="system-notification-timeout"
+ appear
+ >
+ <div v-if="!stopTimeout"
+ class="system-notification-timeout"
+ ref="timeout-counter"></div>
+ </transition>
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'SystemNotification',
+ props: {
+ allowClosing: {
+ type: Boolean,
+ default: true
+ },
+ appendTo: {
+ type: String,
+ default: null
+ },
+ notification: {
+ type: Object,
+ required: true
+ },
+ visibleFor: {
+ type: Number,
+ default: 5000
+ }
+ },
+ data() {
+ return {
+ stopTimeout: false,
+ timeout: null,
+ windowIsBlurred: false,
+ }
+ },
+ computed: {
+ cssClasses() {
+ const classes = [`system-notification-${this.notification.type}`];
+ if (this.isDisrupted) {
+ classes.push('system-notification-disrupted');
+ }
+ return classes;
+ },
+ hasTimeout() {
+ return !['exception', 'error'].includes(this.notification.type);
+ },
+ icon() {
+ let iconShape = 'info-circle';
+ let iconColor = 'info';
+ switch (this.type) {
+ case 'exception':
+ iconShape = 'exclaim-circle';
+ iconColor = 'info_alt';
+ break;
+ case 'error':
+ iconShape = 'exclaim-circle';
+ iconColor = 'status-red';
+ break;
+ case 'warning':
+ iconShape = 'exclaim-circle';
+ iconColor = 'status-yellow';
+ break;
+ case 'success':
+ iconShape = 'check-circle';
+ iconColor = 'status-green';
+ break;
+ }
+ return {shape: iconShape, color: iconColor};
+ },
+ isDisrupted() {
+ return this.timeout !== null && this.stopTimeout;
+ }
+ },
+ methods: {
+ destroyMe() {
+ this.$emit('destroyMe');
+ },
+ disruptTimeout() {
+ this.stopTimeout = true;
+ clearTimeout(this.timeout);
+ },
+ initTimeout() {
+ if (this.hasTimeout && this.visibleFor > 0) {
+ this.stopTimeout = false;
+ this.timeout = setTimeout(
+ () => this.destroyMe(),
+ this.visibleFor
+ );
+ }
+ }
+ },
+ mounted() {
+ if (this.appendTo !== null) {
+ const target = document.querySelector(this.appendTo);
+
+ // Create a live area for screen reader compatibility.
+ const div = document.createElement('div');
+ div.setAttribute('role', 'alert');
+ div.appendChild(this.$el);
+ if (target) {
+ target.prepend(div);
+ }
+ }
+
+ this.initTimeout();
+
+ this.globalOn('disrupt-system-notifications', this.disruptTimeout);
+ this.globalOn('resume-system-notifications', this.initTimeout);
+
+ if (!STUDIP.config?.PERSONAL_NOTIFICATIONS_AUDIO_DEACTIVATED) {
+ const audio = new Audio(STUDIP.ASSETS_URL + '/sounds/blubb.mp3');
+ audio.play();
+ }
+ },
+ destroyed() {
+ this.globalOff('disrupt-system-notifications', this.disruptTimeout);
+ this.globalOff('resume-system-notifications', this.initTimeout);
+ }
+}
+</script>
+<style scoped>
+[v-cloak] {
+ display: none;
+}
+</style>
diff --git a/resources/vue/components/SystemNotificationManager.vue b/resources/vue/components/SystemNotificationManager.vue
new file mode 100644
index 0000000..0acef24
--- /dev/null
+++ b/resources/vue/components/SystemNotificationManager.vue
@@ -0,0 +1,73 @@
+<template>
+ <transition-group name="system-notification-slide"
+ :class="'system-notifications ' + (placement === 'topcenter' ? 'top-center' : 'bottom-right')"
+ tag="div"
+ role="alert"
+ appear
+ >
+ <system-notification v-for="notification in allNotifications"
+ :key="`message-${notification.key}`"
+ :notification="notification"
+ @destroyMe="destroyNotification(notification)"
+ ></system-notification>
+ </transition-group>
+</template>
+
+<script>
+import SystemNotification from './SystemNotification.vue';
+
+export default {
+ name: 'SystemNotificationManager',
+ components: { SystemNotification },
+ props: {
+ appendAllTo: String,
+ notifications: {
+ type: [Array, Object],
+ default: () => []
+ },
+ placement: {
+ type: String,
+ default: 'topcenter',
+ validator: value => {
+ return ['topcenter', 'bottomright'].includes(value);
+ }
+ }
+ },
+ data() {
+ return {
+ allNotifications: [],
+ counter: 0,
+ stoppedNotifications: false
+ }
+ },
+ methods: {
+ destroyNotification(notification) {
+ this.allNotifications = this.allNotifications.filter(n => n !== notification);
+ }
+ },
+ created() {
+ if (Array.isArray(this.notifications)) {
+ this.allNotifications = [...this.notifications];
+ } else {
+ this.allNotifications = Object.values(this.notifications);
+ }
+ },
+ mounted() {
+ this.globalOn('push-system-notification', notification => {
+ this.allNotifications.push({
+ key: this.counter++,
+ ...notification
+ });
+ });
+
+ window.addEventListener('keydown', evt => {
+ if (evt.altKey && evt.ctrlKey && evt.code === 'KeyT') {
+ this.stoppedNotifications = !this.stoppedNotifications;
+
+ const event = this.stoppedNotifications ? 'disrupt-system-notifications' : 'resume-system-notifications';
+ this.globalEmit(event);
+ }
+ });
+ }
+}
+</script>
diff --git a/resources/vue/components/Timepicker.vue b/resources/vue/components/Timepicker.vue
new file mode 100644
index 0000000..e0b0feb
--- /dev/null
+++ b/resources/vue/components/Timepicker.vue
@@ -0,0 +1,37 @@
+<template>
+ <input type="time"
+ ref="visibleInput"
+ class="hasTimepicker"
+ v-model="timeValue"
+ :placeholder="placeholder"
+ :min="mintime"
+ :max="maxtime"
+ :name="name">
+</template>
+
+<script>
+export default {
+ name: 'Timepicker',
+ inheritAttrs: false,
+ props: {
+ name: {
+ type: String,
+ required: false
+ },
+ value: String,
+ mintime: String,
+ maxtime: String,
+ placeholder: String,
+ },
+ computed: {
+ timeValue: {
+ get() {
+ return this.value;
+ },
+ set(value) {
+ this.$emit('input', value);
+ }
+ }
+ }
+}
+</script>
diff --git a/resources/vue/components/blubber/Composer.vue b/resources/vue/components/blubber/Composer.vue
index f4b6aff..72a5d1c 100644
--- a/resources/vue/components/blubber/Composer.vue
+++ b/resources/vue/components/blubber/Composer.vue
@@ -1,6 +1,9 @@
<template>
<div class="writer" :style="composerStyle">
<studip-icon shape="blubber" :size="30" role="info"></studip-icon>
+ <label for="blubber-placeholder" class="sr-only">
+ {{ placeholder || $gettext('Schreib was, frag was. Enter zum Abschicken.') }}
+ </label>
<textarea
:placeholder="placeholder || $gettext('Schreib was, frag was. Enter zum Abschicken.')"
v-model="localText"
@@ -10,6 +13,7 @@
@keyup.up.exact="editPreviousComment"
@keyup="saveCommentToSession"
ref="textarea"
+ id="blubber-placeholder"
></textarea>
<a class="send" @click="submit" :title="$gettext('Abschicken')">
<studip-icon shape="arr_2up" :size="30"></studip-icon>
diff --git a/resources/vue/components/courseware/CoursewareContentPermissions.vue b/resources/vue/components/courseware/CoursewareContentPermissions.vue
index d2412ef..60f0964 100644
--- a/resources/vue/components/courseware/CoursewareContentPermissions.vue
+++ b/resources/vue/components/courseware/CoursewareContentPermissions.vue
@@ -42,7 +42,7 @@
<td class="perm">
<input
class="right"
- :title="$gettextInterpolate($gettext('Leserechte für %{ userName }'), { userName: user_perm.username })"
+ :title="$gettextInterpolate($gettext('Leserechte für %{ userName }'), { userName: user_perm.username }, true)"
type="radio"
:name="`${user_perm.id}_right`"
value="read"
@@ -53,7 +53,7 @@
<td class="perm">
<input
class="right"
- :title="$gettextInterpolate($gettext('Lese- und Schreibrechte für %{ userName }'), { userName: user_perm.username })"
+ :title="$gettextInterpolate($gettext('Lese- und Schreibrechte für %{ userName }'), { userName: user_perm.username }, true)"
type="radio"
:name="`${user_perm.id}_right`"
value="write"
@@ -75,7 +75,7 @@
<td class="actions">
<button
class="cw-permission-delete"
- :title="$gettextInterpolate($gettext('Entfernen der Rechte von %{ userName }'), { userName: user_perm.username })"
+ :title="$gettextInterpolate($gettext('Entfernen der Rechte von %{ userName }'), { userName: user_perm.username }, true)"
@click.prevent="confirmDeleteUserPerm(index)"
>
</button>
diff --git a/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue
index c366618..98fab71 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue
@@ -17,8 +17,8 @@
class="cw-timeline-item"
>
<div class="cw-timeline-item-icon cw-timeline-item-icon-color-studip-blue">
- <studip-icon v-if="item.type === 'school'" shape="doctoral-cap" role="clickable" size="32"/>
- <studip-icon v-if="item.type === 'experience'" shape="tools" role="clickable" size="32"/>
+ <studip-icon v-if="item.type === 'school'" shape="doctoral-cap" role="clickable" :size="32"/>
+ <studip-icon v-if="item.type === 'experience'" shape="tools" role="clickable" :size="32"/>
</div>
<div
class="cw-timeline-item-content cw-timeline-item-content-color-studip-blue"
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
index 89beb0a..eea76fe 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
@@ -3,6 +3,7 @@
<studip-action-menu
:items="menuItems"
:context="block.attributes.title"
+ :collapseAt="1"
@editBlock="editBlock"
@setVisibility="setVisibility"
@showInfo="showInfo"
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue b/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue
index d1ed09f..82d1abc 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue
@@ -2,7 +2,7 @@
<section class="cw-block-edit">
<header v-if="preview">{{ $gettext('Bearbeiten') }}</header>
<div class="cw-block-features-content">
- <div @click="deactivateToolbar(); exitHandler = true;">
+ <div @click="exitHandler = true;">
<slot name="edit" />
</div>
<div class="cw-button-box">
@@ -31,16 +31,6 @@ export default {
beforeMount() {
this.originalBlock = this.block;
},
- methods: {
- ...mapActions({
- coursewareBlockAdder: 'coursewareBlockAdder',
- coursewareShowToolbar: 'coursewareShowToolbar'
- }),
- deactivateToolbar() {
- this.coursewareBlockAdder({});
- this.coursewareShowToolbar(false);
- },
- },
beforeDestroy() {
if (this.exitHandler) {
this.$emit('store');
diff --git a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
index 669a5b8..24a59f5 100644
--- a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
@@ -127,7 +127,7 @@
<studip-file-chooser
v-model="currentFileId"
selectable="file"
- :courseId="studipContext.id"
+ :courseId="context.id"
:userId="userId"
:isImage="true"
:excludedCourseFolderTypes="excludedCourseFolderTypes"
@@ -187,7 +187,7 @@ export default {
currentUserView: 'own',
currentFile: {},
- context: {},
+ canvasContext: {},
paint: false,
write: false,
clickX: [],
@@ -221,7 +221,6 @@ export default {
},
computed: {
...mapGetters({
- studipContext: 'context',
fileRefById: 'file-refs/byId',
getUserDataById: 'courseware-user-data-fields/byId',
relatedUserData: 'user-data-field/related',
@@ -301,9 +300,7 @@ export default {
return this.currentUploadFolderId !== "";
},
canSwitchView() {
- // this feature is not something to offer in the Arbeitsplatz!
- let context = this.$store.getters.context;
- if (context.type !== 'courses') {
+ if (this.context.type !== 'courses') {
return false;
}
if (this.currentShowUserData === 'off') {
@@ -419,7 +416,7 @@ export default {
} else {
canvas.height = 500;
}
- this.context = canvas.getContext('2d');
+ this.canvasContext = canvas.getContext('2d');
this.setColor('blue');
this.currentSize = this.sizes['normal'];
this.currentTool = this.tools['pen'];
@@ -427,7 +424,7 @@ export default {
},
redraw() {
let view = this;
- let context = view.context;
+ let context = view.canvasContext;
context.clearRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas
context.fillStyle = '#ffffff';
context.fillRect(0, 0, context.canvas.width, context.canvas.height); // set background
@@ -658,7 +655,7 @@ export default {
},
async store() {
let user = this.usersById({id: this.userId});
- let imageBase64 = this.context.canvas.toDataURL("image/jpeg", 1.0);
+ let imageBase64 = this.canvasContext.canvas.toDataURL("image/jpeg", 1.0);
let image = await fetch(imageBase64);
let imageBlob = await image.blob();
let file = {};
diff --git a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
index 72cfeb3..55e7b01 100644
--- a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
@@ -131,8 +131,8 @@
</button>
<select v-model="currentScale" :aria-label="$gettext('Zoom')" @change="updateZoom">
<option v-show="false" :value="currentScale">{{ formattedZoom }}%</option>
- <option v-for="(value, index) in scaleValues" :key="index" :value="value">
- {{ value * 100 }}%
+ <option v-for="(value, index) in scaleValues" :key="index" :value="value.scale">
+ {{ value.name }}
</option>
</select>
</div>
@@ -305,7 +305,7 @@ export default {
pdfAnnotationLayer: null,
pdfAnnotation: false,
pdfRotate: 0,
- PdfViewer: null,
+ pdfViewer: null,
pdfEventBus: null,
pdfLinkService: null,
pdfFindController: null,
@@ -322,8 +322,8 @@ export default {
pageNum: 1,
pageCount: 0,
scale: 1,
+ baseScale: 1,
currentScale: 1,
- scaleValues: [0.5, 1, 1.5, 2, 3, 4],
file: null,
srMessage: '',
@@ -361,10 +361,23 @@ export default {
formattedZoom() {
return Number.parseInt(this.scale * 100, 10);
},
+ scaleValues() {
+ const defaultValues = [
+ { name: '25%', scale: 0.25 },
+ { name: '50%', scale: 0.5 },
+ { name: '75%', scale: 0.75 },
+ { name: '100%', scale: 1.0 },
+ { name: '150%', scale: 1.5 },
+ { name: '200%', scale: 2.0 },
+ { name: '300%', scale: 3.0 },
+ ];
+
+ return defaultValues.concat([{ name: this.$gettext('volle Breite'), scale: this.baseScale }]);
+ },
},
watch: {
scale(newValue) {
- let overflow = newValue > 1 ? 'auto' : 'hidden';
+ let overflow = newValue > this.baseScale ? 'auto' : 'hidden';
let container = this.$refs.container;
container.style.overflow = overflow;
this.currentScale = newValue;
@@ -407,7 +420,7 @@ export default {
if (this.currentUrl) {
let view = this;
view.pdfEventBus = new EventBus();
- view.pdfLoadingTask = getDocument(this.currentUrl).promise;
+ view.pdfLoadingTask = getDocument({ url: this.currentUrl, verbosity: 0 }).promise;
view.pdfLoadingTask.__PDFDocumentLoadingTask = true;
// Link Service
view.pdfLinkService = new PDFLinkService({
@@ -473,9 +486,16 @@ export default {
.getPage(parseInt(view.pageNum))
.then((pdfPage) => {
view.pdfPage = pdfPage;
+ const width = outerContainer.offsetWidth;
+ let pdfWidth = pdfPage.view[2];
+ if (pdfPage.rotate === 90 || pdfPage.rotate === 270) {
+ pdfWidth = pdfPage.view[3];
+ }
+ view.baseScale = (width / pdfWidth / 1.33).toFixed(2);
+ view.scale = view.baseScale;
// Creating the page view with default parameters.
let defaultViewport = pdfPage.getViewport({
- scale: 1.35,
+ scale: 1.0,
});
view.pdfBasePage = new PDFViewer({
@@ -512,8 +532,6 @@ export default {
view.pdfViewer.setPdfPage(view.pdfPage);
// Set LinkService viewer
view.pdfLinkService.setViewer(view.pdfViewer);
- // Set outer container height
- outerContainer.style.height = container.offsetHeight + 'px';
view.renderPage();
})
.catch((err) => {
@@ -610,12 +628,12 @@ export default {
this.updateSrMessage(this.$gettext('gedreht'));
},
zoomIn() {
- this.scale = this.scale < 4 ? (this.scale * 10 + 1) / 10 : this.scale;
+ this.scale = this.scale < 4 ? ((this.scale * 10 + 1) / 10).toFixed(1) : this.scale;
this.renderPage();
this.updateSrMessage(this.$gettext('vergrößert'));
},
zoomOut() {
- this.scale = this.scale > 0.1 ? (this.scale * 10 - 1) / 10 : this.scale;
+ this.scale = this.scale > 0.1 ? ((this.scale * 10 - 1) / 10).toFixed(1) : this.scale;
this.renderPage();
this.updateSrMessage(this.$gettext('verkleinert'));
},
diff --git a/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
index be56ab4..4ff7e45 100644
--- a/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
@@ -234,17 +234,16 @@ export default {
this.currentUrlIsValid = this.isValidUrl(this.currentUrl);
},
isValidUrl(urlString) {
- const urlPattern = new RegExp(
- '^(https?:\\/\\/)?' + // validate protocol
- '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name
- '((\\d{1,3}\\.){3}\\d{1,3}))' + // validate OR ip (v4) address
- '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path
- '(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string
- '(\\#[-a-z\\d_]*)?$',
- 'i'
- ); // validate fragment locator
+ if (!urlString.startsWith('http')) {
+ urlString = `${location.protocol}//${urlString}`;
+ }
- return !!urlPattern.test(urlString);
+ try {
+ const url = new URL(urlString);
+ return ['http:', 'https:'].includes(url.protocol);
+ } catch (e) {
+ return false;
+ }
},
updateUrl() {
diff --git a/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
index 6d12980..4f52d4b 100644
--- a/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
@@ -113,7 +113,7 @@
>
<template #open-indicator="selectAttributes">
<span v-bind="selectAttributes"
- ><studip-icon shape="arr_1down" size="10"
+ ><studip-icon shape="arr_1down" :size="10"
/></span>
</template>
<template #no-options>
@@ -141,7 +141,7 @@
>
<template #open-indicator="selectAttributes">
<span v-bind="selectAttributes"
- ><studip-icon shape="arr_1down" size="10"
+ ><studip-icon shape="arr_1down" :size="10"
/></span>
</template>
<template #no-options>
diff --git a/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue b/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue
index a410740..e52b608 100644
--- a/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue
@@ -38,7 +38,7 @@
v-model="currentColor"
>
<template #open-indicator="selectAttributes">
- <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10" /></span>
+ <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span>
</template>
<template #no-options>
{{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
@@ -57,7 +57,7 @@
{{ $gettext('Icon') }}
<studip-select :options="icons" :clearable="false" v-model="currentIcon">
<template #open-indicator="selectAttributes">
- <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10" /></span>
+ <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span>
</template>
<template #no-options>
{{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
diff --git a/resources/vue/components/courseware/containers/CoursewareContainerActions.vue b/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
index 79379bf..875ab1f 100644
--- a/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
+++ b/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
@@ -3,6 +3,7 @@
<studip-action-menu
:items="menuItems"
:context="container.attributes.title"
+ :collapseAt="1"
@editContainer="editContainer"
@changeContainer="changeContainer"
@deleteContainer="deleteContainer"
diff --git a/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue b/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue
index f617e5a..f26c8d7 100644
--- a/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue
@@ -1,15 +1,9 @@
-<template>
- <div class="cw-companion-box" :class="[mood]">
- <div>
- <p v-html="msgCompanion"></p>
- <slot name="companionActions"></slot>
- </div>
- </div>
-</template>
-
<script>
export default {
name: 'courseware-companion-box',
+ render(createElement) {
+ return null;
+ },
props: {
msgCompanion: String,
mood: {
@@ -20,5 +14,40 @@ export default {
}
}
},
+ computed: {
+ msgType() {
+ let type = 'info';
+ switch (this.mood) {
+ case 'special':
+ case 'unsure':
+ type = 'warning';
+ break;
+ case 'sad':
+ type = 'error';
+ break;
+ case 'happy':
+ type = 'success';
+ break
+ case 'pointing':
+ case 'curious':
+ }
+ return type;
+ }
+ },
+ watch: {
+ msgCompanion: {
+ handler(current) {
+ if (current.trim().length === 0) {
+ return;
+ }
+ const notification = {
+ type: this.msgType,
+ message: current
+ };
+ this.globalEmit('push-system-notification', notification);
+ },
+ immediate: true
+ }
+ }
};
-</script> \ No newline at end of file
+</script>
diff --git a/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue b/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue
index ff177f7..bfd0a93 100644
--- a/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue
@@ -1,28 +1,11 @@
-<template>
- <div class="cw-companion-overlay-wrapper">
- <div
- class="cw-companion-overlay"
- :class="[showCompanion ? 'cw-companion-overlay-in' : '', showCompanion ? '' : 'cw-companion-overlay-out', styleCompanion]"
- aria-hidden="true"
- >
- <div class="cw-companion-overlay-content" v-html="msgCompanion"></div>
- <button class="cw-compantion-overlay-close" @click="hideCompanion"></button>
- </div>
- <div
- class="sr-only"
- aria-live="polite"
- role="log"
- >
- <p>{{ msgCompanion }}</p>
- </div>
- </div>
-</template>
-
<script>
import { mapActions, mapGetters } from 'vuex';
export default {
name: 'courseware-companion-overlay',
+ render(createElement) {
+ return null;
+ },
computed: {
...mapGetters({
showCompanion: 'showCompanionOverlay',
@@ -30,6 +13,24 @@ export default {
styleCompanion: 'styleCompanionOverlay',
showToolbar: 'showToolbar',
}),
+ msgType() {
+ let type = 'info';
+ switch (this.styleCompanion) {
+ case 'special':
+ case 'unsure':
+ type = 'warning';
+ break;
+ case 'sad':
+ type = 'error';
+ break;
+ case 'happy':
+ type = 'success';
+ break
+ case 'pointing':
+ case 'curious':
+ }
+ return type;
+ }
},
methods: {
...mapActions({
@@ -49,11 +50,24 @@ export default {
}
},
showToolbar(newValue, oldValue) {
- // hide companion when toolbar is closed
+ // hide companion when toolbar is closed
if (oldValue === true && newValue === false) {
this.hideCompanion();
}
+ },
+ msgCompanion: {
+ handler(current) {
+ if (current.trim().length === 0) {
+ return;
+ }
+ const notification = {
+ type: this.msgType,
+ message: current
+ };
+ this.globalEmit('push-system-notification', notification);
+ },
+ immediate: true
}
- },
+ }
};
</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue b/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue
index 1a739a6..1aa8802 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue
@@ -16,21 +16,21 @@
</div>
<div v-else class="cw-root-content-wrapper">
<div class="cw-root-content" :class="['cw-root-content-' + rootLayout]">
- <div class="cw-root-content-img" :style="image">
+ <div class="cw-root-content-img" :style="bgImage">
<section class="cw-root-content-description" :style="bgColor">
- <img v-if="imageIsSet" class="cw-root-content-description-img" :src="imageURL" />
- <template v-else>
+ <div class="cw-root-content-description-img" :src="imageURL" :style="image"></div>
+ <template v-if="!imageIsSet">
<studip-ident-image
class="cw-root-content-description-img"
- v-model="identImageCanvas"
- :showCanvas="true"
+ v-model="identImage"
:baseColor="bgColorHex"
:pattern="structuralElement.attributes.title"
/>
<studip-ident-image
- v-model="identImage"
- :width="1095"
- :height="withTOC ? 300 : 480"
+ v-model="identBgImage"
+ class="cw-root-content-description-background-img"
+ :width="4380"
+ :height="withTOC ? 1200 : 1920"
:baseColor="bgColorHex"
:pattern="structuralElement.attributes.title"
/>
@@ -46,44 +46,28 @@
</div>
<div v-if="withTOC" class="cw-root-content-toc">
<ul class="cw-tiles">
- <li
- v-for="child in childElements"
- :key="child.id"
- class="tile"
- :class="[child.attributes.payload.color]"
- >
+ <li v-for="child in childElements" :key="child.id">
<router-link :to="'/structural_element/' + child.id" :title="child.attributes.title">
- <div
- v-if="hasImage(child)"
- class="preview-image"
- :style="getChildStyle(child)"
- ></div>
- <studip-ident-image
- v-else
- :baseColor="getColor(child).hex"
- :pattern="child.attributes.title"
- :showCanvas="true"
- />
- <div class="description">
- <header
- :class="[
- child.attributes.purpose !== ''
- ? 'description-icon-' + child.attributes.purpose
- : '',
- ]"
- >
- {{ child.attributes.title || '–' }}
- </header>
- <div class="description-text-wrapper">
- <p>{{ child.attributes.payload.description }}</p>
- </div>
- <footer>
- {{ countChildChildren(child) }}
- <translate :translate-n="countChildChildren(child)" translate-plural="Seiten">
- Seite
- </translate>
- </footer>
- </div>
+ <courseware-tile
+ tag="div"
+ :color="child.attributes.payload.color"
+ :title="child.attributes.title || '–'"
+ :imageUrl="hasImage(child) ? child.relationships?.image?.meta?.['download-url'] : ''"
+ >
+ <template #description>
+ {{ child.attributes.payload.description }}
+ </template>
+ <template #footer>
+ <p class="cw-root-content-toc-tile-footer">
+ {{
+ $gettextInterpolate(
+ $ngettext('%{pages} Seite', '%{pages} Seiten', countChildChildren(child)),
+ { pages: countChildChildren(child) }
+ )
+ }}
+ </p>
+ </template>
+ </courseware-tile>
</router-link>
</li>
</ul>
@@ -93,6 +77,7 @@
<script>
import CoursewareCompanionBox from './../layouts/CoursewareCompanionBox.vue';
+import CoursewareTile from './../layouts/CoursewareTile.vue';
import StudipIdentImage from './../../StudipIdentImage.vue';
import colorMixin from '@/vue/mixins/courseware/colors.js';
import { mapActions, mapGetters } from 'vuex';
@@ -100,18 +85,19 @@ import { mapActions, mapGetters } from 'vuex';
export default {
name: 'courseware-root-content',
mixins: [colorMixin],
- props: {
- structuralElement: Object,
- canEdit: Boolean,
- },
components: {
CoursewareCompanionBox,
+ CoursewareTile,
StudipIdentImage,
},
+ props: {
+ structuralElement: Object,
+ canEdit: Boolean,
+ },
data() {
return {
identImage: '',
- identImageCanvas: '',
+ identBgImage: '',
};
},
computed: {
@@ -130,6 +116,13 @@ export default {
image() {
let style = {};
const backgroundURL = this.imageIsSet ? this.imageURL : this.identImage;
+ style.backgroundImage = 'url(' + backgroundURL + ')';
+
+ return style;
+ },
+ bgImage() {
+ let style = {};
+ const backgroundURL = this.imageIsSet ? this.imageURL : this.identBgImage;
style.backgroundImage = 'url(' + backgroundURL + ')';
style.height = this.withTOC ? '300px' : '480px';
@@ -180,7 +173,7 @@ export default {
},
addPage() {
this.showElementAddDialog(true);
- }
+ },
},
};
</script>
@@ -196,16 +189,20 @@ export default {
}
.cw-root-content-description {
display: flex;
- flex-direction: row;
- margin: 0 8em;
- padding: 2em 4px 2em 2em;
position: relative;
- top: 8em;
+ flex-direction: column;
+ margin: 0 1em;
+ padding: 1em 4px 1em 1em;
+ top: 1em;
+ gap: 10px;
.cw-root-content-description-img {
- width: 240px;
- height: fit-content;
- margin-right: 2em;
+ min-width: 135px;
+ height: 90px;
+ background-color: #fff;
+ background-size: cover;
+ background-position: center;
+ margin-right: 1em;
}
.cw-root-content-description-text {
max-height: calc(480px - 18em);
@@ -233,14 +230,68 @@ export default {
max-width: 1095px;
margin-bottom: 1em;
.cw-root-content-description {
- margin: 0 8em;
- top: 1.5em;
+ height: calc(100% - 4em);
.cw-root-content-description-text {
max-height: calc(300px - 6em);
}
}
+ .cw-root-content-toc-tile-footer {
+ line-height: 4em;
+ }
}
.cw-root-content-hint {
max-width: 1095px;
}
+
+.size-small {
+ .cw-root-content-description {
+ flex-direction: row;
+ padding: 1em 4px 1em 1em;
+
+ .cw-root-content-description-img {
+ min-width: 135px;
+ height: 90px;
+ }
+ }
+
+ .cw-root-content-default {
+ .cw-root-content-description {
+ margin: 0 4em;
+ top: 8em;
+ }
+ }
+ .cw-root-content-toc {
+ .cw-root-content-description {
+ height: calc(100% - 6em);
+ margin: 0 4em;
+ top: 1.5em;
+ }
+ }
+}
+
+.size-large {
+ .cw-root-content-description {
+ flex-direction: row;
+ padding: 2em 4px 2em 2em;
+
+ .cw-root-content-description-img {
+ min-width: 270px;
+ height: 180px;
+ }
+ }
+
+ .cw-root-content-default {
+ .cw-root-content-description {
+ margin: 0 8em;
+ top: 8em;
+ }
+ }
+ .cw-root-content-toc {
+ .cw-root-content-description {
+ height: calc(100% - 7em);
+ margin: 0 8em;
+ top: 1.5em;
+ }
+ }
+}
</style>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue
index 60b404e..019bbf2 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue
@@ -130,7 +130,6 @@ export default {
this.importZipFile = event.target.files[0];
this.setImportFilesProgress(0);
this.setImportStructuresProgress(0);
- this.setImportErrors([]);
},
async importCoursewareArchiv() {
this.importAborted = false;
@@ -199,6 +198,9 @@ export default {
await this.importCourseware(courseware, this.currentElement, files, this.importBehavior, null);
}
+ },
+ mounted() {
+ this.setImportErrors([]);
}
}
</script>
diff --git a/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue b/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue
index 1e076c7..7680167 100644
--- a/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue
+++ b/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue
@@ -179,7 +179,7 @@
:aria-label="
$gettextInterpolate($gettext('%{userName} auswählen'), {
userName: user.formattedname,
- })
+ }, true)
"
/>
</td>
@@ -217,7 +217,7 @@
:aria-label="
$gettextInterpolate($gettext('%{groupName} auswählen'), {
groupName: group.name,
- })
+ }, true)
"
/>
</td>
diff --git a/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue b/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue
index a843341..d7db6e1 100644
--- a/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue
+++ b/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue
@@ -44,7 +44,7 @@
:aria-label="
$gettextInterpolate($gettext('%{userName} auswählen'), {
userName: user.formattedname,
- })
+ }, true)
"
/>
</td>
@@ -77,7 +77,7 @@
:aria-label="
$gettextInterpolate($gettext('%{groupName} auswählen'), {
groupName: group.name,
- })
+ }, true)
"
/>
</td>
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue
index d31e963..dc70348 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue
@@ -2,7 +2,7 @@
<div class="cw-toolbar-wrapper">
<div id="cw-toolbar" class="cw-toolbar" :style="toolbarStyle">
<div v-if="showTools" class="cw-toolbar-tools" :class="{ unfold: unfold, hd: isHd, wqhd: isWqhd }">
- <div class="cw-toolbar-button-wrapper">
+ <div id="cw-toolbar-nav" class="cw-toolbar-button-wrapper">
<button
class="cw-toolbar-button"
:class="{ active: activeTool === 'blockAdder' }"
@@ -35,9 +35,19 @@
<studip-icon shape="arr_2right" :size="24" />
</button>
</div>
- <courseware-toolbar-blocks v-if="activeTool === 'blockAdder'" />
- <courseware-toolbar-containers v-if="activeTool === 'containerAdder'" />
- <courseware-toolbar-clipboard v-if="activeTool === 'clipboard'" />
+ <div class="cw-toolbar-tool-wrapper">
+ <CoursewareToolbarBlocks
+ v-if="activeTool === 'blockAdder'"
+ :toolbarContentHeight="toolbarContentHeight"
+ />
+ <CoursewareToolbarContainers
+ v-if="activeTool === 'containerAdder'"
+ />
+ <CoursewareToolbarClipboard
+ v-if="activeTool === 'clipboard'"
+ :toolbarContentHeight="toolbarContentHeight"
+ />
+ </div>
</div>
<div v-else class="cw-toolbar-folded-wrapper">
<button
@@ -97,20 +107,26 @@ export default {
toolbarActive: 'toolbarActive',
hideEditLayout: 'hideEditLayout',
}),
- toolbarStyle() {
- const scrollTopStyles = window.getComputedStyle(document.getElementById('scroll-to-top'));
+ scrollTopStyles() {
+ return window.getComputedStyle(document.getElementById('scroll-to-top'));
+ },
+ toolbarHeight() {
const scrollTopHeight =
- parseInt(scrollTopStyles['height'], 10) +
- parseInt(scrollTopStyles['padding-top'], 10) +
- parseInt(scrollTopStyles['padding-bottom'], 10) +
- parseInt(scrollTopStyles['margin-bottom'], 10);
- let height = parseInt(
+ parseInt(this.scrollTopStyles['height'], 10) +
+ parseInt(this.scrollTopStyles['padding-top'], 10) +
+ parseInt(this.scrollTopStyles['padding-bottom'], 10) +
+ parseInt(this.scrollTopStyles['margin-bottom'], 10);
+ return parseInt(
Math.min(this.windowInnerHeight * 0.9, this.windowInnerHeight - this.toolbarTop - scrollTopHeight)
);
-
+ },
+ toolbarContentHeight() {
+ return this.toolbarHeight - 55;
+ },
+ toolbarStyle() {
return {
- height: height + 'px',
- minHeight: height + 'px',
+ height: this.toolbarHeight + 'px',
+ minHeight: this.toolbarHeight + 'px',
top: this.toolbarTop + 'px',
};
},
@@ -183,8 +199,8 @@ export default {
},
watch: {
- containers(oldValue, newValue) {
- if (oldValue && newValue && oldValue.length !== newValue.length) {
+ containers(newValue, oldValue) {
+ if (newValue) {
this.resetAdderStorage();
}
},
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
index 2795e62..ceda0f0 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
@@ -1,85 +1,88 @@
<template>
<div class="cw-toolbar-blocks">
- <form @submit.prevent="loadSearch">
- <div class="input-group files-search search cw-block-search">
- <input
- ref="searchBox"
- type="text"
- v-model="searchInput"
- @click.stop
- :label="$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')"
- />
- <span class="input-group-append" @click.stop>
- <button
- v-if="searchInput"
- type="button"
- class="button reset-search"
- id="reset-search"
- :title="$gettext('Suche zurücksetzen')"
- @click="resetSearch"
- >
- <studip-icon shape="decline" :size="20"></studip-icon>
- </button>
- <button
- type="submit"
- class="button"
- id="search-btn"
- :title="$gettext('Suche starten')"
- @click="loadSearch"
- >
- <studip-icon shape="search" :size="20"></studip-icon>
- </button>
- </span>
- </div>
- </form>
+ <div id="cw-toolbar-blocks-header" class="cw-toolbar-tool-header">
+ <form @submit.prevent="loadSearch">
+ <div class="input-group files-search search cw-block-search">
+ <input
+ ref="searchBox"
+ type="text"
+ v-model="searchInput"
+ @click.stop
+ :label="$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')"
+ />
+ <span class="input-group-append" @click.stop>
+ <button
+ v-if="searchInput"
+ type="button"
+ class="button reset-search"
+ id="reset-search"
+ :title="$gettext('Suche zurücksetzen')"
+ @click="resetSearch"
+ >
+ <studip-icon shape="decline" :size="20"></studip-icon>
+ </button>
+ <button
+ type="submit"
+ class="button"
+ id="search-btn"
+ :title="$gettext('Suche starten')"
+ @click="loadSearch"
+ >
+ <studip-icon shape="search" :size="20"></studip-icon>
+ </button>
+ </span>
+ </div>
+ </form>
- <div class="filterpanel">
- <span class="sr-only">{{ $gettext('Kategorien-Filter') }}</span>
- <button
- v-for="category in blockCategories"
- :key="category.type"
- class="button"
- :class="{ 'button-active': category.type === currentFilterCategory }"
- :aria-pressed="category.type === currentFilterCategory ? 'true' : 'false'"
- @click="selectCategory(category.type)"
- >
- {{ category.title }}
- </button>
+ <div class="filterpanel">
+ <span class="sr-only">{{ $gettext('Kategorien-Filter') }}</span>
+ <button
+ v-for="category in blockCategories"
+ :key="category.type"
+ class="button"
+ :class="{ 'button-active': category.type === currentFilterCategory }"
+ :aria-pressed="category.type === currentFilterCategory ? 'true' : 'false'"
+ @click="selectCategory(category.type)"
+ >
+ {{ category.title }}
+ </button>
+ </div>
</div>
-
- <div v-if="filteredBlockTypes.length > 0" class="cw-blockadder-item-list">
- <draggable
- v-if="filteredBlockTypes.length > 0"
- class="cw-blockadder-item-list"
- tag="div"
- role="listbox"
- v-model="filteredBlockTypes"
- handle=".cw-sortable-handle-blockadder"
- :group="{ name: 'blocks', pull: 'clone', put: 'false' }"
- :clone="cloneBlock"
- :sort="false"
- :emptyInsertThreshold="20"
- @start="dragBlockStart($event)"
- @end="dropNewBlock($event)"
- ref="sortables"
- sectionId="0"
- >
- <courseware-blockadder-item
- v-for="(block, index) in filteredBlockTypes"
- :key="index"
- :title="block.title"
- :type="block.type"
- :data-blocktype="block.type"
- :description="block.description"
- @blockAdded="$emit('blockAdded')"
- />
- </draggable>
+ <div class="cw-toolbar-tool-content" :style="toolContentStyle">
+ <div v-if="filteredBlockTypes.length > 0" class="cw-blockadder-item-list">
+ <draggable
+ v-if="filteredBlockTypes.length > 0"
+ class="cw-blockadder-item-list"
+ tag="div"
+ role="listbox"
+ v-model="filteredBlockTypes"
+ handle=".cw-sortable-handle-blockadder"
+ :group="{ name: 'blocks', pull: 'clone', put: 'false' }"
+ :clone="cloneBlock"
+ :sort="false"
+ :emptyInsertThreshold="20"
+ @start="dragBlockStart($event)"
+ @end="dropNewBlock($event)"
+ ref="sortables"
+ sectionId="0"
+ >
+ <courseware-blockadder-item
+ v-for="(block, index) in filteredBlockTypes"
+ :key="index"
+ :title="block.title"
+ :type="block.type"
+ :data-blocktype="block.type"
+ :description="block.description"
+ @blockAdded="$emit('blockAdded')"
+ />
+ </draggable>
+ </div>
+ <courseware-companion-box
+ v-else
+ :msgCompanion="$gettext('Es wurden keine passenden Blöcke gefunden.')"
+ mood="pointing"
+ />
</div>
- <courseware-companion-box
- v-else
- :msgCompanion="$gettext('Es wurden keine passenden Blöcke gefunden.')"
- mood="pointing"
- />
</div>
</template>
@@ -99,6 +102,12 @@ export default {
CoursewareCompanionBox,
draggable,
},
+ props: {
+ toolbarContentHeight: {
+ type: Number,
+ required: true,
+ },
+ },
data() {
return {
searchInput: '',
@@ -132,6 +141,13 @@ export default {
{ title: this.$gettext('Biografie'), type: 'biography' },
];
},
+ toolContentStyle() {
+ const height = this.toolbarContentHeight - 115;
+
+ return {
+ height: height + 'px',
+ };
+ },
},
methods: {
...mapActions({
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
index 98cd573..c6f1e92 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
@@ -1,5 +1,5 @@
<template>
- <div class="cw-toolbar-clipboard">
+ <div class="cw-toolbar-clipboard cw-toolbar-tool-content" :style="toolContentStyle">
<courseware-collapsible-box :title="$gettext('Blöcke')" :open="clipboardBlocks.length > 0">
<template v-if="clipboardBlocks.length > 0">
<div class="cw-element-inserter-wrapper">
@@ -101,7 +101,12 @@ export default {
StudipDialog,
draggable,
},
-
+ props: {
+ toolbarContentHeight: {
+ type: Number,
+ required: true,
+ },
+ },
data() {
return {
showDeleteClipboardDialog: false,
@@ -143,6 +148,11 @@ export default {
}
return '';
},
+ toolContentStyle() {
+ return {
+ height: this.toolbarContentHeight + 'px',
+ };
+ },
},
methods: {
...mapActions({
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue
index f09601e..dc68225 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue
@@ -63,7 +63,7 @@
:question="
$gettextInterpolate($gettext('Möchten Sie das Lernmaterial %{ unitTitle } wirklich löschen?'), {
unitTitle: title,
- })
+ }, true)
"
height="200"
@confirm="executeDelete"
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue
index f956e47..572e171 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue
@@ -1,11 +1,11 @@
<template>
- <studip-dialog
+ <studip-dialog
:title="$gettext('Darstellung')"
:confirmText="$gettext('Speichern')"
confirmClass="accept"
:closeText="$gettext('Schließen')"
closeClass="cancel"
- height="470"
+ height="540"
width="870"
@close="$emit('close')"
@confirm="storeLayout"
@@ -23,12 +23,8 @@
<label v-if="showPreviewImage">
<button class="button" @click="deleteImage">{{ $gettext('Bild löschen') }}</button>
</label>
- <courseware-companion-box
- v-if="uploadFileError"
- :msgCompanion="uploadFileError"
- mood="sad"
- />
- <label v-if="!showPreviewImage">
+ <courseware-companion-box v-if="uploadFileError" :msgCompanion="uploadFileError" mood="sad" />
+ <template v-if="!showPreviewImage">
<img
v-if="currentFile"
:src="uploadImageURL"
@@ -36,14 +32,32 @@
:alt="$gettext('Vorschaubild')"
/>
<div v-else class="cw-structural-element-image-preview-placeholder"></div>
- {{ $gettext('Bild hochladen') }}
- <input class="cw-file-input" ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" />
- </label>
+ <label>
+ {{ $gettext('Bild hochladen') }}
+ <input
+ class="cw-file-input"
+ ref="upload_image"
+ type="file"
+ accept="image/*"
+ @change="checkUploadFile"
+ />
+ </label>
+ {{ $gettext('oder') }}
+ <br />
+ <button class="button" type="button" @click="showStockImageSelector = true">
+ {{ $gettext('Aus dem Bilderpool auswählen') }}
+ </button>
+ <StockImageSelector
+ v-if="showStockImageSelector"
+ @close="showStockImageSelector = false"
+ @select="onSelectStockImage"
+ />
+ </template>
</form>
<form class="default cw-unit-item-dialog-layout-content-settings" @submit.prevent="">
<label>
{{ $gettext('Titel') }}
- <input type="text" v-model="currentElement.attributes.title"/>
+ <input type="text" v-model="currentElement.attributes.title" />
</label>
<label>
{{ $gettext('Beschreibung') }}
@@ -62,13 +76,9 @@
class="cw-vs-select"
>
<template #open-indicator="selectAttributes">
- <span v-bind="selectAttributes"
- ><studip-icon shape="arr_1down" :size="10"
- /></span>
- </template>
- <template #no-options>
- {{ $gettext('Es steht keine Auswahl zur Verfügung') }}.
+ <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span>
</template>
+ <template #no-options> {{ $gettext('Es steht keine Auswahl zur Verfügung') }}. </template>
<template #selected-option="{ name, hex }">
<span class="vs__option-color" :style="{ 'background-color': hex }"></span
><span>{{ name }}</span>
@@ -96,19 +106,19 @@
<script>
import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-
+import StockImageSelector from '../../stock-images/SelectorDialog.vue';
import colorMixin from '@/vue/mixins/courseware/colors.js';
import { mapActions, mapGetters } from 'vuex';
-
export default {
name: 'courseware-unit-item-dialog-layout',
components: {
- CoursewareCompanionBox
+ CoursewareCompanionBox,
+ StockImageSelector,
},
props: {
unit: Object,
- unitElement: Object
+ unitElement: Object,
},
mixins: [colorMixin],
data() {
@@ -120,18 +130,26 @@ export default {
uploadImageURL: null,
currentRootLayout: 'default',
loadingInstance: false,
- }
+ showStockImageSelector: false,
+ selectedStockImage: null,
+ };
},
computed: {
...mapGetters({
context: 'context',
instanceById: 'courseware-instances/byId',
- userId: 'userId'
+ userId: 'userId',
}),
colors() {
- return this.mixinColors.filter(color => color.darkmode);
+ return this.mixinColors.filter((color) => color.darkmode);
},
image() {
+ if (this.selectedStockImage) {
+ return this.selectedStockImage.attributes['download-urls'].small
+ }
+ if (this.uploadImageURL) {
+ return this.uploadImageURL;
+ }
return this.currentElement.relationships?.image?.meta?.['download-url'] ?? null;
},
@@ -140,15 +158,14 @@ export default {
},
instance() {
if (this.inCourseContext) {
- return this.instanceById({id: 'course_' + this.context.id + '_' + this.unit.id});
+ return this.instanceById({ id: 'course_' + this.context.id + '_' + this.unit.id });
} else {
- return this.instanceById({id: 'user_' + this.context.id + '_' + this.unit.id});
+ return this.instanceById({ id: 'user_' + this.context.id + '_' + this.unit.id });
}
-
},
inCourseContext() {
return this.context.type === 'courses';
- }
+ },
},
methods: {
...mapActions({
@@ -162,9 +179,10 @@ export default {
uploadImageForStructuralElement: 'uploadImageForStructuralElement',
deleteImageForStructuralElement: 'deleteImageForStructuralElement',
storeCoursewareSettings: 'storeCoursewareSettings',
+ setStockImageForStructuralElement: 'setStockImageForStructuralElement',
}),
async loadUnitInstance() {
- const context = {type: this.context.type, id: this.context.id, unit: this.unit.id};
+ const context = { type: this.context.type, id: this.context.id, unit: this.unit.id };
await this.loadInstance(context);
},
initData() {
@@ -192,11 +210,13 @@ export default {
this.$emit('close');
await this.loadStructuralElement(this.currentElement.id);
if (
- this.unitElement.relationships['edit-blocker'].data !== null
- && this.unitElement.relationships['edit-blocker'].data?.id !== this.userId
+ this.unitElement.relationships['edit-blocker'].data !== null &&
+ this.unitElement.relationships['edit-blocker'].data?.id !== this.userId
) {
this.companionWarning({
- info: this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, die Daten werden bereits von einem anderen Nutzer bearbeitet.')
+ info: this.$gettext(
+ 'Ihre Änderungen konnten nicht gespeichert werden, die Daten werden bereits von einem anderen Nutzer bearbeitet.'
+ ),
});
return false;
} else {
@@ -207,13 +227,20 @@ export default {
this.uploadImageForStructuralElement({
structuralElement: this.currentElement,
file: this.currentFile,
- }).then(() => {
- this.loadStructuralElement(this.currentElement.id)
- }).catch((error) => {
- console.error(error);
- this.companionWarning({
- info: this.$gettext('Beim Hochladen der Bilddatei ist ein Fehler aufgetretten.')
+ })
+ .then(() => {
+ this.loadStructuralElement(this.currentElement.id);
+ })
+ .catch((error) => {
+ console.error(error);
+ this.companionWarning({
+ info: this.$gettext('Beim Hochladen der Bilddatei ist ein Fehler aufgetretten.'),
+ });
});
+ } else if (this.selectedStockImage) {
+ await this.setStockImageForStructuralElement({
+ structuralElement: this.currentElement,
+ stockImage: this.selectedStockImage,
});
} else if (this.deletingPreviewImage) {
await this.deleteImageForStructuralElement(this.currentElement);
@@ -233,13 +260,21 @@ export default {
instance: currentInstance,
});
}
- }
+ },
+ onSelectStockImage(stockImage) {
+ if (this.$refs?.upload_image) {
+ this.$refs.upload_image.value = null;
+ }
+ this.selectedStockImage = stockImage;
+ this.showStockImageSelector = false;
+ this.deletingPreviewImage = false;
+ },
},
async mounted() {
this.loadingInstance = true;
await this.loadUnitInstance();
this.loadingInstance = false;
this.initData();
- }
-}
+ },
+};
</script>
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
index 7c9de01..31c6c01 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
@@ -17,7 +17,7 @@
<h1>
<a
:href="chapterUrl"
- :title="$gettextInterpolate('%{ pageTitle } öffnen', { pageTitle: selected.name })"
+ :title="$gettextInterpolate('%{ pageTitle } öffnen', { pageTitle: selected.name }, true)"
>
{{ selected.name }}
</a>
diff --git a/resources/vue/components/file-chooser/FileChooserDialog.vue b/resources/vue/components/file-chooser/FileChooserDialog.vue
index 2102d1e..2b21f51 100644
--- a/resources/vue/components/file-chooser/FileChooserDialog.vue
+++ b/resources/vue/components/file-chooser/FileChooserDialog.vue
@@ -93,8 +93,8 @@ export default {
},
data() {
return {
- height: 600,
- width: 1000,
+ height: '600',
+ width: '1000',
scope: 'courses',
coursesTree: [],
usersTree: [],
diff --git a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
index a0a76a2..e507adc 100644
--- a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
+++ b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
@@ -30,14 +30,16 @@
v-model="user.write_permissions"
:aria-label="$gettextInterpolate(
$gettext('Schreibzugriff für %{name}'),
- {name: user.name}
+ {name: user.name},
+ true
)">
</td>
<td class="actions">
<studip-icon shape="trash" aria-role="button" @click="removeContact(user.id)"
:title="$gettextInterpolate(
$gettext('Kalender nicht mehr mit %{name} teilen'),
- {name: user.name}
+ {name: user.name},
+ true
)"></studip-icon>
</td>
</tr>
diff --git a/resources/vue/components/form_inputs/CaptchaInput.vue b/resources/vue/components/form_inputs/CaptchaInput.vue
new file mode 100644
index 0000000..1409aa8
--- /dev/null
+++ b/resources/vue/components/form_inputs/CaptchaInput.vue
@@ -0,0 +1,70 @@
+<template>
+ <div class="formpart">
+ <altcha-widget :challengeurl="challengeUrl" ref="widget"></altcha-widget>
+ </div>
+</template>
+<script>
+import 'altcha';
+import { $gettext } from '../../../assets/javascripts/lib/gettext';
+
+export default {
+ name: 'CaptchaInput',
+ props: {
+ name: {
+ type: String,
+ default: 'altcha'
+ },
+ challengeUrl: {
+ type: String,
+ requird: true,
+ },
+ auto: {
+ type: String,
+ default: null,
+ validator: (value) => ['onfocus', 'onload', 'onsubmit'].includes(value),
+ }
+ },
+ data() {
+ return {};
+ },
+ methods: {
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.$refs.widget.configure({
+ auto: this.auto,
+ name: this.name,
+ hidefooter: false,
+ hidelogo: false,
+ strings: {
+ error: $gettext('Überprüfung fehlgeschlagen. Versuchen Sie es später erneut.'),
+ footer: $gettext('Geschützt von <a href="https://altcha.org/" target="_blank">ALTCHA</a>'),
+ label: $gettext('Ich bin kein Bot'),
+ verified: $gettext('Überprüft'),
+ verifying: $gettext('Überprüfung...'),
+ waitAlert: $gettext('Überprüfung... Bitte warten.'),
+ },
+ });
+
+ this.$refs.widget.addEventListener('statechange', (ev) => {
+ if (ev.detail.state === 'verified') {
+ this.$emit('input', ev.detail.payload);
+ }
+ })
+ });
+ }
+}
+</script>
+<style>
+:root {
+ --altcha-border-width: 0;
+ --altcha-border-radius: 0;
+ --altcha-color-base: transparent;
+ --altcha-color-border: #a0a0a0;
+ --altcha-color-text: currentColor;
+ --altcha-color-border-focus: currentColor;
+ --altcha-color-error-text: var(--red);
+ --altcha-color-footer-bg: none;
+ --altcha-max-width: auto;
+}
+</style>
diff --git a/resources/vue/components/form_inputs/DateListInput.vue b/resources/vue/components/form_inputs/DateListInput.vue
index 05f3c57..d77c993 100644
--- a/resources/vue/components/form_inputs/DateListInput.vue
+++ b/resources/vue/components/form_inputs/DateListInput.vue
@@ -2,11 +2,11 @@
<div class="formpart">
<div class="sr-only" aria-live="polite" ref="list_message_field"></div>
<ul>
- <li v-for="date in selected_date_list" v-bind="selected_date_list" :key="date">
+ <li v-for="date in selected_date_list" v-bind="selected_date_list" :key="getISODate(date)">
<input type="hidden" :name="input_name + '[]'" :value="getISODate(date)">
<studip-date-time :timestamp="Math.floor(date.getTime() / 1000)" :date_only="true"></studip-date-time>
- <studip-icon shape="trash" :title="$gettext('Löschen')" @click="removeDate"
- class="enter-accessible" aria-role="button" tabindex="0"></studip-icon>
+ <studip-icon shape="trash" :title="$gettext('Löschen')" @click="removeDate(date)"
+ class="icon enter-accessible button undecorated" aria-role="button" tabindex="0"></studip-icon>
</li>
</ul>
<label>
@@ -82,13 +82,13 @@ export default {
this.selected_date_list.push(new Date(reformatted_date));
this.$refs.list_message_field.innerText = $gettextInterpolate($gettext('Datum %{date} hinzugefügt'), {date: this.selected_date_value});
},
- removeDate(date_key) {
- if (date_key) {
- let date = this.selected_date_list.at(date_key);
- let formatted_date = STUDIP.DateTime.getStudipDate(date, false, true);
- this.selected_date_list.splice(date_key, 1);
- this.$refs.list_message_field.innerText = $gettextInterpolate($gettext('Datum %{date} entfernt'), {date: formatted_date});
- }
+ removeDate(date) {
+ this.selected_date_list = this.selected_date_list.filter(d => d !== date);
+
+ this.$refs.list_message_field.innerText = $gettextInterpolate(
+ $gettext('Datum %{date} entfernt'),
+ {date: STUDIP.DateTime.getStudipDate(date, false, true)}
+ );
},
getISODate(date) {
return STUDIP.DateTime.getISODate(date);
diff --git a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
index d2cc2f1..fe67db5 100644
--- a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
+++ b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
@@ -42,7 +42,7 @@
<input type="hidden" :name="`${name}_course_ids[${course.id}]`" value="0">
<input type="checkbox" :name="`${name}_course_ids[${course.id}]`"
value="1" :checked="selected_course_id_list.includes(course.id)"
- :title="$gettextInterpolate($gettext('%{course} auswählen'), {course: course.name})">
+ :title="$gettextInterpolate($gettext('%{course} auswählen'), {course: course.name}, true)">
</td>
</tr>
<tr v-if="loadedSemesters.includes(semester_id) && courses.length === 0">
diff --git a/resources/vue/components/questionnaires/FreetextEdit.vue b/resources/vue/components/questionnaires/FreetextEdit.vue
index 29c6f34..58848ed 100644
--- a/resources/vue/components/questionnaires/FreetextEdit.vue
+++ b/resources/vue/components/questionnaires/FreetextEdit.vue
@@ -2,7 +2,7 @@
<div>
<div class="formpart" tabindex="0" ref="autofocus">
{{ $gettext('Frage') }}
- <studip-wysiwyg v-model="val_clone.description" :key="question_id"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
<label>
@@ -13,40 +13,19 @@
</template>
<script>
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
export default {
name: 'freetext-edit',
- components: {
- StudipWysiwyg
+ mixins: [ QuestionnaireComponent ],
+ created() {
+ this.setDefaultValues({
+ description: '',
+ mandatory: '0',
+ });
},
- props: {
- value: {
- type: Object,
- required: false,
- default: function () {
- return {};
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data: function () {
- return {
- val_clone: ''
- };
- },
- mounted: function () {
- this.val_clone = this.value;
+ mounted() {
this.$refs.autofocus.focus();
- },
- watch: {
- value (new_val) {
- this.val_clone = new_val;
- }
}
-
}
</script>
diff --git a/resources/vue/components/questionnaires/InputArray.vue b/resources/vue/components/questionnaires/InputArray.vue
index 37fa6fc..896418f 100644
--- a/resources/vue/components/questionnaires/InputArray.vue
+++ b/resources/vue/components/questionnaires/InputArray.vue
@@ -1,177 +1,170 @@
<template>
<div class="input-array">
<span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span>
- <draggable v-model="options" handle=".dragarea" tag="ol" class="clean options">
- <li v-for="(option, index) in options" :key="index">
- <a class="dragarea"
- v-if="options.length > 1"
- tabindex="0"
- :ref="'draghandle_' + index"
- :title="$gettextInterpolate('Sortierelement für Option %{option}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {option: option})"
- @keydown="keyHandler($event, index)">
- <span class="drag-handle"></span>
- </a>
- <input type="text"
- :placeholder="$gettext('Option')"
- :ref="'option_' + index"
- @paste="(ev) => onPaste(ev, index)"
- v-model="options[index]">
- <button class="as-link"
- :title="$gettext('Option löschen')"
- @click.prevent="askForDeletingOption(index)">
- <studip-icon shape="trash" role="clickable" :size="20" alt=""></studip-icon>
- </button>
- <button v-if="index == options.length - 1"
- class="as-link"
- :title="$gettext('Option hinzufügen')"
- @click.prevent="addOption">
- <studip-icon shape="add" role="clickable" :size="20" alt=""></studip-icon>
- </button>
- </li>
- </draggable>
- <studip-dialog
- v-if="askForDeleting"
- :title="$gettext('Bitte bestätigen Sie die Aktion.')"
- :question="$gettext('Wirklich löschen?')"
- :confirmText="$gettext('Ja')"
- :closeText="$gettext('Nein')"
- closeClass="cancel"
- height="180"
- @confirm="deleteOption"
- @close="askForDeleting = false"
- >
- </studip-dialog>
+ <table class="default nohover">
+ <colgroup>
+ <col style="width: 16px">
+ <col>
+ <col v-for="i in additionalColspan" :key="`colspan-${i}`">
+ <col style="width: 24px">
+ </colgroup>
+ <thead>
+ <tr>
+ <th class="dragcolumn"></th>
+ <th>{{ labelPlural }}</th>
+ <slot name="header-cells" />
+ <th class="actions"></th>
+ </tr>
+ </thead>
+ <Draggable v-model="options" handle=".dragarea" tag="tbody" class="statements">
+ <tr v-for="(option, index) in options" :key="index">
+ <td class="dragcolumn">
+ <a class="dragarea"
+ tabindex="0"
+ :title="$gettextInterpolate($gettext(`Sortierelement für %{label} %{option}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.`), {option, label}, true)"
+ @keydown="keyHandler($event, index)"
+ ref="draghandle">
+ <span class="drag-handle"></span>
+ </a>
+ </td>
+ <td>
+ <input type="text"
+ ref="inputs"
+ :placeholder="label"
+ @paste="(ev) => onPaste(ev, index)"
+ v-model="options[index]">
+ </td>
+ <slot name="body-cells" />
+ <td class="actions">
+ <StudipIcon name="delete"
+ shape="trash"
+ :size="20"
+ @click.prevent="deleteOption(index)"
+ :title="$gettextInterpolate($gettext('%{label} löschen'), {label}, true)"
+ />
+ </td>
+ </tr>
+ </Draggable>
+ <tfoot>
+ <tr>
+ <td :colspan="3 + additionalColspan">
+ <button class="as-link"
+ :title="$gettextInterpolate($gettext('%{label} hinzufügen'), {label}, true)"
+ @click.prevent="addOption()">
+ <StudipIcon shape="add" :size="20" alt="" />
+ </button>
+ </td>
+ </tr>
+ </tfoot>
+ </table>
</div>
</template>
<script>
-import StudipIcon from "../StudipIcon.vue";
-import StudipDialog from "../StudipDialog.vue";
-import draggable from 'vuedraggable';
+import Draggable from 'vuedraggable';
+import { $gettext } from '../../../assets/javascripts/lib/gettext';
+
export default {
name: 'input-array',
- components: {
- StudipIcon,
- StudipDialog,
- draggable
- },
+ components: { Draggable },
props: {
- value: {
- type: Array,
- required: false
- }
+ additionalColspan: {
+ type: Number,
+ default: 0,
+ },
+ label: {
+ type: String,
+ default: $gettext('Option'),
+ },
+ labelPlural: {
+ type: String,
+ default: $gettext('Optionen'),
+ },
+ value: Array,
},
- data: function () {
+ data() {
return {
options: [],
- askForDeleting: false,
- indexOfDeletingOption: 0,
- unique_id: null,
- assistiveLive: ''
+ assistiveLive: '',
};
},
methods: {
- addOption: function (val, position) {
- let data = this.value;
- if (val.target) {
- val = '';
- }
- if (typeof position === "undefined") {
- data.push(val || '');
- position = this.value.length - 1
- } else {
- data.splice(position, 0, val || '');
- }
- this.$emit('input', data);
- let v = this;
- this.$nextTick(function () {
- v.$refs['option_' + position][0].focus();
+ addOption(val = '', position = this.options.length) {
+ this.$set(this.options, position, val.trim());
+
+ this.$nextTick(() => {
+ this.$refs.inputs[position].focus();
});
},
- askForDeletingOption: function (index) {
- this.indexOfDeletingOption = index;
- if (this.value[index]) {
- this.askForDeleting = true;
- } else {
- this.deleteOption();
- }
- },
- deleteOption: function () {
- this.$delete(this.value, this.indexOfDeletingOption);
- this.askForDeleting = false;
+ deleteOption(index) {
+ const question = this.options[index] ? this.$gettext('Wirklich löschen?') : true;
+ STUDIP.Dialog.confirm(question).done(() => {
+ this.$delete(this.options, index);
+ });
},
- onPaste: function (ev, position) {
- let data = ev.clipboardData.getData("text").split("\n");
- for (let i = 0; i < data.length; i++) {
- if (data[i].trim()) {
- this.addOption(data[i], position + i);
- }
- }
+ onPaste(ev, position) {
+ ev.clipboardData
+ .getData('text')
+ .split("\n")
+ .filter(str => str.trim().length > 0)
+ .forEach((value, index) => this.addOption(value, position + index));
+ ev.preventDefault();
},
keyHandler(e, index) {
- switch (e.keyCode) {
- case 38: // up
- e.preventDefault();
- if (index > 0) {
- this.moveUp(index);
- this.$nextTick(function () {
- this.$refs['draghandle_' + (index - 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- 'Aktuelle Position in der Liste: %{pos} von %{listLength}.'
- , {pos: index, listLength: this.options.length}
- );
- });
- }
- break;
- case 40: // down
- e.preventDefault();
- if (index < this.options.length - 1) {
- this.moveDown(index);
- this.$nextTick(function () {
- this.$refs['draghandle_' + (index + 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- 'Aktuelle Position in der Liste: %{pos} von %{listLength}.'
- , {pos: index + 2, listLength: this.options.length}
- );
- });
- }
- break;
- }
- },
- moveDown: function (index) {
- if (index == this.options.length - 1) {
+ if (e.keyCode !== 38 && e.keyCode !== 40) {
return;
}
- let option = this.options[index];
- this.options[index] = this.options[index + 1];
- this.options[index + 1] = option;
- this.$forceUpdate();
+
+ e.preventDefault();
+
+ const moveUp = e.keyCode === 38;
+
+ this.moveElement(index, moveUp ? -1 : 1).then((newIndex) => {
+ this.assistiveLive = this.$gettextInterpolate(
+ this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
+ {pos: newIndex + 1, listLength: this.options.length}
+ );
+
+ this.$nextTick(() => {
+ this.$refs['draghandle'][newIndex].focus();
+ });
+ })
},
- moveUp: function (index) {
- if (index === 0) {
- return;
+ moveElement(index, direction) {
+ if (this.options[index + direction] === undefined) {
+ return Promise.resolve(index);
}
- let option = this.options[index];
- this.options[index] = this.options[index - 1];
- this.options[index - 1] = option;
- this.$forceUpdate();
+
+ const indices = [index, index + direction].sort();
+
+ this.options.splice(
+ Math.min(...indices),
+ 2,
+ ...indices.reverse().map(idx => this.options[idx])
+ );
+
+ return Promise.resolve(index + direction);
}
},
- mounted: function () {
- this.options = this.value;
- this.unique_id = 'array_input_' + Math.floor(Math.random() * 100000000);
- },
watch: {
- options (new_data, old_data) {
- if (typeof old_data === 'undefined' || typeof new_data === 'undefined') {
- return;
- }
- this.$emit('input', new_data);
+ options: {
+ handler(current) {
+ this.$emit('input', current);
+ },
+ deep: true
},
- value (new_val) {
- this.options = new_val;
+ value: {
+ handler(current) {
+ this.options = current;
+ },
+ immediate: true
}
}
}
</script>
+<style scoped>
+.input-array input[type="text"] {
+ max-width: unset;
+}
+</style>
diff --git a/resources/vue/components/questionnaires/LikertEdit.vue b/resources/vue/components/questionnaires/LikertEdit.vue
index c87f9fe..736be6b 100644
--- a/resources/vue/components/questionnaires/LikertEdit.vue
+++ b/resources/vue/components/questionnaires/LikertEdit.vue
@@ -1,66 +1,27 @@
<template>
<div class="likert_edit">
-
<div class="formpart" tabindex="0" ref="autofocus">
{{ $gettext('Einleitungstext' )}}
- <studip-wysiwyg v-model="val_clone.description"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
- <span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span>
+ <InputArray v-model="val_clone.statements"
+ :label="$gettext('Aussage')"
+ :label-plural="$gettext('Aussagen')"
+ :additional-colspan="val_clone.options.length"
+ >
+ <template #header-cells>
+ <th v-for="(option, index) in val_clone.options" class="option-cell" :key="index">
+ {{ option }}
+ </th>
+ </template>
- <table class="default nohover">
- <thead>
- <tr>
- <th class="dragcolumn"></th>
- <th>{{ $gettext('Aussagen') }}</th>
- <th v-for="(option, index) in val_clone.options" :key="index">{{ option }}</th>
- <th class="actions"></th>
- </tr>
- </thead>
- <draggable v-model="val_clone.statements" handle=".dragarea" tag="tbody" class="statements">
- <tr v-for="(statement, index) in val_clone.statements" :key="index">
- <td class="dragcolumn">
- <a class="dragarea"
- tabindex="0"
- :title="$gettextInterpolate($gettext('Sortierelement für Aussage %{statement}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {statement: statement})"
- @keydown="keyHandler($event, index)"
- :ref="'draghandle_' + index">
- <span class="drag-handle"></span>
- </a>
- </td>
- <td>
- <input type="text"
- :ref="'statement_' + index"
- :placeholder="$gettext('Aussage')"
- @paste="(ev) => onPaste(ev, index)"
- v-model="val_clone.statements[index]">
- </td>
- <td v-for="(option, index2) in val_clone.options" :key="index2">
- <input type="radio" disabled :title="option">
- </td>
- <td class="actions">
- <studip-icon name="delete"
- shape="trash"
- :size="20"
- @click.prevent="deleteStatement(index)"
- :title="$gettext('Aussage löschen')"
- ></studip-icon>
- </td>
- </tr>
- </draggable>
- <tfoot>
- <tr>
- <td :colspan="val_clone.options.length + 3">
- <studip-icon name="add"
- shape="add"
- :size="20"
- @click.prevent="addStatement()"
- :title="$gettext('Aussage hinzufügen')"
- ></studip-icon>
- </td>
- </tr>
- </tfoot>
- </table>
+ <template #body-cells>
+ <td v-for="(option, index) in val_clone.options" class="option-cell" :key="index">
+ <input type="radio" disabled :title="option">
+ </td>
+ </template>
+ </InputArray>
<label>
<input type="checkbox" v-model.number="val_clone.mandatory" true-value="1" false-value="0">
@@ -73,17 +34,18 @@
<div>
<div>{{ $gettext('Antwortmöglichkeiten konfigurieren') }}</div>
- <input-array v-model="val_clone.options"></input-array>
+ <InputArray v-model="val_clone.options" />
</div>
</div>
</template>
<script>
-import draggable from 'vuedraggable';
-import InputArray from "./InputArray.vue";
import { $gettext } from '../../../assets/javascripts/lib/gettext';
+import InputArray from "./InputArray.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-const default_value = () => ({
+// This is necesssar since $gettext does not seem to work in data() or created()
+const default_values = () => ({
description: '',
statements: ['', '', '', ''],
mandatory: 0,
@@ -96,115 +58,16 @@ const default_value = () => ({
$gettext('trifft nicht zu'),
],
});
+
export default {
name: 'likert-edit',
- components: {
- draggable,
- InputArray
- },
- props: {
- value: {
- type: Object,
- required: false,
- default() {
- return {...default_value()};
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data() {
- return {
- val_clone: null,
- assistiveLive: ''
- };
- },
- methods: {
- addStatement(val = '', position = null) {
- if (position === null) {
- this.val_clone.statements.push(val || '');
- } else {
- this.val_clone.statements.splice(position, 0, val || '');
- }
- this.$nextTick(() => {
- this.$refs['statement_' + (this.val_clone.statements.length - 1)][0].focus();
- });
- },
- deleteStatement(index) {
- STUDIP.Dialog.confirm(this.$gettext('Wirklich löschen?')).done(() => {
- this.$delete(this.val_clone.statements, index);
- });
- },
- onPaste(ev, position) {
- let data = ev.clipboardData.getData("text").split("\n");
- for (let i = 0; i < data.length; i++) {
- if (data[i].trim()) {
- this.addStatement(data[i], position + i);
- }
- }
- },
- keyHandler(e, index) {
- switch (e.keyCode) {
- case 38: // up
- e.preventDefault();
- if (index > 0) {
- this.moveUp(index);
- this.$nextTick(() => {
- this.$refs['draghandle_' + (index - 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
- {pos: index, listLength: this.val_clone.statements.length}
- );
- });
- }
- break;
- case 40: // down
- e.preventDefault();
- if (index < this.val_clone.statements.length - 1) {
- this.moveDown(index);
- this.$nextTick(() => {
- this.$refs['draghandle_' + (index + 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
- {pos: index + 2, listLength: this.val_clone.statements.length}
- );
- });
- }
- break;
- }
- },
- moveDown(index) {
- this.val_clone.statements.splice(
- index,
- 2,
- this.val_clone.statements[index + 1],
- this.val_clone.statements[index]
- )
- },
- moveUp(index) {
- this.val_clone.statements.splice(
- index - 1,
- 2,
- this.val_clone.statements[index],
- this.val_clone.statements[index - 1]
- )
- }
- },
+ components: { InputArray },
+ mixins: [ QuestionnaireComponent ],
created() {
- this.val_clone = Object.assign({}, default_value(), this.value ?? {});
+ this.setDefaultValues(default_values());
},
mounted() {
this.$refs.autofocus.focus();
- },
- watch: {
- val_clone: {
- handler(current) {
- this.$emit('input', current);
- },
- deep: true
- }
}
}
</script>
diff --git a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue b/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
index bc5e829..83d5fa2 100644
--- a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
+++ b/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
@@ -8,39 +8,26 @@
<div class="formpart">
{{ $gettext('Hinweistext (optional)') }}
- <studip-wysiwyg v-model="val_clone.description" :key="question_id"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
</div>
</template>
<script>
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
export default {
name: 'questionnaire-info-edit',
- components: {
- StudipWysiwyg
+ mixins: [ QuestionnaireComponent ],
+ created() {
+ this.setDefaultValues({
+ url: '',
+ description: ''
+ });
},
- props: {
- value: {
- type: Object,
- required: false,
- default() {
- return {
- url: '',
- description: ''
- };
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data () {
- return {
- val_clone: this.value,
- };
+ mounted() {
+ this.$refs.infoUrl.focus();
+ this.checkValidity();
},
methods: {
checkValidity() {
@@ -53,15 +40,6 @@ export default {
this.$refs.infoUrl.reportValidity();
}
}
- },
- mounted() {
- this.$refs.infoUrl.focus();
- this.checkValidity();
- },
- watch: {
- value (new_val) {
- this.val_clone = new_val;
- }
}
}
</script>
diff --git a/resources/vue/components/questionnaires/RangescaleEdit.vue b/resources/vue/components/questionnaires/RangescaleEdit.vue
index 91aec1c..cd7ce3b 100644
--- a/resources/vue/components/questionnaires/RangescaleEdit.vue
+++ b/resources/vue/components/questionnaires/RangescaleEdit.vue
@@ -3,68 +3,25 @@
<div class="formpart" tabindex="0" ref="autofocus">
{{ $gettext('Einleitungstext') }}
- <studip-wysiwyg v-model="val_clone.description"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
- <span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span>
-
- <table class="default nohover">
- <thead>
- <tr>
- <th class="dragcolumn"></th>
- <th>{{ $gettext('Aussagen') }}</th>
- <th v-for="i in (val_clone.maximum - val_clone.minimum + 1)" :key="i" class="number">{{ (val_clone.minimum - 1 + i) }}</th>
- <th v-if="val_clone.alternative_answer.trim().length > 0">{{ val_clone.alternative_answer }}</th>
- <th class="actions"></th>
- </tr>
- </thead>
- <draggable v-model="val_clone.statements" handle=".dragarea" tag="tbody" class="statements">
- <tr v-for="(statement, index) in val_clone.statements" :key="index">
- <td class="dragcolumn">
- <a class="dragarea"
- tabindex="0"
- :title="$gettextInterpolate($gettext('Sortierelement für Aussage %{statement}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {statement: statement})"
- @keydown="keyHandler($event, index)"
- :ref="'draghandle_' + index">
- <span class="drag-handle"></span>
- </a>
- </td>
- <td>
- <input type="text"
- :ref="'statement_' + index"
- :placeholder="$gettext('Aussage')"
- @paste="(ev) => onPaste(ev, index)"
- v-model="val_clone.statements[index]">
- </td>
- <td v-for="i in (val_clone.maximum - val_clone.minimum + 1)" :key="i">
- <input type="radio" disabled :title="i + val_clone.minimum - 1">
- </td>
- <td v-if="val_clone.alternative_answer.trim().length > 0">
- <input type="radio" disabled :title="val_clone.alternative_answer">
- </td>
- <td class="actions">
- <studip-icon name="delete"
- shape="trash"
- :size="20"
- @click.prevent="deleteStatement(index)"
- :title="$gettext('Aussage löschen')"
- ></studip-icon>
- </td>
- </tr>
- </draggable>
- <tfoot>
- <tr>
- <td :colspan="val_clone.maximum - val_clone.minimum + 4 + (val_clone.alternative_answer.trim().length > 0 ? 1 : 0)">
- <studip-icon name="add"
- shape="add"
- :size="20"
- @click.prevent="addStatement()"
- :title="$gettext('Aussage hinzufügen')"
- ></studip-icon>
- </td>
- </tr>
- </tfoot>
- </table>
+ <InputArray v-model="val_clone.statements"
+ :label="$gettext('Aussage')"
+ :label-plural="$gettext('Aussagen')"
+ :additional-colspan="options.length"
+ >
+ <template #header-cells>
+ <th v-for="(option, index) in options" class="option-cell" :key="index">
+ {{ option }}
+ </th>
+ </template>
+ <template #body-cells>
+ <td v-for="(option, index) in options" class="option-cell" :key="index">
+ <input type="radio" disabled :title="option">
+ </td>
+ </template>
+ </InputArray>
<label>
<input type="checkbox" v-model.number="val_clone.mandatory" true-value="1" false-value="0">
@@ -82,135 +39,48 @@
<label>
{{ $gettext('Minimum') }}
- <input type="number" v-model.number="val_clone.minimum" min="1">
+ <input type="number" v-model.number="val_clone.minimum" min="1" :max="val_clone.maximum">
</label>
<label>
{{ $gettext('Ausweichantwort (leer lassen für keine)') }}
- <input type="text" v-model="val_clone.alternative_answer">
+ <input type="text" v-model.trim="val_clone.alternative_answer">
</label>
</div>
</template>
<script>
-import draggable from 'vuedraggable';
+import InputArray from './InputArray.vue';
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-const default_value = () => ({
- description: '',
- statements: ['', '', '', ''],
- mandatory: 0,
- randomize: 0,
- minimum: 1,
- maximum: 5,
- alternative_answer: ''
-});
export default {
- name: 'likert-edit',
- components: {
- draggable,
- },
- props: {
- value: {
- type: Object,
- required: false,
- default() {
- return default_value();
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data() {
- return {
- val_clone: null,
- assistiveLive: ''
- };
- },
- methods: {
- addStatement(val = '', position = null) {
- if (position === null) {
- this.val_clone.statements.push(val || '');
- } else {
- this.val_clone.statements.splice(position, 0, val || '');
- }
- this.$nextTick(() => {
- this.$refs['statement_' + (this.value.statements.length - 1)][0].focus();
- });
- },
- deleteStatement(index) {
- STUDIP.Dialog.confirm(this.$gettext('Wirklich löschen?')).done(() => {
- this.$delete(this.value.statements, index);
- });
- },
- onPaste(ev, position) {
- let data = ev.clipboardData.getData('text').split("\n");
- for (let i = 0; i < data.length; i++) {
- if (data[i].trim()) {
- this.addStatement(data[i], position + i);
- }
- }
- },
- keyHandler(e, index) {
- switch (e.keyCode) {
- case 38: // up
- e.preventDefault();
- if (index > 0) {
- this.moveUp(index);
- this.$nextTick(() => {
- this.$refs['draghandle_' + (index - 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
- {pos: index, listLength: this.val_clone.statements.length}
- );
- });
- }
- break;
- case 40: // down
- e.preventDefault();
- if (index < this.val_clone.statements.length - 1) {
- this.moveDown(index);
- this.$nextTick(() => {
- this.$refs['draghandle_' + (index + 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
- {pos: index + 2, listLength: this.val_clone.statements.length}
- );
- });
- }
- break;
- }
- },
- moveDown(index) {
- this.val_clone.statements.splice(
- index,
- 2,
- this.val_clone.statements[index + 1],
- this.val_clone.statements[index]
- );
- },
- moveUp(index) {
- this.val_clone.statements.splice(
- index - 1,
- 2,
- this.val_clone.statements[index],
- this.val_clone.statements[index - 1]
- );
- },
- },
+ name: 'rangescale-edit',
+ components: { InputArray },
+ mixins: [ QuestionnaireComponent ],
created() {
- this.val_clone = Object.assign({}, default_value(), this.value ?? {});
+ this.setDefaultValues({
+ alternative_answer: '',
+ description: '',
+ mandatory: 0,
+ maximum: 5,
+ minimum: 1,
+ randomize: 0,
+ statements: ['', '', '', '']
+ });
},
mounted() {
this.$refs.autofocus.focus();
},
- watch: {
- val_clone: {
- handler(current) {
- this.$emit('input', current);
- },
- deep: true
+ computed: {
+ options() {
+ let result = [];
+ for (let i = this.val_clone.minimum; i <= this.val_clone.maximum; i += 1) {
+ result.push(i);
+ }
+ if (this.val_clone.alternative_answer.length > 0) {
+ result.push(this.val_clone.alternative_answer);
+ }
+ return result;
}
}
}
diff --git a/resources/vue/components/questionnaires/VoteEdit.vue b/resources/vue/components/questionnaires/VoteEdit.vue
index 9acb01f..1d6d9cf 100644
--- a/resources/vue/components/questionnaires/VoteEdit.vue
+++ b/resources/vue/components/questionnaires/VoteEdit.vue
@@ -2,10 +2,10 @@
<div class="vote_edit">
<div class="formpart" tabindex="0" ref="autofocus">
{{ $gettext('Frage') }}
- <studip-wysiwyg v-model="val_clone.description" :key="question_id"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
- <input-array v-model="val_clone.options"></input-array>
+ <InputArray v-model="val_clone.options" />
<label>
<input type="checkbox" v-model.number="val_clone.multiplechoice" true-value="1" false-value="0">
@@ -24,47 +24,24 @@
</template>
<script>
-import StudipWysiwyg from "../StudipWysiwyg.vue";
import InputArray from "./InputArray.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
export default {
name: 'vote-edit',
- components: {
- StudipWysiwyg,
- InputArray
+ components: { InputArray },
+ mixins: [QuestionnaireComponent],
+ created() {
+ this.setDefaultValues({
+ description: '',
+ mandatory: '0',
+ multiplechoice: '1',
+ options: ['', '', '', ''],
+ randomize: '0',
+ });
},
- props: {
- value: {
- type: Object,
- required: false,
- default: function () {
- return {};
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data: function () {
- return {
- val_clone: {}
- };
- },
- mounted: function () {
- this.val_clone = this.value;
- if (!this.value.description) {
- this.$emit('input', {
- multiplechoice: 1,
- options: ['', '', '', '']
- });
- }
+ mounted() {
this.$refs.autofocus.focus();
- },
- watch: {
- value (new_val) {
- this.val_clone = new_val;
- }
}
}
</script>
diff --git a/resources/vue/components/responsive/ResponsiveContentBar.vue b/resources/vue/components/responsive/ResponsiveContentBar.vue
index d64ac52..eb6dd96 100644
--- a/resources/vue/components/responsive/ResponsiveContentBar.vue
+++ b/resources/vue/components/responsive/ResponsiveContentBar.vue
@@ -131,7 +131,11 @@ export default {
.classList.add('contentbar-wrapper-right');
}
- document.getElementById('responsive-contentbar-container').prepend(this.realContentbar);
+ const contentbarContainer = document.getElementById('responsive-contentbar-container');
+
+ contentbarContainer.prepend(this.realContentbar);
+
+ document.getElementById('content-wrapper').style.marginTop = `${contentbarContainer.clientHeight}px`;
} else {
this.realContentbar.id = 'contentbar';
document.getElementById('toggle-sidebar').remove();
@@ -145,6 +149,8 @@ export default {
}
document.querySelector(this.realContentbarSource).prepend(this.realContentbar);
+
+ document.getElementById('content-wrapper').style.marginTop = 'initial';
}
}
},
diff --git a/resources/vue/components/responsive/ResponsiveNavigation.vue b/resources/vue/components/responsive/ResponsiveNavigation.vue
index 1c07e73..d9c391d 100644
--- a/resources/vue/components/responsive/ResponsiveNavigation.vue
+++ b/resources/vue/components/responsive/ResponsiveNavigation.vue
@@ -130,10 +130,6 @@ export default {
type: String,
default: ''
},
- hasSidebar: {
- type: Boolean,
- default: true
- },
navigation: {
type: Object,
required: true,
@@ -162,6 +158,7 @@ export default {
classObserver: null,
dialogObserver: null,
hasSkiplinks: document.querySelector('#skiplink_list') !== null,
+ hasSidebar: false,
hasContentbar: false,
contentbarTitle: ''
}
@@ -494,6 +491,8 @@ export default {
}
},
mounted() {
+ this.hasSidebar = document.querySelectorAll('#sidebar .sidebar-widget:not(#sidebar-navigation)').length > 0;
+
const cache = STUDIP.Cache.getInstance('responsive.');
const fullscreen = cache.get('fullscreen-mode') ?? false;
const fullscreenDocument = document.documentElement.classList.contains('fullscreen-mode');
@@ -564,18 +563,6 @@ export default {
attributeFilter: ['class']
});
- // Check for closed dialog, re-mounting the Vue component.
- this.dialogObserver = new MutationObserver(mutations => {
- if (mutations[0].removedNodes.length > 0 &&
- mutations[0].removedNodes[0].classList.contains('ui-widget-overlay')) {
- document.getElementById('responsive-menu').replaceChildren(this.$el);
- }
- });
-
- this.dialogObserver.observe(document.body, {
- childList: true
- });
-
this.globalOn('has-contentbar', value => {
this.hasContentbar = value;
if (value && this.isFullscreen) {
@@ -594,6 +581,9 @@ export default {
attributeFilter: ['class']
})
});
+
+ // Check initial state after load
+ this.headerMagic = document.querySelector('body').classList.contains('fixed');
},
beforeDestroy() {
this.classObserver.disconnect();
diff --git a/resources/vue/components/tree/StudipTree.vue b/resources/vue/components/tree/StudipTree.vue
index 136fc95..fc6dc41 100644
--- a/resources/vue/components/tree/StudipTree.vue
+++ b/resources/vue/components/tree/StudipTree.vue
@@ -33,14 +33,21 @@
<MountingPortal v-if="withSearch" mountTo="#search-widget" name="sidebar-search">
<search-widget v-if="currentNode" :min-length="3" ref="searchWidget"></search-widget>
</MountingPortal>
+ <MountingPortal v-if="!editable && !isSearching && !isLoading && currentNode"
+ mountTo="#views-widget"
+ name="sidebar-views">
+ <studip-tree-view-widget :config="viewConfig" />
+ </MountingPortal>
</div>
</template>
<script>
import axios from 'axios';
import { TreeMixin } from '../../mixins/TreeMixin';
+import PageLayout from '../../../assets/javascripts/lib/page_layout';
import StudipProgressIndicator from '../StudipProgressIndicator.vue';
import SearchWidget from '../SearchWidget.vue';
+import StudipTreeViewWidget from './StudipTreeViewWidget.vue';
import StudipTreeList from './StudipTreeList.vue';
import StudipTreeTable from './StudipTreeTable.vue';
import StudipTreeNode from './StudipTreeNode.vue';
@@ -49,7 +56,13 @@ import TreeSearchResult from './TreeSearchResult.vue';
export default {
name: 'StudipTree',
components: {
- TreeSearchResult, SearchWidget, StudipProgressIndicator, StudipTreeList, StudipTreeTable, StudipTreeNode
+ TreeSearchResult,
+ SearchWidget,
+ StudipTreeViewWidget,
+ StudipProgressIndicator,
+ StudipTreeList,
+ StudipTreeTable,
+ StudipTreeNode
},
mixins: [ TreeMixin ],
props: {
@@ -163,12 +176,21 @@ export default {
isLoading: false,
showStructuralNavigation: false,
searchConfig: {},
- isSearching: false
+ isSearching: false,
+ viewConfig: null,
+ pageTitle: document.title
}
},
methods: {
changeCurrentNode(node) {
this.currentNode = node;
+ this.viewConfig = {
+ view: this.viewType,
+ node: this.currentNode,
+ semester: this.semester,
+ semClass: this.semClass
+ };
+ this.setPageTitle(this.currentNode.attributes.name);
this.$nextTick(() => {
document.getElementById('tree-breadcrumb-' + node.attributes.id)?.focus();
});
@@ -187,6 +209,10 @@ export default {
form.appendChild(input);
}
input.setAttribute('value', searchterm);
+ },
+ setPageTitle(nodeTitle) {
+ const title = this.pageTitle.split('-');
+ PageLayout.title = title.slice(0, -1).join('-') + '/ ' + nodeTitle + ' -' + title[title.length - 1];
}
},
mounted() {
@@ -206,6 +232,13 @@ export default {
this.currentNode = this.startNode;
this.loaded = true;
this.isLoading = false;
+ this.viewConfig = {
+ view: this.viewType,
+ node: this.currentNode,
+ semester: this.semester,
+ semClass: this.semClass
+ };
+ this.setPageTitle(this.currentNode.attributes.name);
});
axios.interceptors.request.eject(loadingIndicator);
diff --git a/resources/vue/components/tree/StudipTreeList.vue b/resources/vue/components/tree/StudipTreeList.vue
index 4ba08bc..6214234 100644
--- a/resources/vue/components/tree/StudipTreeList.vue
+++ b/resources/vue/components/tree/StudipTreeList.vue
@@ -15,7 +15,7 @@
<a v-if="editable && currentNode.attributes.id !== 'root'"
:href="editUrl + '/' + currentNode.attributes.id"
@click.prevent="editNode(editUrl, currentNode.id)" data-dialog="size=medium"
- :title="$gettextInterpolate($gettext('%{name} bearbeiten'), {name: currentNode.attributes.name})">
+ :title="$gettextInterpolate($gettext('%{name} bearbeiten'), {name: currentNode.attributes.name}, true)">
<studip-icon shape="edit" :size="20"></studip-icon>
</a>
@@ -36,7 +36,7 @@
<li v-for="(child, index) in children" :key="index" class="studip-tree-child">
<a v-if="editable && children.length > 1" class="drag-link"
tabindex="0"
- :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name})"
+ :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name}, true)"
@keydown="keyHandler($event, index)"
:ref="'draghandle-' + index">
<span class="drag-handle"></span>
@@ -91,9 +91,12 @@
<tbody>
<tr v-for="(course) in courses" :key="course.id" class="studip-tree-child studip-tree-course">
<td>
- <a :href="courseUrl(course.id)"
- :title="$gettextInterpolate($gettext('Zur Veranstaltung %{ course }'),
- { course: course.attributes.title })">
+ <a :href="courseUrl(course.id)" tabindex="0"
+ :title="$gettextInterpolate(
+ $gettext('Zur Veranstaltung %{ title }'),
+ { title: course.attributes.title },
+ true
+ )">
<studip-icon shape="seminar" :size="26"></studip-icon>
<template v-if="course.attributes['course-number']">
{{ course.attributes['course-number'] }}
diff --git a/resources/vue/components/tree/StudipTreeTable.vue b/resources/vue/components/tree/StudipTreeTable.vue
index 03a9d7b..093dd9c 100644
--- a/resources/vue/components/tree/StudipTreeTable.vue
+++ b/resources/vue/components/tree/StudipTreeTable.vue
@@ -79,7 +79,7 @@
<td>
<a v-if="editable && children.length > 1" class="drag-link" role="option"
tabindex="0"
- :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name})"
+ :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name}, true)"
@keydown="keyHandler($event, index)"
:ref="'draghandle-' + index">
<span class="drag-handle"></span>
@@ -93,7 +93,7 @@
<a :href="nodeUrl(child.id, semester !== 'all' ? semester : null)" tabindex="0"
@click.prevent="openNode(child)"
:title="$gettextInterpolate($gettext('Unterebene %{ node } öffnen'),
- { node: node.attributes.name })">
+ { node: node.attributes.name }, true)">
{{ child.attributes.name }}
</a>
</td>
@@ -112,8 +112,11 @@
</td>
<td>
<a :href="courseUrl(course.id)" tabindex="0"
- :title="$gettextInterpolate($gettext('Zur Veranstaltung %{ course }'),
- { course: course.attributes.title })">
+ :title="$gettextInterpolate(
+ $gettext('Zur Veranstaltung %{ title }'),
+ { title: course.attributes.title },
+ true
+ )">
<template v-if="course.attributes['course-number']">
{{ course.attributes['course-number'] }}
</template>
diff --git a/resources/vue/components/tree/StudipTreeViewWidget.vue b/resources/vue/components/tree/StudipTreeViewWidget.vue
new file mode 100644
index 0000000..30e2d5c
--- /dev/null
+++ b/resources/vue/components/tree/StudipTreeViewWidget.vue
@@ -0,0 +1,56 @@
+<template>
+ <sidebar-widget id="views-widget" class="sidebar-views" :title="$gettext('Ansicht')">
+ <template #content>
+ <ul class="widget-list widget-links sidebar-views">
+ <li :class="{ active: config.view === 'list' }">
+ <a :href="getUrl('list')"
+ :title="$gettext('Verzeichnis als Liste anzeigen')"
+ tabindex="0">
+ {{ $gettext('Als Liste anzeigen') }}
+ </a>
+ </li>
+ <li :class="{ active: config.view === 'table' }">
+ <a :href="getUrl('table')"
+ :title="$gettext('Verzeichnis als Tabelle anzeigen')"
+ tabindex="0">
+ {{ $gettext('Als Tabelle anzeigen') }}
+ </a>
+ </li>
+ </ul>
+ </template>
+ </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../SidebarWidget.vue';
+
+export default {
+ name: 'StudipTreeViewWidget',
+ components: {
+ SidebarWidget
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true
+ }
+ },
+ methods: {
+ getUrl(showAs) {
+ const url = new URL(window.location);
+ url.searchParams.set('show_as', showAs);
+ url.searchParams.set('node_id', this.config.node.id);
+
+ if (this.config.semester !== '') {
+ url.searchParams.set('semester', this.config.semester);
+ }
+
+ if (this.config.semClass !== 0) {
+ url.searchParams.set('semclass', this.config.semClass);
+ }
+
+ return url.toString();
+ }
+ }
+}
+</script>
diff --git a/resources/vue/components/tree/TreeBreadcrumb.vue b/resources/vue/components/tree/TreeBreadcrumb.vue
index 33b04b3..a8c3dd5 100644
--- a/resources/vue/components/tree/TreeBreadcrumb.vue
+++ b/resources/vue/components/tree/TreeBreadcrumb.vue
@@ -10,7 +10,7 @@
<a :href="nodeUrl(ancestor.classname + '_' + ancestor.id)" :ref="ancestor.id"
@click.prevent="openNode(ancestor.id, ancestor.classname)" tabindex="0"
:id="'tree-breadcrumb-' + ancestor.id"
- :title="$gettextInterpolate($gettext('%{ node } öffnen'), { node: ancestor.name})">
+ :title="$gettextInterpolate($gettext('%{ node } öffnen'), { node: ancestor.name}, true)">
{{ ancestor.name }}
</a>
<template v-if="index !== node.attributes.ancestors.length - 1">
diff --git a/resources/vue/components/tree/TreeCourseDetails.vue b/resources/vue/components/tree/TreeCourseDetails.vue
index 609349d..020cc33 100644
--- a/resources/vue/components/tree/TreeCourseDetails.vue
+++ b/resources/vue/components/tree/TreeCourseDetails.vue
@@ -4,14 +4,16 @@
({{ details.semester }})
</div>
<div class="admission-state" v-if="details.admissionstate">
- <studip-icon :shape="details.admissionstate.icon" :role="details.admissionstate.role"
- :title="details.admissionstate.info"></studip-icon>
+ <studip-icon :shape="details.admissionstate.icon"
+ :role="details.admissionstate.role"
+ :alt="details.admissionstate.info"></studip-icon>
</div>
<div class="course-lecturers">
<span v-for="(lecturer, index) in details.lecturers" :key="index">
<a :href="profileUrl(lecturer.username)"
:title="$gettextInterpolate($gettext('Zum Profil von %{ user }'),
- { user: lecturer.name })">
+ { user: lecturer.name }, true)"
+ tabindex="0">
{{ lecturer.name }}
</a><template v-if="details.lecturers.length > 1 && index < details.lecturers.length - 1">, </template>
</span>
diff --git a/resources/vue/components/tree/TreeNodeCoursePath.vue b/resources/vue/components/tree/TreeNodeCoursePath.vue
index 26ab88e..71f69ea 100644
--- a/resources/vue/components/tree/TreeNodeCoursePath.vue
+++ b/resources/vue/components/tree/TreeNodeCoursePath.vue
@@ -1,6 +1,12 @@
<template>
<div>
- <studip-icon shape="info-circle" @click="togglePathInfo"></studip-icon>
+ <button type="button"
+ @click.prevent="togglePathInfo"
+ :title="showPaths
+ ? $gettext('Pfad im Verzeichnis ausblenden')
+ : $gettext('Pfad im Verzeichnis anzeigen')">
+ <studip-icon shape="info-circle"></studip-icon>
+ </button>
<ul v-if="showPaths" class="studip-tree-course-path">
<li v-for="(path, pindex) in paths" :key="pindex">
<button @click.prevent="openNode(path[path.length - 1].id)">
diff --git a/resources/vue/components/tree/TreeNodeTile.vue b/resources/vue/components/tree/TreeNodeTile.vue
index dab2bdf..698cc25 100644
--- a/resources/vue/components/tree/TreeNodeTile.vue
+++ b/resources/vue/components/tree/TreeNodeTile.vue
@@ -1,6 +1,6 @@
<template>
<a :href="url" @click.prevent="openNode" :title="$gettextInterpolate($gettext('Unterebene %{ node } öffnen'),
- { node: node.attributes.name })">
+ { node: node.attributes.name }, true)">
<p class="studip-tree-child-title">
{{ node.attributes.name }}
</p>
diff --git a/resources/vue/components/tree/TreeSearchResult.vue b/resources/vue/components/tree/TreeSearchResult.vue
index e5093eb..9799dae 100644
--- a/resources/vue/components/tree/TreeSearchResult.vue
+++ b/resources/vue/components/tree/TreeSearchResult.vue
@@ -31,13 +31,14 @@
</td>
<td>
<a :href="courseUrl(course.id)"
- :title="$gettextInterpolate($gettext('Zur Veranstaltung %{name}'), {name: + course.attributes.title})">
+ :title="$gettextInterpolate($gettext('Zur Veranstaltung %{title}'), {title: course.attributes.title}, true)"
+ tabindex="0">
<template v-if="course.attributes['course-number']">
{{ course.attributes['course-number'] }}
</template>
{{ course.attributes.title }}
- <div :id="'course-dates-' + course.id" class="course-dates"></div>
</a>
+ <div :id="'course-dates-' + course.id" class="course-dates"></div>
<tree-node-course-path :node-class="searchConfig.classname"
:course-id="course.id"></tree-node-course-path>
</td>
diff --git a/resources/vue/mixins/QuestionnaireComponent.js b/resources/vue/mixins/QuestionnaireComponent.js
new file mode 100644
index 0000000..277f21c
--- /dev/null
+++ b/resources/vue/mixins/QuestionnaireComponent.js
@@ -0,0 +1,24 @@
+export const QuestionnaireComponent = {
+ props: {
+ value: Object
+ },
+ data () {
+ return {val_clone: this.value};
+ },
+ methods: {
+ setDefaultValues(value) {
+ this.val_clone = Object.assign(value, this.value);
+ }
+ },
+ watch: {
+ val_clone: {
+ handler(current) {
+ this.$emit('input', current);
+ },
+ deep: true
+ },
+ value (new_val) {
+ this.val_clone = new_val;
+ }
+ }
+};
diff --git a/resources/vue/mixins/courseware/import.js b/resources/vue/mixins/courseware/import.js
index a7c9420..547280f 100644
--- a/resources/vue/mixins/courseware/import.js
+++ b/resources/vue/mixins/courseware/import.js
@@ -53,7 +53,6 @@ export default {
this.elementCounter = await this.countImportElements([element]);
this.setImportStructuresState('');
this.importElementCounter = 0;
- this.setImportErrors([]);
if (importBehavior === 'default') {
await this.importStructuralElement([element], rootId, files);
@@ -286,8 +285,14 @@ export default {
let new_file = this.file_mapping[files[i].id].new;
let payload = JSON.stringify(block.attributes.payload);
- payload = payload.replaceAll(old_file.id, new_file.id);
- payload = payload.replaceAll(old_file.folder.id, new_file.relationships.parent.data.id);
+ if (new_file) {
+ payload = payload.replaceAll(old_file.id, new_file.id);
+ payload = payload.replaceAll(old_file.folder.id, new_file.relationships.parent.data.id);
+ } else {
+ payload = payload.replaceAll(old_file.id, '');
+ payload = payload.replaceAll(old_file.folder.id, '');
+ }
+
block.attributes.payload = JSON.parse(payload);
}
@@ -395,13 +400,20 @@ export default {
// create new blob with correct type
let filedata = zip_filedata.slice(0, zip_filedata.size, files[i].attributes['mime-type']);
-
- let file = await this.createFile({
- file: files[i],
- filedata: filedata,
- folder: folders[files[i].folder.id]
- });
- this.setImportFilesState(this.$gettext('Erzeuge Datei') + ': ' + files[i].attributes.name);
+ let file = null;
+ try {
+ file = await this.createFile({
+ file: files[i],
+ filedata: filedata,
+ folder: folders[files[i].folder.id]
+ });
+ } catch (error) {
+ this.currentImportErrors.push(this.$gettext('Import einer Datei fehlgeschlagen.'));
+ this.setImportFilesState(this.$gettext('Fehler beim Anlegen der Datei'));
+ }
+ if (file !== null) {
+ this.setImportFilesState(this.$gettext('Erzeuge Datei') + ': ' + files[i].attributes.name);
+ }
this.setImportFilesProgress(parseInt(i / files.length * 100));
//file mapping
diff --git a/resources/vue/store/ContentModulesStore.js b/resources/vue/store/ContentModulesStore.js
index 9dc4609..141a178 100644
--- a/resources/vue/store/ContentModulesStore.js
+++ b/resources/vue/store/ContentModulesStore.js
@@ -52,7 +52,7 @@ export default {
attributes: { value: view === 'tiles' }
};
- return STUDIP.jsonapi.PATCH(`config-values/${documentId}`, { data: { data } }) ;
+ return STUDIP.jsonapi.withPromises().patch(`config-values/${documentId}`, { data: { data } }) ;
},
exchangeModules({ commit, state }, modules) {
const order = modules.filter(module => module.active)
diff --git a/resources/vue/store/MyCoursesStore.js b/resources/vue/store/MyCoursesStore.js
index 08c0389..af8e0fc 100644
--- a/resources/vue/store/MyCoursesStore.js
+++ b/resources/vue/store/MyCoursesStore.js
@@ -71,7 +71,7 @@ export default {
attributes: { value: configValue[configKey] }
};
- return STUDIP.jsonapi.PATCH(`config-values/${documentId}`, { data: { data } })
+ return STUDIP.jsonapi.withPromises().patch(`config-values/${documentId}`, { data: { data } })
},
toggleOpenGroup ({ state, dispatch }, group) {
let open_groups = [ ...state.config.open_groups ];
diff --git a/resources/vue/store/blubber.js b/resources/vue/store/blubber.js
index 9cc474a..d0bd7f6 100644
--- a/resources/vue/store/blubber.js
+++ b/resources/vue/store/blubber.js
@@ -166,7 +166,7 @@ export default {
// if total is missing, there are more comments to fetch
const total = rootGetters['blubber-comments/lastMeta']?.page?.total;
- const hasMore = !total;
+ const hasMore = total ?? true;
commit('setMoreOlder', { id, hasMore });
},
diff --git a/resources/vue/store/courseware/courseware-shelf.module.js b/resources/vue/store/courseware/courseware-shelf.module.js
index dc92a23..d907ee4 100644
--- a/resources/vue/store/courseware/courseware-shelf.module.js
+++ b/resources/vue/store/courseware/courseware-shelf.module.js
@@ -249,33 +249,23 @@ export const actions = {
}
},
async companionInfo({ dispatch }, { info }) {
- await dispatch('setStyleCompanionOverlay', 'default');
- await dispatch('setMsgCompanionOverlay', info);
- return dispatch('setShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'info', message: info});
},
async companionSuccess({ dispatch }, { info }) {
- await dispatch('setStyleCompanionOverlay', 'happy');
- await dispatch('setMsgCompanionOverlay', info);
- return dispatch('setShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'success', message: info});
},
async companionError({ dispatch }, { info }) {
- await dispatch('setStyleCompanionOverlay', 'sad');
- await dispatch('setMsgCompanionOverlay', info);
- return dispatch('setShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'error', message: info});
},
async companionWarning({ dispatch }, { info }) {
- await dispatch('setStyleCompanionOverlay', 'alert');
- await dispatch('setMsgCompanionOverlay', info);
- return dispatch('setShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'warning', message: info});
},
async companionSpecial({ dispatch }, { info }) {
- await dispatch('setStyleCompanionOverlay', 'special');
- await dispatch('setMsgCompanionOverlay', info);
- return dispatch('setShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'info', message: info});
},
coursewareShowCompanionOverlay({dispatch}, { data }) {
return dispatch('setShowCompanionOverlay', data);
@@ -310,7 +300,7 @@ export const actions = {
return dispatch(loadUnits, state.context.id);
},
-
+
async sortUnits({ dispatch, state }, data) {
let loadUnits = null;
if (state.context.type === 'courses') {
@@ -321,7 +311,7 @@ export const actions = {
}
await state.httpClient.post(`${state.context.type}/${state.context.id}/courseware-units/sort`, {data: data});
-
+
return dispatch(loadUnits, state.context.id);
},
diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js
index c44bba1..784b750 100644
--- a/resources/vue/store/courseware/courseware.module.js
+++ b/resources/vue/store/courseware/courseware.module.js
@@ -874,33 +874,23 @@ export const actions = {
},
async companionInfo({ dispatch }, { info }) {
- await dispatch('coursewareStyleCompanionOverlay', 'default');
- await dispatch('coursewareMsgCompanionOverlay', info);
- return dispatch('coursewareShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'info', message: info});
},
async companionSuccess({ dispatch }, { info }) {
- await dispatch('coursewareStyleCompanionOverlay', 'happy');
- await dispatch('coursewareMsgCompanionOverlay', info);
- return dispatch('coursewareShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'success', message: info});
},
async companionError({ dispatch }, { info }) {
- await dispatch('coursewareStyleCompanionOverlay', 'sad');
- await dispatch('coursewareMsgCompanionOverlay', info);
- return dispatch('coursewareShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'error', message: info});
},
async companionWarning({ dispatch }, { info }) {
- await dispatch('coursewareStyleCompanionOverlay', 'alert');
- await dispatch('coursewareMsgCompanionOverlay', info);
- return dispatch('coursewareShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'exception', message: info});
},
async companionSpecial({ dispatch }, { info }) {
- await dispatch('coursewareStyleCompanionOverlay', 'special');
- await dispatch('coursewareMsgCompanionOverlay', info);
- return dispatch('coursewareShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'warning', message: info});
},
// adds a favorite block type using the `type` of the BlockType