diff options
Diffstat (limited to 'resources/assets/javascripts/lib')
21 files changed, 379 insertions, 442 deletions
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 } |
