diff options
| author | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
|---|---|---|
| committer | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
| commit | 4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch) | |
| tree | 5c07151ae61276d334e88f6309c30d439a85c12e /resources/assets | |
| parent | da0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff) | |
| parent | 97a188592c679890a25c37ab78463add76a52ff7 (diff) | |
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'resources/assets')
77 files changed, 1172 insertions, 1195 deletions
diff --git a/resources/assets/javascripts/bootstrap/application.js b/resources/assets/javascripts/bootstrap/application.js index a9f53df..4ad248f 100644 --- a/resources/assets/javascripts/bootstrap/application.js +++ b/resources/assets/javascripts/bootstrap/application.js @@ -353,6 +353,7 @@ jQuery(document).on('click', 'a[data-behaviour~="ajax-toggle"]', function (event (function ($) { $(document).on('click', 'form[name=course-details] fieldset legend', function () { $('#open_variable').attr('value', $(this).parent('fieldset').data('open')); + $(this).parent('fieldset').attr('aria-expanded', $(this).parent('fieldset').attr('aria-expanded') == 'true' ? 'false' : 'true'); }); }(jQuery)); diff --git a/resources/assets/javascripts/bootstrap/article.js b/resources/assets/javascripts/bootstrap/article.js index fbfc131..04316f5 100644 --- a/resources/assets/javascripts/bootstrap/article.js +++ b/resources/assets/javascripts/bootstrap/article.js @@ -13,6 +13,7 @@ // Open the contentbox article.toggleClass('open').removeClass('new'); + article.attr('aria-expanded', article.attr('aria-expanded') === 'true' ? 'false' : 'true'); }); // Open closed article contents when location hash matches diff --git a/resources/assets/javascripts/bootstrap/clipboard.js b/resources/assets/javascripts/bootstrap/clipboard.js index e525b35..a64a605 100644 --- a/resources/assets/javascripts/bootstrap/clipboard.js +++ b/resources/assets/javascripts/bootstrap/clipboard.js @@ -25,7 +25,9 @@ STUDIP.domReady(function () { jQuery(document).on('click', '.clipboard-remove-button', function (event) { event.preventDefault(); - STUDIP.Dialog.confirm($(this).data('confirm-message'), function() { + + const message = $(this).data('confirm-message'); + STUDIP.Dialog.confirm(message).done(() => { STUDIP.Clipboard.handleRemoveClick(event.target); }); }); @@ -62,10 +64,11 @@ STUDIP.domReady(function () { }); }); - jQuery(document).on('submit', '.clipboard-widget .new-clipboard-form', function (event) { - event.preventDefault(); - STUDIP.Clipboard.handleAddForm(event); - }); + jQuery(document).on( + 'submit', + '.clipboard-widget .new-clipboard-form', + STUDIP.Clipboard.handleAddForm + ); jQuery(document).on('click', '.clipboard-add-item-button', function (event) { event.preventDefault(); diff --git a/resources/assets/javascripts/bootstrap/consultations.js b/resources/assets/javascripts/bootstrap/consultations.js index ef79d9c..51ffa85 100644 --- a/resources/assets/javascripts/bootstrap/consultations.js +++ b/resources/assets/javascripts/bootstrap/consultations.js @@ -10,9 +10,9 @@ $(document).on('click', '.consultation-delete-check:not(.ignore)', event => { } let requests = ids.map(id => { - return STUDIP.jsonapi.GET(`consultation-slots/${id}/bookings`).then(result => result.data.length); + return STUDIP.jsonapi.withPromises().get(`consultation-slots/${id}/bookings`).then(response => response.data.length); }); - $.when(...requests).done((...results) => { + Promise.all(requests).then((...results) => { if (results.some(result => result > 0)) { $(event.target).addClass('ignore').click().removeClass('ignore'); } else { diff --git a/resources/assets/javascripts/bootstrap/contentbox.js b/resources/assets/javascripts/bootstrap/contentbox.js index 42c5df1..3f05331 100644 --- a/resources/assets/javascripts/bootstrap/contentbox.js +++ b/resources/assets/javascripts/bootstrap/contentbox.js @@ -13,5 +13,6 @@ $(document).on('click', 'section.contentbox article header h1 a', function(e) { // Open the contentbox article.toggleClass('open').removeClass('new'); + article.attr('aria-expanded', article.attr('aria-expanded') === 'true' ? 'false' : 'true'); } }); diff --git a/resources/assets/javascripts/bootstrap/copyable_links.js b/resources/assets/javascripts/bootstrap/copyable_links.js index 521eae4..b4f6fc6 100644 --- a/resources/assets/javascripts/bootstrap/copyable_links.js +++ b/resources/assets/javascripts/bootstrap/copyable_links.js @@ -16,24 +16,8 @@ $(document).on('click', 'a.copyable-link', function (event) { document.execCommand('Copy'); dummy.remove(); - // Show visual hint using a deferred (this way we don't need to - // duplicate the functionality in the done() handler) - (new Promise((resolve, reject) => { - let confirmation = $('<div class="copyable-link-confirmation copyable-link-success">'); - confirmation.text($gettext('Link wurde kopiert')); - confirmation.insertBefore('#content'); - - // Resolve deferred when animation has ended or after 2 seconds as a - // fail safe - let timeout = setTimeout(() => { - $(this).parent().off('animationend'); - resolve(confirmation); - }, 1500); - $(this).parent().one('animationend', () => { - clearTimeout(timeout); - resolve(confirmation); - }); - })).then((confirmation, parent) => { - confirmation.remove(); - }); + STUDIP.eventBus.emit( + 'push-system-notification', + { type: 'success', message: $gettext('Link wurde kopiert') } + ); }); diff --git a/resources/assets/javascripts/bootstrap/forms.js b/resources/assets/javascripts/bootstrap/forms.js index c34a0c1..7643612 100644 --- a/resources/assets/javascripts/bootstrap/forms.js +++ b/resources/assets/javascripts/bootstrap/forms.js @@ -1,4 +1,5 @@ import { $gettext, $gettextInterpolate } from '../lib/gettext'; +import Report from '../lib/report.js'; // Allow fieldsets to collapse $(document).on( @@ -291,9 +292,12 @@ STUDIP.ready(function () { url: v.STUDIPFORM_AUTOSAVEURL, data: params, type: 'post', - success() { - if (v.STUDIPFORM_REDIRECTURL) { - window.location.href = v.STUDIPFORM_REDIRECTURL + success(output) { + if (output === 'STUDIPFORM_STORE_SUCCESS' && v.STUDIPFORM_REDIRECTURL) { + //The form has been stored successfully: + window.location.href = v.STUDIPFORM_REDIRECTURL; + } else if (output !== 'STUDIPFORM_STORE_SUCCESS') { + Report.error($gettext('Es ist ein Fehler aufgetreten'), output); } } }); diff --git a/resources/assets/javascripts/bootstrap/global_search.js b/resources/assets/javascripts/bootstrap/global_search.js index 4d0738e..0e179b7 100644 --- a/resources/assets/javascripts/bootstrap/global_search.js +++ b/resources/assets/javascripts/bootstrap/global_search.js @@ -27,15 +27,47 @@ STUDIP.domReady(() => { // Enlarge search input on focus and show hints. $('#globalsearch-input').on('focus', function() { STUDIP.GlobalSearch.toggleSearchBar(true, false); - }); - - // Start search on Enter - $('#globalsearch-input').on('keypress', function(e) { - if (e.which === 13) { + }).on('keypress', (e) => { + // Start search on Enter + if (e.key === 'Enter') { STUDIP.GlobalSearch.doSearch(); return false; } }); + $('#globalsearch-searchbar').on('keydown', function(e) { + if (!['ArrowDown', 'ArrowUp'].includes(e.key)) { + return; + } + + e.preventDefault(); + + // Get all possible items + const items = $('#globalsearch-list [role=listitem]:visible'); + + // Find focussed element + const focussed = items.filter(':focus'); + + // Get index of focussed element in all items + let index = focussed.length > 0 ? items.index(focussed[0]) : null; + + // Move focussed element up or down in items + if (e.key === 'ArrowDown') { + index = (index ?? -1) + 1; + } else { + index = (index ?? items.length) - 1; + } + + // Clamp index to sane boundaries + if (index < 0) { + index = 0; + } else if (index > items.length - 1) { + index = items.length - 1; + } + + // Focus new element by index + items.get(index).focus(); + }); + // Close search on click on page. $('#navigation-level-1, #current-page-structure, #main-footer').on('click', function() { diff --git a/resources/assets/javascripts/bootstrap/resources.js b/resources/assets/javascripts/bootstrap/resources.js index 8c89b7f..7eb6a68 100644 --- a/resources/assets/javascripts/bootstrap/resources.js +++ b/resources/assets/javascripts/bootstrap/resources.js @@ -416,7 +416,7 @@ STUDIP.ready(function () { $("#BookingEndDateInput").prop('defaultValue', $(this).val()); $("#BookingEndDateInput").val($(this).val()).trigger('change'); } - updateRepeatEndSemesterByTimestamp(Math.floor(d / 1000)); + updateRepeatEndSemesterByTimestamp(d); } else if ($(this).attr('id') == 'BookingEndDateInput') { $("#end_date-weekdays span").addClass('invisible'); $("#end_date-weekdays #" + day_numer).removeClass('invisible'); @@ -545,38 +545,41 @@ STUDIP.ready(function () { } ); - function updateRepeatEndSemesterByTimestamp(timestamp, api_url = 'api.php/semesters') { - var semester = null; - jQuery.ajax( - STUDIP.URLHelper.getURL(api_url), - { - method: 'get', - dataType: 'json', - success: function (data) { - if (data) { - Object.values(data.collection).forEach(item => { - if (timestamp >= item.begin && timestamp < item.end) { - semester = item; - } - }); - if (semester) { - $("#semester_course_name").text(semester.title); - $(".semester-time-option").prop('disabled', false); - } else { - if (data.pagination && data.pagination.links.next != api_url) { - semester = updateRepeatEndSemesterByTimestamp(timestamp, data.pagination.links.next); - } else { - $("#semester_course_name").text('außerhalb definierter Zeiten'); - $(".semester-time-option").prop('checked', false); - $(".semester-time-option").prop('disabled', true); - $(".manual-time-option").prop('checked', true); - $(".manual-time-option").trigger('change'); - } - } - } - } + function updateRepeatEndSemesterByTimestamp(timestamp) { + (new Promise((resolve, reject) => { + const cache = STUDIP.Cache.getInstance('jsonapi'); + if (cache.has('semesters')) { + resolve(cache.get('semesters')); + } else { + STUDIP.jsonapi.GET('semesters', { data: { page: { limit: 100000 }}}) + .done(({data}) => { + cache.set('semesters', data); + resolve(data) + }) + .fail(() => { + reject(new Error('Could not load semesters')); + }); + } + })).then(semesters => { + const semester = semesters.find(({attributes}) => { + return new Date(attributes.start) <= timestamp + && timestamp <= new Date(attributes.end); + }); + + if (semester) { + $('#semester_course_name').text(semester.attributes.title); + $('.semester-time-option').prop('disabled', false); + } else { + $('#semester_course_name').text('außerhalb definierter Zeiten'); + $('.semester-time-option').prop({ + checked: false, + disabled: true + }); + $('.manual-time-option') + .prop('checked', true) + .trigger('change'); } - ); + }); } function updateViewURL(defaultView) { diff --git a/resources/assets/javascripts/bootstrap/responsive-navigation.js b/resources/assets/javascripts/bootstrap/responsive-navigation.js index aa81107..ad39d2b 100644 --- a/resources/assets/javascripts/bootstrap/responsive-navigation.js +++ b/resources/assets/javascripts/bootstrap/responsive-navigation.js @@ -1,6 +1,6 @@ import ResponsiveNavigation from '../../../vue/components/responsive/ResponsiveNavigation.vue'; -STUDIP.ready(() => { +STUDIP.domReady(() => { STUDIP.Vue.load().then(({ createApp }) => { createApp({ el: '#responsive-menu', diff --git a/resources/assets/javascripts/bootstrap/search.js b/resources/assets/javascripts/bootstrap/search.js index 6a0559b..7d4fb76 100644 --- a/resources/assets/javascripts/bootstrap/search.js +++ b/resources/assets/javascripts/bootstrap/search.js @@ -2,8 +2,6 @@ STUDIP.domReady(() => { var cache = STUDIP.Search.getCache(); // initially hide all filters except for the semester filter $('#reset-search').hide(); - STUDIP.Search.hideAllFilters(); - $('div#semester_filter').show(); STUDIP.Search.setActiveCategory('show_all_categories'); STUDIP.Search.showActiveFilters(STUDIP.Search.getFilter()); diff --git a/resources/assets/javascripts/bootstrap/studip_helper_attributes.js b/resources/assets/javascripts/bootstrap/studip_helper_attributes.js index c106de3..aea1823 100644 --- a/resources/assets/javascripts/bootstrap/studip_helper_attributes.js +++ b/resources/assets/javascripts/bootstrap/studip_helper_attributes.js @@ -276,6 +276,15 @@ $(document).on('click keydown', '[data-toggles]', function (event) { $(target).toggle(); } + const controls = $(event.currentTarget).attr('aria-controls'); + if (controls) { + // Find elements which control the expanded status of the same element. + const elements = $('[aria-controls="' + controls + '"]'); + const expanded = $(event.currentTarget).attr('aria-expanded') === 'true'; + // Set the aria-expanded status accordingly. + elements.attr('aria-expanded', !expanded); + } + event.preventDefault(); } }); diff --git a/resources/assets/javascripts/bootstrap/system-notifications.js b/resources/assets/javascripts/bootstrap/system-notifications.js new file mode 100644 index 0000000..7a85fcd --- /dev/null +++ b/resources/assets/javascripts/bootstrap/system-notifications.js @@ -0,0 +1,11 @@ +import SystemNotificationManager from '../../../vue/components/SystemNotificationManager.vue'; + +STUDIP.domReady(() => { + document.getElementById('system-notifications')?.classList.add('vueified'); + STUDIP.Vue.load().then(({ createApp }) => { + createApp({ + el: '#system-notifications', + components: { SystemNotificationManager } + }); + }); +}); diff --git a/resources/assets/javascripts/bootstrap/tooltip.js b/resources/assets/javascripts/bootstrap/tooltip.js deleted file mode 100644 index c84042b..0000000 --- a/resources/assets/javascripts/bootstrap/tooltip.js +++ /dev/null @@ -1,67 +0,0 @@ -// Attach global hover handler for tooltips. -// Applies to all elements having a "data-tooltip" attribute. -// Tooltip may be provided in the data-attribute itself or by -// defining a title attribute. The latter is prefered due to -// the obvious accessibility issues. - -var timeout = null; - -STUDIP.Tooltip.threshold = 6; - -$(document).on('mouseenter mouseleave focusin focusout', '[data-tooltip],.tooltip:has(.tooltip-content)', function(event) { - let data = $(this).data(); - - const visible = event.type === 'mouseenter' || event.type === 'focusin'; - const offset = $(this).offset(); - const x = offset.left + $(this).outerWidth(true) / 2; - const y = offset.top; - const delay = data.tooltipDelay ?? 300; - - let content; - let tooltip; - - if (!data.tooltipObject) { - // If tooltip has not yet been created (first hover), obtain it's - // contents and create the actual tooltip object. - if (!data.tooltip || !$.isPlainObject(data.tooltip)) { - content = $('<div/>').text(data.tooltip || $(this).attr('title')).html(); - } else if (data.tooltip.html !== undefined) { - content = data.tooltip.html; - } else if (data.tooltip.text !== undefined) { - content = data.tooltip.text; - } else { - throw "Invalid content for tooltip via data"; - } - if (!content) { - content = $(this).find('.tooltip-content').remove().html(); - } - $(this).attr('title', null); - $(this).attr('data-tooltip', content); - - tooltip = new STUDIP.Tooltip(x, y, content); - - data.tooltipObject = tooltip; - $(this).attr('aria-describedby', tooltip.id); - - $(this).on('remove', function() { - tooltip.remove(); - }); - } else if (visible) { - // If tooltip has already been created, update it's position. - // This is neccessary if the surrounding content is scrollable AND has - // been scrolled. Otherwise the tooltip would appear at it's previous - // and now wrong location. - data.tooltipObject.position(x, y); - } - - if (visible) { - $('.studip-tooltip').not(data.tooltipObject).hide(); - data.tooltipObject.show(); - } else { - timeout = setTimeout(() => data.tooltipObject.hide(), delay); - } -}).on('mouseenter focusin', '.studip-tooltip', () => { - clearTimeout(timeout); -}).on('mouseleave focusout', '.studip-tooltip', function() { - $(this).hide(); -}); diff --git a/resources/assets/javascripts/bootstrap/treeview.js b/resources/assets/javascripts/bootstrap/treeview.js index 998a70e..d132775 100644 --- a/resources/assets/javascripts/bootstrap/treeview.js +++ b/resources/assets/javascripts/bootstrap/treeview.js @@ -1,7 +1,8 @@ import StudipTree from '../../../vue/components/tree/StudipTree.vue' STUDIP.ready(() => { - document.querySelectorAll('[data-studip-tree]').forEach(element => { + document.querySelectorAll('[data-studip-tree]:not(.vueified)').forEach(element => { + element.classList.add('vueified'); STUDIP.Vue.load().then(({ createApp }) => { createApp({ el: element, diff --git a/resources/assets/javascripts/bootstrap/vue.js b/resources/assets/javascripts/bootstrap/vue.js index b8c938d..c6816a2 100644 --- a/resources/assets/javascripts/bootstrap/vue.js +++ b/resources/assets/javascripts/bootstrap/vue.js @@ -28,27 +28,17 @@ STUDIP.ready(() => { }); STUDIP.Vue.load().then(async ({createApp, store}) => { - let vm; if (config.store) { const storeConfig = await import(`../../../vue/store/${config.store}.js`); - console.log('store', storeConfig.default); store.registerModule(config.id, storeConfig.default, {root: true}); Object.keys(data).forEach(command => { store.commit(`${config.id}/${command}`, data[command]); }); - vm = createApp({components}); - } else { - vm = createApp({data, components}); } - // import myCoursesStore from '../stores/MyCoursesStore.js'; - // - // myCoursesStore.namespaced = true; - // - // store.registerModule('my-courses', myCoursesStore); - vm.$mount(this); + createApp({components, data}).$mount(this); }); $(this).attr('data-vue-app-created', ''); diff --git a/resources/assets/javascripts/bootstrap/wysiwyg.js b/resources/assets/javascripts/bootstrap/wysiwyg.js index fb158bb..9e18cf8 100644 --- a/resources/assets/javascripts/bootstrap/wysiwyg.js +++ b/resources/assets/javascripts/bootstrap/wysiwyg.js @@ -6,8 +6,11 @@ STUDIP.domReady(() => { $(document).on('focus blur', '.studip-dialog .ck-editor__editable_inline', function(event) { let height = this.clientHeight; let editor = this.ckeditorInstance; - editor.editing.view.change(writer => { - writer.setStyle('height', height + 'px', editor.editing.view.document.getRoot()); + // this is needed on Chrome, see https://gitlab.studip.de/studip/studip/-/issues/3510 + setTimeout(() => { + editor.editing.view.change(writer => { + writer.setStyle('height', height + 'px', editor.editing.view.document.getRoot()); + }); }); }); diff --git a/resources/assets/javascripts/chunk-loader.js b/resources/assets/javascripts/chunk-loader.js index dcd95cc..f372286 100644 --- a/resources/assets/javascripts/chunk-loader.js +++ b/resources/assets/javascripts/chunk-loader.js @@ -8,95 +8,100 @@ export const loadScript = function (script_name) { }); }; -export const loadChunk = (function () { - let mathjax_promise = null; +let mathjax_promise = null; - return function (chunk) { - let promise = null; - switch (chunk) { +/** This function dynamically loads JS features organized in chunks. + * + * @param {string} chunk The name of the chunk to load. + * @param {{ silent: boolean }} options Options for loading the chunk. + * Pass `{ silent: true }` to supress + * error messages. + * @return {Promise} + */ +export const loadChunk = function (chunk, { silent = false } = {}) { + let promise = null; + switch (chunk) { + case 'code-highlight': + promise = import( + /* webpackChunkName: "code-highlight" */ + './chunks/code-highlight' + ).then(({ default: hljs }) => { + return hljs; + }); + break; - case 'code-highlight': - promise = import( - /* webpackChunkName: "code-highlight" */ - './chunks/code-highlight' - ).then(({default: hljs}) => { - return hljs; - }); - break; + case 'courseware': + promise = Promise.all([ + STUDIP.loadChunk('vue'), + import( + /* webpackChunkName: "courseware" */ + './chunks/courseware' + ), + ]).then(([Vue]) => Vue); + break; - case 'courseware': - promise = Promise.all([ - STUDIP.loadChunk('vue'), - import( - /* webpackChunkName: "courseware" */ - './chunks/courseware' - ), - ]).then(([Vue]) => Vue); - break; + case 'chartist': + promise = import( + /* webpackChunkName: "chartist" */ + './chunks/chartist' + ).then(({ default: Chartist }) => Chartist); + break; - case 'chartist': - promise = import( - /* webpackChunkName: "chartist" */ - './chunks/chartist' - ).then(({ default: Chartist }) => Chartist); - break; + case 'fullcalendar': + promise = import( + /* webpackChunkName: "fullcalendar" */ + './chunks/fullcalendar' + ); + break; - case 'fullcalendar': - promise = import( - /* webpackChunkName: "fullcalendar" */ - './chunks/fullcalendar' - ); - break; + case 'tablesorter': + promise = import( + /* webpackChunkName: "tablesorter" */ + './chunks/tablesorter' + ); + break; - case 'tablesorter': - promise = import( - /* webpackChunkName: "tablesorter" */ - './chunks/tablesorter' - ); - break; - - case 'mathjax': - if (mathjax_promise === null) { - mathjax_promise = STUDIP.loadScript( - 'javascripts/mathjax/MathJax.js?config=TeX-AMS_HTML,default' - ).then(() => { + case 'mathjax': + if (mathjax_promise === null) { + mathjax_promise = STUDIP.loadScript('javascripts/mathjax/MathJax.js?config=TeX-AMS_HTML,default') + .then(() => { (function (origPrint) { window.print = function () { - window.MathJax.Hub.Queue( - ['Delay', window.MathJax.Callback, 700], - origPrint - ); + window.MathJax.Hub.Queue(['Delay', window.MathJax.Callback, 700], origPrint); }; })(window.print); return window.MathJax; - }).catch(() => { - console.log('Could not load mathjax') + }) + .catch(() => { + throw new Error('Could not load mathjax'); }); - } - promise = mathjax_promise; - break; + } + promise = mathjax_promise; + break; - case 'vue': - promise = import( - /* webpackChunkName: "vue.js" */ - './chunks/vue' - ); - break; + case 'vue': + promise = import( + /* webpackChunkName: "vue.js" */ + './chunks/vue' + ); + break; - case 'wysiwyg': - promise = import( - /* webpackChunkName: "wysiwyg.js" */ - './chunks/wysiwyg' - ); - break; + case 'wysiwyg': + promise = import( + /* webpackChunkName: "wysiwyg.js" */ + './chunks/wysiwyg' + ); + break; - default: - promise = Promise.reject(new Error(`Unknown chunk: ${chunk}`)); - } + default: + promise = Promise.reject(new Error(`Unknown chunk: ${chunk}`)); + } - return promise.catch((error) => { + return promise.catch((error) => { + if (!silent) { console.error(`Could not load chunk ${chunk}`, error); - }); - }; -}()); + } + throw error; + }); +}; diff --git a/resources/assets/javascripts/chunks/vue.js b/resources/assets/javascripts/chunks/vue.js index b98cc27..8d506a1 100644 --- a/resources/assets/javascripts/chunks/vue.js +++ b/resources/assets/javascripts/chunks/vue.js @@ -39,6 +39,9 @@ Vue.mixin({ globalOn(...args) { eventBus.on(...args); }, + globalOff(...args) { + eventBus.off(...args); + }, getStudipConfig: store.getters['studip/getConfig'] }, }); diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js index 73b1aaa..5f88c30 100644 --- a/resources/assets/javascripts/entry-base.js +++ b/resources/assets/javascripts/entry-base.js @@ -16,6 +16,8 @@ import "./init.js" import "./bootstrap/responsive.js" import "./bootstrap/vue.js" +import "./bootstrap/system-notifications.js" + import "./bootstrap/my-courses.js"; import "./studip-ui.js" @@ -55,7 +57,6 @@ import "./bootstrap/article.js" import "./bootstrap/copyable_links.js" import "./bootstrap/selection.js" import "./bootstrap/data_secure.js" -import "./bootstrap/tooltip.js" import "./bootstrap/lightbox.js" import "./bootstrap/application.js" import "./bootstrap/global_search.js" diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js index 2d592be..1d7d5ac 100644 --- a/resources/assets/javascripts/init.js +++ b/resources/assets/javascripts/init.js @@ -38,7 +38,7 @@ import HeaderMagic from './lib/header_magic.js'; import i18n from './lib/i18n.js'; import Instschedule from './lib/instschedule.js'; import InlineEditing from './lib/inline-editing.js'; -import JSONAPI, { jsonapi } from './lib/jsonapi.js'; +import JSONAPI, { jsonapi } from './lib/jsonapi.ts'; import JSUpdater from './lib/jsupdater.js'; import Lightbox from './lib/lightbox.js'; import Markup from './lib/markup.js'; @@ -64,7 +64,6 @@ import register from './lib/register.js'; import Report from './lib/report.js'; import Resources from './lib/resources.js'; import Responsive from './lib/responsive.js'; -import RESTAPI, { api } from './lib/restapi.js'; import Schedule from './lib/schedule.js'; import Screenreader from './lib/screenreader.js'; import Scroll from './lib/scroll.js'; @@ -76,7 +75,6 @@ import Statusgroups from './lib/statusgroups.js'; import study_area_selection from './lib/study_area_selection.js'; import Table from './lib/table.js'; import TableOfContents from './lib/table-of-contents.js'; -import Tooltip from './lib/tooltip.js'; import Tour from './lib/tour.js'; import * as Gettext from './lib/gettext'; import UserFilter from './lib/user_filter.js'; @@ -93,7 +91,6 @@ window.STUDIP = _.assign(window.STUDIP || {}, { admin_sem_class, AdminCourses, Admission, - api, Arbeitsgruppen, Archive, Avatar, @@ -152,7 +149,6 @@ window.STUDIP = _.assign(window.STUDIP || {}, { register, Report, Responsive, - RESTAPI, Schedule, Scroll, Screenreader, @@ -164,7 +160,6 @@ window.STUDIP = _.assign(window.STUDIP || {}, { study_area_selection, Table, TableOfContents, - Tooltip, Tour, URLHelper, UserFilter, diff --git a/resources/assets/javascripts/lib/RestrictedDatesHelper.ts b/resources/assets/javascripts/lib/RestrictedDatesHelper.ts new file mode 100644 index 0000000..bcc0af2 --- /dev/null +++ b/resources/assets/javascripts/lib/RestrictedDatesHelper.ts @@ -0,0 +1,89 @@ +import { jsonapi } from "./jsonapi"; + +type RestrictedDate = { + year: Number, + month: Number, + day: Number, + + reason: string | null, + lock: boolean +} + +class RestrictedDatesHelper +{ + static #loadedYears : Number[] = []; + static #restrictedDates: RestrictedDate[] = []; + + static isDateRestricted(date: Date, returnBoolean: Boolean = false): RestrictedDate | Boolean { + const restrictedDate : RestrictedDate | undefined = this.#restrictedDates.find(item => { + return item.year === date.getFullYear() + && item.month === date.getMonth() + 1 + && item.day === date.getDate(); + }); + + if (returnBoolean) { + return !!restrictedDate; + } + + return restrictedDate ?? this.#convertDate(date, null, false); + } + + static async loadRestrictedDatesByYear(year: Number): Promise<void> { + if (this.#loadedYears.includes(year)) { + return Promise.reject(); + } + + this.#loadedYears.push(year); + + jsonapi.withPromises().request('holidays', {data: { + 'filter[year]': year + }}).then((response: [] | Object) => { + // Since PHP will return an empty object as an array, + // we need to check + if (Array.isArray(response)) { + return; + } + + for (const [date, data] of Object.entries(response)) { + this.#addRestrictedDate( + new Date(date), + data.holiday, + data.mandatory + ); + } + }); + } + + static #addRestrictedDate(date: Date, reason: string, lock: boolean = true): void { + const restricted = this.#convertDate(date, reason, lock); + + this.#restrictedDates = this.#restrictedDates.filter(item => { + return item.year !== restricted.year + || item.month !== restricted.month + || item.day !== restricted.day; + }); + + this.#restrictedDates.push(restricted); + } + + static removeRestrictedDate(date: Date): void { + this.#restrictedDates = this.#restrictedDates.filter(item => { + return item.year !== date.getFullYear() + || item.month !== date.getMonth() + 1 + || item.day !== date.getDate(); + }); + } + + static #convertDate(date: Date, reason: string | null, lock: boolean): RestrictedDate { + return { + year: date.getFullYear(), + month: date.getMonth() + 1, + day: date.getDate(), + + reason, + lock + }; + } +} + +export default RestrictedDatesHelper; diff --git a/resources/assets/javascripts/lib/abstract-api.js b/resources/assets/javascripts/lib/abstract-api.js index eafca85..95ae015 100644 --- a/resources/assets/javascripts/lib/abstract-api.js +++ b/resources/assets/javascripts/lib/abstract-api.js @@ -1,5 +1,20 @@ import Overlay from './overlay.js'; +class APIError extends Error +{ + static createWithJqXhr(message, jqXhr) { + const error = new APIError(message); + error.setJqXhr(jqXhr); + return error; + } + + jqXhr = null; + + setJqXhr(jqXhr) { + this.jqXhr = jqXhr; + } +} + class AbstractAPI { static get supportedMethods() { @@ -52,6 +67,8 @@ class AbstractAPI var deferred; + const request = this.#createRequest(url, options); + if (options.async && this.request_count > 0) { // Request should be sent asynchronous after every other request // is finished. The configuration for this particular request is @@ -73,10 +90,10 @@ class AbstractAPI this.total_requests += 1; // Actual request - deferred = $.ajax(STUDIP.URLHelper.getURL(`${this.base_url}/${url}`, {}, true), { + deferred = $.ajax(request.url, { contentType: options.contentType || 'application/x-www-form-urlencoded; charset=UTF-8', method: options.method.toUpperCase(), - data: this.encodeData(options.data, options.method.toUpperCase()), + data: this.encodeData(request.data, options.method.toUpperCase()), headers: options.headers }).always(() => { // Decrease request counter, remove overlay if neccessary @@ -93,6 +110,54 @@ class AbstractAPI } }).promise(); } + + #createRequest(url, options) { + const hasBody = ['post', 'put', 'patch'].includes(options.method.toLowerCase()); + const query = hasBody ? '' : `?${this.convertDataToRequestParameters(options.data)}`; + + return { + url: STUDIP.URLHelper.getURL(`${this.base_url}/${url}${query}`, {}, true), + data: hasBody ? options.data : {}, + }; + } + + convertDataToRequestParameters(data, prefix = '') { + return Object.entries(data).filter(([key, value]) => { + return value !== null; + }).map(([key, value]) => { + const name = prefix ? `${prefix}[${key}]` : `${key}`; + if (value.constructor?.name === 'Object') { + return this.convertDataToRequestParameters(value, name); + } else { + return `${name}=${value}`; + } + }).join('&'); + } + + withPromises() { + return new Proxy(this, { + get(target, prop, receiver) { + // This will allow http methods to be written as lowercase when called as methods + // (e.g. api.patch() instead of api.PATCH()) + if (target[prop] === undefined && AbstractAPI.supportedMethods.includes(prop.toUpperCase())) { + prop = prop.toUpperCase(); + } + + // Only handle calls to request methods + if (prop !== 'request') { + return Reflect.get(target, prop, receiver); + } + + // Return a wrapped promise that handles the deferred + return (url, options = {}) => new Promise((resolve, reject) => { + target[prop].apply(target, [url, options]).then( + (response) => resolve(response), + (jqXhr, textStatus, errorThrown) => reject(APIError.createWithJqXhr(errorThrown || textStatus, jqXhr)) + ); + }); + } + }) + } } // Create shortcut methods for easier access by method diff --git a/resources/assets/javascripts/lib/activityfeed.js b/resources/assets/javascripts/lib/activityfeed.js index 74c27f9..12f0bac 100644 --- a/resources/assets/javascripts/lib/activityfeed.js +++ b/resources/assets/javascripts/lib/activityfeed.js @@ -6,13 +6,13 @@ const ActivityFeed = { maxheight: null, filter: null, - init: function() { + init() { STUDIP.ActivityFeed.maxheight = parseInt($('#stream-container').css('max-height').replace(/[^-\d.]/g, '')); STUDIP.ActivityFeed.loadFeed(STUDIP.ActivityFeed.filter); - $('#stream-container').scroll(function () { - var scrollBottom = $('#stream-container').scrollTop() + $('#stream-container').height() + 250; + $('#stream-container').scroll(() => { + const scrollBottom = $('#stream-container').scrollTop() + $('#stream-container').height() + 250; if ($('#stream-container').prop('scrollHeight') < scrollBottom) { STUDIP.ActivityFeed.loadFeed(STUDIP.ActivityFeed.filter); @@ -23,7 +23,7 @@ const ActivityFeed = { $(document).on('click', '.provider_circle', function () { $(this).parent().parent().children('.activity-content').toggle(); }).on('click', '#toggle-all-activities,#toggle-user-activities', function () { - var toggled = $(this).is(':not(.toggled)'); + const toggled = $(this).is(':not(.toggled)'); $(this).toggleClass('toggled', toggled); STUDIP.ActivityFeed.setToggleStatus(); @@ -32,11 +32,11 @@ const ActivityFeed = { }); }, - getTemplate: _.memoize(function(name) { - return _.template($("script." + name).html()); + getTemplate: _.memoize(name => { + return _.template($(`script.${name}`).html()); }), - loadFeed: function(filtertype) { + loadFeed(filtertype) { if (STUDIP.ActivityFeed.user_id === null) { console.log('Could not retrieve activities, no valid user id found!'); return false; @@ -48,17 +48,18 @@ const ActivityFeed = { STUDIP.ActivityFeed.polling = true; - STUDIP.api.GET(['user', STUDIP.ActivityFeed.user_id, 'activitystream'], { - data: { - filtertype: JSON.stringify(filtertype), - scrollfrom: STUDIP.ActivityFeed.scrolledfrom - } - }).done(function (activities) { - var stream = STUDIP.ActivityFeed.getTemplate('activity_stream'); - var activity = STUDIP.ActivityFeed.getTemplate('activity'); - var activity_urls = STUDIP.ActivityFeed.getTemplate('activity-urls'); - var num_entries = Object.keys(activities).length; - var lastelem = $(activities).last(); + const url = STUDIP.URLHelper.getURL('dispatch.php/activityfeed/load', { + filtertype: JSON.stringify(filtertype), + scrollfrom: STUDIP.ActivityFeed.scrolledfrom, + }); + fetch(url).then( + response => response.json(), + ).then(activities => { + const stream = STUDIP.ActivityFeed.getTemplate('activity_stream'); + const activity = STUDIP.ActivityFeed.getTemplate('activity'); + const activity_urls = STUDIP.ActivityFeed.getTemplate('activity-urls'); + const num_entries = Object.keys(activities).length; + const lastelem = $(activities).last(); if (lastelem[0]) { STUDIP.ActivityFeed.scrolledfrom = lastelem[0].mkdate; @@ -79,15 +80,15 @@ const ActivityFeed = { if ($('#stream-container').height() < STUDIP.ActivityFeed.maxheight) { STUDIP.ActivityFeed.loadFeed(''); } - }).fail(function () { - var template = STUDIP.ActivityFeed.getTemplate('activity-load-error'); + }).catch(() => { + const template = STUDIP.ActivityFeed.getTemplate('activity-load-error'); STUDIP.ActivityFeed.writeToStream(template()); - }).always(function () { + }).finally(() => { STUDIP.ActivityFeed.polling = false; }); }, - writeToStream: function (html) { + writeToStream(html) { if (STUDIP.ActivityFeed.initial) { // replace data in DOM $('#stream-container').html(''); @@ -98,9 +99,9 @@ const ActivityFeed = { $('#stream-container').append(html); }, - setToggleStatus: function() { - var show_details = $('#toggle-all-activities').is('.toggled'), - show_own = $('#toggle-user-activities').is('.toggled'); + setToggleStatus() { + const show_details = $('#toggle-all-activities').is('.toggled'); + const show_own = $('#toggle-user-activities').is('.toggled'); // update toggle status fir activity contents $('.activity-content').toggle(show_details); @@ -109,7 +110,7 @@ const ActivityFeed = { $('.activity:has(.provider_circle.right)').toggle(show_own); }, - updateFilter: function(filter) { + updateFilter(filter) { STUDIP.ActivityFeed.filter = filter; STUDIP.ActivityFeed.initial = true; STUDIP.ActivityFeed.scrolledfrom = Math.floor(Date.now() / 1000); diff --git a/resources/assets/javascripts/lib/blubber.js b/resources/assets/javascripts/lib/blubber.js index 3e93985..4e724b4 100644 --- a/resources/assets/javascripts/lib/blubber.js +++ b/resources/assets/javascripts/lib/blubber.js @@ -47,6 +47,7 @@ const Blubber = { subscribe: follow, }); }).then(() => { + elements.attr('aria-pressed', follow ? 'true' : 'false'); elements.toggleClass('unfollowed', !follow); }).finally(() => { elements.removeClass('loading'); diff --git a/resources/assets/javascripts/lib/clipboard.js b/resources/assets/javascripts/lib/clipboard.js index e5890ab..6fd1489 100644 --- a/resources/assets/javascripts/lib/clipboard.js +++ b/resources/assets/javascripts/lib/clipboard.js @@ -1,4 +1,14 @@ -import {$gettext} from './gettext'; +function extractAttribute(node, attribute) { + return node.querySelector(`input[name="${attribute}"]`)?.value.trim(); +} + +function extractAttributes(node, attributes) { + const result = {}; + for (const key of attributes) { + result[key] = extractAttribute(node, key); + } + return result; +} const Clipboard = { switchClipboard: function(event) { @@ -32,32 +42,30 @@ const Clipboard = { } }, - handleAddForm: function(event) { - if (!event) { - return false; - } - + handleAddForm(event) { + event.preventDefault(); + const attributes = extractAttributes(event.target, ['name', 'allowed_item_class']); //Check if a name is entered in the form: - let name_input = jQuery(event.target).find('input[type="text"][name="name"]'); + const name_input = event.target.querySelector('input[name="name"]'); if (!name_input) { //Something is wrong with the HTML: return false; } - let name = jQuery(name_input).val().trim(); - if (!name) { + if (!attributes.name) { //The name field is empty. Why send an empty field? return false; } - //Submit the form via AJAX: - STUDIP.api.POST( - 'clipboard/add', - { - data: jQuery(event.target).serialize() - } - ).done(STUDIP.Clipboard.add); + // Submit the form via AJAX: + STUDIP.jsonapi.POST('clipboards', {data: {data: {attributes}}}).done(({data}) => { + STUDIP.Clipboard.add({ + id: data.id, + name: data.attributes.name, + widget_id: extractAttribute(event.target, 'widget_id') + }); + }); }, add: function(data) { @@ -134,11 +142,9 @@ const Clipboard = { jQuery(widget_node).find('#clipboard-group-container').removeClass('invisible'); //Call the droppable jQuery method on the new clipboard area: - jQuery(clipboard_node).droppable( - { - drop: STUDIP.Clipboard.handleItemDrop - } - ); + jQuery(clipboard_node).droppable({ + drop: STUDIP.Clipboard.handleItemDrop + }); //Clear the text input in the "add clipboard" form: jQuery(widget_node).find( @@ -238,17 +244,19 @@ const Clipboard = { } //Add the item to the clipboard via AJAX: - STUDIP.api.POST( - 'clipboard/' + clipboard_id + '/item', - { + STUDIP.jsonapi.POST(`clipboards/${clipboard_id}/items`, { + data: { data: { - 'range_id': range_id, - 'range_type': range_type, - 'widget_id': widget_id + attributes: { range_id, range_type } } } - ).done(function(data) { - STUDIP.Clipboard.addDroppedItem(data); + }).done(({data}) => { + STUDIP.Clipboard.addDroppedItem({ + id: data.id, + name: data.attributes.name, + range_id: data.attributes.range_id, + widget_id + }); }); }, @@ -263,6 +271,7 @@ const Clipboard = { let widget = jQuery('#ClipboardWidget_' + response_data['widget_id']); let clipboard_id = jQuery(widget).find(".clipboard-selector").val(); + if (!widget) { //The widget with the speicified widget-ID //is not present on the current page. @@ -302,7 +311,6 @@ const Clipboard = { jQuery(new_item_node).removeClass('invisible'); let name_column = jQuery(new_item_node).find('td.item-name'); - console.log(name_column); jQuery('<span/>').text(response_data['name']).appendTo(name_column) let id_field = jQuery(new_item_node).find("input[name='selected_clipboard_items[]']"); jQuery(id_field).val(checkbox_id); @@ -325,25 +333,16 @@ const Clipboard = { ); }, - rename: function(widget_id) { - if (!widget_id) { - //Required data are missing! - return; - } + rename(widget_id) { + const widget = jQuery('#ClipboardWidget_' + widget_id); + const clipboard_id = widget.find('.clipboard-selector').val(); + const name = widget.find('input.clipboard-name').val(); - let widget = jQuery('#ClipboardWidget_' + widget_id); - let clipboard_id = jQuery(widget).find(".clipboard-selector").val(); - let namer = jQuery(widget).find("input.clipboard-name"); - - STUDIP.api.PUT( - 'clipboard/' + clipboard_id, - { - data: { - name: namer.val() - } - } - ).done(function(data) { - STUDIP.Clipboard.update(data, widget_id) + STUDIP.jsonapi.PATCH(`clipboards/${clipboard_id}`, {data: {data: {attributes: {name}}}}).done(({data}) => { + STUDIP.Clipboard.update({ + id: data.id, + name: data.attributes.name, + }, widget_id) }); }, @@ -358,7 +357,7 @@ const Clipboard = { STUDIP.Clipboard.toggleEditButtons(widget_id); }, - remove: function(clipboard_id, widget_id) { + remove(clipboard_id, widget_id) { if (!clipboard_id || !widget_id) { //Required data are missing! return; @@ -427,10 +426,6 @@ const Clipboard = { }, handleRemoveClick: function(delete_icon) { - if (!delete_icon) { - return; - } - //Get the data of the clipboard: let clipboard_select = jQuery(delete_icon).siblings('.clipboard-selector')[0]; if (!clipboard_select) { @@ -444,52 +439,42 @@ const Clipboard = { //Another case where something is wrong with the HTML. return; } - let widget_id = jQuery(widget).data('widget_id'); - STUDIP.api.DELETE( - 'clipboard/' + clipboard_id, - { - data: { - widget_id: widget_id - } - } - ).done(function() { + const widget_id = jQuery(widget).data('widget_id'); + + STUDIP.jsonapi.DELETE(`clipboards/${clipboard_id}`).done(() => { STUDIP.Clipboard.remove(clipboard_id, widget_id); }); }, - removeItem: function(delete_icon) { - if (!delete_icon) { - return; - } - - //Get the item-ID: - let item_html = jQuery(delete_icon).parents('tr'); - let range_id = jQuery(item_html).data('range_id'); - let clipboard_element = jQuery(item_html).parents('table'); - let clipboard_id = jQuery(clipboard_element).data('id'); + removeItem(delete_icon) { + // Get the item-ID: + const item_element = jQuery(delete_icon).parents('tr'); + const range_id = jQuery(item_element).data('range_id'); + const clipboard_element = jQuery(item_element).parents('table'); + const clipboard_id = jQuery(clipboard_element).data('id'); if (!range_id || !clipboard_id) { //We cannot proceed without the item-ID and the clipboard-ID! return; } - STUDIP.api.DELETE( - 'clipboard/' + clipboard_id + '/item/' + range_id - ).done(function() { + STUDIP.jsonapi.DELETE(`clipboards/${clipboard_id}/items`, { + data: { + filter: { range_id } + } + }).done(() => { //Check if the item has siblings: - let siblings = jQuery(item_html).siblings(); + let siblings = item_element.siblings(); if (siblings.length < 3) { //Only the "no items" element and the template //are siblings of the item. //We must display the "no items" element: - jQuery(item_html).siblings( - '.empty-clipboard-message' - ).removeClass('invisible'); + item_element.siblings('.empty-clipboard-message').removeClass('invisible'); jQuery("#clipboard-group-container").find('.widget-links').addClass('invisible'); } //Finally remove the item: - jQuery(item_html).remove(); + item_element.remove(); }); }, diff --git a/resources/assets/javascripts/lib/dialog.js b/resources/assets/javascripts/lib/dialog.js index b5cab54..8f6e50a 100644 --- a/resources/assets/javascripts/lib/dialog.js +++ b/resources/assets/javascripts/lib/dialog.js @@ -386,7 +386,12 @@ Dialog.show = function(content, options = {}) { .before(element); } - $(this).parent().find('.ui-dialog-title').attr('title', options.title); + $(this).parent().find('.ui-dialog-title').attr({ + title: options.title, + role: 'heading', + 'aria-level': 2 + }); + $(this).parents('.studip-dialog').attr('aria-modal', 'true'); instance.open = true; // Execute scripts diff --git a/resources/assets/javascripts/lib/extract_callback.js b/resources/assets/javascripts/lib/extract_callback.js index bf7ac79..a630275 100644 --- a/resources/assets/javascripts/lib/extract_callback.js +++ b/resources/assets/javascripts/lib/extract_callback.js @@ -56,8 +56,9 @@ export default function extractCallback(cmd, payload, root = window) { } } - if (callback[chunk] === undefined) { - throw 'Error: Undefined callback ' + cmd; + if (callback === null || callback[chunk] === undefined) { + console.log('Error: Undefined callback ' + cmd); + return; } if (typeof callback[chunk] === 'function' && parameters !== null) { diff --git a/resources/assets/javascripts/lib/fullcalendar.js b/resources/assets/javascripts/lib/fullcalendar.js index 49274f4..5b7d032 100644 --- a/resources/assets/javascripts/lib/fullcalendar.js +++ b/resources/assets/javascripts/lib/fullcalendar.js @@ -622,8 +622,13 @@ class Fullcalendar $('.fc-slats tr:odd .fc-widget-content:not(.fc-axis)').remove(); } - STUDIP.api.GET(`semester/${timestamp}/week`).done((data) => { + if (document.getElementById('booking-plan-header-semname') === null) { + return; + } + $.getJSON( + STUDIP.URLHelper.getURL(`dispatch.php/resources/ajax/semester_week/${timestamp}`) + ).done((data) => { if (data) { $('#booking-plan-header-semname').text(data.semester_name); if (data.sem_week) { @@ -640,7 +645,7 @@ class Fullcalendar $('#booking-plan-header-semrow').hide(); $('#booking-plan-header-semweek-part').hide(); } - }) + }); }, resourceRender (renderInfo) { if ($(renderInfo.view.context.calendar.el).hasClass('room-group-booking-plan')) { @@ -731,12 +736,31 @@ class Fullcalendar //Get the timestamp: let timestamp = changedMoment.getTime() / 1000; - jQuery('a.resource-bookings-actions').each(function () { + jQuery('a.resource-bookings-actions, a.calendar-action').each(function () { const url = new URL(this.href); - url.searchParams.set('timestamp', timestamp) + url.searchParams.set('timestamp', timestamp.toString()) url.searchParams.set('defaultDate', changed_date) this.href = url.toString(); }); + jQuery('.sidebar-widget.calendar-action').each(function() { + //Each sidebar widget is different. The placement of the defaultDate URL parameter + //has to reflect that. + jQuery(this).find('button[formaction]').each(function() { + //Modify the formaction attribute: + let url = new URL(jQuery(this).attr('formaction')); + url.searchParams.set('defaultDate', changed_date); + jQuery(this).attr('formaction', url.toString()); + }); + jQuery(this).find('form[action]').each(function() { + //Add a hidden input with the defaultDate: + let hidden_input = jQuery(this).find('input[name="defaultDate"]')[0]; + if (!hidden_input) { + hidden_input = jQuery('<input type="hidden" name="defaultDate">'); + jQuery(this).append(hidden_input); + } + jQuery(hidden_input).val(changed_date); + }); + }); // Now change the URL of the window. const url = new URL(window.location.href); diff --git a/resources/assets/javascripts/lib/global_search.js b/resources/assets/javascripts/lib/global_search.js index dbd045b..394b7e3 100644 --- a/resources/assets/javascripts/lib/global_search.js +++ b/resources/assets/javascripts/lib/global_search.js @@ -9,6 +9,7 @@ const GlobalSearch = { */ toggleSearchBar: function(visible, cleanup) { $('#globalsearch-searchbar').toggleClass('is-visible', visible); + $('#globalsearch-input').attr('aria-expanded', visible ? 'true' : 'false'); $('#globalsearch-input').toggleClass('hidden-small-down', !visible); $('#globalsearch-icon').toggleClass('hidden-small-down', visible); $('#globalsearch-clear').toggleClass('hidden-small-down', !visible); @@ -70,7 +71,7 @@ const GlobalSearch = { // Iterate over each result category. $.each(json, function(name, value) { // Create an <article> for category. - var category = $(`<article id="globalsearch-${name}">`), + var category = $(`<article id="globalsearch-${name}" role="list">`), header = $('<header>').appendTo(category), counter = 0; @@ -96,7 +97,7 @@ const GlobalSearch = { // Process results and create corresponding entries. $.each(value.content, function(index, result) { // Create single result entry. - var single = $('<section>'), + var single = $(`<a href="${result.url}" role="listitem" ${dataDialog}>`), data = $('<div class="globalsearch-result-data">'), details = $('<div class="globalsearch-result-details">'); @@ -107,17 +108,17 @@ const GlobalSearch = { // Which result types should be opened via dialog? const openInDialog = ['GlobalSearchFiles', 'GlobalSearchMessages']; var dataDialog = (openInDialog.indexOf(name) >= 0 ? dataDialog = 'data-dialog' : dataDialog = ''); - var link = $(`<a href="${result.url}" ${dataDialog}>`).appendTo(single); + //var link = $(`<a href="${result.url}" ${dataDialog}>`).appendTo(single); // Optional image... if (result.img !== null) { - $(`<img src="${result.img}">`) + $(`<img src="${result.img}" alt="">`) .wrap('<div class="globalsearch-result-img">') .parent() // Element is now the wrapper - .appendTo(link); + .appendTo(single); } - link.append(data); + single.append(data); // Name/title $('<div class="globalsearch-result-title">') @@ -144,7 +145,7 @@ const GlobalSearch = { if (result.date !== null) { $('<div class="globalsearch-result-time">') .html(result.date) - .appendTo(link); + .appendTo(single); } // "Expand" attribute for further, result-related search @@ -178,6 +179,7 @@ const GlobalSearch = { GlobalSearch.lastSearch = null; $('#globalsearch-searchbar').removeClass('is-visible has-value'); + $('#globalsearch-input').attr('aria-expanded', 'false'); $('#globalsearch-input').val(''); $('#globalsearch-results').html(''); $('#globalsearch-input').focus(); diff --git a/resources/assets/javascripts/lib/header_magic.js b/resources/assets/javascripts/lib/header_magic.js index f465e7e..a581107 100644 --- a/resources/assets/javascripts/lib/header_magic.js +++ b/resources/assets/javascripts/lib/header_magic.js @@ -17,7 +17,7 @@ const scroll = function(scrolltop) { const HeaderMagic = { enable() { fold = $('#navigation-level-1').height(); - Scroll.addHandler('header', scroll); + Scroll.addHandler('header', scroll, true); }, disable() { Scroll.removeHandler('header'); diff --git a/resources/assets/javascripts/lib/jsonapi.js b/resources/assets/javascripts/lib/jsonapi.ts index f3217bc..80176cc 100644 --- a/resources/assets/javascripts/lib/jsonapi.js +++ b/resources/assets/javascripts/lib/jsonapi.ts @@ -3,11 +3,11 @@ import AbstractAPI from './abstract-api.js'; // Actual JSONAPI object class JSONAPI extends AbstractAPI { - constructor(version = 1) { + constructor(version: number = 1) { super(`jsonapi.php/v${version}`); } - encodeData (data, method) { + encodeData (data: any, method: string): any { data = super.encodeData(data); if (['DELETE', 'GET', 'HEAD'].includes(method)) { @@ -21,11 +21,11 @@ class JSONAPI extends AbstractAPI return JSON.stringify(data); } - request (url, options = {}) { + request (url: string, options: any = {}) { options.contentType = 'application/vnd.api+json'; return super.request(url, options); } } export default JSONAPI; -export const jsonapi = new JSONAPI(); +export const jsonapi: JSONAPI = new JSONAPI(); diff --git a/resources/assets/javascripts/lib/messages.js b/resources/assets/javascripts/lib/messages.js index 7ce5328..8e27f8f 100644 --- a/resources/assets/javascripts/lib/messages.js +++ b/resources/assets/javascripts/lib/messages.js @@ -252,6 +252,8 @@ const Messages = { if (jQuery('#' + name).is(':visible')) { jQuery('#' + name)[0].scrollIntoView(false); } + jQuery('#toggle-' + name) + .attr('aria-expanded', jQuery('#toggle-' + name).attr('aria-expanded') !== 'true'); } }; diff --git a/resources/assets/javascripts/lib/personal_notifications.js b/resources/assets/javascripts/lib/personal_notifications.js index 90f1053..392e8b0 100644 --- a/resources/assets/javascripts/lib/personal_notifications.js +++ b/resources/assets/javascripts/lib/personal_notifications.js @@ -116,6 +116,11 @@ const PersonalNotifications = { .click(STUDIP.PersonalNotifications.activate); } } + + // Special handling for personal notifications: + $('#notification-container').on('mouseover mouseout', function (event) { + $(this).attr('aria-expanded', $(this).attr('aria-expanded') === 'true' ? 'false' : 'true'); + }); }, activate () { Promise.resolve(Notification.requestPermission()).then(permission => { diff --git a/resources/assets/javascripts/lib/questionnaire.js b/resources/assets/javascripts/lib/questionnaire.js index 2bca8c6..9a89348 100644 --- a/resources/assets/javascripts/lib/questionnaire.js +++ b/resources/assets/javascripts/lib/questionnaire.js @@ -86,7 +86,7 @@ const Questionnaire = { } $.post(STUDIP.URLHelper.getURL('dispatch.php/questionnaire/store/' + (this.data.id || '')), { questionnaire: data, - questions_data: questions, + questions_data: JSON.stringify(questions), range_type: this.range_type, range_id: this.range_id }).done(() => { @@ -112,7 +112,7 @@ const Questionnaire = { id: id, questiontype: this.questions[i].questiontype, internal_name: this.questions[i].internal_name, - questiondata: Object.assign({}, this.questions[i].questiondata) + questiondata: JSON.parse(JSON.stringify(this.questions[i].questiondata)), }); this.activeTab = id; }, diff --git a/resources/assets/javascripts/lib/resources.js b/resources/assets/javascripts/lib/resources.js index 3287b42..6ff4156 100644 --- a/resources/assets/javascripts/lib/resources.js +++ b/resources/assets/javascripts/lib/resources.js @@ -50,7 +50,7 @@ class Resources jQuery(row_tds[user_td_index]).children('input').removeAttr('disabled'); if (username) { - jQuery(row_tds[user_td_index]).append(username); + jQuery('<span>').text(username).appendTo(row_tds[user_td_index]); } else { jQuery(row_tds[user_td_index]).append('ID ' + user_id); } @@ -60,8 +60,6 @@ class Resources } jQuery(user_id_input).val(user_id); - var perm_select = jQuery(row_tds[user_td_index + 1]).children()[0]; - if (temp_perms_row) { //Set the time input fields to useful values: @@ -134,22 +132,19 @@ class Resources jQuery(table_element).trigger('update'); }; - STUDIP.api.GET( - `user/${user_id}` - ).done(function (data) { - var username = data.name.family - + ', ' - + data.name.given; - if (data.name.prefix) { - username += ', ' + data.name.prefix; + STUDIP.jsonapi.GET(`users/${user_id}`).done(data => { + const attributes = data.data.attributes; + + let username = `${attributes['family-name']}, ${attributes['given-name']}`; + if (attributes['name-prefix']) { + username += `, ${attributes['name-prefix']}`; } - if (data.name.suffix) { - username += ' ' + data.name.suffix; + if (attributes['name-suffix']) { + username += ` ${attributes['name-suffix']}`; } - username += ' (' + data.name.username + ')' - + ' (' + data.perms + ')'; + username += ` (${attributes.username}) (${attributes.permission})`; insert_function(user_id, username); - }).fail(function () { + }).fail(() => { insert_function(user_id); }); } @@ -160,23 +155,13 @@ class Resources return; } - STUDIP.api.GET( - `course/${course_id}/members`, - { - data: { - //The limit '0' results in a division by zero. - //Hopefully, the limit is set to a value high enough: - limit: 1000000 - } - } - ).done(function (data) { - for (var attribute in data.collection) { - var user_id = data.collection[attribute].member.id; + STUDIP.jsonapi.GET(`courses/${course_id}/memberships`, {data: {page: {limit: 1000000}}}).done(data => { + data.data.forEach(membership => { STUDIP.Resources.addUserToPermissionList( - user_id, + membership.relationships.user.data.id, table_element ); - } + }); }); } diff --git a/resources/assets/javascripts/lib/restapi.js b/resources/assets/javascripts/lib/restapi.js deleted file mode 100644 index b6e31df..0000000 --- a/resources/assets/javascripts/lib/restapi.js +++ /dev/null @@ -1,12 +0,0 @@ -import AbstractAPI from './abstract-api.js'; - -// Actual RESTAPI object -class RESTAPI extends AbstractAPI -{ - constructor() { - super('api.php'); - } -} - -export default RESTAPI; -export const api = new RESTAPI(); diff --git a/resources/assets/javascripts/lib/scroll.js b/resources/assets/javascripts/lib/scroll.js index a4d24d5..f4ffc66 100644 --- a/resources/assets/javascripts/lib/scroll.js +++ b/resources/assets/javascripts/lib/scroll.js @@ -6,49 +6,54 @@ * Updates/calls to the callback are synchronized to screen refresh by using * the animation frame method (which will fallback to a timer based solution). */ -var handlers = {}; -var animId = false; +const handlers = {}; +let animId = false; -var lastTop = null; -var lastLeft = null; - -function scrollHandler() { - var scrollTop = $(document).scrollTop(); - var scrollLeft = $(document).scrollLeft(); - - if (scrollTop !== lastTop || scrollLeft !== lastLeft) { - $.each(handlers, function(index, handler) { - handler(scrollTop, scrollLeft); - }); - - lastTop = scrollTop; - lastLeft = scrollLeft; - } - - animId = false; - - engageScrollTrigger(); -} +let lastTop = null; +let lastLeft = null; function refresh() { - var hasHandlers = !$.isEmptyObject(handlers); + const hasHandlers = Object.keys(handlers).length > 0; if (!hasHandlers && animId !== false) { window.cancelAnimationFrame(animId); animId = false; } else if (hasHandlers && animId === false) { - animId = window.requestAnimationFrame(scrollHandler); + animId = window.requestAnimationFrame(() => Scroll.executeHandlers()); } } function engageScrollTrigger() { - $(window).off('scroll.studip-handler'); - $(window).one('scroll.studip-handler', refresh); + window.removeEventListener('scroll', refresh); + window.addEventListener('scroll', refresh, {once: true}); } const Scroll = { - addHandler(index, handler) { + executeHandlers(only_these = []) { + const scrollTop = document.scrollingElement.scrollTop; + const scrollLeft = document.scrollingElement.scrollLeft; + + if (scrollTop !== lastTop || scrollLeft !== lastLeft) { + for (const [index, handler] of Object.entries(handlers)) { + if (only_these.length === 0 || only_these.includes(index)) { + handler(scrollTop, scrollLeft); + } + } + + lastTop = scrollTop; + lastLeft = scrollLeft; + } + + animId = false; + + engageScrollTrigger(); + }, + addHandler(index, handler, immediate = false) { handlers[index] = handler; engageScrollTrigger(); + + if (immediate) { + Scroll.executeHandlers([index]); + } }, removeHandler(index) { delete handlers[index]; diff --git a/resources/assets/javascripts/lib/scroll_to_top.js b/resources/assets/javascripts/lib/scroll_to_top.js index 2a75402..9b0d3ee 100644 --- a/resources/assets/javascripts/lib/scroll_to_top.js +++ b/resources/assets/javascripts/lib/scroll_to_top.js @@ -4,9 +4,9 @@ let fold; let was_below_the_fold = false; const back_to_top = function(scrolltop) { - var is_below_the_fold = scrolltop > fold; + let is_below_the_fold = scrolltop > fold; if (is_below_the_fold !== was_below_the_fold) { - $('#scroll-to-top').toggleClass('hide', !is_below_the_fold); + document.getElementById('scroll-to-top').classList.toggle('hide', !is_below_the_fold); was_below_the_fold = is_below_the_fold; } }; @@ -23,15 +23,21 @@ const ScrollToTop = { }, disable() { Scroll.removeHandler('header'); - $('#scroll-to-top').addClass('hide'); + document.getElementById('scroll-to-top').classList.add('hide'); }, moveBack() { - $('#scroll-to-top').on('click', function(e) { - $('html, body').stop().animate({ - scrollTop: (0) - }, 1000, 'easeInOutExpo'); - e.preventDefault(); + document.getElementById('scroll-to-top').addEventListener('click', (evt) => { + evt.preventDefault(); + this.toTop(); }); + document.getElementById('scroll-to-top').addEventListener('keypress', (evt) => { + if (evt.code === 'Space') { + this.toTop(); + } + }); + }, + toTop() { + window.scroll({top: 0, left: 0, behavior: 'smooth'}); } }; diff --git a/resources/assets/javascripts/lib/search.js b/resources/assets/javascripts/lib/search.js index 5d39f43..f8108cd 100644 --- a/resources/assets/javascripts/lib/search.js +++ b/resources/assets/javascripts/lib/search.js @@ -200,7 +200,7 @@ const Search = { // Optional image... if (result.img !== null) { $('<div class="search-result-img hidden-tiny-down">') - .append(`<img src="${result.img}">`) + .append(`<img src="${result.img}" alt="">`) .appendTo(link); } @@ -339,7 +339,8 @@ const Search = { * Hide all select filters in the sidebar. */ hideAllFilters: function () { - $('div[id$="_filter"]').hide(); + $('#filter_widget').hide(); + $('#filter_widget *[id$="_filter"]').hide(); }, /** @@ -350,12 +351,11 @@ const Search = { showFilter: function (category) { var filters = $('#search-results').data('filters'); STUDIP.Search.hideAllFilters(); - if (filters && filters[category] !== undefined && category != 'show_all_categories') { + if (filters && filters[category] !== undefined && filters[category].length > 0) { + $('#filter_widget').show(); for (let i = 0; i < filters[category].length; i++) { $(`#${filters[category][i]}_filter`).show(); } - } else if (category === 'show_all_categories') { - $('#semester_filter').show(); } }, @@ -547,7 +547,7 @@ const Search = { if (item != 'category') { var value = filter[item]; if (value.trim()) { - var name = $(`#${item}_filter .sidebar-widget-header`).text().trim(); + var name = $(`#${item}_filter .label-text`).text().trim(); var value_text = $(`#${item}_select option:selected`).text().trim(); var filterItem = $('<button></button>').addClass('button remove-filter').text(name + ': ' + value_text).attr('data-filter-name', item); filterItem.on('click', function () { diff --git a/resources/assets/javascripts/lib/tooltip.js b/resources/assets/javascripts/lib/tooltip.js deleted file mode 100644 index 2cdac27..0000000 --- a/resources/assets/javascripts/lib/tooltip.js +++ /dev/null @@ -1,227 +0,0 @@ -import CSS from './css.js'; - -/** - * Tooltip library for Stud.IP - * - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @copyright Stud.IP Core Group 2014 - * @license GPL2 or any later version - * @since Stud.IP 3.1 - */ - -let count = 0; -let threshold = 0; - -class Tooltip { - static get count() { - return count; - } - - static set count(value) { - count = value; - } - - // Threshold used for "edge detection" (imagine a padding along the edges) - static get threshold() { - return threshold; - } - - static set threshold(value) { - threshold = value; - } - - /** - * Returns a new unique id of a tooltip. - * - * @return {string} Unique id - * @static - */ - static getId() { - const id = `studip-tooltip-${Tooltip.count}`; - Tooltip.count += 1; - return id; - } - - /** - * Constructs a new tooltip at given location with given content. - * The applied css class may be changed by the fourth parameter. - * - * @class - * @classdesc Stud.IP tooltips provide an improved layout and handling - * of contents (including html) than the browser's default - * tooltip through title attribute would - * - * @param {int} x - Horizontal position of the tooltip - * @param {int} y - Vertical position of the tooltip - * @param {string} content - Content of the tooltip (may be html) - * @param {string} css_class - Optional name of the applied css class / - * defaults to 'studip-tooltip' - */ - constructor(x, y, content, css_class) { - // Obtain unique id of the tooltip - this.id = Tooltip.getId(); - - // Create dom element of the tooltip, apply id and class and attach - // to dom - this.element = $('<div>'); - this.element.addClass(css_class || 'studip-tooltip'); - this.element.attr('id', this.id); - this.element.attr('role', 'tooltip'); - this.element.appendTo('body'); - - // Set position and content and paint the tooltip - this.position(x, y); - this.update(content); - this.paint(); - } - - /** - * Translates the arrow(s) under a tooltip using css3 translate - * transforms. This is needed at the edges of the screen. - * This implies that a current browser is used. The translation could - * also be achieved by adjusting margins but that way we would need - * to hardcode values into this function since it's a struggle to - * obtain the neccessary values from the CSS pseudo selectors in JS. - * - * Internal, css rules are dynamically created and applied to the current - * document by using the methods provided in the file studip-css.js. - * - * @param {int} x - Horizontal offset - * @param {int} y - Vertical offset - */ - translateArrows(x, y, left_arrow = false) { - CSS.removeRule(`#${this.id}::before`); - CSS.removeRule(`#${this.id}::after`); - - if (x !== 0 || y !== 0) { - let before_rule = { - transform: `translate(${x}px, ${y}px);` - }; - if (left_arrow) { - before_rule.transform = `translate(${x}px, ${y}px) rotate(90deg);`; - } - let after_rule = before_rule; - if (left_arrow) { - after_rule['border-width'] = '9px'; - } - CSS.addRule(`#${this.id}::before`, before_rule, ['-ms-', '-webkit-']); - CSS.addRule(`#${this.id}::after`, after_rule, ['-ms-', '-webkit-']); - } - } - - /** - * Updates the position of the tooltip. - * - * @param {int} x - Horizontal position of the tooltip - * @param {int} y - Vertical position of the tooltip - */ - position(x, y) { - this.x = x; - this.y = y; - } - - /** - * Updates the contents of the tooltip. - * - * @param {string} content - Content of the tooltip (may be html) - */ - update(content) { - this.element.html(content); - } - - /** - * "Paints" the tooltip. This method actually computes the dimensions of - * the tooltips, checks for screen edges and calculates the actual offset - * in the current document. - * This method is neccessary due to the fact that position and content - * can be changed apart from each other. - * Thus: Don't forget to repaint after adjusting any of the two. - */ - paint() { - const width = this.element.outerWidth(true); - const height = this.element.outerHeight(true); - const maxWidth = $(document).width(); - const maxHeight = $(document).height(); - let x = this.x - width / 2; - let y = this.y - height; - //The arrow offset is the offset from the bottom right corner of - //the tooltip "frame". - let arrow_offset_x = 0; - let arrow_offset_y = 0; - let left_arrow = false; - - if (y < 0) { - y = 0; - x = this.x + 20; - //Put the arrow on the left side and move the tooltip, - //if there is still enough place left on the right. - left_arrow = true; - arrow_offset_y = -height + this.y + 10; - if (arrow_offset_y > -20) { - y+= arrow_offset_y + 20; - arrow_offset_y = -20; - } - arrow_offset_x = -width / 2 - 8; - } else if (y + height > maxHeight) { - y = maxHeight - height; - } - - if (x < 0) { - arrow_offset_x = 0; - x = 0; - } else if (x + width > maxWidth) { - arrow_offset_x = x + width - maxWidth; - x = maxWidth - width; - } - this.translateArrows(arrow_offset_x, arrow_offset_y, left_arrow); - - this.element.css({ - left: x, - top: y - }); - } - - /** - * Toggles the visibility of the tooltip. If no state is provided, - * the tooltip will be hidden if visible and vice versa. Pretty straight - * forward and no surprises here. - * This method implicitely calls paint before a tooltip is shown (in case - * it was forgotten). - * - * @param {bool} visible - Optional visibility parameter to set the - * tooltip to a certain state - */ - toggle(visible) { - if (visible) { - this.paint(); - } - this.element.toggle(visible); - } - - /** - * Reveals the tooltip. - * - * @see Tooltip.toggle - */ - show() { - this.toggle(true); - } - - /** - * Hides the tooltip. - * - * @see Tooltip.toggle - */ - hide() { - this.toggle(false); - } - - /** - * Removes the tooltip - */ - remove() { - this.element.remove(); - } -} - -export default Tooltip; diff --git a/resources/assets/javascripts/lib/wysiwyg.js b/resources/assets/javascripts/lib/wysiwyg.js index f9acb81..47c64d0 100644 --- a/resources/assets/javascripts/lib/wysiwyg.js +++ b/resources/assets/javascripts/lib/wysiwyg.js @@ -13,17 +13,17 @@ const wysiwyg = { isHtml: function isHtml(text) { // NOTE keep this function in sync with - // Markup::isHtml in Markup.class.php + // Markup::isHtml in Markup.php return this.hasHtmlMarker(text); }, hasHtmlMarker: function hasHtmlMarker(text) { // NOTE keep this function in sync with - // Markup::hasHtmlMarker in Markup.class.php + // Markup::hasHtmlMarker in Markup.php return this.htmlMarkerRegExp.test(text); }, markAsHtml: function markAsHtml(text) { // NOTE keep this function in sync with - // Markup::markAsHtml in Markup.class.php + // Markup::markAsHtml in Markup.php if (this.hasHtmlMarker(text) || text.trim() == '') { return text; // marker already set, don't set twice } diff --git a/resources/assets/javascripts/mvv.js b/resources/assets/javascripts/mvv.js index a339624..92dcd68 100644 --- a/resources/assets/javascripts/mvv.js +++ b/resources/assets/javascripts/mvv.js @@ -67,10 +67,17 @@ jQuery(function ($) { }); $(document).on('click', '.stgfile .remove_attachment', function($event) { - STUDIP.MVV.Document.remove_attachment($(this)); + STUDIP.Dialog.confirm($gettext('Soll die Datei wirklich gelöscht werden?')).done(() => { + STUDIP.MVV.Document.remove_attachment(this); + }); return false; }); + $(document).on('click', '.stgfile .refresh_attachment', (event) => { + STUDIP.MVV.Document.refresh_attachment(event.target); + event.preventDefault(); + }); + STUDIP.dialogReady( function() { @@ -663,27 +670,55 @@ STUDIP.MVV.Document = { }) }, 100); }, - remove_attachment: function(item) { - jQuery.ajax({ - url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/materialien/files/delete_attachment', - data: { - mvvfile_id: jQuery('#mvvfile_id').val(), - fileref_id: item.closest('li') - .find('input[name=document_id]') - .val() - }, - type: 'POST' + refresh_attachment(item) { + const language = item.closest('button').dataset.language; + const document_id = item.closest('.stgfile').querySelector('[name="document_id"]').value; + + const input = document.createElement('input'); + input.type = 'file'; + input.hidden = true; + + input.addEventListener('cancel', () => { + input.remove(); }); - item.parents('td').find('.attachments').toggle(); - item.closest('li') - .fadeOut(300, function() { - jQuery(this).remove(); + input.addEventListener('change', () => { + const fd = new FormData(); + fd.append('file', input.files[0], input.files[0].name); + fd.append('mvvfile_id', jQuery('#mvvfile_id').val()); + fd.append('range_id', jQuery('#range_id').val()); + fd.append('document_id', document_id); + fd.append('file_language', language); + + const statusbar = $('#statusbar_container .statusbar') + .first() + .clone() + .show(); + statusbar.appendTo('#statusbar_container'); + + STUDIP.MVV.Document.upload_file(fd, statusbar, true).then(() => { + input.remove(); + }); + }); + + item.parentNode.after(input); + input.click(); + }, + remove_attachment(item) { + const url = STUDIP.URLHelper.getURL('dispatch.php/materialien/files/delete_attachment', { + mvvfile_id: document.getElementById('mvvfile_id').value, + fileref_id: item.closest('li').querySelector('input[name=document_id]').value, + }); + $.post(url).done(() => { + $(item).closest('td').find('.attachments').toggle(); + $(item).closest('li').fadeOut(300, function () { + this.remove(); jQuery('#upload_chooser').show(); }); + }); }, - upload_from_input: function(input, file_language) { + upload_from_input(input, file_language) { STUDIP.MVV.Document.upload_files(input.files, file_language); - jQuery(input).val(''); + input.value = ''; }, fileIDQueue: 1, upload_files: function(files, file_language) { @@ -701,82 +736,94 @@ STUDIP.MVV.Document = { STUDIP.MVV.Document.upload_file(fd, statusbar); } }, - upload_file: function(formdata, statusbar) { - $.ajax({ - xhr: function() { - var xhrobj = $.ajaxSettings.xhr(); - if (xhrobj.upload) { - xhrobj.upload.addEventListener( - 'progress', - function(event) { - var percent = 0; - var position = event.loaded || event.position; - var total = event.total; - if (event.lengthComputable) { - percent = Math.ceil((position / total) * 100); - } - //Set progress - statusbar.find('.progress').css({ 'min-width': percent + '%', 'max-width': percent + '%' }); - statusbar - .find('.progresstext') - .text(percent === 100 ? jQuery('#upload_finished').text() : percent + '%'); - }, - false - ); + upload_file(formdata, statusbar, update = false) { + return new Promise((resolve, reject) => { + $.ajax({ + xhr() { + var xhrobj = $.ajaxSettings.xhr(); + if (xhrobj.upload) { + xhrobj.upload.addEventListener( + 'progress', + function(event) { + var percent = 0; + var position = event.loaded || event.position; + var total = event.total; + if (event.lengthComputable) { + percent = Math.ceil((position / total) * 100); + } + //Set progress + statusbar.find('.progress').css({ 'min-width': percent + '%', 'max-width': percent + '%' }); + statusbar + .find('.progresstext') + .text(percent === 100 ? jQuery('#upload_finished').text() : percent + '%'); + }, + false + ); + } + return xhrobj; + }, + url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/materialien/files/upload_attachment', + type: 'POST', + contentType: false, + processData: false, + cache: false, + data: formdata, + dataType: 'json' + }).done((data) => { + const language = formdata.get('file_language'); + + statusbar.find('.progress').css({ 'min-width': '100%', 'max-width': '100%' }); + var file = jQuery('#fileselector_'+formdata.get('file_language')).find('.stgfiles > .stgfile') + .first() + .clone(); + file.find('.name').text(data.name); + if (data.size < 1024) { + file.find('.size').text(data.size + 'B'); } - return xhrobj; - }, - url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/materialien/files/upload_attachment', - type: 'POST', - contentType: false, - processData: false, - cache: false, - data: formdata, - dataType: 'json' - }) - .done(function(data) { - statusbar.find('.progress').css({ 'min-width': '100%', 'max-width': '100%' }); - var file = jQuery('#fileselector_'+formdata.get('file_language')).find('.stgfiles > .stgfile') - .first() - .clone(); - file.find('.name').text(data.name); - if (data.size < 1024) { - file.find('.size').text(data.size + 'B'); - } - if (data.size > 1024 && data.size < 1024 * 1024) { - file.find('.size').text(Math.floor(data.size / 1024) + 'KB'); - } - if (data.size > 1024 * 1024 && data.size < 1024 * 1024 * 1024) { - file.find('.size').text(Math.floor(data.size / 1024 / 1024) + 'MB'); - } - if (data.size > 1024 * 1024 * 1024) { - file.find('.size').text(Math.floor(data.size / 1024 / 1024 / 1024) + 'GB'); - } - file.find('.icon').html(data.icon); - file.find('input[name=document_id]').attr('value', data.document_id); - jQuery('#fileviewer_'+formdata.get('file_language')).find('.stgfiles').append(file); - jQuery('#fileselector_'+formdata.get('file_language')).toggle(); - jQuery('#fileselector_'+formdata.get('file_language')).parents('.attachments').toggle(); - jQuery('#fileselector_'+formdata.get('file_language')).parents('.attachments').find('span').toggle(); - file.fadeIn(300); - statusbar.find('.progresstext').text(jQuery('#upload_received_data').text()); - statusbar.delay(1000).fadeOut(300, function() { - jQuery('#upload_chooser').hide(); - jQuery(this).remove(); - }); - }) - .fail(function(jqxhr, status, errorThrown) { - var error = jqxhr.responseJSON.error; - - statusbar - .find('.progress') - .addClass('progress-error') - .attr('title', error); - statusbar.find('.progresstext').html(error); - statusbar.on('click', function() { - jQuery(this).fadeOut(300, function() { - jQuery(this).remove(); + if (data.size > 1024 && data.size < 1024 * 1024) { + file.find('.size').text(Math.floor(data.size / 1024) + 'KB'); + } + if (data.size > 1024 * 1024 && data.size < 1024 * 1024 * 1024) { + file.find('.size').text(Math.floor(data.size / 1024 / 1024) + 'MB'); + } + if (data.size > 1024 * 1024 * 1024) { + file.find('.size').text(Math.floor(data.size / 1024 / 1024 / 1024) + 'GB'); + } + file.find('.icon').html(data.icon); + file.find('input[name=document_id]').attr('value', data.document_id); + if (update) { + $(`#fileviewer_${language} .stgfiles`).empty().append(file); + file.show(); + } else { + $(`#fileviewer_${language}`).find('.stgfiles').append(file); + $(`#fileselector_${language}`) + .toggle() + .parents('.attachments').toggle() + .find('span').toggle(); + file.fadeIn(300); + } + statusbar.find('.progresstext').text(jQuery('#upload_received_data').text()); + statusbar.delay(1000).fadeOut(300, function() { + $('#upload_chooser').hide(); + this.remove(); }); + + resolve(); + }).fail(function(jqxhr, status, errorThrown) { + var error = jqxhr.responseJSON.error; + + statusbar + .find('.progress') + .addClass('progress-error') + .attr('title', error); + statusbar.find('.progresstext').html(error); + statusbar.on('click', function() { + jQuery(this).fadeOut(300, function() { + jQuery(this).remove(); + }); + }); + + reject(new Error(error)); }); }); } diff --git a/resources/assets/javascripts/studip-ui.js b/resources/assets/javascripts/studip-ui.js index f581295..f98ba94 100644 --- a/resources/assets/javascripts/studip-ui.js +++ b/resources/assets/javascripts/studip-ui.js @@ -1,5 +1,6 @@ import { $gettext } from './lib/gettext'; import eventBus from "./lib/event-bus.ts"; +import RestrictedDatesHelper from './lib/RestrictedDatesHelper'; /** * This file contains extensions/adjustments for jQuery UI. @@ -28,33 +29,11 @@ import eventBus from "./lib/event-bus.ts"; } function disableHolidaysBeforeShow(date) { - const year = date.getFullYear(); - - if (STUDIP.UI.restrictedDates[year] === undefined) { - STUDIP.UI.restrictedDates[year] = {}; - - STUDIP.jsonapi.GET('holidays', {data: { - 'filter[year]': year - }}).done(response => { - // Since PHP will return an empty object as an array, - // we need to check - if (Array.isArray(response)) { - return; - } - - for (const [date, data] of Object.entries(response)) { - STUDIP.UI.addRestrictedDate( - new Date(date), - data.holiday, - data.mandatory - ); - } - - $(this).datepicker('refresh'); - }); - } - - const {reason, lock} = STUDIP.UI.isDateRestricted(date, false); + RestrictedDatesHelper.loadRestrictedDatesByYear(date.getFullYear()).then( + () => $(this).datepicker('refresh'), + () => null + ); + const {reason, lock} = RestrictedDatesHelper.isDateRestricted(date); return [!lock, lock ? 'ui-datepicker-is-locked' : null, reason]; } @@ -83,57 +62,8 @@ import eventBus from "./lib/event-bus.ts"; return; } + STUDIP.UI = {}; // Setup Stud.IP's own datepicker extensions - STUDIP.UI = Object.assign(STUDIP.UI || {}, { - restrictedDates: {}, - addRestrictedDate(date, reason = '', lock = true) { - if (this.isDateRestricted(date)) { - return; - } - - const [year, month, day] = this.convertDateForRestriction(date); - if (this.restrictedDates[year] === undefined) { - this.restrictedDates[year] = {}; - } - if (this.restrictedDates[year][month] === undefined) { - this.restrictedDates[year][month] = {}; - } - - this.restrictedDates[year][month][day] = {reason, lock}; - }, - removeRestrictedDate(date) { - if (!this.isDateRestricted(date)) { - return false; - } - const [year, month, day] = this.convertDateForRestriction(date); - - delete this.restrictedDates[year][month][day]; - - if (Object.keys(this.restrictedDates[year][month]).length === 0) { - delete this.restrictedDates[year][month]; - } - - return true; - }, - isDateRestricted(date, return_bool = true) { - const [year, month, day] = this.convertDateForRestriction(date); - if ( - this.restrictedDates[year] === undefined - || this.restrictedDates[year][month] === undefined - || this.restrictedDates[year][month][day] === undefined - ) { - return return_bool ? false : { - reason: null, - lock: false, - }; - } - - return return_bool ? true : this.restrictedDates[year][month][day]; - }, - convertDateForRestriction(date) { - return [date.getFullYear(), date.getMonth() + 1, date.getDate()]; - } - }); STUDIP.UI.Datepicker = { selector: '.has-date-picker,[data-date-picker]', // Initialize all datepickers that not yet been initialized (e.g. in dialogs) diff --git a/resources/assets/stylesheets/highcontrast.scss b/resources/assets/stylesheets/highcontrast.scss index 4e1e4ef..277e383 100644 --- a/resources/assets/stylesheets/highcontrast.scss +++ b/resources/assets/stylesheets/highcontrast.scss @@ -1309,3 +1309,18 @@ form.default { border: 1px solid var(--black); } } + +.studip-tree-course { + .course-dates { + color: var(--black) !important; + + } +} + +div.avatar-widget { + .profile-avatar { + .avatar-overlay { + background-color: fade-out($base-color, 0.1); + } + } +} diff --git a/resources/assets/stylesheets/mixins/misc.scss b/resources/assets/stylesheets/mixins/misc.scss index 90d61c5..1fe9d08 100644 --- a/resources/assets/stylesheets/mixins/misc.scss +++ b/resources/assets/stylesheets/mixins/misc.scss @@ -15,10 +15,6 @@ clear: both; } } -@mixin list-unstyled { - padding-left: 0; - list-style: none; -} @mixin size($height, $width) { diff --git a/resources/assets/stylesheets/mixins/studip.scss b/resources/assets/stylesheets/mixins/studip.scss index 344f802..1bbd7d5 100644 --- a/resources/assets/stylesheets/mixins/studip.scss +++ b/resources/assets/stylesheets/mixins/studip.scss @@ -259,19 +259,3 @@ /* Opera doesn't support this in the shorthand */ background-attachment: local, local, scroll, scroll; } - -@mixin list-unstyled { - padding-left: 0; - list-style: none; -} - -@mixin list-inline { - @include list-unstyled(); - margin-left: -5px; - - > li { - display: inline-block; - padding-left: 5px; - padding-right: 5px; - } -} diff --git a/resources/assets/stylesheets/print.scss b/resources/assets/stylesheets/print.scss index bace5fb..7f6c043 100644 --- a/resources/assets/stylesheets/print.scss +++ b/resources/assets/stylesheets/print.scss @@ -3,7 +3,6 @@ @import "scss/visibility"; @import "scss/fullcalendar-print"; @import "scss/resources-print"; -@import "scss/schedule" ; /******************************************************************************* diff --git a/resources/assets/stylesheets/scss/admin-courses.scss b/resources/assets/stylesheets/scss/admin-courses.scss index 753e56b..09ec6ca 100644 --- a/resources/assets/stylesheets/scss/admin-courses.scss +++ b/resources/assets/stylesheets/scss/admin-courses.scss @@ -29,10 +29,6 @@ } } -#admin-filter-widget .label-text { - display: block; -} - .action-menu.filter { margin-left: 1em; } diff --git a/resources/assets/stylesheets/scss/blubber.scss b/resources/assets/stylesheets/scss/blubber.scss index 1893fd5..76bafe4 100644 --- a/resources/assets/stylesheets/scss/blubber.scss +++ b/resources/assets/stylesheets/scss/blubber.scss @@ -1,7 +1,7 @@ .blubber_panel { display: flex; align-items: stretch; - height: calc(100vh - 174px); + height: calc(100vh - 130px); transition: opacity 100ms, filter 100ms; &.waiting { filter: blur(1px); @@ -447,15 +447,12 @@ } -form.default { - .blubber_composer_select_container { - input, select, .container { - width: calc(100% - 50px); - display: inline-block; - } +.blubber_composer_select_container { + input, select, .container { + width: 90%; + display: inline-block; } } - .float_right { float: right; } @@ -523,4 +520,7 @@ ol.tagcloud { .blubber_thread { margin-right: 0; } + .blubber_threads_widget .sidebar-widget-content { + max-height: calc(100vh - 230px); + } } diff --git a/resources/assets/stylesheets/scss/buttons.scss b/resources/assets/stylesheets/scss/buttons.scss index 6c5d070..c148b72 100644 --- a/resources/assets/stylesheets/scss/buttons.scss +++ b/resources/assets/stylesheets/scss/buttons.scss @@ -140,7 +140,8 @@ button, .button { &.as-link, &.styleless, - &.undecorated { + &.undecorated, + &.icon-button { background-color: unset; border: 0; } diff --git a/resources/assets/stylesheets/scss/contents.scss b/resources/assets/stylesheets/scss/contents.scss index fc8a6c9..8e02284 100644 --- a/resources/assets/stylesheets/scss/contents.scss +++ b/resources/assets/stylesheets/scss/contents.scss @@ -8,7 +8,7 @@ width: 100%; .content-item { - height: 100px; + min-height: 100px; .content-item-link { padding: 5px; @@ -46,7 +46,7 @@ background-color: var(--dark-gray-color-5); border: solid thin var(--light-gray-color-40); display: flex; - height: 150px; + min-height: 150px; justify-content: stretch; .content-item-link { diff --git a/resources/assets/stylesheets/scss/copyable-links.scss b/resources/assets/stylesheets/scss/copyable-links.scss deleted file mode 100644 index f5ff73a..0000000 --- a/resources/assets/stylesheets/scss/copyable-links.scss +++ /dev/null @@ -1,24 +0,0 @@ -.copyable-link-confirmation { - position: fixed; - bottom: 60px; - right: 12px; - height: 60px; - line-height: 60px; - max-width: calc(100% - 140px); - z-index: 42000; - border: solid thin var(--content-color-40); - background-color: var(--white); - background-repeat: no-repeat; - background-position: 1em center; - background-size: 100px; - box-shadow: 5px 5px var(--dark-gray-color-10); - padding: 5px 100px; - transition: transform .5s ease; - - &.copyable-link-success { - @include background-icon(check-circle, status-green, 24); - } - &.copyable-link-error { - @include background-icon(decline-circle, status-red, 24); - } -} diff --git a/resources/assets/stylesheets/scss/courseware/blockadder.scss b/resources/assets/stylesheets/scss/courseware/blockadder.scss index 93325dd..f0e993a 100644 --- a/resources/assets/stylesheets/scss/courseware/blockadder.scss +++ b/resources/assets/stylesheets/scss/courseware/blockadder.scss @@ -7,6 +7,7 @@ .cw-blockadder-item-wrapper { display: flex; + position: relative; border: solid thin var(--content-color-40); max-width: 268px; @@ -54,6 +55,7 @@ } .cw-containeradder-item-wrapper { + position: relative; border: solid thin var(--content-color-40); margin-bottom: 4px; .cw-sortable-handle { @@ -136,7 +138,7 @@ } .cw-element-inserter-wrapper { display: grid; - grid-template-columns: repeat(auto-fit, minmax(225px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); grid-auto-rows: auto; grid-gap: 4px; margin-bottom: 8px; diff --git a/resources/assets/stylesheets/scss/courseware/blocks/document.scss b/resources/assets/stylesheets/scss/courseware/blocks/document.scss index fb230b2..0c69cda 100644 --- a/resources/assets/stylesheets/scss/courseware/blocks/document.scss +++ b/resources/assets/stylesheets/scss/courseware/blocks/document.scss @@ -109,6 +109,7 @@ .cw-pdf-outer-container { position: relative; width: 100%; + overflow: hidden; .cw-pdf-content { display: flex; diff --git a/resources/assets/stylesheets/scss/courseware/toolbar.scss b/resources/assets/stylesheets/scss/courseware/toolbar.scss index fdabbf2..0670486 100644 --- a/resources/assets/stylesheets/scss/courseware/toolbar.scss +++ b/resources/assets/stylesheets/scss/courseware/toolbar.scss @@ -14,8 +14,7 @@ min-height: 100%; border: solid thin var(--content-color-40); background-color: var(--white); - overflow-y: auto; - overflow-x: hidden; + overflow: hidden; position: relative; padding: 0 4px; top: 0; @@ -33,6 +32,11 @@ right: 0; } + .cw-toolbar-tool-content { + overflow-y: auto; + padding-right: 8px; + } + .cw-toolbar-blocks { .input-group.files-search { &.search { @@ -96,6 +100,12 @@ } } + + .cw-toolbar-clipboard { + .cw-collapsible { + margin-bottom: 4px; + } + } } .cw-toolbar-folded-wrapper { display: flex; @@ -124,6 +134,7 @@ &.cw-toolbar-button-toggle { text-align: end; + flex-grow: 1; } &.active { @@ -142,7 +153,7 @@ .cw-toolbar-tools.hd { .cw-toolbar-button-wrapper { .cw-toolbar-button { - width: 128px; + min-width: 110px; padding: 2px 16px 0 16px; &.cw-toolbar-button-toggle { text-align: end; diff --git a/resources/assets/stylesheets/scss/dashboard.scss b/resources/assets/stylesheets/scss/dashboard.scss index 6020d6e..e353f72 100644 --- a/resources/assets/stylesheets/scss/dashboard.scss +++ b/resources/assets/stylesheets/scss/dashboard.scss @@ -77,7 +77,7 @@ padding-bottom: 1em; ul { - @include list-inline(); + @extend .list-inline; img { margin-left: 0.25em; diff --git a/resources/assets/stylesheets/scss/evaluation.scss b/resources/assets/stylesheets/scss/evaluation.scss deleted file mode 100644 index 63fcef6..0000000 --- a/resources/assets/stylesheets/scss/evaluation.scss +++ /dev/null @@ -1,40 +0,0 @@ -/* classes for the evaluation modules in Stud.IP ---------------------------- */ -.eval_title { - font-size: 1.2em; - font-weight: bold; - color: var(--base-color); -} - -.eval_error { - color: var(--red); -} - -.eval_success { - color: var(--green); -} - -.eval_info { - color: var(--base-gray); -} - -.eval_metainfo { - font-size: 0.8em; -} - -.eval_highlight { - background-color: var(--content-color-60); -} - -.eval_gray { - background: var(--dark-gray-color-20) none; -} -.evaluation_item { - box-sizing: border-box; - margin: 3px; -} - -h3.eval { - font-size: 1.3em; - color: var(--black); - font-weight: bold; -} diff --git a/resources/assets/stylesheets/scss/files.scss b/resources/assets/stylesheets/scss/files.scss index 7808f15..a75f8f3 100644 --- a/resources/assets/stylesheets/scss/files.scss +++ b/resources/assets/stylesheets/scss/files.scss @@ -406,19 +406,20 @@ form.default { display: flex; justify-content: space-between; align-items: center; - padding: 6px 6px 2px; + padding: 0 10px 0; margin-bottom: 0; border-top: none; > .text { width: 100%; margin-left: 10px; } - > .arrow { - margin-right: 5px; - } > .check { display: none; } + + > .icon { + margin-top: 6px; + } } > label:first-of-type { border-top: 1px solid var(--content-color-40); diff --git a/resources/assets/stylesheets/scss/forms.scss b/resources/assets/stylesheets/scss/forms.scss index c647ee8..888cf56 100644 --- a/resources/assets/stylesheets/scss/forms.scss +++ b/resources/assets/stylesheets/scss/forms.scss @@ -621,6 +621,28 @@ form.inline { } } +.studip-dialog { + form[data-vue-app] { + display: flex; + flex-direction: column; + min-height: 100%; + + fieldset { + flex: 0; + } + + footer[data-dialog-button] { + background: var(--white); + border-top-color: var(--base-color-20); + bottom: -0.5em; + margin-top: auto; + padding: 1.3em 0; + position: sticky; + text-align: center; + } + } +} + @media (min-width: 800px) { form.default .form-columns { display: flex; diff --git a/resources/assets/stylesheets/scss/globalsearch.scss b/resources/assets/stylesheets/scss/globalsearch.scss index 4faee5f..ef7c6e6 100644 --- a/resources/assets/stylesheets/scss/globalsearch.scss +++ b/resources/assets/stylesheets/scss/globalsearch.scss @@ -171,7 +171,7 @@ } } - section { + a[role=listitem] { display: flex; flex-direction: row; flex-wrap: nowrap; @@ -189,7 +189,7 @@ display: none; } - & > a { + & > span.detail { display: flex; flex-direction: row; flex-wrap: nowrap; diff --git a/resources/assets/stylesheets/scss/lists.scss b/resources/assets/stylesheets/scss/lists.scss index 8ef1eb7..80796b7 100644 --- a/resources/assets/stylesheets/scss/lists.scss +++ b/resources/assets/stylesheets/scss/lists.scss @@ -10,9 +10,25 @@ ol { } } +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + @extend .list-unstyled; + margin-left: -5px; + + > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; + } +} + //comma separated .list-csv { - @include list-inline(); + @extend .list-inline; margin-left: 0; > li { @@ -22,8 +38,11 @@ ol { content: ","; } - &:last-child::after { - content: unset; + &:last-child { + padding-right: 0; + &::after { + content: unset; + } } } @@ -35,7 +54,7 @@ ol { } .list-pipe-separated { - @include list-inline(); + @extend .list-inline; display: flex; // Prevents the mystery gap between elements > li { @@ -47,6 +66,19 @@ ol { } } +.list-slash-separated-small { + @extend .list-csv; + + > li { + padding-right: 0; + font-size: small; + + &::after { + content: "/"; + } + } +} + dl { dt { font-weight: bold; diff --git a/resources/assets/stylesheets/scss/messagebox.scss b/resources/assets/stylesheets/scss/messagebox.scss index a928bb6..7f77f0f 100644 --- a/resources/assets/stylesheets/scss/messagebox.scss +++ b/resources/assets/stylesheets/scss/messagebox.scss @@ -72,7 +72,7 @@ div.messagebox_details { // Define modal messagebox .modaloverlay { - background: fadeout($base-color, 50%); + background: fade-out($base-color, 0.5); position: fixed; top: 0; left: 0; diff --git a/resources/assets/stylesheets/scss/mvv.scss b/resources/assets/stylesheets/scss/mvv.scss index c93b214..ce85701 100644 --- a/resources/assets/stylesheets/scss/mvv.scss +++ b/resources/assets/stylesheets/scss/mvv.scss @@ -253,22 +253,6 @@ ul { dd { margin: 0; } - - &.even { - background-color: var(--dark-gray-color-10); - - &:hover { - background-color: var(--content-color-60); - } - } - - &.odd { - background-color: var(--dark-gray-color-5); - - &:hover { - background-color: var(--content-color-40); - } - } } &.mvv-modul li { @@ -559,14 +543,6 @@ form.default .mvv-inst-chooser select { h3 { margin-top: 1em; } - - &.odd { - background-color: var(--dark-gray-color-5); - } - - &.even { - background-color: var(--content-color-20); - } } #lvgruppe_selection_chosen { diff --git a/resources/assets/stylesheets/scss/my_courses.scss b/resources/assets/stylesheets/scss/my_courses.scss index b82ec2b..769f3d2 100644 --- a/resources/assets/stylesheets/scss/my_courses.scss +++ b/resources/assets/stylesheets/scss/my_courses.scss @@ -9,7 +9,7 @@ background: var(--white); } -.mycourses-group-selector { +form.default .mycourses-group-selector { position: relative; background-clip: padding-box; @@ -19,26 +19,41 @@ @extend .sr-only; &:checked + label { - @include background-icon(accept, info); + .group-number { + display: none; + } + .checked-icon { + display: inline; + } } } &:hover label { - @include background-icon(accept, info); + .group-number { + display: none; + } + .checked-icon { + display: inline; + } } label { - @include hide-text(); + text-align: center; + font-size: large; + font-weight: bold; + cursor: pointer; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; + background-color: var(--white); + margin-bottom: 0; + text-indent: 0; - background-position: center; - background-repeat: no-repeat; + height: 1.2em; - cursor: pointer; + .group-number { + display: inline; + } + .checked-icon { + display: none; + } } } diff --git a/resources/assets/stylesheets/scss/profile.scss b/resources/assets/stylesheets/scss/profile.scss index ef7e75a..c180e03 100644 --- a/resources/assets/stylesheets/scss/profile.scss +++ b/resources/assets/stylesheets/scss/profile.scss @@ -13,7 +13,7 @@ padding: 0 1em; } .profile-view-actions { - @include list-unstyled(); + @extend .list-unstyled; img { vertical-align: text-top; } diff --git a/resources/assets/stylesheets/scss/questionnaire.scss b/resources/assets/stylesheets/scss/questionnaire.scss index f5e726b..fde7d32 100644 --- a/resources/assets/stylesheets/scss/questionnaire.scss +++ b/resources/assets/stylesheets/scss/questionnaire.scss @@ -1,8 +1,6 @@ $width: 270px; .questionnaire_edit { - - .editor { display: flex; flex-direction: row-reverse; @@ -14,14 +12,16 @@ $width: 270px; min-width: $width; width: $width; .questions_container { - padding: 0px; + padding: 0; .questions { display: flex; flex-direction: column; } } - > .admin, > .add_question, .questions > * { + > .admin, + > .add_question, + .questions > * { width: calc(100% - 8px); padding: 4px; border-bottom: 1px solid var(--content-color-40); @@ -42,8 +42,8 @@ $width: 270px; &::before { content: ''; position: absolute; - height: 0px; - width: 0px; + height: 0; + width: 0; border-top: 25px transparent solid; border-bottom: 25px transparent solid; border-left: 7px var(--content-color-40) solid; @@ -52,8 +52,8 @@ $width: 270px; &::after { content: ''; position: absolute; - height: 0px; - width: 0px; + height: 0; + width: 0; border-top: 25px transparent solid; border-bottom: 25px transparent solid; border-left: 7px var(--yellow-40) solid; @@ -93,42 +93,11 @@ $width: 270px; border: 1px solid var(--content-color-40); border-left: none; flex-grow: 1; - padding: 10px; - padding-left: 15px; + padding: 10px 10px 10px 15px; min-height: 150px; min-width: 0; } - .vote_edit { - .options { - > li { - display: flex; - align-items: center; - > * { - margin-right: 10px; - } - } - } - } - .rangescale_edit table.default > thead > tr > th.number { - padding-left: 12px; - } - - .dragcolumn { - max-width: 1px; - padding-bottom: 0px; - > .dragarea { - display: inline-block; - height: 27px; - } - } - - .input-array { - margin-left: 4px; - } - .likert_edit .input-array { - margin-left: 7px; - } .inline_editing { width: 100%; display: flex; @@ -150,114 +119,25 @@ $width: 270px; justify-items: center; } } - .drag-handle { - display: inline-block; - height: 24px; - } - } - /* ab hier der alte kram */ - - section { - border: thin solid var(--black); - margin: 3px; - } - - .options { - padding: 0; - list-style-type: none; - - > li { - margin-top: 5px; - margin-bottom: 5px; - - > .move { - cursor: move; - display: inline-block; - vertical-align: middle; - } - - > input { - display: inline-block; - vertical-align: middle; - } - - > input[type=text] { - width: calc(100% - 70px); - } - - .delete { + .dragcolumn { + max-width: 1px; + padding-bottom: 0; + > .dragarea { display: inline-block; - vertical-align: middle; - cursor: pointer; - } - - .add { - display: none; - vertical-align: middle; - cursor: pointer; + height: 27px; } } - > li:last-child .delete { - display: none; - } - - > li:last-child .add { + .drag-handle { display: inline-block; + height: 24px; } - > li:only-child .move { - display: none; - } - - } - - .all_questions { - .question:first-child .move_up { - display: none; - } - - .question:last-child .move_down { - display: none; - } - } - - .add_questions { - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: stretch; - border: thin dashed var(--content-color-40); - - > a { - background-color: transparent; - margin: 10px; - border: thin solid var(--content-color-20); - padding: 5px; - width: 100px; - min-width: 100px; - max-width: 100px; - height: 100px; - min-height: 100px; - max-height: 100px; - overflow: hidden; - display: flex; - flex-direction: column; - justify-content: space-around; - align-items: center; + .option-cell { text-align: center; - - > img { - margin-left: auto; - margin-right: auto; - } } } - - .questionnaire_metadata { - margin-top: 10px; - } } .questionnaire_results { @@ -308,7 +188,8 @@ $width: 270px; } -.questionnaire_answer, .questionnaire_results { +.questionnaire_answer, +.questionnaire_results { .description_container { display: flex; > .icon_container { @@ -335,7 +216,7 @@ $width: 270px; border: none; > :first-child { - margin-top: 0px; + margin-top: 0; } .invalidation_notice { @@ -351,9 +232,6 @@ $width: 270px; font-size: 0.7em; padding-left: 5px; } - .rangescale_center { - text-align: center; - } .centerline { border-top: 1px solid var(--base-color); position: relative; @@ -387,39 +265,13 @@ $width: 270px; } } } +} - .centerline { - border-top: 1px solid var(--base-color); - position: relative; - top: 35px; - margin-left: -5px; - margin-right: -5px; - z-index: 2; - } - .questionnaire-evaluation-circle-container { +.questionnaire_edit, +.questionnaire_answer, +.questionnaire_results { + .option-cell { text-align: center; - display: block; - .questionnaire-evaluation-circle { - width: 70px; - height: 70px; - display: flex; - justify-content: center; - align-items: center; - margin-left: auto; - margin-right: auto; - z-index: 3; - position: relative; - > .value { - border-radius: 100px; - color: white; - display: flex; - justify-content: center; - align-items: center; - background-color: var(--base-color); - width: 100%; - height: 100%; - } - } } } diff --git a/resources/assets/stylesheets/scss/responsive.scss b/resources/assets/stylesheets/scss/responsive.scss index c4317e9..e84a8a8 100644 --- a/resources/assets/stylesheets/scss/responsive.scss +++ b/resources/assets/stylesheets/scss/responsive.scss @@ -537,6 +537,26 @@ $sidebarOut: -330px; } } } + + #system-notifications { + top: 0; + position: fixed; + left: 0; + width: 100%; + z-index: 1001; + } + + .system-notification { + &.system-notification-slide-enter, + &.system-notification-slide-leave-to { + transform: translateY(-100%); + } + + &.system-notification-slide-leave, + &.system-notification-slide-enter-to { + transform: translateY(0); + } + } } /* Settings especially for fullscreen mode */ diff --git a/resources/assets/stylesheets/scss/schedule.scss b/resources/assets/stylesheets/scss/schedule.scss index c8c8f5a..a09e3ad 100644 --- a/resources/assets/stylesheets/scss/schedule.scss +++ b/resources/assets/stylesheets/scss/schedule.scss @@ -185,10 +185,6 @@ td.schedule-adminbind { } #color_picker { - div { - display: flex; - flex-wrap: wrap; - } span { flex: 0 0 auto; @@ -198,7 +194,7 @@ td.schedule-adminbind { } input[type="radio"] { - display: none; + @extend .sr-only; &:checked + label { outline: 1px solid var(--black); @@ -266,6 +262,15 @@ td.schedule-adminbind { &.schedule-category15 { background-color: $calendar-category-15; } + &.schedule-category16 { + background-color: $calendar-category-16; + } + &.schedule-category17 { + background-color: $calendar-category-17; + } + &.schedule-category18 { + background-color: $calendar-category-18; + } &.schedule-category255 { background-color: $calendar-category-255; } @@ -440,5 +445,38 @@ div.schedule_entry { color: contrast($calendar-category-15-aux, $black, $white); } } + &.schedule-category16 { + background-color: $calendar-category-16-aux; + border: 1px solid $calendar-category-16; + dt { + background-color: $calendar-category-16; + color: contrast($calendar-category-16, $black, $white); + } + dd { + color: contrast($calendar-category-16-aux, $black, $white); + } + } + &.schedule-category17 { + background-color: $calendar-category-17-aux; + border: 1px solid $calendar-category-17; + dt { + background-color: $calendar-category-17; + color: contrast($calendar-category-17, $black, $white); + } + dd { + color: contrast($calendar-category-17-aux, $black, $white); + } + } + &.schedule-category18 { + background-color: $calendar-category-18-aux; + border: 1px solid $calendar-category-18; + dt { + background-color: $calendar-category-18; + color: contrast($calendar-category-18, $black, $white); + } + dd { + color: contrast($calendar-category-18-aux, $black, $white); + } + } } } diff --git a/resources/assets/stylesheets/scss/sidebar.scss b/resources/assets/stylesheets/scss/sidebar.scss index 9a97976..c161778 100644 --- a/resources/assets/stylesheets/scss/sidebar.scss +++ b/resources/assets/stylesheets/scss/sidebar.scss @@ -313,6 +313,7 @@ select.sidebar-selectlist { flex: 1; padding: .25em .5em; width: 100%; + order: 1; } .submit-search { @@ -324,17 +325,22 @@ select.sidebar-selectlist { cursor: pointer; font: 0/0 a; text-shadow: none; + order: 3; } .reset-search { - background-color: transparent; - border: 1px solid var(--dark-gray-color-30); - border-left: 0; - border-right: 0; + background: unset; display: inline-block; - padding-right: 5px; - padding-top: 4px; cursor: pointer; + order: 2; + height: 100%; + box-sizing: border-box; + margin-right: 2px; + margin-left: -22px; + + img { + padding-top: 4px; + } } } diff --git a/resources/assets/stylesheets/scss/system-notifications.scss b/resources/assets/stylesheets/scss/system-notifications.scss new file mode 100644 index 0000000..34c341b --- /dev/null +++ b/resources/assets/stylesheets/scss/system-notifications.scss @@ -0,0 +1,178 @@ +#system-notifications { + &.bottom-right { + bottom: 50px; + right: 15px; + + .system-notification { + box-shadow: 5px 5px var(--dark-gray-color-10); + + &.system-notification-slide-enter, + &.system-notification-slide-leave-to { + opacity: 0; + transform: translateX(100%); + } + + &.system-notification-slide-leave, + &.system-notification-slide-enter-to { + opacity: 1; + transform: translateX(0); + } + } + } + + &.top-center { + left: calc(50% - 300px); + top: 0; + + .system-notification { + box-shadow: 0 0 0 3px var(--dark-gray-color-10); + + &.system-notification-slide-enter, + &.system-notification-slide-leave-to { + opacity: 0; + transform: translateY(-100%); + } + + &.system-notification-slide-leave, + &.system-notification-slide-enter-to { + opacity: 1; + transform: translateY(0); + } + } + } + + &:not(.system-notifications-login) { + position: fixed; + width: 600px; + } + + &.system-notifications-login { + margin-bottom: 15px; + } + + overflow: hidden; + z-index: 1001; +} + +.system-notification { + background-color: var(--content-color-20); + border: thin solid var(--content-color-40); + color: var(--black); + cursor: pointer; + display: flex; + padding: 10px; + position: relative; + + &.system-notification-slide-enter-active, + &.system-notification-slide-leave-active { + transition: all var(--transition-duration-slow) ease-in-out; + } + + .system-notification-icon { + flex: 0; + padding: 10px; + } + + .system-notification-content { + flex: 1; + height: auto; + margin: auto 25px auto 10px; + word-wrap: break-word; + + .system-notification-details { + .system-notification-details-content { + padding-left: 10px; + } + } + } + + .system-notification-close { + align-self: normal; + flex: 0; + height: 20px; + width: 20px; + + img, svg { + cursor: pointer; + position: absolute; + right: 10px; + top: 10px; + } + } + + .system-notification-timeout { + &.system-notification-timeout-enter-active, + &.system-notification-timeout-leave-active { + transition: width 5s linear; + } + + &.system-notification-timeout-enter { + width: 100%; + } + + &.system-notification-timeout-leave { + width: 0; + } + + background-color: var(--base-color-40); + bottom: 0; + height: 5px; + left: 0; + position: absolute; + width: 0; + } + + &.system-notification-disrupted .system-notification-timeout { + display: none; + } + + a:not(.system-notification-message) { + color: var(--black); + text-decoration-line: underline; + } + + a.system-notification-message { + color: var(--text-color); + text-decoration: unset; + } +} + +.system-notification-exception { + background-color: var(--red-40); + + .system-notification-timeout { + background-color: var(--red); + } +} + +.system-notification-error { + background-color: var(--red-20); + + .system-notification-timeout { + background-color: var(--red-80); + } +} + +.system-notification-warning { + background-color: var(--yellow-20); + + .system-notification-timeout { + background-color: var(--yellow-80); + } +} + +.system-notification-success { + background-color: var(--green-20); + + .system-notification-timeout { + background-color: var(--green-80); + } +} + +.system-notification-info { + background-color: var(--dark-violet-20); + + .system-notification-timeout { + background-color: var(--dark-violet-60); + } +} diff --git a/resources/assets/stylesheets/scss/tables.scss b/resources/assets/stylesheets/scss/tables.scss index 9e05f01..665a6f3 100644 --- a/resources/assets/stylesheets/scss/tables.scss +++ b/resources/assets/stylesheets/scss/tables.scss @@ -504,7 +504,7 @@ table.default { > thead { > tr > th { background-color: var(--content-color-20); - border-bottom: 1px solid fadeout($brand-color-lighter, 80%); + border-bottom: 1px solid fade-out($brand-color-lighter, 0.8); border-top: 1px solid var(--brand-color-darker); font-size: 1.0em; } @@ -515,7 +515,7 @@ table.default { > th { background-color: var(--content-color-20); border-top: 1px solid var(--brand-color-darker); - border-bottom: 1px solid fadeout($brand-color-lighter, 80%); + border-bottom: 1px solid fade-out($brand-color-lighter, 0.8); text-align: left; } @@ -580,7 +580,7 @@ table.default { // Hover effect &:not(.nohover) > tbody:not(.nohover) > tr:not(.nohover):hover > td:not(.nohover) { - background-color: fadeout($light-gray-color, 80%); + background-color: fade-out($light-gray-color, 0.8); } &:not(.nohover) > tbody:not(.nohover) > tr.selected:not(.nohover):hover > td:not(.nohover) { @@ -742,7 +742,7 @@ table.withdetails { } > tbody > tr.open > td { - background-color: fadeout($light-gray-color, 80%); + background-color: fade-out($light-gray-color, 0.8); } > tbody > tr.open > td:first-child { diff --git a/resources/assets/stylesheets/scss/tabs.scss b/resources/assets/stylesheets/scss/tabs.scss index b37005e..63bf1a0 100644 --- a/resources/assets/stylesheets/scss/tabs.scss +++ b/resources/assets/stylesheets/scss/tabs.scss @@ -31,8 +31,6 @@ div.clear .quiet img { opacity: 0.25; } li { - background-color: var(--dark-gray-color-10); - &:last-child { border-right: none; } diff --git a/resources/assets/stylesheets/scss/talk-bubble.scss b/resources/assets/stylesheets/scss/talk-bubble.scss index bc44600..380fb6d 100644 --- a/resources/assets/stylesheets/scss/talk-bubble.scss +++ b/resources/assets/stylesheets/scss/talk-bubble.scss @@ -8,6 +8,13 @@ $ownColor: var(--petrol-40); .talk-bubble-avatar { padding: 8px; + width: 40px; + height: 40px; + + img { + width: 100%; + height: 100%; + } } .talk-bubble { @@ -136,4 +143,4 @@ $ownColor: var(--petrol-40); flex-direction: row-reverse; } } -}
\ No newline at end of file +} diff --git a/resources/assets/stylesheets/scss/tooltip.scss b/resources/assets/stylesheets/scss/tooltip.scss index 4b7b36b..ac570a8 100644 --- a/resources/assets/stylesheets/scss/tooltip.scss +++ b/resources/assets/stylesheets/scss/tooltip.scss @@ -7,7 +7,7 @@ box-shadow: 0 1px 0 fade-out($white, 0.5) inset; font-size: var(--font-size-base); margin-bottom: 8px; - max-width: 230px; + max-width: $grid-element-width; padding: 10px; position: absolute; text-align: left; @@ -38,11 +38,13 @@ @extend %tooltip; display: none; } - &:hover .tooltip-content { + + &:hover .tooltip-content, + &:focus .tooltip-content { bottom: 100%; display: inline-block; left: 50%; - margin-left: -129px; - width: 230px; + margin-left: - calc($grid-element-width / 2) - 10px; + width: $grid-element-width; } } diff --git a/resources/assets/stylesheets/scss/wiki.scss b/resources/assets/stylesheets/scss/wiki.scss index e19b9b1..323fbb5 100644 --- a/resources/assets/stylesheets/scss/wiki.scss +++ b/resources/assets/stylesheets/scss/wiki.scss @@ -190,12 +190,12 @@ article.studip.wiki { padding: 0; li { margin-bottom: 5px; + display: flex; } a { background-position: left top; background-repeat: no-repeat; background-size: var(--avatar-small); - display: block; min-height: var(--avatar-small); padding-left: calc(var(--avatar-small) + 1ex); } diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index d19621a..bf4022a 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -32,7 +32,6 @@ @import "scss/contents"; @import "scss/content"; @import "scss/comments"; -@import "scss/copyable-links"; @import "scss/cronjobs"; @import "scss/coursewizard"; @import "scss/css_tree"; @@ -42,7 +41,6 @@ @import "scss/documents"; @import "scss/drag-handle"; @import "scss/enrolment"; -@import "scss/evaluation"; @import "scss/files"; @import "scss/feedback"; @import "scss/forms"; @@ -97,6 +95,7 @@ @import "scss/studygroup"; @import "scss/studip-overlay"; @import "scss/studip-selection"; +@import "scss/system-notifications"; @import "scss/table_of_contents"; @import "scss/tables"; @import "scss/tabs"; @@ -244,8 +243,6 @@ ol.clean { padding: 5px; } - - .minor { color: var(--black); font-size: 0.75em; |
