aboutsummaryrefslogtreecommitdiff
path: root/resources/assets/javascripts
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/assets/javascripts
parentda0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff)
parent97a188592c679890a25c37ab78463add76a52ff7 (diff)
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'resources/assets/javascripts')
-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
44 files changed, 734 insertions, 842 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)