diff options
Diffstat (limited to 'resources/assets/javascripts')
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) |
