aboutsummaryrefslogtreecommitdiff
path: root/resources/assets/javascripts/lib
diff options
context:
space:
mode:
authorJan-Hendrik Willms <tleilax+github@gmail.com>2021-07-22 16:07:19 +0200
committerJan-Hendrik Willms <tleilax+github@gmail.com>2021-07-22 16:19:12 +0200
commita3da1483a9e689846179159355badfec8073dbec (patch)
tree770dcca6bdf5f6f2a11b0e7fcbbeda6919a3fc52 /resources/assets/javascripts/lib
current code from svn, revision 62608
Diffstat (limited to 'resources/assets/javascripts/lib')
-rw-r--r--resources/assets/javascripts/lib/abstract-api.js108
-rw-r--r--resources/assets/javascripts/lib/actionmenu.js231
-rw-r--r--resources/assets/javascripts/lib/admin_sem_class.js217
-rw-r--r--resources/assets/javascripts/lib/admission.js294
-rw-r--r--resources/assets/javascripts/lib/arbeitsgruppen.js17
-rw-r--r--resources/assets/javascripts/lib/archive.js15
-rw-r--r--resources/assets/javascripts/lib/audio.js44
-rw-r--r--resources/assets/javascripts/lib/avatar.js111
-rw-r--r--resources/assets/javascripts/lib/big_image_handler.js139
-rw-r--r--resources/assets/javascripts/lib/blubber.js221
-rw-r--r--resources/assets/javascripts/lib/browse.js7
-rw-r--r--resources/assets/javascripts/lib/cache.js229
-rw-r--r--resources/assets/javascripts/lib/calendar.js117
-rw-r--r--resources/assets/javascripts/lib/calendar_dialog.js64
-rw-r--r--resources/assets/javascripts/lib/clipboard.js539
-rw-r--r--resources/assets/javascripts/lib/cookie.js35
-rw-r--r--resources/assets/javascripts/lib/course_wizard.js559
-rw-r--r--resources/assets/javascripts/lib/css.js66
-rw-r--r--resources/assets/javascripts/lib/dates.js56
-rw-r--r--resources/assets/javascripts/lib/dialog.js751
-rw-r--r--resources/assets/javascripts/lib/dialogs.js28
-rw-r--r--resources/assets/javascripts/lib/drag_and_drop_upload.js21
-rw-r--r--resources/assets/javascripts/lib/enrollment.js111
-rw-r--r--resources/assets/javascripts/lib/event-bus.js5
-rw-r--r--resources/assets/javascripts/lib/extract_callback.js80
-rw-r--r--resources/assets/javascripts/lib/files.js345
-rw-r--r--resources/assets/javascripts/lib/files_dashboard.js19
-rw-r--r--resources/assets/javascripts/lib/folders.js87
-rw-r--r--resources/assets/javascripts/lib/forms.js59
-rw-r--r--resources/assets/javascripts/lib/fullcalendar.js601
-rw-r--r--resources/assets/javascripts/lib/fullscreen.js60
-rw-r--r--resources/assets/javascripts/lib/gettext.js92
-rw-r--r--resources/assets/javascripts/lib/global_search.js234
-rw-r--r--resources/assets/javascripts/lib/header_magic.js49
-rw-r--r--resources/assets/javascripts/lib/i18n.js43
-rw-r--r--resources/assets/javascripts/lib/inline-editing.js138
-rw-r--r--resources/assets/javascripts/lib/instschedule.js19
-rw-r--r--resources/assets/javascripts/lib/jsonapi.js27
-rw-r--r--resources/assets/javascripts/lib/jsupdater.js233
-rw-r--r--resources/assets/javascripts/lib/lightbox.js148
-rw-r--r--resources/assets/javascripts/lib/markup.js41
-rw-r--r--resources/assets/javascripts/lib/members.js24
-rw-r--r--resources/assets/javascripts/lib/messages.js304
-rw-r--r--resources/assets/javascripts/lib/multi_person_search.js160
-rw-r--r--resources/assets/javascripts/lib/multi_select.js48
-rw-r--r--resources/assets/javascripts/lib/navigation_shrinker.js51
-rw-r--r--resources/assets/javascripts/lib/news.js147
-rwxr-xr-xresources/assets/javascripts/lib/oer.js230
-rw-r--r--resources/assets/javascripts/lib/old_upload.js64
-rw-r--r--resources/assets/javascripts/lib/overlapping.js94
-rw-r--r--resources/assets/javascripts/lib/overlay.js108
-rw-r--r--resources/assets/javascripts/lib/page_layout.js29
-rw-r--r--resources/assets/javascripts/lib/parse_options.js89
-rw-r--r--resources/assets/javascripts/lib/personal_notifications.js219
-rw-r--r--resources/assets/javascripts/lib/plus.js23
-rw-r--r--resources/assets/javascripts/lib/qr_code.js60
-rw-r--r--resources/assets/javascripts/lib/questionnaire.js233
-rw-r--r--resources/assets/javascripts/lib/quick_search.js172
-rw-r--r--resources/assets/javascripts/lib/raumzeit.js19
-rw-r--r--resources/assets/javascripts/lib/ready.js60
-rw-r--r--resources/assets/javascripts/lib/register.js134
-rw-r--r--resources/assets/javascripts/lib/report.js48
-rw-r--r--resources/assets/javascripts/lib/resources.js907
-rw-r--r--resources/assets/javascripts/lib/responsive.js156
-rw-r--r--resources/assets/javascripts/lib/restapi.js12
-rw-r--r--resources/assets/javascripts/lib/schedule.js254
-rw-r--r--resources/assets/javascripts/lib/scroll.js59
-rw-r--r--resources/assets/javascripts/lib/scroll_to_top.js38
-rw-r--r--resources/assets/javascripts/lib/search.js566
-rw-r--r--resources/assets/javascripts/lib/sidebar.js75
-rw-r--r--resources/assets/javascripts/lib/skip_links.js143
-rw-r--r--resources/assets/javascripts/lib/smiley_picker.js128
-rw-r--r--resources/assets/javascripts/lib/startpage.js64
-rw-r--r--resources/assets/javascripts/lib/statusgroups.js77
-rw-r--r--resources/assets/javascripts/lib/studip-vue.js15
-rw-r--r--resources/assets/javascripts/lib/study_area_selection.js120
-rw-r--r--resources/assets/javascripts/lib/table-of-contents.js11
-rw-r--r--resources/assets/javascripts/lib/table.js54
-rw-r--r--resources/assets/javascripts/lib/toolbar.js135
-rw-r--r--resources/assets/javascripts/lib/toolbar_buttonset.js77
-rw-r--r--resources/assets/javascripts/lib/tooltip.js198
-rw-r--r--resources/assets/javascripts/lib/tour.js655
-rw-r--r--resources/assets/javascripts/lib/url_helper.js89
-rw-r--r--resources/assets/javascripts/lib/user_filter.js172
-rw-r--r--resources/assets/javascripts/lib/wysiwyg.js569
85 files changed, 13150 insertions, 0 deletions
diff --git a/resources/assets/javascripts/lib/abstract-api.js b/resources/assets/javascripts/lib/abstract-api.js
new file mode 100644
index 0000000..4239118
--- /dev/null
+++ b/resources/assets/javascripts/lib/abstract-api.js
@@ -0,0 +1,108 @@
+import Overlay from './overlay.js';
+
+class AbstractAPI
+{
+ static get supportedMethods() {
+ return ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE'];
+ }
+
+ // Helper function that normalizes options
+ static adjustOptions (options = {}) {
+ return Object.assign({}, {
+ method: 'get',
+ parameters: {},
+ headers: {},
+ data: {},
+ overlay: false,
+ async: false,
+ before: false
+ }, options || {});
+ }
+
+ constructor (base_url) {
+ if (this.constructor === AbstractAPI) {
+ throw new TypeError('You should not instantiate the abstract api');
+ }
+
+ this.total_requests = 0;
+ this.request_count = 0;
+ this.queue = [];
+ this.base_url = base_url;
+ }
+
+ encodeData (data) {
+ if (data instanceof Function) {
+ data = data();
+ }
+ return data;
+ }
+
+ request (url, options = {}) {
+ // Normalize parameters
+ if (Array.isArray(url)) {
+ // Remove empty trailing chunks
+ while (url[url.length - 1] === '') {
+ delete url[url.length - 1];
+ }
+ // Convert array to string
+ url = url.join('/');
+ }
+
+ options = this.constructor.adjustOptions(options);
+
+ var deferred;
+
+ 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
+ // stored in a deferred which is then queued for execution.
+ deferred = $.Deferred();
+ deferred.then(() => this.request(url, options));
+
+ this.queue.push(deferred);
+ } else if (options.before instanceof Function && !options.before()) {
+ // A before function was defined and returned false, so the request
+ // is canceled
+ deferred = $.Deferred((dfd) => dfd.reject());
+ } else {
+ // Increase request counters, show overlay if neccessary
+ if (this.request_count === 0 && options.overlay) {
+ Overlay.show(true, null, true);
+ }
+ this.request_count += 1;
+ this.total_requests += 1;
+
+ // Actual request
+ deferred = $.ajax(STUDIP.URLHelper.getURL(`${this.base_url}/${url}`, {}, true), {
+ contentType: options.contentType || 'application/x-www-form-urlencoded; charset=UTF-8',
+ method: options.method.toUpperCase(),
+ data: this.encodeData(options.data),
+ headers: options.headers
+ }).always(() => {
+ // Decrease request counter, remove overlay if neccessary
+ this.request_count -= 1;
+ if (this.request_count === 0 && options.overlay) {
+ Overlay.hide();
+ }
+ });
+ }
+ return deferred.always(() => {
+ // Check if any request was queued
+ if (this.request_count === 0 && this.queue.length > 0) {
+ this.queue.shift().resolve();
+ }
+ }).promise();
+ };
+}
+
+// Create shortcut methods for easier access by method
+AbstractAPI.supportedMethods.forEach((method) => {
+ AbstractAPI.prototype[method] = function (url, options = {}) {
+ options = this.constructor.adjustOptions(options);
+ options.method = method;
+
+ return this.request.call(this, url, options);
+ };
+});
+
+export default AbstractAPI;
diff --git a/resources/assets/javascripts/lib/actionmenu.js b/resources/assets/javascripts/lib/actionmenu.js
new file mode 100644
index 0000000..7422ac8
--- /dev/null
+++ b/resources/assets/javascripts/lib/actionmenu.js
@@ -0,0 +1,231 @@
+/*jslint esversion: 6*/
+
+/**
+ * Determine whether the menu should be opened in dialog or regular layout.
+ * @type {[type]}
+ */
+function determineBreakpoint(element) {
+ return $(element).closest('.ui-dialog-content').length > 0 ? '.ui-dialog-content' : '#layout_content';
+}
+
+/**
+ * Obtain all parents of the given element that have scrollable content.
+ */
+function getScrollableParents(element, menu_width, menu_height) {
+ const offset = $(element).offset();
+ const breakpoint = determineBreakpoint(element);
+
+ var elements = [];
+ $(element).parents().each(function () {
+ // Stop at breakpoint
+ if ($(this).is(breakpoint)) {
+ return false;
+ }
+
+ // Exit early if overflow is visible
+ const overflow = $(this).css('overflow');
+ if (overflow === 'visible' || overflow === 'inherit') {
+ return;
+ }
+
+ // Check whether element is overflown
+ const overflown = this.scrollHeight > this.clientHeight || this.scrollWidth > this.clientWidth;
+ if (overflow === 'hidden' && overflown) {
+ elements.push(this);
+ return;
+ }
+
+ // Check if menu fits inside element
+ const offs = $(this).offset();
+ const w = $(this).width();
+ const h = $(this).height();
+
+ if (offset.left + menu_width > offs.left + w) {
+ elements.push(this);
+ } else if (offset.top + menu_height > offs.top + h) {
+ elements.push(this);
+ }
+ });
+
+ return elements;
+}
+
+/**
+ * Scroll handler for all scroll related events.
+ * This will reposition the menu(s) according to the scrolled distance.
+ */
+function scrollHandler(event) {
+ const data = $(event.target).data('action-menu-scroll-data');
+
+ const diff_x = event.target.scrollLeft - data.left;
+ const diff_y = event.target.scrollTop - data.top;
+
+ data.menus.forEach((menu) => {
+ const offset = menu.offset();
+ menu.offset({
+ left: offset.left - diff_x,
+ top: offset.top - diff_y
+ });
+ });
+
+ data.left = event.target.scrollLeft;
+ data.top = event.target.scrollTop;
+
+ $(event.target).data('action-menu-scroll-data', data);
+}
+
+const stash = new Map();
+const secret = typeof Symbol === 'undefined' ? Math.random().toString(36).substring(2, 15) : Symbol();
+
+class ActionMenu {
+ /**
+ * Create menu using a singleton pattern for each element.
+ */
+ static create(element, position = true) {
+ const id = $(element).uniqueId().attr('id');
+ const breakpoint = determineBreakpoint(element);
+ if (!stash.has(id)) {
+ const menu_offset = $(element).offset().top + $('.action-menu-content', element).height();
+ const max_offset = $(breakpoint).offset().top + $(breakpoint).height();
+ const reversed = menu_offset > max_offset;
+
+ stash.set(id, new ActionMenu(secret, element, reversed, position));
+ }
+
+ return stash.get(id);
+ }
+
+ /**
+ * Closes all menus.
+ * @return {[type]} [description]
+ */
+ static closeAll() {
+ stash.forEach((menu) => menu.close());
+ }
+
+ /**
+ * Private constructor by implementing the secret/passed_secret mechanism.
+ */
+ constructor(passed_secret, element, reversed, position) {
+ // Enforce use of create (would use a private constructor if I could)
+ if (secret !== passed_secret) {
+ throw new Error('Cannot create ActionMenu. Use ActionMenu.create()!');
+ }
+
+ const offset = $(element).offset();
+ const height = $('.action-menu-content').height();
+ const width = $('.action-menu-content').width();
+ const breakpoint = determineBreakpoint(element);
+
+ this.element = $(element);
+ this.menu = this.element;
+ this.content = $('.action-menu-content', element);
+ this.is_reversed = reversed;
+ this.is_open = false;
+
+ const menu_width = this.content.width();
+ const menu_height = this.content.height();
+
+ // Reposition the menu?
+ if (position) {
+ var parents = getScrollableParents(this.element, menu_width, menu_height);
+ if (parents.length > 0) {
+ this.menu = $('<div class="action-menu-wrapper">').append(this.content.remove());
+ $('.action-menu-icon', element).clone().data('action-menu-element', element).prependTo(this.menu);
+
+ this.menu
+ .offset(this.element.offset())
+ .appendTo(breakpoint);
+
+ // Always add breakpoint
+ parents.push(breakpoint);
+ parents.forEach((parent, index) => {
+ var data = $(parent).data('action-menu-scroll-data') || {
+ menus: [],
+ left: parent.scrollLeft,
+ top: parent.scrollTop
+ };
+ data.menus.push(this.menu);
+
+ $(parent).data('action-menu-scroll-data', data);
+
+ if (data.menus.length < 2) {
+ $(parent).scroll(scrollHandler);
+ }
+ });
+ }
+ }
+
+ this.update();
+ }
+
+ /**
+ * Adds a class to the menu's element.
+ */
+ addClass(name) {
+ this.menu.addClass(name);
+ }
+
+ /**
+ * Open the menu.
+ */
+ open() {
+ this.toggle(true);
+ }
+
+ /**
+ * Close the menu.
+ */
+ close() {
+ this.toggle(false);
+ }
+
+ /**
+ * Toggle the menus display state. Pass a state to enforce it.
+ */
+ toggle(state = null) {
+ this.is_open = state === null ? !this.is_open : state;
+
+ this.update();
+ }
+
+ /**
+ * Update the menu element's attributes.
+ */
+ update() {
+ this.element.toggleClass('is-open', this.is_open);
+
+ this.menu.toggleClass('is-open', this.is_open);
+ this.menu.toggleClass('is-reversed', this.is_reversed);
+ this.menu.attr('aria-expanded', this.is_open ? 'true' : 'false');
+ }
+
+ /**
+ * Confirms an action in the action menu that calls a JavaScript function
+ * instead of linking to another URL.
+ */
+ confirmJSAction(element = null) {
+ //Show visual hint using a deferred. This way we don't need to
+ //duplicate the functionality in the done() handler.
+ //(code copied from copyable_link.js and modified)
+ (new Promise((resolve, reject) => {
+ var confirmation = $('<div class="js-action-confirmation">');
+ confirmation.text = jQuery(element).data('confirmation_text');
+ confirmation.insertBefore(element);
+ jQuery(element).parent().addClass('js-action-confirm-animation');
+ var timeout = setTimeout(() => {
+ jQuery(element).parent().off('animationend');
+ resolve(confirmation);
+ }, 1500);
+ jQuery(element).parent().one('animationend', () => {
+ clearTimeout(timeout);
+ resolve(confirmation);
+ });
+ })).then((confirmation, parent) => {
+ confirmation.remove();
+ jQuery(element).parent().removeClass('js-action-confirm-animation');
+ });
+ }
+}
+
+export default ActionMenu;
diff --git a/resources/assets/javascripts/lib/admin_sem_class.js b/resources/assets/javascripts/lib/admin_sem_class.js
new file mode 100644
index 0000000..e4fc20d
--- /dev/null
+++ b/resources/assets/javascripts/lib/admin_sem_class.js
@@ -0,0 +1,217 @@
+/* ------------------------------------------------------------------------
+ * SemClass administration - only for root-user
+ * ------------------------------------------------------------------------ */
+
+const admin_sem_class = {
+ make_sortable: function() {
+ var after_update = function(event, ui) {
+ if (
+ jQuery(ui.item).is('.core') &&
+ jQuery(this).is('#activated_plugins .droparea, #nonactivated_plugins .droparea')
+ ) {
+ jQuery('#deactivated_modules .droparea').append(
+ jQuery(ui.item)
+ .clone()
+ .fadeIn(1500)
+ );
+ jQuery(ui.item).remove();
+ }
+ if (jQuery(ui.item).is('.plugin:not(.core)') && jQuery(this).is('#deactivated_modules .droparea')) {
+ jQuery('#nonactivated_plugins .droparea').append(
+ jQuery(ui.item)
+ .clone()
+ .fadeIn(1500)
+ );
+ jQuery(ui.item).remove();
+ }
+
+ jQuery('.droparea.limited').each(function(index, droparea) {
+ if (jQuery(this).children().length === 0) {
+ jQuery(this).removeClass('full');
+ } else {
+ jQuery(this).addClass('full');
+ }
+ });
+ admin_sem_class.make_sortable();
+ };
+ jQuery('.droparea').sortable({
+ connectWith: '.droparea:not(.full)',
+ revert: 200,
+ update: after_update
+ });
+ jQuery('#plugins .droparea').sortable({
+ connectWith: '.droparea:not(.full, #deactivated_modules .droparea)',
+ revert: 200,
+ update: after_update
+ });
+ jQuery('#deactivated_modules .droparea').sortable({
+ connectWith: '.droparea:not(.full, #plugins .droparea)',
+ revert: 200,
+ update: after_update
+ });
+ },
+ saveData: function() {
+ var core_module_slots = {};
+ jQuery.each(
+ [
+ 'overview',
+ 'forum',
+ 'admin',
+ 'documents',
+ 'participants',
+ 'schedule',
+ 'literature',
+ 'scm',
+ 'wiki',
+ 'calendar',
+ 'elearning_interface',
+ 'resources'
+ ],
+ function(index, element) {
+ var module = jQuery('div[container=' + element + '] .droparea > div.plugin').attr('id');
+ if (module) {
+ module = module.substr(module.indexOf('_') + 1);
+ }
+ core_module_slots[element] = module ? module : '0';
+ }
+ );
+ var modules = {};
+ jQuery('div.plugin').each(function() {
+ var activated = jQuery(this)
+ .find('input[name=active]')
+ .is(':checked');
+ var sticky =
+ !jQuery(this)
+ .find('input[name=nonsticky]')
+ .is(':checked') || jQuery(this).is('#deactivated_modules div.plugin');
+ var module_name = jQuery(this).attr('id');
+ if (module_name) {
+ module_name = module_name.substr(module_name.indexOf('_') + 1);
+ }
+ modules[module_name] = {
+ activated: +activated,
+ sticky: +sticky
+ };
+ });
+ jQuery('#message_below').html('');
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/admin/sem_classes/save',
+ data: {
+ sem_class_id: jQuery('#sem_class_id').val(),
+ sem_class_name: jQuery('#sem_class_name').val(),
+ sem_class_description: jQuery('#sem_class_description').val(),
+ title_dozent: !jQuery('#title_dozent_isnull').is(':checked') ? jQuery('#title_dozent').val() : '',
+ title_dozent_plural: !jQuery('#title_dozent_isnull').is(':checked')
+ ? jQuery('#title_dozent_plural').val()
+ : '',
+ title_tutor: !jQuery('#title_tutor_isnull').is(':checked') ? jQuery('#title_tutor').val() : '',
+ title_tutor_plural: !jQuery('#title_tutor_isnull').is(':checked')
+ ? jQuery('#title_tutor_plural').val()
+ : '',
+ title_autor: !jQuery('#title_autor_isnull').is(':checked') ? jQuery('#title_autor').val() : '',
+ title_autor_plural: !jQuery('#title_autor_isnull').is(':checked')
+ ? jQuery('#title_autor_plural').val()
+ : '',
+ core_module_slots: core_module_slots,
+ modules: modules,
+ workgroup_mode: jQuery('#workgroup_mode').is(':checked') ? 1 : 0,
+ studygroup_mode: jQuery('#studygroup_mode').is(':checked') ? 1 : 0,
+ only_inst_user: jQuery('#only_inst_user').is(':checked') ? 1 : 0,
+ default_read_level: jQuery('#default_read_level').val(),
+ default_write_level: jQuery('#default_write_level').val(),
+ bereiche: jQuery('#bereiche').is(':checked') ? 1 : 0,
+ module: jQuery('#module').is(':checked') ? 1 : 0,
+ show_browse: jQuery('#show_browse').is(':checked') ? 1 : 0,
+ write_access_nobody: jQuery('#write_access_nobody').is(':checked') ? 1 : 0,
+ topic_create_autor: jQuery('#topic_create_autor').is(':checked') ? 1 : 0,
+ visible: jQuery('#visible').is(':checked') ? 1 : 0,
+ course_creation_forbidden: jQuery('#course_creation_forbidden').is(':checked') ? 1 : 0,
+ create_description: jQuery('#create_description').val(),
+ admission_prelim_default: jQuery('#admission_prelim_default').val(),
+ admission_type_default: jQuery('#admission_type_default').val(),
+ show_raumzeit: jQuery('#show_raumzeit').is(':checked') ? 1 : 0,
+ is_group: jQuery('#is_group').is(':checked') ? 1 : 0
+ },
+ type: 'POST',
+ dataType: 'json',
+ success: function(data) {
+ jQuery('#message_below').html(data.html);
+ }
+ });
+ },
+ delete_sem_type_question: function() {
+ var sem_type = jQuery(this)
+ .closest('li')
+ .attr('id');
+ sem_type = sem_type.substr(sem_type.lastIndexOf('_') + 1);
+ jQuery('#sem_type_for_deletion').val(sem_type);
+ jQuery('#sem_type_delete_question').dialog({
+ title: jQuery('#sem_type_delete_question_title').text()
+ });
+ },
+ add_sem_type: function() {
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/admin/sem_classes/add_sem_type',
+ type: 'post',
+ data: {
+ sem_class: jQuery('#sem_class_id').val(),
+ name: jQuery('#new_sem_type').val()
+ },
+ success: function(ret) {
+ jQuery('#sem_type_list').append(jQuery(ret));
+ jQuery('#new_sem_type')
+ .val('')
+ .closest('li')
+ .children()
+ .toggle();
+ },
+ error: function() {
+ jQuery('#new_sem_type')
+ .val('')
+ .closest('li')
+ .children()
+ .toggle();
+ }
+ });
+ },
+ delete_sem_type: function() {
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/admin/sem_classes/delete_sem_type',
+ data: {
+ sem_type: jQuery('#sem_type_for_deletion').val()
+ },
+ type: 'post',
+ success: function() {
+ jQuery('#sem_type_' + jQuery('#sem_type_for_deletion').val()).remove();
+ jQuery('#sem_type_delete_question').dialog('close');
+ }
+ });
+ },
+ rename_sem_type: function() {
+ jQuery(this)
+ .closest('span.name_container')
+ .children()
+ .toggle();
+ var name = this.value;
+ var old_name = jQuery(this)
+ .closest('.name_container')
+ .find('.name_html');
+ var sem_type = jQuery(this)
+ .closest('li')
+ .attr('id');
+ sem_type = sem_type.substr(sem_type.lastIndexOf('_') + 1);
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/admin/sem_classes/rename_sem_type',
+ data: {
+ sem_type: sem_type,
+ name: name
+ },
+ type: 'post',
+ success: function() {
+ old_name.text(name);
+ }
+ });
+ }
+};
+
+export default admin_sem_class;
diff --git a/resources/assets/javascripts/lib/admission.js b/resources/assets/javascripts/lib/admission.js
new file mode 100644
index 0000000..33e96aa
--- /dev/null
+++ b/resources/assets/javascripts/lib/admission.js
@@ -0,0 +1,294 @@
+/* ------------------------------------------------------------------------
+ * Anmeldeverfahren und -sets
+ * ------------------------------------------------------------------------ */
+import { $gettext } from './gettext.js';
+import Dialog from './dialog.js';
+import Dialogs from './dialogs.js';
+
+const Admission = {
+ getCourses: function(targetUrl) {
+ var courseFilter = $('input[name="course_filter"]').val();
+ if (courseFilter == '') {
+ courseFilter = '%%%';
+ }
+ var data = {
+ 'courses[]': _.map($('#courselist input:checked'), 'id'),
+ course_filter: courseFilter,
+ semester: $('select[name="semester"]').val(),
+ 'institutes[]': $.merge(
+ _.map($('input[name="institutes[]"]:hidden'), 'value'),
+ _.map($('input[name="institutes[]"]:checked'), 'value')
+ )
+ };
+ var loading = $gettext('Wird geladen');
+ $('#instcourses').empty();
+ $('<img/>', {
+ src: STUDIP.ASSETS_URL + 'images/ajax_indicator_small.gif'
+ }).appendTo('#instcourses');
+ $('#instcourses').append(loading);
+ $('#instcourses').load(targetUrl, data);
+ return false;
+ },
+
+ configureRule: function(ruleType, targetUrl, ruleId) {
+ var urlparts = targetUrl.split('?');
+ targetUrl = urlparts[0] + '/' + ruleType;
+ if (urlparts[1]) {
+ targetUrl += '?' + urlparts[1];
+ }
+
+ Dialog.fromURL(targetUrl, {
+ method: 'post',
+ size: 'auto',
+ title: $gettext('Anmelderegel konfigurieren'),
+ id: 'configurerule',
+ data: { ruleId: ruleId, rules: _.map($('#rules input[name="rules[]"]'), 'value') }
+ });
+
+ return false;
+ },
+
+ selectRuleType: function(source) {
+ Dialog.fromURL(source, {
+ title: $gettext('Anmelderegel konfigurieren'),
+ size: 'auto',
+ data: { rules: _.map($('#rules input[name="rules[]"]'), 'value') },
+ method: 'post',
+ id: 'configurerule'
+ });
+ return false;
+ },
+
+ saveRule: function(ruleId, targetId, targetUrl) {
+ if ($('#action').val() !== 'cancel') {
+ $.ajax({
+ type: 'post',
+ url: targetUrl,
+ data: $('#ruleform').serialize(),
+ dataType: 'html',
+ success: function(data, textStatus, jqXHR) {
+ if (data !== '') {
+ var result = '';
+ if ($('#norules').length > 0) {
+ $('#norules').remove();
+ $('#' + targetId).prepend('<div id="rulelist"></div>');
+ }
+ result += data;
+ if ($('#rule_' + ruleId).length !== 0) {
+ $('#rule_' + ruleId).replaceWith(result);
+ } else {
+ $('#rulelist').append(result);
+ }
+ }
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ alert('Status: ' + textStatus + '\nError: ' + errorThrown);
+ }
+ });
+ }
+ Admission.closeDialog('configurerule');
+ Admission.toggleNotSavedAlert();
+ return false;
+ },
+
+ removeRule: function(targetId, containerId) {
+ var parent = $('#' + targetId).parent();
+ $('#' + targetId).remove();
+ if (parent.children('div').length === 0) {
+ parent.remove();
+ var norules = $gettext('Sie haben noch keine Anmelderegeln festgelegt.');
+ $('#' + containerId).prepend('<span id="norules">' + '<i>' + norules + '</i></span>');
+ }
+ Dialogs.closeConfirmDialog();
+ Admission.toggleNotSavedAlert();
+ },
+
+ toggleRuleDescription: function(targetId) {
+ $('#' + targetId).toggle();
+ return false;
+ },
+
+ toggleDetails: function(arrowId, detailId) {
+ var oldSrc = $('#' + arrowId).attr('src');
+ var newSrc = $('#' + arrowId).attr('rel');
+ $('#' + arrowId).attr('src', newSrc);
+ $('#' + arrowId).attr('rel', oldSrc);
+ $('#' + detailId).slideToggle();
+ return false;
+ },
+
+ /**
+ *
+ * @param String ruleId The rule to save.
+ * @param String errorTarget Target element ID where error messages will be
+ * shown.
+ * @param String validateUrl URL to call for validation.
+ * @param String savedTarget Target element ID where the saved rule will be
+ * displayed.
+ * @param String saveUrl URL to save the rule.
+ */
+ checkAndSaveRule: function(ruleId, errorTarget, validateUrl, savedTarget, saveUrl) {
+ if (Admission.validateRuleConfig(errorTarget, validateUrl)) {
+ Admission.saveRule(ruleId, savedTarget, saveUrl);
+ Dialog.close({ id: 'configurerule' });
+ }
+ return false;
+ },
+
+ validateRuleConfig: function(containerId, targetUrl) {
+ var valid = true;
+ var error = $.ajax({
+ type: 'post',
+ async: false,
+ url: targetUrl,
+ data: $('#ruleform').serialize(),
+ dataType: 'html',
+
+ error: function(jqXHR, textStatus, errorThrown) {
+ alert('Status: ' + textStatus + '\nError: ' + errorThrown);
+ }
+ }).responseText;
+ error = error.replace(/(\r\n|\n|\r)/gm, '');
+ if ($.trim(error) != '') {
+ $('#' + containerId).html(error);
+ valid = false;
+ }
+ return valid;
+ },
+
+ removeUserFromUserlist: function(userId) {
+ var parent = $('#user_' + userId).parent();
+ $('#user_' + userId).remove();
+ if (parent.children('li').length === 0) {
+ var nousers = $gettext('Sie haben noch niemanden hinzugefügt.');
+ $(parent)
+ .parent()
+ .append('<span id="nousers">' + '<i>' + nousers + '</i></span>');
+ }
+ return false;
+ },
+
+ /**
+ * Creates a tree view from the HTML list in <elementId> using the
+ * given data for special node types.
+ *
+ * @param String elementId
+ * @param typesData JS object with tree nodes types
+ * (@see http://www.jstree.com/documentation/types)
+ */
+ makeTree: function(elementId, typesData) {
+ var config = {
+ core: {
+ animation: 100,
+ open_parents: true,
+ initially_open: ['root']
+ },
+ checkbox: {
+ real_checkboxes: true,
+ selected_parent_open: true,
+ override_ui: false,
+ two_state: true
+ },
+ plugins: ['html_data', 'themes', 'types', 'checkbox', 'ui']
+ };
+ config.types = { types: typesData };
+ $('#' + elementId)
+ .on('loaded.jstree', function(event, data) {
+ // Show checked checkboxes.
+ var checkedItems = $('#' + elementId).find('.jstree-checked');
+ checkedItems.removeClass('jstree-unchecked');
+ // Open parent nodes of checked nodes.
+ checkedItems.parents().each(function() {
+ data.inst.open_node(this, false, true);
+ });
+ })
+ .jstree(config);
+ },
+
+ updateInstitutes: function(elementId, instURL, courseURL, mode) {
+ if (elementId !== '') {
+ var query = '';
+ $('.institute').each(function() {
+ query += '&institutes[]=' + this.value;
+ });
+ switch (mode) {
+ case 'delete':
+ $('#' + elementId).remove();
+ break;
+ case 'add':
+ query += '&institutes[]=' + elementId;
+ $.post(instURL, query, function(data) {
+ $('#institutes').html(data);
+ });
+ break;
+ }
+ $('#instcourses :checked').each(function() {
+ query += '&courses[]=' + this.value;
+ });
+ this.getCourses(courseURL);
+ Admission.toggleNotSavedAlert();
+ }
+ },
+
+ checkRuleActivation: function(target) {
+ var form = $('#' + target);
+ var globalActivation = form.find('input[name=enabled]');
+ if (globalActivation.prop('checked')) {
+ $('#activation').show();
+ if (form.find('input[name=activated]:checked').val() === 'studip') {
+ $('#institutes_activation').hide();
+ } else {
+ $('#institutes_activation').show();
+ }
+ } else {
+ $('#activation').hide();
+ $('#institutes_activation').hide();
+ }
+ },
+
+ closeDialog: function(elementId) {
+ $('#' + elementId).remove();
+ },
+
+ checkUncheckAll: function(inputName, mode) {
+ switch (mode) {
+ case 'check':
+ $('input[name*="' + inputName + '"]').each(function() {
+ $(this).prop('checked', true);
+ });
+ break;
+ case 'uncheck':
+ $('input[name*="' + inputName + '"]').each(function() {
+ $(this).prop('checked', false);
+ });
+ break;
+ case 'invert':
+ $('input[name*="' + inputName + '"]').each(function() {
+ $(this).prop('checked', !$(this).prop('checked'));
+ });
+ break;
+ }
+ return false;
+ },
+
+ toggleNotSavedAlert: function() {
+ $('.hidden-alert').show();
+ },
+
+ autosaveCourseset: function(event) {
+ $.post({
+ url: $('#courseset-form').attr('action'),
+ data: $('#courseset-form').serialize() + '&submit=1',
+ dataType: 'html',
+ success: function(data, textStatus, jqXHR) {
+ $('.hidden-alert').hide();
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ alert('Status: ' + textStatus + '\nError: ' + errorThrown);
+ }
+ });
+ }
+
+};
+
+export default Admission;
diff --git a/resources/assets/javascripts/lib/arbeitsgruppen.js b/resources/assets/javascripts/lib/arbeitsgruppen.js
new file mode 100644
index 0000000..fa0f97e
--- /dev/null
+++ b/resources/assets/javascripts/lib/arbeitsgruppen.js
@@ -0,0 +1,17 @@
+/* ------------------------------------------------------------------------
+ * Studentische Arbeitsgruppen
+ * ------------------------------------------------------------------------ */
+
+const Arbeitsgruppen = {
+ toggleOption: function(user_id) {
+ if (jQuery('#user_opt_' + user_id).is(':hidden')) {
+ jQuery('#user_opt_' + user_id).show('slide', { direction: 'left' }, 400, function() {
+ jQuery('#user_opt_' + user_id).css('display', 'inline-block');
+ });
+ } else {
+ jQuery('#user_opt_' + user_id).hide('slide', { direction: 'left' }, 400);
+ }
+ }
+};
+
+export default Arbeitsgruppen;
diff --git a/resources/assets/javascripts/lib/archive.js b/resources/assets/javascripts/lib/archive.js
new file mode 100644
index 0000000..64c689b
--- /dev/null
+++ b/resources/assets/javascripts/lib/archive.js
@@ -0,0 +1,15 @@
+const Archive = {
+ removeArchivedCourses: function(courseIds) {
+ /*
+ * Removes courses that are archived from the course list
+ * seen in the admin/courses controller.
+ */
+
+ for (var i = 0; i < courseIds.length; i++) {
+ courseIds[i] = '#course-' + courseIds[i];
+ jQuery(courseIds[i]).remove();
+ }
+ }
+};
+
+export default Archive;
diff --git a/resources/assets/javascripts/lib/audio.js b/resources/assets/javascripts/lib/audio.js
new file mode 100644
index 0000000..7b23055
--- /dev/null
+++ b/resources/assets/javascripts/lib/audio.js
@@ -0,0 +1,44 @@
+var initialised = false,
+ loaded = false,
+ queue = [],
+ load_audioplayer = function() {
+ AudioPlayer.setup(STUDIP.ASSETS_URL + 'flash/player.swf', {
+ animation: 'no',
+ transparentpagebg: 'yes',
+ width: 300
+ });
+ loaded = true;
+
+ // Process queue
+ var item = queue.shift();
+ while (item) {
+ Audio.handle(item);
+ item = queue.shift();
+ }
+ },
+ initialise = function() {
+ if (!initialised) {
+ var script = document.createElement('script');
+ script.src = STUDIP.ASSETS_URL + 'javascripts/audio-player.js';
+ script.onload = load_audioplayer;
+ document.getElementsByTagName('head')[0].appendChild(script);
+ initialised = true;
+ }
+ return loaded;
+ };
+
+const Audio = {
+ handle: function(element) {
+ if (!initialise()) {
+ queue.push(element);
+ } else {
+ AudioPlayer.embed(element.id, {
+ soundFile: encodeURIComponent(element.src),
+ titles: element.title,
+ width: element.clientWidth || 300
+ });
+ }
+ }
+};
+
+export default Audio;
diff --git a/resources/assets/javascripts/lib/avatar.js b/resources/assets/javascripts/lib/avatar.js
new file mode 100644
index 0000000..a034910
--- /dev/null
+++ b/resources/assets/javascripts/lib/avatar.js
@@ -0,0 +1,111 @@
+const Avatar = {
+ cropper: null,
+
+ init: function(inputSelector) {
+ jQuery(document).on('change', inputSelector, function() {
+ Avatar.readFile(this);
+
+ jQuery(document)
+ .off('submit.avatar', 'form.settings-avatar')
+ .on('submit.avatar', 'form.settings-avatar', function() {
+ var data = Avatar.cropper.getData();
+ return Avatar.checkImageSize(data);
+ });
+ });
+ },
+
+ readFile: function(input) {
+ if (window.FileReader && input.files && input.files[0]) {
+ var reader = new window.FileReader();
+
+ if (input.files[0].size <= jQuery(input).data('max-size')) {
+ var container = jQuery('div#avatar-preview'),
+ dialog = container.closest('div[role="dialog"]');
+ // We are in a modal dialog
+ if (dialog.length > 0) {
+ // Adjust maximal cropper container height to dialog dimensions.
+ container.css('height', dialog.height() - 200);
+ container.css('width', dialog.width() - 220);
+ container.css('max-height', dialog.height() - 200);
+ container.css('max-width', dialog.width() - 220);
+ // No dialog, full page.
+ } else {
+ dialog = jQuery('#layout_content');
+ // Responsive view.
+ if (jQuery('html').hasClass('responsified')) {
+ // Adjust maximal cropper container height to page dimensions.
+ container.css('height', dialog.height() - 220);
+ container.css('width', 0.95 * dialog.width());
+ container.css('max-height', dialog.height() * 220);
+ container.css('max-width', 0.95 * dialog.width());
+ // Non-dialog, non-responsive view.
+ } else {
+ // Adjust maximal cropper container height to page dimensions.
+ container.css('height', dialog.height() - 100);
+ container.css('width', dialog.width() - 200);
+ container.css('max-height', dialog.height() * 220);
+ container.css('max-width', dialog.width() - 100);
+ }
+ }
+
+ reader.onload = function(event) {
+ var image = document.getElementById('new-avatar');
+ if (image) {
+ image.src = event.target.result;
+
+ import(/* webpackChunkName: "avatarcropper" */ 'cropperjs/dist/cropper.js')
+ .then(function(cropperjs) {
+ var Cropper = cropperjs['default'];
+ Avatar.cropper = new Cropper(image, {
+ aspectRatio: 1,
+ viewMode: 2
+ });
+ })
+ .catch(function(error) {
+ console.log('An error occurred while loading the croppers lib', error);
+ });
+ }
+ };
+
+ reader.readAsDataURL(input.files[0]);
+
+ jQuery('#avatar-buttons').removeClass('hidden-js');
+ jQuery('label.file-upload').hide();
+ jQuery('#avatar-zoom-in').on('click', function() {
+ Avatar.cropper.zoom(0.1);
+ return false;
+ });
+ jQuery('#avatar-zoom-out').on('click', function() {
+ Avatar.cropper.zoom(-0.1);
+ return false;
+ });
+ jQuery('#avatar-rotate-clockwise').on('click', function() {
+ Avatar.cropper.rotate(90);
+ return false;
+ });
+ jQuery('#avatar-rotate-counter-clockwise').on('click', function() {
+ Avatar.cropper.rotate(-90);
+ return false;
+ });
+
+ jQuery('#submit-avatar').on('click', function() {
+ jQuery('#cropped-image').attr('value', Avatar.cropper.getCroppedCanvas().toDataURL('image/jpeg', 0.7));
+ });
+ } else {
+ alert(jQuery(input).data('message-too-large'));
+ }
+ } else {
+ alert("Sorry - your browser doesn't support the FileReader API");
+ }
+ },
+
+ checkImageSize: function(data) {
+ // Show a warning if cropped area is smaller than 250x250px.
+ if (data.width < 250 || data.height < 250) {
+ return confirm(jQuery('#new-avatar').data('message-too-small'));
+ }
+ return true;
+ }
+};
+
+export default Avatar;
diff --git a/resources/assets/javascripts/lib/big_image_handler.js b/resources/assets/javascripts/lib/big_image_handler.js
new file mode 100644
index 0000000..5130997
--- /dev/null
+++ b/resources/assets/javascripts/lib/big_image_handler.js
@@ -0,0 +1,139 @@
+/**
+ * Handle oversized a.k.a. "big" images that are originally greater in
+ * width or height than they are displayed.
+ *
+ * Any oversized image will be clickable and is displayed in an overlay
+ * as long as it does not meet certain criteria that will exclude it from
+ * this mechanism (see method shouldSkip for more info).
+ *
+ * The big image handler my be enabled and disabled by an api bound to
+ * the global STUDIP object (see methods STUDIP.BigImageHandler.enable and
+ * STUDIP.BigImageHandler.disable).
+ *
+ * Images are only handled if they exceed a certain threshold in any
+ * direction. This threshold can be adjusted in the variable
+ * STUDIP.BigImageHandler.threshold.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license GPL2 or any later version
+ * @since Stud.IP 3.4
+ */
+import { $gettext } from './gettext.js';
+
+var pixelRatio = window.devicePixelRatio || 1,
+ dataAttribute = 'big-image-handled';
+
+// Determines whether the image should not be handled due to one of the
+// following reasons:
+//
+// - image is inside an editable element (wysiwyg)
+// - image is an avatar
+// - image is an icon
+// - image is a svg
+// - image is linked to something else than itself
+// - image has the class "ignore-size"
+function shouldSkip(img) {
+ var $img = $(img),
+ $link = $img.closest('a'),
+ src = $img.attr('src');
+ return (
+ $img.data(dataAttribute) ||
+ $img.closest('[contenteditable]').length > 0 ||
+ ($link.length > 0 && $link.attr('href') !== src) ||
+ src.match(/\.svg$/)
+ );
+}
+
+// The actual handler for images. This determines whether the image
+// is considered big and should be treated that way.
+// If the image is oversized, store the actual width and height of the
+// image in it's data storage and add the "oversized-image" class to it.
+//
+// This function will return a function to be used as an onload handler.
+function oversizedHandler(img) {
+ var display_width = Math.max(BigImageHandler.threshold, parseInt($(img).width(), 10)),
+ display_height = Math.max(BigImageHandler.threshold, parseInt($(img).height(), 10));
+ return function() {
+ var width = this.width,
+ height = this.height,
+ title =
+ $(this).prop('title') ||
+ $gettext('Dieses Bild wird verkleinert dargestellt. Klicken Sie für eine größere Darstellung.'),
+ highdpi_check = width / display_width === pixelRatio && height / display_height === pixelRatio;
+ if (!highdpi_check && (width > display_width || height > display_height)) {
+ $(img)
+ .data('oversized', {
+ width: width,
+ height: height
+ })
+ .prop('title', title)
+ .addClass('oversized-image');
+ }
+ };
+}
+
+// Set up global js api
+const BigImageHandler = {
+ // Threshold for activating the handler, images must be greater
+ // than this value in any direction to trigger the handler
+ threshold: 64
+};
+
+// Enables the mechanism
+BigImageHandler.enable = function() {
+ // Global handlers:
+ // - check if an image is oversized on mouseenter
+ // - create overlay/zoom on click on the image
+ // - remove overlay/zoom on click on itself or escape key
+ $(document)
+ .on('mouseenter.big-image-handler', '.formatted-content img', function() {
+ if (!shouldSkip(this)) {
+ var img = new Image();
+ img.onload = oversizedHandler(this);
+ img.src = this.src;
+ $(this).data(dataAttribute, true);
+ }
+ })
+ .on('click.big-image-handler', 'img.oversized-image', function(event) {
+ var src = $(this).attr('src'),
+ data = $(this).data('oversized'),
+ zoomed = $('<span>').css('background-image', 'url(' + src + ')'),
+ overlay = $('<div class="oversized-image-zoom">');
+
+ // Set dimensions
+ zoomed.width(data.width);
+ zoomed.height(data.height);
+
+ // Add invisible image (see css) to allow right click "view image"
+ $('<img>')
+ .attr('src', src)
+ .appendTo(zoomed);
+
+ // Append overlay
+ overlay.append(zoomed).appendTo('body');
+
+ // Stop event
+ event.stopPropagation();
+ event.preventDefault();
+ })
+ .on('click.big-image-handler', '.oversized-image-zoom', function() {
+ // remove overlay
+ $(this).remove();
+ })
+ .on('keypress.big-image-handler', 'body:has(.oversized-image-zoom)', function(event) {
+ if (event.key === 'Escape') {
+ $('.oversized-image-zoom').remove();
+
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ });
+};
+
+// Disable the mechanism
+BigImageHandler.disable = function() {
+ $('img.oversized-image').removeClass('oversized-image');
+ $(document).off('.big-image-handler');
+};
+
+export default BigImageHandler;
diff --git a/resources/assets/javascripts/lib/blubber.js b/resources/assets/javascripts/lib/blubber.js
new file mode 100644
index 0000000..c2d85b2
--- /dev/null
+++ b/resources/assets/javascripts/lib/blubber.js
@@ -0,0 +1,221 @@
+/*jslint esversion: 6*/
+import { $gettext } from '../lib/gettext.js';
+import BlubberGlobalstream from '../../../vue/components/BlubberGlobalstream.vue';
+import BlubberPublicComposer from '../../../vue/components/BlubberPublicComposer.vue';
+import BlubberThread from '../../../vue/components/BlubberThread.vue';
+import BlubberThreadWidget from '../../../vue/components/BlubberThreadWidget.vue';
+
+const components = {
+ BlubberGlobalstream,
+ BlubberPublicComposer,
+ BlubberThread,
+ BlubberThreadWidget,
+};
+
+const Blubber = {
+ App: null, //This app is not always available. The app is blubber with a widget and the threads next to it.
+ threads: [],
+ init () {
+ if ($('#blubber-index, #messenger-course, .blubber_panel.vueinstance').length) {
+ STUDIP.JSUpdater.register('blubber', Blubber.updateState, Blubber.getParamsForPolling);
+
+ let panel_data = $('.blubber_panel').data();
+ STUDIP.Vue.load().then(({createApp}) => {
+ STUDIP.Blubber.App = createApp({
+ el: '#layout_container',
+ data: {
+ threads: $('.blubber_threads_widget').data('threads_data'),
+ thread_data: panel_data.thread_data,
+ active_thread: panel_data.active_thread,
+ threads_more_down: panel_data.threads_more_down,
+ waiting: false,
+ display_context_posting: 0
+ },
+ methods: {
+ changeActiveThread: function (thread_id) {
+ this.waiting = true;
+ let search = jQuery("form.sidebar-search input[name=search]").val();
+ let parameters = search ? {data: {"search": search}} : {};
+ STUDIP.api.GET(`blubber/threads/${thread_id}`, parameters).done((data) => {
+ this.active_thread = thread_id;
+ this.thread_data = data;
+ }).always(() => {
+ this.waiting = false;
+ }).fail(() => {
+ window.alert($gettext("Konnte die Konversation nicht laden. Probieren Sie es nachher erneut."));
+ });
+ for (let i in this.threads) {
+ if (this.threads[i].thread_id === thread_id) {
+ this.threads[i].unseen_comments = 0;
+ }
+ }
+ }
+ },
+ components,
+ });
+ });
+
+ jQuery("form.sidebar-search").on("submit", function (event) {
+ this.waiting = true;
+ let search = jQuery("form.sidebar-search input[name=search]").val();
+ if ($('#messenger-course').length === 0) {
+ STUDIP.api.GET(`blubber/threads`, {data: {"search": search}}).done((data) => {
+ STUDIP.Blubber.App.threads = data.threads;
+ STUDIP.Blubber.App.threads_more_down = data.more_down;
+ $('.blubber_thread_widget')[0].__vue__.display_more_down = data.more_down;
+ }).always(() => {
+ this.waiting = false;
+ }).fail(() => {
+ window.alert($gettext("Konnte die Suche nicht ausführen. Probieren Sie es nachher erneut."));
+ });
+ }
+ let parameters = search ? {"search": search} : {"modifier": "olderthan"};
+ STUDIP.api.GET(`blubber/threads/` + STUDIP.Blubber.App.active_thread + `/comments`, {data: parameters}).done((data) => {
+ STUDIP.Blubber.App.thread_data.comments = data.comments;
+ STUDIP.Blubber.App.thread_data.more_up = data.more_up;
+ STUDIP.Blubber.App.thread_data.more_down = data.more_down;
+ $('.blubber_thread')[0].__vue__.scrollDown();
+ }).always(() => {
+ this.waiting = false;
+ }).fail(() => {
+ window.alert($gettext("Konnte die Suche nicht ausführen. Probieren Sie es nachher erneut."));
+ });
+ event.preventDefault();
+ return false;
+ });
+ jQuery('#blubber-index, #messenger-course').on("click", 'a.blubber_hashtag', function (event) {
+ let tag = jQuery(this).closest("a").data("tag");
+ jQuery("form.sidebar-search input[name=search]").val("#" + tag);
+ jQuery("form.sidebar-search").trigger("submit");
+ event.preventDefault();
+ return false;
+ });
+ }
+
+ $(document).on('dialog-open', function() {
+ $('.studip-dialog .blubber_panel').each(function () {
+ STUDIP.JSUpdater.register('blubber', Blubber.updateState, Blubber.getParamsForPolling);
+
+ let panel_data = $(this).data();
+ STUDIP.Vue.load().then(({createApp}) => {
+ createApp({
+ el: this,
+ data: {
+ threads: panel_data.threads_data,
+ thread_data: panel_data.thread_data,
+ active_thread: panel_data.active_thread,
+ threads_more_down: panel_data.threads_more_down,
+ waiting: false,
+ display_context_posting: 0
+ },
+ components,
+ });
+ });
+ });
+ });
+ },
+ updateState(datagram) {
+ for (const [method, data] of Object.entries(datagram)) {
+ if (method in Blubber) {
+ Blubber[method](data);
+ }
+ }
+ },
+ getParamsForPolling () {
+ const data = {
+ threads: [],
+ };
+ $('.blubber_thread').each(function () {
+ data.threads.push(this.__vue__._props.thread_data.thread_posting.thread_id);
+ });
+
+ return data;
+ },
+ addNewComments (blubberdata) {
+ $('.blubber_thread').each(function () {
+ for (let thread_id in blubberdata) {
+ if (this.__vue__._props.thread_data.thread_posting.thread_id === thread_id) {
+ this.__vue__.addComments(blubberdata[thread_id], true);
+ this.__vue__.scrollDown();
+ }
+ }
+ });
+ },
+ removeDeletedComments: function (comment_ids) {
+ $('.blubber_thread').each(function () {
+ this.__vue__.removeDeletedComments(comment_ids);
+ });
+ },
+ updateThreadWidget (threaddata) {
+ for (let i in threaddata) {
+ let exists = false;
+ for (let k in STUDIP.Blubber.App.threads) {
+ if (STUDIP.Blubber.App.threads[k].thread_id == threaddata[i].thread_id) {
+ exists = true;
+ STUDIP.Blubber.App.threads[k].name = threaddata[i].name;
+ STUDIP.Blubber.App.threads[k].timestamp = threaddata[i].timestamp;
+ STUDIP.Blubber.App.threads[k].avatar = threaddata[i].avatar;
+ }
+ }
+ if (!exists) {
+ STUDIP.Blubber.App.threads.push(threaddata[i]);
+ }
+ }
+ },
+ refreshThread (data) {
+ STUDIP.Blubber.App.changeActiveThread(data.thread_id);
+ },
+ followunfollow (thread_id, follow) {
+ const elements = $(`.blubber_panel .followunfollow[data-thread_id="${thread_id}"]`);
+ if (follow === undefined) {
+ follow = elements.hasClass('unfollowed');
+ }
+ elements.addClass('loading');
+
+ const promise = follow
+ ? STUDIP.api.POST(`blubber/threads/${thread_id}/follow`)
+ : STUDIP.api.DELETE(`blubber/threads/${thread_id}/follow`);
+
+ return promise.then(() => {
+ elements.toggleClass('unfollowed', !follow);
+ return follow;
+ }).always(() => {
+ elements.removeClass('loading');
+ }).promise();
+ },
+ Composer: {
+ vue: null,
+ async init () {
+ STUDIP.Blubber.Composer.vue = await STUDIP.Vue.load().then(({createApp}) => {
+ return createApp({
+ el: '#blubber_contact_ids',
+ data: {
+ users: []
+ },
+ methods: {
+ addUser: function (user_id, name) {
+ this.users.push({
+ user_id: user_id,
+ name: name
+ });
+ },
+ removeUser: function (event) {
+ let user_id = $(event.target).closest('li').find('input').val();
+ for (let i in this.users) {
+ if (this.users[i].user_id === user_id) {
+ this.$delete(this.users, i);
+ }
+ }
+ },
+ clearUsers: function () {
+ this.users = [];
+ }
+ },
+ components,
+ });
+ });
+ }
+ }
+};
+
+export default Blubber;
diff --git a/resources/assets/javascripts/lib/browse.js b/resources/assets/javascripts/lib/browse.js
new file mode 100644
index 0000000..74e6d31
--- /dev/null
+++ b/resources/assets/javascripts/lib/browse.js
@@ -0,0 +1,7 @@
+const Browse = {
+ selectUser: function(username) {
+ window.location.href = STUDIP.URLHelper.getURL('dispatch.php/profile', { username: username });
+ }
+};
+
+export default Browse;
diff --git a/resources/assets/javascripts/lib/cache.js b/resources/assets/javascripts/lib/cache.js
new file mode 100644
index 0000000..0423fb1
--- /dev/null
+++ b/resources/assets/javascripts/lib/cache.js
@@ -0,0 +1,229 @@
+/*jslint esversion: 6*/
+import Cookie from './cookie.js';
+
+/**
+ * Stud.IP: Caching in JavaScript
+ *
+ * Uses local storage for persistent storage across browser sessions
+ * for items with a given expiry or as a tab spanning session storage
+ * when no expiry is given.
+ *
+ * Example:
+ *
+ * var cache = STUDIP.Cache.getInstance(),
+ * foo = cache.get('foo');
+ * if (typeof foo === undefined) {
+ * foo = 'bar';
+ * cache.set('foo', foo);
+ * }
+ *
+ * Pass set() an expiry duration in seconds to allow persistent storage
+ * across browser sessions.
+ *
+ * Example:
+ *
+ * var cache = STUDIP.Cache.getInstance(),
+ * tmp;
+ * cache.set('foo', 'bar', 5);
+ * tmp = cache.get('foo');
+ * setTimeout(function () {
+ * console.log([tmp, cache.get('foo')]);
+ * }, 6000);
+ * // Will result in ['bar', undefined] after 6 seconds have passed
+ *
+ * You may pass get() a creator function as an optional second parameter
+ * so the value will be generated on the fly if not found in cache.
+ *
+ * Example:
+ *
+ * var cache = STUDIP.Cache.getInstance(),
+ * creator = function (index) { return 'Hello ' + index; };
+ * cache.remove('World');
+ * console.log(cache.get('World', creator));
+ * // Will result in 'Hello World' both on the console and in cache
+ *
+ * Cache instances may use prefixes to avoid conflicts with other js
+ * functions (this is the single reason why the lib was designed to use a
+ * getInstance() method).
+ *
+ * Example:
+ *
+ * var cache0 = STUDIP.Cache.getInstance(''),
+ * cache1 = STUDIP.Cache.getInstance('foo');
+ * cache0.set('foobar', 'baz');
+ * console.log([cache0.get('bar'), cache1.get('bar')]);
+ * // Will result in [undefined, 'baz']
+ *
+ * If the browser does not support any of the storage types, a dummy polyfill
+ * will be used that doesn't actually store data.
+ *
+ * Internally, all items are prefixed with a 'studip.' in order to avoid
+ * clashes.
+ *
+ * This implementation does not use sessionStorage due to the fact that the
+ * cache should work across tabs and windows. A session is indicated by a
+ * session cookie that this implementation will use.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license GPL2 or any later version
+ * @copyright Stud.IP core group
+ * @since Stud.IP 3.2
+ */
+
+// Use localstorage or dummy
+var cache;
+try {
+ let test_key = '__storageTest123';
+ window.localStorage.setItem(test_key, 'foo');
+ window.localStorage.removeItem(test_key);
+ cache = window.localStorage;
+} catch (e) {
+ cache = new class {
+ constructor() { this.length = 0; }
+ clear() {}
+ getItem() { return undefined; }
+ key() { return undefined; }
+ removeItem() {}
+ setItem() {}
+ }();
+}
+
+class Cache {
+ /**
+ * @param string prefix Optional prefix for the cache
+ */
+ constructor(prefix, session_id) {
+ this.prefix = 'studip.' + (prefix || '');
+ this.session_id = session_id;
+ }
+
+ /**
+ * Locates an item in the caches.
+ *
+ * @param String index Key of the item to look up
+ * @return mixed false if item is not found, item's value otherwise
+ */
+ locate(index) {
+ index = this.prefix + index;
+
+ if (cache.hasOwnProperty(index)) {
+ const now = new Date().getTime();
+
+ let item = JSON.parse(cache.getItem(index));
+
+ if (!item.expires || item.expires > now) {
+ return item.value;
+ }
+
+ cache.removeItem(index);
+ }
+
+ return undefined;
+ }
+
+ /**
+ * Returns whether the cache has an item stored for the given key.
+ *
+ * @param String index Key used to store the item
+ * @return bool
+ */
+ has(index) {
+ return this.locate(index) !== undefined;
+ }
+
+ /**
+ * Retrieves an object from the cache for the given key.
+ * You may provide an additional creator function if the
+ * value was not found to immediately create and set it.
+ * The function will be passed the index as it's only argument.
+ *
+ * @param String index Key used to store the item
+ * @param mixed creator Optional creator function for the value
+ * @param mixed expires Optional storage duration in seconds
+ * @return mixed Value of the item or undefined if not found.
+ */
+ get(index, setter, expires) {
+ var result = this.locate(index);
+ if (result === undefined && setter && typeof setter === 'function') {
+ result = setter(index);
+ this.set(index, result, expires);
+ }
+ return result;
+ }
+
+ /**
+ * Store an item in the cache.
+ *
+ * @param String index Key used to store the item
+ * @param mixed value Value of the item
+ * @param mixed expires Optional storage duration in seconds
+ */
+ set(index, value, expires) {
+ index = this.prefix + index;
+
+ cache.setItem(index, JSON.stringify({
+ value: value,
+ expires: expires ? new Date().getTime() + expires * 1000 : false,
+ session: this.session_id
+ }));
+ }
+
+ /**
+ * Removes an item from the cache.
+ *
+ * @param String index Key used to store the item
+ */
+ remove(index) {
+ if (this.has(index)) {
+ index = this.prefix + index;
+ cache.removeItem(index);
+ }
+ }
+
+ /**
+ * Clears the cache completely. Respects the prefix, so only
+ * the prefixed items will be removed.
+ */
+ prune() {
+ if (this.prefix) {
+ for (let key in cache) {
+ if (cache.hasOwnProperty(key) && key.indexOf(this.prefix) === 0) {
+ cache.removeItem(key);
+ }
+ }
+ } else {
+ cache.clear();
+ }
+ }
+}
+
+/**
+ * Expose the Cache object with it's getInstance method to the global
+ * STUDIP object.
+ */
+const CacheFacade = {
+ getInstance: function (prefix) {
+ // Initialized browser session?
+ const now = new Date().getTime();
+ var session_id = Cookie.get('cache_session');
+ if (session_id === undefined) {
+ session_id = new Date().getTime().toString();
+ Cookie.set('cache_session', session_id);
+
+ for (let key in cache) {
+ if (!cache.hasOwnProperty(key) || key.indexOf('studip.') !== 0) {
+ continue;
+ }
+
+ var item = JSON.parse(cache.getItem(key));
+ if (item.expires < now || (item.expires === false && item.session !== session_id)) {
+ cache.removeItem(key);
+ }
+ }
+ }
+
+ return new Cache(prefix, session_id);
+ }
+};
+
+export default CacheFacade;
diff --git a/resources/assets/javascripts/lib/calendar.js b/resources/assets/javascripts/lib/calendar.js
new file mode 100644
index 0000000..a693f6c
--- /dev/null
+++ b/resources/assets/javascripts/lib/calendar.js
@@ -0,0 +1,117 @@
+import { $gettext } from '../lib/gettext.js';
+
+/* ------------------------------------------------------------------------
+ * calendar gui
+ * ------------------------------------------------------------------------ */
+const Calendar = {
+ cell_height: 20,
+ the_entry_content: null,
+ entry: null,
+ click_start_hour: -1,
+ click_entry: null,
+ click_in_progress: false,
+
+ day_names: [
+ $gettext('Montag'),
+ $gettext('Dienstag'),
+ $gettext('Mittwoch'),
+ $gettext('Donnerstag'),
+ $gettext('Freitag'),
+ $gettext('Samstag'),
+ $gettext('Sonntag')
+ ],
+
+ /**
+ * this function is called, whenever an existing entry in the
+ * calendar is clicked. It calls the passed function with the
+ * calculcate id of the clicked element
+ *
+ * @param object a function or a reference to a function
+ * @param object the element in the dom, that has been clicked
+ * @param object the click-event itself
+ */
+ clickEngine: function(func, target, event) {
+ event.cancelBubble = true;
+ var id = jQuery(target).parent()[0].id;
+ id = id.substr(id.lastIndexOf('_') + 1);
+ func(id);
+ },
+
+ /**
+ * check, that the submited input-field cotains of a valid hour
+ *
+ * @param object the input-element to check
+ */
+ validateHour: function(element) {
+ var hour = parseInt(jQuery(element).val(), 10);
+
+ if (hour > 23) {
+ hour = 23;
+ }
+ if (hour < 0 || isNaN(hour)) {
+ hour = 0;
+ }
+
+ jQuery(element).val(hour);
+ },
+
+ /**
+ * check, that the submited input-field cotains of a valid minute
+ *
+ * @param object the input-element to check
+ */
+ validateMinute: function(element) {
+ var minute = parseInt(jQuery(element).val(), 10);
+
+ if (minute > 59) {
+ minute = 59;
+ }
+ if (minute < 0 || isNaN(minute)) {
+ minute = 0;
+ }
+
+ jQuery(element).val(minute);
+ },
+
+ /**
+ * checks if at least one day is selected
+ *
+ * @return: bool true if selected days > 0
+ */
+ validateNumberOfDays: function() {
+ var days = $("input[name='days[]']:checked")
+ .map(function() {
+ return $(this).val();
+ })
+ .get();
+ if (days.length === 0) {
+ jQuery('.settings > span[class=invalid_message]').show();
+ return false;
+ } else {
+ return true;
+ }
+ },
+
+ /**
+ * check, that the submitted input-fields contain a valid time-range
+ *
+ * @param object the input-element to check (start-hour)
+ * @param object the input-element to check (start-minute)
+ * @param object the input-element to check (end-hour)
+ * @param object the input-element to check (end-minute)
+ *
+ * @return: bool true if valid time-range, false otherwise
+ */
+ checkTimeslot: function(start_hour, start_minute, end_hour, end_minute) {
+ if (
+ parseInt(start_hour.val(), 10) * 100 + parseInt(start_minute.val(), 10) >=
+ parseInt(end_hour.val(), 10) * 100 + parseInt(end_minute.val(), 10)
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+export default Calendar;
diff --git a/resources/assets/javascripts/lib/calendar_dialog.js b/resources/assets/javascripts/lib/calendar_dialog.js
new file mode 100644
index 0000000..e42a149
--- /dev/null
+++ b/resources/assets/javascripts/lib/calendar_dialog.js
@@ -0,0 +1,64 @@
+import Dialog from './dialog.js';
+
+const CalendarDialog = {
+ closeMps: function(form) {
+ var added_users = [];
+ jQuery('#calendar-manage_access_selectbox option:selected').each(function() {
+ added_users[added_users.length] = jQuery(this).attr('value');
+ });
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/calendar/single/add_users/',
+ data: {
+ added_users: added_users
+ },
+ type: 'post'
+ });
+ jQuery(form)
+ .closest('.ui-dialog-content')
+ .dialog('close');
+ Dialog.fromURL(jQuery('#calendar-open-manageaccess').attr('href'));
+ return false;
+ },
+
+ removeUser: function(element) {
+ var url = jQuery(element).attr('href');
+ jQuery(element).removeAttr('href');
+ jQuery.ajax({
+ url: url,
+ type: 'get',
+ success: function() {
+ var head_tr = jQuery(element)
+ .closest('tr')
+ .prev('.calendar-user-head');
+ jQuery(element)
+ .closest('tr')
+ .remove();
+ if (head_tr.nextUntil('.calendar-user-head').length === 0) {
+ head_tr.remove();
+ }
+ }
+ });
+ return false;
+ },
+
+ addException: function() {
+ var exc_date = jQuery('#exc-date').val();
+ var exists = jQuery('#exc-dates input').is("input[value='" + exc_date + "']");
+ if (!exists) {
+ var compiled = _.template(
+ '<li><label>' +
+ '<input type="checkbox" name="del_exc_dates[]" value="<%- excdate %>" style="display: none">' +
+ '<span><%- excdate %><img src="' +
+ STUDIP.ASSETS_URL +
+ 'images/icons/blue/trash.svg' +
+ '"></span></label>' +
+ '<input type="hidden" name="exc_dates[]" value="<%- excdate %>">' +
+ '</li>'
+ );
+ jQuery('#exc-dates').append(compiled({ excdate: exc_date, link: '' }));
+ }
+ return false;
+ }
+};
+
+export default CalendarDialog;
diff --git a/resources/assets/javascripts/lib/clipboard.js b/resources/assets/javascripts/lib/clipboard.js
new file mode 100644
index 0000000..86a2c62
--- /dev/null
+++ b/resources/assets/javascripts/lib/clipboard.js
@@ -0,0 +1,539 @@
+const Clipboard = {
+
+ current_delete_icon: null,
+
+ switchClipboard: function(event) {
+ var select = jQuery(event.target);
+
+ if (!select) {
+ return;
+ }
+
+ var selected_clipboard_id = jQuery(select).val();
+
+ //Make all clipboard areas of that clipboard invisible, except the one
+ //that has been selected:
+ var clipboard_areas = jQuery(select).parent().parent().find('.clipboard-area');
+
+ for (var clipboard of clipboard_areas) {
+ var current_clipboard_id = jQuery(clipboard).attr('data-id');
+
+ if (current_clipboard_id) {
+ if (current_clipboard_id == selected_clipboard_id) {
+ jQuery(clipboard).removeClass('invisible');
+ if (jQuery(clipboard).find(".empty-clipboard-message").hasClass("invisible")) {
+ jQuery("#clipboard-group-container").find('.widget-links').removeClass('invisible');
+ } else {
+ jQuery("#clipboard-group-container").find('.widget-links').addClass('invisible');
+ }
+ } else {
+ jQuery(clipboard).addClass('invisible');
+ }
+ }
+ }
+ },
+
+ handleAddForm: function(event) {
+ if (!event) {
+ return false;
+ }
+
+ event.preventDefault();
+
+ //Check if a name is entered in the form:
+ var name_input = jQuery(event.target).find('input[type="text"][name="name"]');
+ if (!name_input) {
+ //Something is wrong with the HTML:
+ return false;
+ }
+ var name = jQuery(name_input).val().trim();
+ if (!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);
+ },
+
+ add: function(data) {
+ if (!data['id'] || !data['name'] || !data['widget_id']) {
+ //Required data are missing!
+ return;
+ }
+
+ //Get the clipboard template:
+ var widget_node = jQuery('#ClipboardWidget_' + data['widget_id'])[0];
+ if (!widget_node) {
+ //No widget? No clipboard.
+ return;
+ }
+
+ var clipboard_template = jQuery(widget_node).find(
+ '.clipboard-area.clipboard-template'
+ )[0];
+
+ if (!clipboard_template) {
+ //Something is wrong with the HTML
+ return;
+ }
+
+ var clipboard_node = jQuery(clipboard_template).clone();
+
+ //Remove classes:
+ jQuery(clipboard_node).removeClass('clipboard-template');
+ jQuery(clipboard_node).removeClass('invisible');
+
+ var clipboard_html = jQuery('<div></div>').append(clipboard_node).html();
+
+ //Replace placeholders for CLIPBOARD_ID:
+ clipboard_html = clipboard_html.replace(/CLIPBOARD_ID/g, data['id']);
+
+ //Get the widget content element to append the clipboard:
+ var content_node = jQuery(widget_node).find('.sidebar-widget-content');
+
+ //Append the new clipboard's HTML code to the last clipboard:
+ var clipboards = jQuery(content_node).find('.clipboard-area');
+ var last_clipboard = undefined;
+ if (clipboards.length > 0) {
+ last_clipboard = clipboards[clipboards.length -1];
+ } else {
+ //No clipboards: Something is wrong with the HTML.
+ return;
+ }
+
+ //Add the select option:
+ var clipboard_selector = jQuery(widget_node).find('.clipboard-selector')[0];
+ if (!clipboard_selector) {
+ //Something is wrong with the HTML.
+ return;
+ }
+ var old_options = jQuery(clipboard_selector).find('option');
+ jQuery(old_options).removeAttr('selected');
+
+ var new_option = jQuery('<option></option>');
+ jQuery(new_option).val(data['id']);
+ jQuery(new_option).text(data['name']);
+ jQuery(new_option).attr('selected', 'selected');
+ jQuery(clipboard_selector).append(new_option);
+ //Remove the "disabled" attribute, if it exists
+ //for the clipboard selector:
+ jQuery(clipboard_selector).removeAttr('disabled');
+ //Change the icon next to the clipboard selector:
+ jQuery('.clipboard-edit-button').removeClass('invisible');
+ jQuery('.clipboard-remove-button').removeClass('invisible');
+
+ //Make all the other clipboards invisible and add the new one:
+ clipboard_node = jQuery(clipboard_html);
+ jQuery(clipboards).addClass('invisible');
+ jQuery(last_clipboard).after(clipboard_node);
+ 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
+ }
+ );
+
+ //Clear the text input in the "add clipboard" form:
+ jQuery(widget_node).find(
+ 'form.new-clipboard-form input[type="text"][name="name"]'
+ ).val('');
+ },
+
+ handleItemDrop: function(event, ui_element) {
+
+ event.preventDefault();
+
+ var range_id = jQuery(ui_element.helper).data('id');
+ var range_type = jQuery(ui_element.helper).data('range_type');
+
+ var clipboard = event.target;
+ if (!clipboard) {
+ //An event without a target. Nothing to do here.
+ return;
+ }
+
+ STUDIP.Clipboard.prepareAddingItem(clipboard, range_id, range_type);
+ },
+
+ handleAddItemButtonClick: function (event) {
+ if (!event) {
+ return;
+ }
+ event.preventDefault();
+
+ var button = event.target;
+ if (!button) {
+ return;
+ }
+
+ var clipboard_id = jQuery(button).data('clipboard_id');
+ var clipboard_widget = jQuery('#ClipboardWidget_' + clipboard_id)[0];
+ if (!clipboard_widget) {
+ return;
+ }
+ var clipboard = jQuery(clipboard_widget).find('.clipboard-area:not(.invisible)')[0];
+ if (!clipboard) {
+ return;
+ }
+
+ var range_id = jQuery(button).data('range_id');
+ var range_type = jQuery(button).data('range_type');
+
+ STUDIP.Clipboard.prepareAddingItem(clipboard, range_id, range_type);
+ STUDIP.ActionMenu.confirmJSAction(event.target);
+ },
+
+ prepareAddingItem: function(clipboard = null, range_id = null, range_type = null) {
+ if (!clipboard || !range_id || !range_type) {
+ return false;
+ }
+
+ var clipboard_id = clipboard.getAttribute('data-id');
+ var widget_id = jQuery(clipboard).parents('.clipboard-widget').data('widget_id');
+
+ var allowed_classes = clipboard.getAttribute('data-allowed_classes');
+ if (allowed_classes) {
+ //A list of allowed classes is set. Check if the specified
+ //range_type is in the list of allowed classes.
+ //Although this check can easily be overridden by users
+ //it doesn't matter in this case since in the database
+ //the classes whose objects can be linked in a specific clipboard
+ //are not stored so that every clipboard can contain IDs
+ //of any SORM object that implements the StudipItem interface.
+ //If a user overrides the check for allowed classes then
+ //the clipboard widget may display objects of classes who
+ //don't belong on the displayed page. That's all.
+
+ allowed_classes = allowed_classes.replace(' ', '').split(',');
+ if (allowed_classes.indexOf(range_type) == -1) {
+ //The dropped item does not belong to the right class.
+ //Set the "not allowed" CSS class
+ //for the "not allowed" animation.
+
+ jQuery(clipboard).removeClass('invalid-drop');
+ jQuery(clipboard).addClass('invalid-drop');
+ return false;
+ }
+ }
+
+ if (!clipboard_id || !widget_id) {
+ //We can't do anything without the clipboard's ID
+ //or the ID of the widget it is inside!
+ return false;
+ }
+
+ //Check for duplicates:
+ var already_existing_entry = jQuery(clipboard).find(
+ ".clipboard-item[data-range_id='" + range_id + "']"
+ ).length > 0;
+ if (already_existing_entry) {
+ //Nothing to do here.
+ return false;
+ }
+
+ //Add the item to the clipboard via AJAX:
+ STUDIP.api.POST(
+ 'clipboard/' + clipboard_id + '/item',
+ {
+ data: {
+ 'range_id': range_id,
+ 'range_type': range_type,
+ 'widget_id': widget_id
+ }
+ }
+ ).done(STUDIP.Clipboard.addDroppedItem);
+ },
+
+ addDroppedItem: function(response_data) {
+ if (!response_data['id'] || !response_data['range_id']
+ || !response_data['name'] || !response_data['widget_id']) {
+ //We cannot create a new entry if at least one of those fields
+ //is missing.
+ return;
+ }
+
+ var widget = jQuery('#ClipboardWidget_' + response_data['widget_id']);
+ var clipboard_id = jQuery(widget).find(".clipboard-selector").val();
+
+ if (!widget) {
+ //The widget with the speicified widget-ID
+ //is not present on the current page.
+ return;
+ }
+
+ var clipboard = jQuery(widget).find(
+ '.clipboard-area[data-id="' + clipboard_id + '"]'
+ )[0];
+ if (!clipboard) {
+ //We need the clipboard node!
+ return;
+ }
+
+ //Check for duplicates:
+ var already_existing_entry = jQuery(clipboard).find(
+ ".clipboard-item[data-range_id='" + response_data['range_id'] + "']"
+ ).length > 0;
+ if (already_existing_entry) {
+ //Nothing to do here.
+ return;
+ }
+
+ var template = jQuery(clipboard).find('.clipboard-item-template')[0];
+ if (!template) {
+ //What is the use of continuing when there is no template?
+ return;
+ }
+
+ var new_item_node = jQuery(template).clone();
+ var checkbox_id = "item_" + clipboard_id + "_" + response_data['range_type'] + "_" + response_data['range_id'];
+
+ //Set some HTML attributes of the template:
+ jQuery(new_item_node).attr('data-range_id', response_data['range_id']);
+ jQuery(new_item_node).attr('id', checkbox_id);
+ jQuery(new_item_node).removeClass('clipboard-item-template');
+ jQuery(new_item_node).removeClass('invisible');
+
+ var name_label = jQuery(new_item_node).find('label');
+ jQuery(name_label).text(response_data['name']);
+ var id_field = jQuery(new_item_node).find("input[name='selected_clipboard_items[]']");
+ jQuery(id_field).val(checkbox_id);
+
+ var new_item_html = jQuery('<div></div>').append(new_item_node).html();
+ //Replace RANGE_ID with an escaped real range-ID:
+ new_item_html = new_item_html.replace(/RANGE_ID/g, _.escape(response_data['range_id']));
+
+ //Append the template to the clipboard:
+ jQuery(clipboard).append(jQuery(new_item_html));
+
+ jQuery(clipboard).find('.empty-clipboard-message').addClass('invisible');
+ jQuery("#clipboard-group-container").find('.widget-links').removeClass('invisible');
+
+ //Run the item drop animation:
+ jQuery(clipboard).addClass('animated-drop');
+ //Remove the animation class after the end of the animation:
+ window.setTimeout(
+ function() {jQuery(clipboard).removeClass('animated-drop');},
+ 500
+ );
+ },
+
+ rename: function(data) {
+ if (!data['widget_id']) {
+ //Required data are missing!
+ return;
+ }
+
+ var widget = jQuery('#ClipboardWidget_' + data['widget_id']);
+ var clipboard_id = jQuery(widget).find(".clipboard-selector").val();
+ var namer = jQuery(widget).find("input.clipboard-name");
+
+ var widget_id = data['widget_id'];
+ STUDIP.api.PUT(
+ 'clipboard/' + clipboard_id,
+ {
+ data: {
+ name: namer.val()
+ }
+ }
+ ).done(function(data) {
+ STUDIP.Clipboard.update(data, widget_id)
+ });
+ },
+
+ update: function(data, widget_id) {
+ if (!widget_id || !data['id'] || !data['name']) {
+ //Required data are missing!
+ return;
+ }
+
+ var widget = jQuery('#ClipboardWidget_' + widget_id);
+ var selector = jQuery(widget).find("select.clipboard-selector");
+ selector.find("option[value=" + data['id'] + "]").text(data['name']);
+ STUDIP.Clipboard.toggleEditButtons(widget_id);
+ },
+
+ remove: function(clipboard_id, widget_id) {
+ if (!clipboard_id || !widget_id) {
+ //Required data are missing!
+ return;
+ }
+
+ var widget = jQuery('#ClipboardWidget_' + widget_id);
+
+ var clipboard_selector = jQuery(widget).find('.clipboard-selector')[0];
+ if (!clipboard_selector) {
+ //Something is wrong with the HTML.
+ return;
+ }
+
+ //Get the option and the corresponding clipboard area
+ //for the deleted clipboard:
+ var clipboard_select_option = jQuery(clipboard_selector).find(
+ 'option[value="' + clipboard_id + '"]'
+ )[0];
+ var clipboard_area = jQuery(widget).find(
+ '.clipboard-area[data-id="' + clipboard_id + '"]'
+ )[0];
+
+ jQuery(clipboard_select_option).addClass('invisible');
+ jQuery(clipboard_area).addClass('invisible');
+
+ //Display the previous or the next select option
+ //and the previous or next clipboard area:
+ var new_selected_clipboard_id = null;
+ var predecessor = jQuery(clipboard_select_option).prev();
+ if (predecessor.length > 0) {
+ jQuery(predecessor).attr('selected', 'selected');
+ new_selected_clipboard_id = jQuery(predecessor).val();
+ } else {
+ var successor = jQuery(clipboard_select_option).next();
+ if (successor.length > 0) {
+ jQuery(successor).attr('selected', 'selected');
+ new_selected_clipboard_id = jQuery(successor).val();
+ }
+ //No else here: If no select options are left
+ //we have an empty select element.
+ }
+
+ //Now make the clipboard area visible which corresponds to the
+ //selected option:
+ if (new_selected_clipboard_id) {
+ //Another clipboard has been selected: Make it visible.
+ jQuery(widget).find(
+ '.clipboard-area[data-id="' + new_selected_clipboard_id + '"]'
+ ).removeClass('invisible');
+ } else {
+ //No other clipboard selected: Display the "no clipboards" message
+ //and disable the clipboard select field:
+ jQuery(widget).find('#clipboard-group-container').addClass('invisible');
+ jQuery(clipboard_selector).attr('disabled', 'disabled');
+ //Change the icon next to the clipboard selector:
+ var active_icon = jQuery(clipboard_selector).next();
+ var inactive_icon = jQuery(active_icon).next();
+ jQuery(active_icon).addClass('invisible');
+ jQuery(inactive_icon).removeClass('invisible');
+ }
+
+ //We have no need for the elements of the removed clipboard anymore.
+ //Now we can remove them:
+ jQuery(clipboard_select_option).remove();
+ jQuery(clipboard_area).remove();
+ },
+
+
+ confirmRemoveClick: function(event) {
+ STUDIP.Clipboard.current_delete_icon = event.target;
+ STUDIP.Dialog.confirm(
+ 'Sind Sie sicher?',
+ STUDIP.Clipboard.handleRemoveClick
+ );
+ },
+
+
+ handleRemoveClick: function() {
+ var delete_icon = STUDIP.Clipboard.current_delete_icon;
+ if (!delete_icon) {
+ return;
+ }
+
+ //Get the data of the clipboard:
+ var clipboard_select = jQuery(delete_icon).siblings('.clipboard-selector')[0];
+ if (!clipboard_select) {
+ //Something is wrong with the HTML.
+ return;
+ }
+
+ var clipboard_id = jQuery(clipboard_select).val();
+ var widget = jQuery(delete_icon).parents('.clipboard-widget')[0];
+ if (!widget) {
+ //Another case where something is wrong with the HTML.
+ return;
+ }
+ var widget_id = jQuery(widget).data('widget_id');
+
+ STUDIP.api.DELETE(
+ 'clipboard/' + clipboard_id,
+ {
+ data: {
+ widget_id: widget_id
+ }
+ }
+ ).done(function() {
+ STUDIP.Clipboard.remove(clipboard_id, widget_id);
+ });
+ },
+
+
+ confirmRemoveItemClick: function(event) {
+ STUDIP.Clipboard.current_delete_icon = event.target;
+ STUDIP.Dialog.confirm(
+ 'Sind Sie sicher?',
+ STUDIP.Clipboard.removeItem
+ );
+ },
+
+ removeItem: function() {
+ var delete_icon = STUDIP.Clipboard.current_delete_icon;
+ if (!delete_icon) {
+ return;
+ }
+
+ //Get the item-ID:
+ var item_html = jQuery(delete_icon).parents('tr');
+ var range_id = jQuery(item_html).data('range_id');
+ var clipboard_element = jQuery(item_html).parents('table');
+ var 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() {
+ //Check if the item has siblings:
+ var siblings = jQuery(item_html).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');
+ jQuery("#clipboard-group-container").find('.widget-links').addClass('invisible');
+ }
+ //Finally remove the item:
+ jQuery(item_html).remove();
+ });
+ },
+
+ toggleEditButtons: function(widget_id) {
+ if (!widget_id) {
+ //Required data are missing!
+ return;
+ }
+ var widget = jQuery('#ClipboardWidget_' + widget_id);
+ jQuery(widget).find("img.clipboard-edit-accept").toggle();
+ jQuery(widget).find("img.clipboard-edit-cancel").toggle();
+ jQuery(widget).find("img.clipboard-edit-button").toggle();
+ jQuery(widget).find("img.clipboard-remove-button").toggle();
+
+ var selector = jQuery(widget).find("select.clipboard-selector");
+ var namer = jQuery(widget).find("input.clipboard-name");
+ selector.toggle();
+ namer.val(selector.find("option:selected").text().trim());
+ namer.toggle();
+ namer.focus();
+ },
+};
+
+export default Clipboard;
diff --git a/resources/assets/javascripts/lib/cookie.js b/resources/assets/javascripts/lib/cookie.js
new file mode 100644
index 0000000..7823312
--- /dev/null
+++ b/resources/assets/javascripts/lib/cookie.js
@@ -0,0 +1,35 @@
+/*jslint esversion: 6*/
+class Cookie {
+ static set(name, value, expiry_days) {
+ var chunks = [name + '=' + value, 'SameSite=strict'];
+ if (expiry_days !== undefined) {
+ let date = new Date();
+ date.setTime(date.getTime() + expiry_days * 24 * 60 * 60 * 1000);
+
+ chunks.push(`expires=${date.toUTCString()}`);
+ }
+ chunks.push(
+ 'path=/' + STUDIP.URLHelper.getURL('a', {}, true)
+ .slice(0, -1)
+ .split('/')
+ .slice(3)
+ .map(encodeURIComponent)
+ .join('/')
+ );
+
+ document.cookie = chunks.join(';');
+ }
+
+ static get(name) {
+ let chunks = document.cookie.split(';');
+ var data = {};
+ chunks.forEach(chunk => {
+ let chunks = chunk.split('=');
+ data[chunks[0].trim()] = chunks.slice(1).join('=');
+ });
+
+ return data.hasOwnProperty(name) ? data[name] : undefined;
+ }
+}
+
+export default Cookie;
diff --git a/resources/assets/javascripts/lib/course_wizard.js b/resources/assets/javascripts/lib/course_wizard.js
new file mode 100644
index 0000000..898fe1f
--- /dev/null
+++ b/resources/assets/javascripts/lib/course_wizard.js
@@ -0,0 +1,559 @@
+const CourseWizard = {
+ /**
+ * Adds a new participating institute to the course.
+ * @param id Stud.IP institute ID
+ * @param name Full name
+ * @param inputName name of the for input to generate
+ * @param elClass desired CSS class name
+ * @param elId ID of the target container to append to
+ * @param otherInput name of other inputs to check
+ *
+ * (e.g. deputies if adding a lecturer)
+ */
+ addParticipatingInst: function(id, name) {
+ // Check if already set.
+ if ($('input[name="participating[' + id + ']"]').length == 0) {
+ var wrapper = $('<div>').addClass('institute');
+ $('#wizard-participating')
+ .children('div.description')
+ .removeClass('hidden-js');
+ var input = $('<input>')
+ .attr('type', 'hidden')
+ .attr('name', 'participating[' + id + ']')
+ .attr('id', id)
+ .attr('value', '1');
+ var trash = $('<input>')
+ .attr('type', 'image')
+ .attr('src', STUDIP.ASSETS_URL + 'images/icons/blue/trash.svg')
+ .attr('name', 'remove_participating[' + id + ']')
+ .attr('value', '1')
+ .attr('onclick', "return STUDIP.CourseWizard.removeParticipatingInst('" + id + "')")
+ .addClass('text-bottom')
+ .css({
+ width: 16,
+ height: 16
+ });
+ wrapper.append(input);
+ var nametext = $('<span>')
+ .html(name)
+ .text();
+ wrapper.append(nametext);
+ wrapper.append(trash);
+ $('#wizard-participating').append(wrapper);
+ }
+ },
+
+ /**
+ * Remove a participating institute from the list.
+ * @param id ID of the institute to remove
+ * @returns {boolean}
+ */
+ removeParticipatingInst: function(id) {
+ var parent = $('input#' + id).parent();
+ var grandparent = parent.parent();
+ parent.remove();
+ if (grandparent.children('div').length == 0) {
+ grandparent.children('div.description').addClass('hidden-js');
+ }
+ return false;
+ },
+
+ /**
+ * Adds a new person to the course.
+ * @param id Stud.IP user ID
+ * @param name Full name
+ * @param inputName name of the for input to generate
+ * @param elClass desired CSS class name
+ * @param elId ID of the target container to append to
+ * @param otherInput name of other inputs to check
+ *
+ * (e.g. deputies if adding a lecturer)
+ */
+ addPerson: function(id, name, inputName, elClass, elId, otherInput) {
+ // Check if already set.
+ if ($('input[name="' + inputName + '[' + id + ']"]').length == 0) {
+ var wrapper = $('<div>').addClass(elClass);
+ $('#' + elId)
+ .children('div.description')
+ .removeClass('hidden-js');
+ var input = $('<input>')
+ .attr('type', 'hidden')
+ .attr('name', inputName + '[' + id + ']')
+ .attr('id', id)
+ .attr('value', '1');
+ var trash = $('<input>')
+ .attr('type', 'image')
+ .attr('src', STUDIP.ASSETS_URL + 'images/icons/blue/trash.svg')
+ .attr('name', 'remove_' + elClass + '[' + id + ']')
+ .attr('value', '1')
+ .attr('onclick', "return STUDIP.CourseWizard.removePerson('" + id + "')");
+ wrapper.append(input);
+ var nametext = $('<span>')
+ .html(name)
+ .text();
+ wrapper.append(nametext);
+ wrapper.append(trash);
+ $('#' + elId).append(wrapper);
+ // Remove as deputy if set.
+ $('input[name="' + otherInput + '[' + id + ']"]')
+ .parent()
+ .remove();
+ }
+ },
+
+ /**
+ * Adds a new lecturer to the course.
+ * @param id Stud.IP user ID
+ * @param name Full name
+ */
+ addLecturer: function(id, name) {
+ CourseWizard.addPerson(id, name, 'lecturers', 'lecturer', 'wizard-lecturers', 'deputies');
+ // Add deputies if applicable.
+ CourseWizard.addDefaultDeputies(id);
+ },
+
+ /**
+ * Adds a new deputy to the course.
+ * @param id Stud.IP user ID
+ * @param name Full name
+ */
+ addDeputy: function(id, name) {
+ CourseWizard.addPerson(id, name, 'deputies', 'deputy', 'wizard-deputies', 'lecturers');
+ },
+
+ addTutor: function(id, name) {
+ CourseWizard.addPerson(id, name, 'tutors', 'tutor', 'wizard-tutors', 'lecturers');
+ },
+
+ /**
+ * Adds the default deputies of given user to the course.
+ * @param id Stud.IP user ID
+ */
+ addDefaultDeputies: function(id) {
+ var lecturerDiv = $('#wizard-lecturers');
+ if ($('input[name="deputy_id_parameter"]').length > 0 && lecturerDiv.data('default-enabled') == '1') {
+ var params = 'step=' + $('input[name="step"]').val() + '&method=getDefaultDeputies' + '&parameter[]=' + id;
+ $.ajax(lecturerDiv.data('ajax-url'), {
+ data: params,
+ success: function(data, status, xhr) {
+ if (data.length > 0) {
+ for (var i = 0; i < data.length; i++) {
+ CourseWizard.addDeputy(data[i].id, data[i].name);
+ }
+ }
+ }
+ });
+ }
+ },
+
+ /**
+ * Remove a person (lecturer or deputy) from the list.
+ * @param id ID of the person to remove
+ * @returns {boolean}
+ */
+ removePerson: function(id) {
+ var parent = $('input#' + id).parent();
+ var grandparent = parent.parent();
+ parent.remove();
+ if (grandparent.children('div[class!="description"]').length == 0) {
+ grandparent.children('div.description').addClass('hidden-js');
+ }
+ return false;
+ },
+
+ /**
+ * Fetches the children of a given sem tree node.
+ * @param node the ID of the parent.
+ * @param assignable is the given node part of the
+ * full sem tree or the tree of already
+ * assigned nodes?
+ * @returns {boolean}
+ */
+ getTreeChildren: function(node, assignable) {
+ var target = $('.' + (assignable ? 'sem-tree-' : 'sem-tree-assign-') + node);
+ if (!target.hasClass('tree-loaded') && target.find('.tree-loading').length == 0) {
+ var params =
+ 'step=' +
+ $('input[name="step"]').val() +
+ '&method=getSemTreeLevel' +
+ '&parameter[]=' +
+ $('#' + node).attr('id');
+ $.ajax($('#studyareas').data('ajax-url'), {
+ data: params,
+ beforeSend: function(xhr, settings) {
+ target.children('ul').append(
+ $('<li class="tree-loading">').html(
+ $('<img>')
+ .attr('src', STUDIP.ASSETS_URL + 'images/ajax-indicator-black.svg')
+ .css('width', '16')
+ .css('height', '16')
+ )
+ );
+ },
+ success: function(data, status, xhr) {
+ target.find('li.sem-tree-result').remove();
+ var items = $.parseJSON(data);
+ target.find('.tree-loading').remove();
+ if (items.length > 0) {
+ var list = target.children('ul');
+ for (var i = 0; i < items.length; i++) {
+ list.append(CourseWizard.createTreeNode(items[i], assignable));
+ }
+ }
+ target.addClass('tree-loaded');
+ },
+ error: function(xhr, status, error) {
+ alert(error);
+ }
+ });
+ }
+ if (!target.hasClass('tree-open')) {
+ target.removeClass('tree-closed').addClass('tree-open');
+ } else {
+ target.removeClass('tree-open').addClass('tree-closed');
+ }
+ var checkbox = target.children('input[id="' + node + '"]');
+ checkbox.prop('checked', !checkbox.prop('checked'));
+ return false;
+ },
+
+ /**
+ * Search the sem tree for a given term and show all matching nodes.
+ * @returns {boolean}
+ */
+ searchTree: function() {
+ var searchterm = $('#sem-tree-search').val();
+ if (searchterm != '') {
+ var params =
+ 'step=' + $('input[name="step"]').val() + '&method=searchSemTree' + '&parameter[]=' + searchterm;
+ $.ajax($('#studyareas').data('ajax-url'), {
+ data: params,
+ beforeSend: function(xhr, settings) {
+ $('#sem-tree-search-start')
+ .parent()
+ .append(
+ $('<img>')
+ .attr('src', STUDIP.ASSETS_URL + 'images/ajax-indicator-black.svg')
+ .attr('id', 'sem-tree-search-loading')
+ .css('width', '16')
+ .css('height', '16')
+ );
+ CourseWizard.loadingOverlay($('div#studyareas ul.css-tree'));
+ },
+ success: function(data, status, xhr) {
+ $('#loading-overlay').remove();
+ $('#sem-tree-search-loading').remove();
+ var items = $.parseJSON(data);
+ if (items.length > 0) {
+ $('#sem-tree-search-reset')
+ .removeClass('hidden-js')
+ .css('display', '');
+ $('#studyareas li input[type="checkbox"]').prop('checked', false);
+ $('#studyareas li')
+ .not('.keep-node')
+ .addClass('css-tree-hidden');
+ CourseWizard.buildPartialTree(items, true, '');
+ $('#sem-tree-assign-all').removeClass('hidden-js');
+ $('li.sem-tree-root input#root').prop('checked', true);
+ } else {
+ alert($('#studyareas').data('no-search-result'));
+ }
+ },
+ error: function(xhr, status, error) {
+ alert(error);
+ }
+ });
+ }
+ return false;
+ },
+
+ /**
+ * Reset a search and restore the "normal" sem tree view.
+ * @returns {boolean}
+ */
+ resetSearch: function() {
+ $('li.css-tree-hidden').removeClass('css-tree-hidden');
+ $('#sem-tree-search-reset').addClass('hidden-js');
+ $('#sem-tree-search').val('');
+ $('.css-tree-hidden').removeClass('css-tree-hidden');
+ var notloaded = $('#studyareas li').not('.tree-loaded');
+ notloaded.children('input[type="checkbox"]').prop('checked', false);
+ notloaded.children('ul').empty();
+ $('#sem-tree-assign-all').addClass('hidden-js');
+ $('input[name="searchterm"]').remove();
+ return false;
+ },
+
+ /**
+ * Build a partial sem tree, containing (or showing) only selected nodes.
+ * @param items items to show in the resulting tree
+ * @param assignable are the nodes part of the full
+ * sem tree whose entries can be assigned?
+ * @param source_node the single node that initiated the tree building,
+ * useful for marking elements.
+ * @returns {boolean}
+ */
+ buildPartialTree: function(items, assignable, source_node) {
+ if (assignable) {
+ var classPrefix = 'sem-tree-';
+ } else {
+ var classPrefix = 'sem-tree-assigned-';
+ }
+ for (var i = 0; i < items.length; i++) {
+ var parent = $('.' + classPrefix + items[i].parent);
+ var node = $('.' + classPrefix + items[i].id);
+ if (node.length == 0) {
+ if (!assignable && source_node == items[i].id) {
+ var selected = true;
+ } else {
+ var selected = false;
+ }
+ var node = CourseWizard.createTreeNode(items[i], assignable, selected);
+ parent.children('ul').append(node);
+ } else {
+ node.removeClass('css-tree-hidden');
+ if (!assignable && items[i].id == source_node) {
+ var input = $('<input>')
+ .attr('type', 'hidden')
+ .attr('name', 'studyareas[]')
+ .attr('value', items[i].id);
+ node.children('ul').before(input);
+ var unassign = $('<input>')
+ .attr('type', 'image')
+ .attr('name', 'unassign[' + items[i].id + ']')
+ .attr('src', STUDIP.ASSETS_URL + 'images/icons/blue/trash.svg')
+ .attr('width', '16')
+ .height('height', '16')
+ .attr('onclick', "return STUDIP.CourseWizard.unassignNode('" + items[i].id + "')");
+ node.children('input[name="studyareas[]"]').before(unassign);
+ }
+ }
+ node.children('input#' + items[i].id).prop('checked', true);
+ if (items[i].assignable) {
+ node.addClass('sem-tree-result');
+ }
+ parent.children('input[id="' + items[i].parent + '"]').attr('checked', true);
+ if (items[i].has_children) {
+ CourseWizard.buildPartialTree(items[i].children, assignable, source_node);
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Creates a tree node element from given data.
+ * @param values values for the node
+ * @param assignable is the node part of the full
+ * sem tree whose entries can be assigned?
+ * @returns {*|jQuery}
+ */
+ createTreeNode: function(values, assignable, selected) {
+ // Node in "All study areas" tree.
+ if (assignable) {
+ var item = $('<li>').addClass('sem-tree-' + values.id);
+ var assign = $('<input>')
+ .attr('type', 'image')
+ .attr('name', 'assign[' + values.id + ']')
+ .attr('src', STUDIP.ASSETS_URL + 'images/icons/yellow/arr_2left.svg')
+ .attr('width', '16')
+ .height('height', '16')
+ .attr('onclick', "return STUDIP.CourseWizard.assignNode('" + values.id + "')");
+ if (values.assignable) {
+ item.append(assign);
+ item.append(document.createTextNode(' '));
+ }
+ if (values.has_children) {
+ var input = $('<input>')
+ .attr('type', 'checkbox')
+ .attr('id', values.id);
+ var label = $('<label>')
+ .addClass('undecorated')
+ .attr('for', values.id)
+ .attr('onclick', "return STUDIP.CourseWizard.getTreeChildren('" + values.id + "', true)");
+ // Build link for opening the current node.
+ var link = $('div#studyareas').data('forward-url');
+ if (link.indexOf('?') > -1) {
+ link += '&open_node=' + values.id;
+ } else {
+ link += '?open_node=' + values.id;
+ }
+ var openLink = $('<a>').attr('href', link);
+ openLink.html(
+ $('<div/>')
+ .text(values.name)
+ .html()
+ );
+ label.append(openLink);
+ item.append(input);
+ item.append(label);
+ if (values.has_children) {
+ item.append('<ul>');
+ }
+ if (values.assignable) {
+ if ($('#assigned li.sem-tree-assigned-' + values.id).length > 0) {
+ assign.css('display', 'none');
+ }
+ }
+ } else {
+ if ($('#assigned li.sem-tree-assigned-' + values.id).length > 0) {
+ assign.css('display', 'none');
+ }
+ item.html(
+ item.html() +
+ $('<div/>')
+ .text(values.name)
+ .html()
+ );
+ item.addClass('tree-node');
+ }
+ // Node in "assigned study areas" tree.
+ } else {
+ var item = $('<li>').addClass('sem-tree-assigned-' + values.id);
+ item.html(
+ $('<div/>')
+ .text(values.name)
+ .html()
+ );
+ if ((!values.has_children || values.assignable) && selected) {
+ var unassign = $('<input>')
+ .attr('type', 'image')
+ .attr('name', 'unassign[' + values.id + ']')
+ .attr('src', STUDIP.ASSETS_URL + 'images/icons/blue/trash.svg')
+ .attr('width', '16')
+ .height('height', '16')
+ .attr('onclick', "return STUDIP.CourseWizard.unassignNode('" + values.id + "')");
+ item.append(unassign);
+ }
+ if (values.assignable && selected) {
+ var input = $('<input>')
+ .attr('type', 'hidden')
+ .attr('name', 'studyareas[]')
+ .attr('value', values.id);
+ item.append(input);
+ }
+ item.append('<ul>');
+ }
+ $(item).data('id', values.id);
+ return item;
+ },
+
+ /**
+ * Assign a given node to the course.
+ * @param id sem tree ID to assign
+ * @returns {boolean}
+ */
+ assignNode: function(id) {
+ var root = $('#sem-tree-assigned-nodes');
+ var params = 'step=' + $('input[name="step"]').val() + '&method=getAncestorTree' + '&parameter[]=' + id;
+ $.ajax($('#studyareas').data('ajax-url'), {
+ data: params,
+ beforeSend: function(xhr, settings) {
+ CourseWizard.loadingOverlay($('div#assigned ul.css-tree'));
+ },
+ success: function(data, status, xhr) {
+ $('#loading-overlay').remove();
+ var items = $.parseJSON(data);
+ CourseWizard.buildPartialTree(items, false, id);
+ $('.sem-tree-assigned-root').removeClass('hidden-js');
+ $('input[name="assign[' + id + ']"]').hide();
+ $('svg[name="assign[' + id + ']"]').hide();
+ },
+ error: function(xhr, status, error) {
+ alert(error);
+ }
+ });
+ return false;
+ },
+
+ /**
+ * Remove a node from the assigned ones.
+ * @param id sem tree ID to unassign
+ * @returns {boolean}
+ */
+ unassignNode: function(id) {
+ var target = $('li.sem-tree-assigned-' + id);
+ if (target.children('ul').children('li').length > 0) {
+ target.children('input[name="studyareas[]"]').remove();
+ target.children('input[name="unassign[' + id + ']"]').remove();
+ target.children('a').remove();
+ } else {
+ CourseWizard.cleanupAssignTree(target);
+ }
+ $('input[name="assign[' + id + ']"]').show();
+ $('svg[name="assign[' + id + ']"]').show();
+ return false;
+ },
+
+ /**
+ * Assign all visible nodes, e.g. search results.
+ * The nodes to assign are marked by the class
+ * "sem-tree-result".
+ * @returns {boolean}
+ */
+ assignAllNodes: function() {
+ $('.sem-tree-result').each(function(index, element) {
+ var id = $(element).data('id');
+ if ($('li.sem-tree-assigned-' + id).length == 0) {
+ CourseWizard.assignNode(id);
+ }
+ });
+ CourseWizard.resetSearch();
+ return false;
+ },
+
+ /**
+ * On unassigning a node, we need to check if the
+ * parent node has other children which are still
+ * assigned. If not, we can remove the parent node
+ * as well.
+ * @param element
+ */
+ cleanupAssignTree: function(element) {
+ var parent = element.parent();
+ var grandparent = parent.parent();
+ if (
+ parent.children('li').length == 1 &&
+ !grandparent.hasClass('keep-node') &&
+ grandparent.children('input[type="hidden"][name="studyareas[]"]').length == 0
+ ) {
+ CourseWizard.cleanupAssignTree(element.parent().parent());
+ } else {
+ element.remove();
+ }
+ var root = $('li.sem-tree-assigned-root');
+ if (root.children('ul').children('li').length < 1) {
+ root.addClass('hidden-js');
+ }
+ },
+
+ /**
+ * Show some visible indicator that there is
+ * AJAX work in progress.
+ * @param parent
+ */
+ loadingOverlay: function(parent) {
+ var pos = parent.offset();
+ var div = $('<div>')
+ .attr('id', 'loading-overlay')
+ .addClass('ui-widget-overlay')
+ .width($(parent).width())
+ .height($(parent).height())
+ .css({
+ position: 'absolute',
+ top: pos.top,
+ left: pos.left
+ });
+ var loading = $('<img>')
+ .attr('src', STUDIP.ASSETS_URL + 'images/ajax-indicator-black.svg')
+ .css({
+ width: 32,
+ height: 32,
+ 'margin-left': div.width() / 2 - 32,
+ 'margin-top': div.height() / 2 - 32
+ });
+ div.append(loading);
+ parent.append(div);
+ }
+};
+
+export default CourseWizard;
diff --git a/resources/assets/javascripts/lib/css.js b/resources/assets/javascripts/lib/css.js
new file mode 100644
index 0000000..79d40da
--- /dev/null
+++ b/resources/assets/javascripts/lib/css.js
@@ -0,0 +1,66 @@
+/**
+ * Add methods to dynamically insert and remove css styles.
+ *
+ * @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
+ */
+// "Private" stylesheet rules are applied to, generated from a dynamically
+// inserted style tag in the document's header
+var sheet = null;
+
+/**
+ * Dynamically add a ruleset for a given selector to the current site
+ *
+ * @param {string} selector - CSS selector to add rules for
+ * @param {object} css - Actual css rules as hash object
+ * @param {array} vendors - Optional array of vendor prefixes to apply
+ */
+function addRule(selector, css, vendors) {
+ vendors = vendors || [];
+ vendors.push('');
+
+ var style, propText;
+ if (sheet === null) {
+ style = document.createElement('style');
+ sheet = document.head.appendChild(style).sheet;
+ }
+
+ propText = Object.keys(css)
+ .map(function(p) {
+ var result = [],
+ i;
+ for (i = 0; i < vendors.length; i += 1) {
+ result.push(vendors[i] + p + ':' + css[p]);
+ }
+ return result.join(';');
+ })
+ .join(';');
+
+ sheet.insertRule(selector + '{' + propText + '}', sheet.cssRules.length);
+}
+
+/**
+ * Removes a currently added, dynamic ruleset.
+ *
+ * @param {string} selector - CSS selector to remove rules for
+ */
+function removeRule(selector) {
+ var i;
+ if (sheet !== null) {
+ for (i = sheet.cssRules.length - 1; i >= 0; i -= 1) {
+ if (sheet.cssRules[i].selectorText === selector) {
+ sheet.deleteRule(i);
+ }
+ }
+ }
+}
+
+// Expose functions to global STUDIP object, namespaced under CSS
+const CSS = {
+ addRule,
+ removeRule
+};
+
+export default CSS;
diff --git a/resources/assets/javascripts/lib/dates.js b/resources/assets/javascripts/lib/dates.js
new file mode 100644
index 0000000..1fc9830
--- /dev/null
+++ b/resources/assets/javascripts/lib/dates.js
@@ -0,0 +1,56 @@
+const Dates = {
+ addTopic: function() {
+ var topic_name = $('#new_topic').val(),
+ termin_id = $('#new_topic')
+ .closest('[data-termin-id]')
+ .data().terminId;
+
+ if (!topic_name) {
+ $('#new_topic').focus();
+ return;
+ }
+
+ $.post(STUDIP.URLHelper.getURL('dispatch.php/course/dates/add_topic'), {
+ title: topic_name,
+ termin_id: termin_id
+ }).done(function(response) {
+ if (response.hasOwnProperty('li')) {
+ $('#new_topic')
+ .closest('[data-termin-id]')
+ .find('.themen-list')
+ .append(response.li);
+ $('#date_' + termin_id)
+ .find('.themen-list')
+ .append(response.li);
+ }
+
+ $('#new_topic')
+ .val('')
+ .focus();
+ });
+ },
+ removeTopicFromIcon: function() {
+ var topic_id = $(this)
+ .closest('li')
+ .data('issue_id'),
+ termin_id = $(this)
+ .closest('[data-termin-id]')
+ .data().terminId;
+ Dates.removeTopic(termin_id, topic_id);
+ },
+ removeTopic: function(termin_id, topic_id) {
+ $.ajax({
+ url: STUDIP.URLHelper.getURL('dispatch.php/course/dates/remove_topic'),
+ data: {
+ issue_id: topic_id,
+ termin_id: termin_id
+ },
+ dataType: 'json',
+ type: 'post'
+ }).done(function() {
+ $('.topic_' + termin_id + '_' + topic_id).remove();
+ });
+ }
+};
+
+export default Dates;
diff --git a/resources/assets/javascripts/lib/dialog.js b/resources/assets/javascripts/lib/dialog.js
new file mode 100644
index 0000000..de016ed
--- /dev/null
+++ b/resources/assets/javascripts/lib/dialog.js
@@ -0,0 +1,751 @@
+import { $gettext } from '../lib/gettext.js';
+
+/*jslint esversion: 6*/
+
+/**
+ * Specialized dialog handler
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @version 1.0
+ * @since Stud.IP 3.1
+ * @license GLP2 or any later version
+ * @copyright 2014 Stud.IP Core Group
+ * @todo Handle file uploads <http://goo.gl/PnSra8>
+ */
+
+import parseOptions from './parse_options.js';
+import extractCallback from './extract_callback.js';
+import Overlay from './overlay.js';
+import PageLayout from './page_layout.js';
+
+var dialog_margin = 0;
+
+/**
+ * Extract buttons from given element.
+ */
+function extractButtons(element) {
+ var buttons = {};
+ $('[data-dialog-button]', element)
+ .hide()
+ .find('a,button')
+ .addBack()
+ .filter('a,button')
+ .each(function() {
+ var label = $(this).text();
+ var cancel = $(this).is('.cancel');
+ var index = cancel ? 'cancel' : label;
+ var classes = $(this).attr('class') || '';
+
+ classes = classes.replace(/\bbutton\b/, '').trim();
+
+ if ($(this).is('.accept,.cancel')) {
+ buttons[index] = {
+ text: label,
+ click: () => this.click()
+ };
+ } else {
+ buttons[index] = () => this.click();
+ }
+
+ if ($(this).is(':disabled')) {
+ classes = classes + ' disabled';
+ }
+
+ buttons[index]['class'] = classes;
+ });
+
+ return buttons;
+}
+
+const Dialog = {
+ instances: {},
+ stack: [],
+ hasInstance: function(id) {
+ id = id || 'default';
+ return this.instances.hasOwnProperty(id);
+ },
+ getInstance: function(id) {
+ id = id || 'default';
+ if (!this.hasInstance(id)) {
+ this.instances[id] = {
+ open: false,
+ fixedDimensions: false,
+ element: $('<div>'),
+ options: {},
+ previous: this.stack[0] || false
+ };
+
+ this.stack.unshift(id);
+ }
+ return this.instances[id];
+ },
+ removeInstance: function(id) {
+ id = id || 'default';
+ if (this.hasInstance(id)) {
+ delete this.instances[id];
+
+ var index = this.stack.indexOf(id);
+ this.stack.splice(index, 1);
+ }
+ },
+ /**
+ * legacy method, remove in future
+ * @return bool
+ */
+ shouldOpen: function() {
+ return true;
+// return !$('html').is('.responsive-display') && $(window).innerHeight() >= 400;
+ },
+ handlers: {
+ header: {}
+ }
+};
+
+// Handler for HTTP header X-Location: Relocate to another location
+Dialog.handlers.header['X-Location'] = function(location, options) {
+ location = decodeURIComponent(location);
+
+ if (document.location.href === location) {
+ document.location.reload(true);
+ } else {
+ $(window)
+ .on('hashchange', function() {
+ document.location.reload(true);
+ })
+ .on('unload', function() {
+ $(window).off('hashchange');
+ });
+ }
+
+ Dialog.close(options);
+ document.location = location;
+
+ return false;
+};
+// Handler for HTTP header X-Dialog-Execute: Execute arbitrary function
+Dialog.handlers.header['X-Dialog-Execute'] = function(value, options, xhr) {
+ var callback = window,
+ payload = xhr.getResponseHeader('Content-Type').match(/json/)
+ ? $.parseJSON(xhr.responseText)
+ : xhr.responseText;
+
+ // Try to parse value as JSON (value might be {func: 'foo', payload: {}})
+ try {
+ value = $.parseJSON(value);
+ } catch (e) {
+ value = { func: value };
+ }
+
+ // Check for invalid call
+ if (!value.hasOwnProperty('func')) {
+ throw 'Dialog: Invalid value for X-Dialog-Execute';
+ }
+
+ // Populate payload if not set
+ if (!value.hasOwnProperty('payload')) {
+ value.payload = xhr.getResponseHeader('Content-Type').match(/json/)
+ ? $.parseJSON(xhr.responseText)
+ : xhr.responseText;
+ }
+
+ // Find callback
+ callback = extractCallback(value.func, payload);
+
+ // Check callback
+ if (typeof callback !== 'function') {
+ throw 'Dialog: Given callback is not a valid function';
+ }
+
+ // Execute callback
+ return callback(value.payload, xhr);
+};
+// Handler for HTTP header X-Dialog-Close: Close the dialog
+Dialog.handlers.header['X-Dialog-Close'] = function(value, options) {
+ Dialog.close(options);
+ return false;
+};
+// Handler for HTTP header X-Wikilink: Set the options' wiki link
+Dialog.handlers.header['X-Wikilink'] = function(link, options) {
+ options.wiki_link = link;
+};
+// Handler for HTTP header X-Title: Set the dialog title
+Dialog.handlers.header['X-Title'] = function(title, options) {
+ title = decodeURIComponent(title);
+ if (title !== $('title').data().original) {
+ options.title = title || options.title;
+ }
+};
+// Handler for HTTP header X-No-Buttons: Decide whether to show dialog buttons
+Dialog.handlers.header['X-No-Buttons'] = function(value, options) {
+ options.buttons = false;
+};
+
+// Creates a dialog from an anchor, a button or a form element.
+// Will update the dialog if it is already open
+Dialog.fromElement = function(element, options) {
+ options = options || {};
+
+ if ($(element).is(':disabled') || !Dialog.shouldOpen()) {
+ return;
+ }
+
+ if (options.close) {
+ Dialog.close(options);
+ return;
+ }
+
+ if (!$(element).is('a,button,form,input[type=image],input[type=submit]')) {
+ throw 'Dialog.fromElement called on an unsupported element.';
+ }
+
+ options.origin = element;
+ options.title =
+ options.title ||
+ Dialog.getInstance(options.id).options.title ||
+ $(element).attr('title') ||
+ $(element).find('[title]').first().attr('title') ||
+ $(element).filter('a,button').text();
+ options.method = 'get';
+ options.data = {};
+
+ var url, fd;
+
+ // Predefine options
+ if ($(element).is('form,button,input')) {
+ url = $(element).attr('formaction') ||
+ $(element).closest('form').data('formaction') ||
+ $(element).closest('form').attr('action');
+ options.method = $(element).closest('form').attr('method');
+ options.data = $(element).closest('form').serializeArray();
+
+ if ($(element).is('button,input')) {
+ options.data.push({
+ name: $(element).attr('name'),
+ value: $(element).val()
+ });
+ } else if ($(element).data().triggeredBy) {
+ options.data.push($(element).data().triggeredBy);
+ }
+ $(element).closest('form').removeData('formaction');
+
+ if ($(element).closest('form').attr('enctype') === 'multipart/form-data') {
+ options.processData = false;
+
+ fd = new FormData();
+ options.data.forEach(function(item) {
+ fd.append(item.name, item.value);
+ });
+
+ $(element).closest('form').find('input[type=file]').each(function() {
+ var name = $(this).attr('name'),
+ i;
+ for (i = 0; i < this.files.length; i += 1) {
+ fd.append(name, this.files[i]);
+ }
+ });
+
+ options.data = fd;
+ }
+ } else {
+ url = $(element).attr('href');
+ }
+
+ return Dialog.fromURL(url, options);
+};
+
+// Creates a dialog from a passed url
+Dialog.fromURL = function(url, options) {
+ options = options || {};
+
+ // Check if dialog should actually open
+ if (!Dialog.shouldOpen()) {
+ location.href = url;
+ }
+
+ // Append overlay
+ if (Dialog.getInstance(options.id).open) {
+ Overlay.show(true, Dialog.getInstance(options.id).element.parent());
+ } else {
+ Overlay.show(true);
+ }
+
+ // Send ajax request
+ $.ajax({
+ url: url,
+ type: (options.method || 'get').toUpperCase(),
+ data: options.data || {},
+ headers: { 'X-Dialog': true },
+ cache: false,
+ contentType:
+ options.hasOwnProperty('processData') && !options.processData
+ ? false
+ : 'application/x-www-form-urlencoded; charset=UTF-8',
+ processData: options.hasOwnProperty('processData') ? options.processData : true
+ })
+ .done(function(response, status, xhr) {
+ var advance = true;
+
+ // Trigger event
+ $(options.origin || document).trigger('dialog-load', { xhr: xhr, options: options });
+
+ // Execute all defined header handlers
+ var handlers = Object.assign(
+ Dialog.handlers.header,
+ STUDIP.Dialog.handlers.header
+ );
+ $.each(handlers, (header, handler) => {
+ var value = xhr.getResponseHeader(header),
+ result = true;
+ if (value !== null) {
+ result = handler(value, options, xhr);
+ }
+ advance = advance && result !== false;
+ return result;
+ });
+
+ Overlay.hide(0);
+
+ if (advance) {
+ Dialog.show(response, options);
+ }
+ })
+ .fail(() => {
+ Overlay.hide();
+ });
+
+ return true;
+};
+
+// Opens or updates the dialog
+Dialog.show = function(content, options = {}) {
+ options = Object.assign({}, Dialog.options, options);
+
+ options.wikilink = options.wikilink === undefined ? true : options.wikilink;
+
+ var scripts = $('<div>' + content + '</div>').filter('script'); // Extract scripts
+ var dialog_options = {};
+ var instance = Dialog.getInstance(options.id);
+
+ if (instance.open) {
+ options.title = options.title || instance.element.dialog('option', 'title');
+ }
+
+ if (options['center-content']) {
+ content = '<div class="studip-dialog-centered-helper">' + content + '</div>';
+ }
+
+ // Hide and update container
+ instance.element.hide().html(content);
+
+ // Store options and dimensions
+ instance.options = options;
+ instance.dimensions = Dialog.calculateDimensions(instance, content, options);
+ instance.previous_title = instance.previous_title || PageLayout.title;
+
+ // Set dialog options
+ dialog_options = $.extend(dialog_options, {
+ width: instance.dimensions.width,
+ height: instance.dimensions.height,
+ dialogClass: Dialog.getClasses(options),
+ buttons: options.buttons || {},
+ title: options.title,
+ modal: true,
+ resizable: options.resize ?? true,
+ create: function(event) {
+ $(event.target)
+ .parent()
+ .css('position', 'fixed');
+ },
+ resizeStop: function(event, ui) {
+ var position = [
+ Math.floor(ui.position.left) - $(window).scrollLeft(),
+ Math.floor(ui.position.top) - $(window).scrollTop()
+ ];
+ $(event.target)
+ .parent()
+ .css('position', 'fixed');
+ $(event.target).dialog('option', 'position', position);
+
+ instance.fixedDimensions = true;
+ instance.dimensions = ui.size;
+ },
+ open: function() {
+ PageLayout.title = dialog_options.title;
+
+ var helpbar_element = $('.helpbar a[href*="hilfe.studip.de"]');
+ var tooltip = helpbar_element.text();
+ var link = options.wiki_link || helpbar_element.attr('href');
+ var element = $('<a class="ui-dialog-titlebar-wiki"' + ' target="_blank" rel="noopener noreferrer">')
+ .attr('href', link)
+ .attr('title', tooltip);
+ var buttons = $(this)
+ .parent()
+ .find('.ui-dialog-buttonset .ui-button');
+
+ if (options.wikilink) {
+ $(this)
+ .siblings('.ui-dialog-titlebar')
+ .addClass('with-wiki-link')
+ .find('.ui-dialog-titlebar-close')
+ .before(element);
+ }
+
+ $(this).parent().find('.ui-dialog-title').attr('title', options.title);
+
+ instance.open = true;
+ // Execute scripts
+ $('head').append(scripts);
+
+ $(options.origin || document).trigger('dialog-open', { dialog: this, options: options });
+
+ // Transfer defined classes from options to actual displayed buttons
+ // This should work natively, but it kinda does not
+ Object.keys(dialog_options.buttons).forEach(function(label, index) {
+ var classes = dialog_options.buttons[label]['class'];
+ $(buttons.get(index)).addClass(classes);
+ });
+ },
+ close: function(event) {
+ $(options.origin || document).trigger('dialog-close', { dialog: this, options: options });
+
+ PageLayout.title = instance.previous_title;
+
+ Dialog.close(options);
+ }
+ });
+
+ // Create buttons
+ if (!options.hasOwnProperty('buttons') || (options.buttons && !$.isPlainObject(options.buttons))) {
+ dialog_options.buttons = extractButtons.call(this, instance.element);
+ // Create 'close' button
+ if (!dialog_options.buttons.hasOwnProperty('cancel')) {
+ dialog_options.buttons.cancel = {
+ text: $gettext('Schließen'),
+ 'class': 'cancel'
+ };
+ }
+ dialog_options.buttons.cancel.click = function() {
+ Dialog.close(options);
+ };
+ }
+
+ // Create/update dialog
+ instance.element.dialog(dialog_options);
+ instance.element.scrollTo(0, 0);
+
+ // Trigger update event on document since options.origin might have been removed
+ $(document).trigger('dialog-update', { dialog: instance.element, options: options });
+};
+
+// Closes the dialog for good
+Dialog.close = function(options) {
+ options = options || {};
+
+ if (Dialog.hasInstance(options.id)) {
+ var instance = Dialog.getInstance(options.id);
+
+ if (instance.open) {
+ instance.open = false;
+ try {
+ instance.element.dialog('close');
+ instance.open = instance.element.dialog('isOpen');
+ } catch (ignore) {}
+
+ // Apparently the close event has been canceled, so don't force
+ // a close
+ if (instance.open) {
+ return false;
+ }
+
+ try {
+ instance.element.dialog('destroy');
+ instance.element.remove();
+ } catch (ignore) {}
+ }
+
+ Dialog.removeInstance(options.id);
+ }
+
+ if (options['reload-on-close'] && !options.hasOwnProperty('is-reloading')) {
+ window.location.reload();
+ options['is-reloading'] = true;
+ }
+};
+
+Dialog.getClasses = function (options) {
+ var classes = ['studip-dialog'];
+
+ if (options.dialogClass) {
+ classes.push(options.dialogClass);
+ } else if (options['center-content']) {
+ classes.push('studip-dialog-centered');
+ }
+
+ return classes.join(' ');
+};
+
+Dialog.calculateDimensions = function (instance, content, options) {
+ var previous = instance.previous !== false ? Dialog.getInstance(instance.previous) : false;
+ var width = options.width || ($(window).width() * 2) / 3;
+ var height = options.height || ($(window).height() * 2) / 3;
+ var max_width = $(window).width() * 0.95;
+ var max_height = $(window).height() * 0.9;
+ var helper;
+ var temp;
+
+ if (instance.fixedDimensions) {
+ return instance.dimensions;
+ }
+
+ if ($('html').is('.responsive-display')) {
+ max_width = $(window).width() - 6; // Subtract border
+ max_height = $(window).height();
+
+ if (!options.hasOwnProperty('width')) {
+ width = $(window).width() * 0.95;
+ height = $(window).height() * 0.98;
+ }
+ }
+
+ // Adjust size if neccessary
+ if (!options.size) {
+ width = instance.dimensions?.width ?? width;
+ height = instance.dimensions?.height ?? height;
+ } else if (options.size === 'auto' || options.size === 'fit') {
+ // Render off screen
+ helper = $('<div class="ui-dialog ui-widget ui-widget-content">');
+ helper.addClass(Dialog.getClasses(options));
+
+ var helper_title = $('<span class="ui-dialog-title">')
+ .text(options.title)
+ .appendTo(helper)
+ .wrap('<div class="ui-dialog-titlebar ui-helper-clearfix">')
+ .after('<button class="ui-button ui-button-icon-only ui-dialog-titlebar-close">close</button>');
+ if (options.wikilink) {
+ helper_title.parent().append('<a class="ui-dialog-titlebar-wiki"></a>').addClass('with-wiki-link');
+ }
+
+
+ $('<div class="ui-dialog-content">').html($.parseHTML(content)).appendTo(helper);
+ // Prevent buttons from wrapping
+ $('[data-dialog-button]', helper).css('white-space', 'nowrap');
+ // Add cancel button if missing
+ if ((!options.hasOwnProperty('buttons') || options.buttons !== false)) {
+ $('<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"></div>')
+ .append('<div class="ui-dialog-buttonset"><button class="ui-button ui-widget ui-corner-all cancel">Foo</button></div>')
+ .appendTo(helper)
+ }
+
+ helper.css({
+ position: 'absolute',
+ left: '-10000px',
+ top: '-10000px',
+ width: 'auto'
+ }).appendTo('body');
+
+ // Calculate width and height
+ width = Math.min(helper.outerWidth(true) + dialog_margin, max_width);
+ height = Math.min(helper.outerHeight(true), max_height);
+
+ if (options.size === 'auto') {
+ width = Math.max(300, width);
+ height = Math.max(200, height);
+ }
+ // Remove helper element
+ helper.remove();
+ } else if (options.size === 'big') {
+ width = $('body').width() * 0.9;
+ height = $('body').height() * 0.8;
+ } else if (options.size === 'medium') {
+ width = $('body').width() * 0.6;
+ height = $('body').height() * 0.5;
+ } else if (options.size === 'medium-43') {
+ //Medium size in 4:3 aspect ratio
+ height = $('body').height() * 0.8;
+ width = parseInt(height) * 1.33333333;
+ if (width > $('body').width()) {
+ width = $('body').width() * 0.9;
+ }
+ } else if (options.size === 'small') {
+ width = 300;
+ height = 200;
+ } else if (options.size.match(/^\d+x\d+$/)) {
+ temp = options.size.split('x');
+ width = temp[0];
+ height = temp[1];
+ } else if (!options.size.match(/\D/)) {
+ width = height = options.size;
+ }
+
+ // Ensure dimensions fit in viewport
+ width = Math.min(width, max_width);
+ height = Math.min(height, max_height);
+ if (
+ previous &&
+ previous.hasOwnProperty('dimensions') &&
+ width > previous.dimensions.width &&
+ height > previous.dimensions.height
+ ) {
+ width = width > previous.dimensions.width ? previous.dimensions.width * 0.95 : width;
+ height = height > previous.dimensions.height ? previous.dimensions.height * 0.95 : height;
+ }
+
+ return {
+ width: width,
+ height: height
+ };
+};
+
+// Specialized confirmation dialog
+Dialog.confirm = function(question, yes_callback, no_callback) {
+ return $.Deferred(function(defer) {
+ if (question === true) {
+ defer.resolve();
+ } else if (question === false) {
+ defer.reject();
+ } else {
+ Dialog.show(_.escape(question).replace("\n", '<br>'), {
+ id: 'confirmation-dialog',
+ title: $gettext('Bitte bestätigen Sie die Aktion'),
+ size: 'fit',
+ wikilink: false,
+ dialogClass: 'studip-confirmation',
+ buttons: {
+ accept: {
+ text: $gettext('Ja'),
+ click: defer.resolve,
+ class: 'accept'
+ },
+ cancel: {
+ text: $gettext('Nein'),
+ click: defer.reject,
+ class: 'cancel'
+ }
+ }
+ });
+ }
+ $(document).one('dialog-close', function() {
+ if (defer.state() === 'pending') {
+ defer.reject();
+ }
+ });
+ })
+ .then(yes_callback, no_callback)
+ .always(function() {
+ Dialog.close({ id: 'confirmation-dialog' });
+ })
+ .promise();
+};
+
+Dialog.confirmAsPost = function(question, action) {
+ var form = $('<form/>', {
+ action: action,
+ method: 'post'
+ });
+ $('<input/>', {
+ type: 'hidden',
+ name: STUDIP.CSRF_TOKEN.name,
+ value: STUDIP.CSRF_TOKEN.value
+ }).appendTo(form);
+
+ $('body').append(form);
+
+ Dialog.confirm(question).done(function() {
+ form.submit();
+ });
+
+ return false;
+};
+
+Dialog.registerHeaderHandler = function (header, handler) {
+ Dialog.handlers.header[header] = handler;
+};
+Dialog.removeHeaderHandler = function (header) {
+ if (Dialog.handlers.header.hasOwnProperty(header)) {
+ delete Dialog.handlers.header[header];
+ }
+};
+
+Dialog.initialize = function() {
+ // Actual dialog handler
+ function dialogHandler(event) {
+ if (!event.isDefaultPrevented()) {
+ var target = $(event.target).closest('[data-dialog]');
+ var options = target.data().dialog;
+ if (Dialog.fromElement(target, parseOptions(options))) {
+ event.preventDefault();
+ }
+ }
+ }
+
+ function clickHandler(event) {
+ if (!event.isDefaultPrevented()) {
+ var element = $(event.target).closest(':submit,input[type="image"]');
+ var form = element.closest('form');
+ var action = element.attr('formaction');
+ form.data('triggeredBy', {
+ name: $(event.target).attr('name'),
+ value: $(event.target).val()
+ });
+ if (action) {
+ form.data('formaction', action);
+ }
+ }
+ }
+
+ // Calculate dialogs margins (outer width - inner width of the dialog) in
+ // order to properly calculated needed dialog widths. Otherwise horizontal
+ // scrollbars will occur. This is located here because it is only
+ // used in Dialog.show().
+ var temp = $('<div class="ui-dialog" style="position: absolute;left:-1000px;top:-1000px;"></div>');
+ temp.html('<div class="ui-dialog-content ui-widget-content"><div style="width: 100%">foo</div></div>');
+ temp.appendTo('body');
+ dialog_margin = temp.outerWidth(true) - $('.ui-dialog-content', temp).width();
+ temp.remove();
+
+ // Handle links, buttons and forms
+ $(document)
+ .on(
+ 'click',
+ 'a[data-dialog],button[data-dialog],input[type=image][data-dialog],input[type=submit][data-dialog]',
+ dialogHandler
+ )
+ .on('click', 'form[data-dialog] :submit', clickHandler)
+ .on('click', 'form[data-dialog] input[type=image]', clickHandler)
+ .on('submit', 'form[data-dialog]', dialogHandler);
+
+ // Close dialog on click outside of it
+ $(document).on('click', '.ui-widget-overlay', function() {
+ if ($('.ui-dialog').length > 0 && Dialog.stack.length) {
+ Dialog.close({
+ id: Dialog.stack[0]
+ });
+ }
+ });
+
+ // Recalculate dialog dimensions upon window resize. This is throttled
+ // since the resize event keeps on firing during the resizing.
+ var timeout = null;
+ $(window).on('resize', (event) => {
+ if (event.target !== window) {
+ return;
+ }
+
+ clearTimeout(timeout);
+ setTimeout(() => {
+ Dialog.stack.forEach((id) => {
+ var instance = Dialog.getInstance(id);
+ instance.dimensions = Dialog.calculateDimensions(
+ instance,
+ $(instance.element).html(),
+ instance.options
+ );
+
+ $(instance.element).dialog('option', 'width', instance.dimensions.width);
+ $(instance.element).dialog('option', 'height', instance.dimensions.height);
+ });
+ }, 10);
+ });
+};
+
+export default Dialog;
diff --git a/resources/assets/javascripts/lib/dialogs.js b/resources/assets/javascripts/lib/dialogs.js
new file mode 100644
index 0000000..371c987
--- /dev/null
+++ b/resources/assets/javascripts/lib/dialogs.js
@@ -0,0 +1,28 @@
+/* ------------------------------------------------------------------------
+ * Standard dialogs for confirmation or messages
+ * ------------------------------------------------------------------------ */
+
+const Dialogs = {
+ showConfirmDialog: function(question, confirm) {
+ // compile template
+ var getTemplate = _.memoize(function(name) {
+ return _.template(jQuery('#' + name).html());
+ });
+
+ var confirmDialog = getTemplate('confirm_dialog');
+ $('body').append(
+ confirmDialog({
+ question: question,
+ confirm: confirm
+ })
+ );
+
+ return false;
+ },
+
+ closeConfirmDialog: function() {
+ $('div.modaloverlay').remove();
+ }
+};
+
+export default Dialogs;
diff --git a/resources/assets/javascripts/lib/drag_and_drop_upload.js b/resources/assets/javascripts/lib/drag_and_drop_upload.js
new file mode 100644
index 0000000..ae37350
--- /dev/null
+++ b/resources/assets/javascripts/lib/drag_and_drop_upload.js
@@ -0,0 +1,21 @@
+/* Drag and drop file upload */
+const DragAndDropUpload = {
+ bind: function(form) {
+ form = form || document;
+
+ jQuery('input[type=file]', form).change(function() {
+ jQuery(this)
+ .closest('form')
+ .submit();
+ });
+
+ // The drag event handling is seriously messed up
+ // see http://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html
+ jQuery(form).on('dragover dragleave', function(event) {
+ jQuery(this).toggleClass('hovered', event.type === 'dragover');
+ return false;
+ });
+ }
+};
+
+export default DragAndDropUpload;
diff --git a/resources/assets/javascripts/lib/enrollment.js b/resources/assets/javascripts/lib/enrollment.js
new file mode 100644
index 0000000..5f0c129
--- /dev/null
+++ b/resources/assets/javascripts/lib/enrollment.js
@@ -0,0 +1,111 @@
+export default function enrollment() {
+ /**
+ * Filter logic for courses on both sides
+ */
+ $('#enrollment').on('keyup', 'input[name="filter"]', function () {
+ var text = $(this).val().trim(),
+ list = $(this).next('ul');
+
+ if (text.length > 0) {
+ var exp = new RegExp(text, 'gi');
+
+ list.children('li').each(function() {
+ var name = $(this).text();
+ $(this).toggle(name.search(exp) !== -1);
+ });
+ } else {
+ list.children('li:not(.empty)').show();
+ }
+ }).on('click', '.actions input, .button input', function () {
+ var action = $(this).closest('form').attr('action'),
+ data = $(this).closest('form').serializeArray();
+
+ data.push({name: this.name, value: this.value});
+
+ STUDIP.Overlay.show(true, null, null, null, 300);
+
+ $.post(action, data).done(function (response) {
+ var enrollment = $('#enrollment', response);
+ $('#enrollment').html(enrollment);
+ }).always(function () {
+ STUDIP.Overlay.hide();
+ });
+
+ return false;
+ });
+
+ // Disable drag and drop features for small displays
+ if (!$('html').hasClass('size-medium')) {
+ return;
+ }
+
+ /**
+ * Allow courses to be sorted via drag and drop according to their
+ * priorities.
+ */
+ $('#enrollment #selected-courses').sortable({
+ appendTo: '#selected-courses',
+ axis: 'y',
+ cancel: 'li.empty,li:nth-child(2):last-child',
+ cursor: 'move',
+ items: 'li:not(.empty)',
+ placeholder: 'ui-state-highlight',
+ tolerance: 'pointer',
+
+ helper: function (event, element) {
+ return $(element).clone().width($(element).width()).css({
+ overflow: 'hidden'
+ });
+ },
+ receive: function (event, ui) {
+ ui.helper.width('auto');
+ ui.item.removeClass('visible');
+ },
+ update: function(event, ui) {
+ // Adjust priority and add neccessary elements if missing
+ $(this).find('li:not(.empty)').each(function (index) {
+ var id = $(this).data().id,
+ hiddenElement = $(this).find('input[type="hidden"]');
+
+ index += 1;
+
+ if ($(this).find('.delete').length === 0) {
+ var delete_icon = $('script#delete-icon-template').html();
+ $(this).append(delete_icon);
+ }
+
+ if (hiddenElement.length === 0) {
+ $(this).append('<input type="hidden" name="admission_prio[' + id + ']" value="' + index + '">');
+ hiddenElement = $(this).find('input');
+ }
+
+ hiddenElement.val(index);
+ });
+ }
+ }).on('click', '.delete', function() {
+ var id = $(this).closest('li').remove().data().id;
+
+ $('#available-courses [data-id="' + id + '"]').addClass('visible');
+
+ $('#enrollment #selected-courses li:not(.empty)').each(function (index) {
+ $(this).find('input[type="hidden"]').val(index + 1);
+ });
+
+ return false;
+ }).disableSelection();
+
+ /**
+ * Allow courses to be dragged to the above defined sortable.
+ */
+ $('#enrollment #available-courses li').draggable({
+ activeClass: 'ui-state-highlight',
+ appendTo: '#available-courses',
+ connectToSortable: '#selected-courses',
+ containment: '#enrollment',
+ cursor: 'move',
+
+ helper: function () {
+ return $(this).clone().width($(this).width());
+ }
+ }).disableSelection();
+}
diff --git a/resources/assets/javascripts/lib/event-bus.js b/resources/assets/javascripts/lib/event-bus.js
new file mode 100644
index 0000000..3f6a58d
--- /dev/null
+++ b/resources/assets/javascripts/lib/event-bus.js
@@ -0,0 +1,5 @@
+import mitt from 'mitt';
+
+const eventBus = mitt();
+
+export default eventBus;
diff --git a/resources/assets/javascripts/lib/extract_callback.js b/resources/assets/javascripts/lib/extract_callback.js
new file mode 100644
index 0000000..5b88607
--- /dev/null
+++ b/resources/assets/javascripts/lib/extract_callback.js
@@ -0,0 +1,80 @@
+export default function extractCallback(cmd, payload) {
+ var command = cmd,
+ chunks,
+ last_chunk = null,
+ callback = window,
+ previous = null;
+
+ // Try to decode URI component in case it is encoded
+ try {
+ command = window.decodeURIComponent(command);
+ } catch (ignore) {}
+
+ // Try to parse value as JSON (value might be {func: 'foo', payload: {}})
+ try {
+ command = $.parseJSON(command);
+ } catch (e) {
+ command = { func: command };
+ }
+
+ // Check for invalid call
+ if (!command.hasOwnProperty('func')) {
+ throw 'Dialog: Invalid value for X-Dialog-Execute';
+ }
+
+ // Populate payload if not set
+ if (!command.hasOwnProperty('payload')) {
+ command.payload = payload;
+ }
+
+ // Find callback
+ chunks = command.func.trim().split(/\./);
+ $.each(chunks, function(index, chunk) {
+ // Check if last chunk was unfinished
+ if (last_chunk !== null) {
+ chunk = last_chunk + '.' + chunk;
+ last_chunk = null;
+ }
+
+ // Check for not finished/closed chunk
+ if (chunk.match(/\([^\)]*$/)) {
+ last_chunk = chunk;
+ return;
+ }
+
+ previous = callback;
+
+ var match = chunk.match(/\((.*)\);?$/),
+ parameters = null;
+
+ if (match !== null) {
+ chunk = chunk.replace(match[0], '');
+ try {
+ parameters = $.parseJSON('[' + match[1].replace(/'/g, '"') + ']');
+ } catch (e) {
+ console.log('error parsing json', match);
+ }
+ }
+
+ if (callback[chunk] === undefined) {
+ throw 'Error: Undefined callback ' + cmd;
+ }
+
+ if ($.isFunction(callback[chunk]) && parameters !== null) {
+ callback = callback[chunk].apply(callback, parameters);
+ } else {
+ callback = callback[chunk];
+ }
+ });
+
+ // Check callback
+ if (!$.isFunction(callback)) {
+ return function() {
+ return callback;
+ };
+ }
+
+ return function(p) {
+ return callback.apply(previous, [p || payload]);
+ };
+}
diff --git a/resources/assets/javascripts/lib/files.js b/resources/assets/javascripts/lib/files.js
new file mode 100644
index 0000000..59ae95f
--- /dev/null
+++ b/resources/assets/javascripts/lib/files.js
@@ -0,0 +1,345 @@
+/*jslint esversion: 6*/
+import { $gettext } from './gettext.js';
+import Dialog from './dialog.js';
+import FilesTable from '../../../vue/components/FilesTable.vue';
+
+const Files = {
+ init () {
+ if ($('#files-index, #files-system, #course-files-index, #institute-files-index, #files-flat, #course-files-flat, #institute-files-flat, #files-overview').length
+ && jQuery("#files_table_form").length) {
+
+ STUDIP.Vue.load().then(({createApp}) => {
+ this.filesapp = createApp({
+ el: "#layout_content",
+ data: {
+ "files": jQuery("#files_table_form").data("files") || [],
+ "folders": jQuery("#files_table_form").data("folders") || [],
+ "topfolder": jQuery("#files_table_form").data("topfolder"),
+ "breadcrumbs": jQuery("#files_table_form").data("breadcrumbs") || []
+ },
+ methods: {
+ hasFilesOfType (type) {
+ for (let i in this.files) {
+ if (this.files[i].mime_type.indexOf(type) === 0) {
+ return true;
+ }
+ }
+ return false;
+ },
+ removeFile(id) {
+ this.files = this.files.filter(file => file.id != id)
+ }
+ },
+ components: { FilesTable, },
+ });
+ });
+ }
+
+ //The following is only for (read only) vue file tables where multiple
+ //tables are displayed in one page.
+ var tables = jQuery('.vue-file-table');
+ if (tables.length) {
+ for (var table of tables) {
+ STUDIP.Vue.load().then(({createApp}) => {
+ createApp({
+ el: table,
+ data: {
+ "files": jQuery(table).data("files") || [],
+ "folders": jQuery(table).data("folders") || [],
+ "topfolder": jQuery(table).data("topfolder"),
+ "breadcrumbs": jQuery(table).data("breadcrumbs") || []
+ },
+ components: { FilesTable, },
+ });
+ });
+ }
+ }
+ },
+
+ openAddFilesWindow: function(folder_id) {
+ var responsive_mode = jQuery('html').first().hasClass('responsive-display');
+ if ($('.files_source_selector').length > 0) {
+ Dialog.show($('.files_source_selector').html(), {
+ title: $gettext('Dokument hinzufügen'),
+ size: (responsive_mode ? undefined : 'auto')
+ });
+ } else {
+ Dialog.fromURL(STUDIP.URLHelper.getURL('dispatch.php/file/add_files_window/' + folder_id), {
+ title: $gettext('Dokument hinzufügen'),
+ size: (responsive_mode ? undefined : 'auto')
+ });
+ }
+ },
+
+ validateUpload: function(file) {
+ if (!Files.uploadConstraints) {
+ return true;
+ }
+ if (file.size > Files.uploadConstraints.filesize) {
+ return false;
+ }
+ var ending = file.name.lastIndexOf('.') !== -1 ? file.name.substr(file.name.lastIndexOf('.') + 1) : '';
+
+ if (Files.uploadConstraints.type === 'allow') {
+ return $.inArray(ending, Files.uploadConstraints.file_types) === -1;
+ }
+
+ return $.inArray(ending, Files.uploadConstraints.file_types) !== -1;
+ },
+
+ upload: function(filelist) {
+ var files = 0;
+ var folder_id = $('.files_source_selector').data('folder_id');
+ var thresholds = []
+ var data = new FormData();
+ var updater_enabled = STUDIP.jsupdate_enable;
+
+ //Open upload-dialog
+ const nameslist = $('.file_upload_window .filenames').show().empty();
+ $('.file_upload_window .errorbox').hide().find('.errormessage').empty();
+
+ var total_size = 0;
+ $.each(filelist, function(index, file) {
+ if (Files.validateUpload(file)) {
+ data.append('file[]', file, file.name);
+
+ var id = `upload-element-${index}`;
+ var li = $('<li/>').attr('id', id).appendTo(nameslist);
+ $('<span/>').text(file.name).appendTo(li);
+ $('<span class="upload-progress"/>').appendTo(li);
+
+ thresholds.push({
+ position: total_size,
+ threshold: total_size + file.size,
+ name: file.name,
+ size: file.size,
+ element: id
+ });
+
+ total_size += file.size;
+ files += 1;
+ } else {
+ const errorMessage = file.name + ': ' + $gettext('Datei ist zu groß oder hat eine nicht erlaubte Endung.') + "<br>";
+ $('.file_upload_window .errorbox').show().find('.errormessage').append(errorMessage);
+ }
+ });
+ if ($('.file_uploader').length > 0) {
+ Dialog.show($('.file_uploader').html(), {
+ title: $gettext('Datei hochladen')
+ });
+ } else {
+ Dialog.fromURL(STUDIP.URLHelper.getURL('dispatch.php/file/upload_window'), {
+ title: $gettext('Datei hochladen')
+ });
+ }
+
+ //start upload
+ $('form.drag-and-drop.files').removeClass('hovered');
+ if (files > 0) {
+ STUDIP.JSUpdater.stop();
+
+ $('.file_upload_window .uploadbar').show().filter('.uploadbar-inner').css({
+ right: '100%'
+ });
+ $.ajax({
+ url: STUDIP.URLHelper.getURL(`dispatch.php/file/upload/${folder_id}`),
+ data: data,
+ cache: false,
+ contentType: false,
+ processData: false,
+ type: 'POST',
+ xhr: () => {
+ var xhr = $.ajaxSettings.xhr();
+ if (xhr.upload) {
+ const uploadbar = $('.file_upload_window .uploadbar-inner');
+ const uploadprogress = $('.file_upload_window .uploadbar .upload-progress');
+ var last = null;
+ xhr.upload.addEventListener('progress', event => {
+ if (event.lengthComputable) {
+ //Set progress
+ const position = event.loaded || event.position;
+ const total = event.total;
+ const percent = Math.round(position / total * 100 * 100) / 100;
+
+ uploadbar.css('right', `${100 - percent}%`);
+ uploadprogress.text(`${percent}%`);
+
+ const current = thresholds.find(element => element.threshold >= position);
+ if (current) {
+ const current_percent = Math.round((position - current.position) / current.size * 100);
+ $(`#${current.element} .upload-progress`).text(`${current_percent}%`);
+
+ if (current.element !== last && last !== null) {
+ $(`#${last} .upload-progress`).text(`100%`).closest('li').prevAll('li').find('.upload-progress').text('100%');
+ }
+ last = current.element;
+ }
+ }
+ }, false);
+ }
+
+ $(document).on('dialog-close.xhr-upload', () => xhr.abort());
+
+ return xhr;
+ }
+ }).done(json => {
+ $('.file_upload_window .uploadbar-inner').css('right', '0');
+ $('.file_upload_window .upload-progress').text(`100%`);
+
+ $(document).off('.xhr-upload');
+ }).fail((jqxhr, textStatus, error) => {
+ const errorMessage = $gettext('Es gab einen Fehler beim Hochladen der Datei(en):') + ' ' + error;
+ $('.file_upload_window .errorbox').show().find('.errormessage').text(errorMessage);
+ $('.file_upload_window').children('.filenames,.uploadbar').hide();
+ }).always(() => {
+ if (updater_enabled) {
+ STUDIP.JSUpdater.start();
+ }
+ });
+ } else {
+ $('.file_upload_window .uploadbar').hide();
+ }
+ },
+
+ addFile: (payload, delay = 0, hide_dialog = true) => {
+ var redirect = false;
+ var html = [];
+
+ if (payload.hasOwnProperty('html') && payload.html !== undefined) {
+ redirect = payload.redirect;
+ html = payload.html;
+ }
+
+ if (redirect) {
+ Dialog.fromURL(redirect);
+ } else if (hide_dialog) {
+ window.setTimeout(Dialog.close, 20);
+ }
+
+ if ($('table.documents').length > 0) {
+ // on files page
+ Files.addFileDisplay(html, delay);
+ } else if (payload.url) {
+ //not on files page
+
+ Dialog.handlers.header['X-Location'](payload.url);
+ }
+ },
+
+ addFileDisplay: (html, delay = 0) => {
+ if (!Array.isArray(html)) {
+ html = html === null ? [] : [html];
+ }
+ html.forEach((value, i) => {
+ let insert = true;
+ for (let i in STUDIP.Files.filesapp.files) {
+ if (value.id == STUDIP.Files.filesapp.files[i].id) {
+ STUDIP.Files.filesapp.files[i] = value;
+ insert = false;
+ }
+ }
+ if (insert) {
+ STUDIP.Files.filesapp.files.push(value);
+ }
+ });
+ $(document).trigger('refresh-handlers');
+ },
+
+ removeFileDisplay: function (ids) {
+ if (!Array.isArray(ids)) {
+ ids = [ids];
+ }
+
+ var count = ids.length;
+ ids.forEach((id) => {
+ STUDIP.Files.filesapp.removeFile(id);
+ });
+ $(document).trigger('refresh-handlers');
+ },
+
+ addFolderDisplay: function (html, delay = 0) {
+ if (!Array.isArray(html)) {
+ html = html === null ? [] : [html];
+ }
+ html.forEach((value, i) => {
+ STUDIP.Files.filesapp.folders.push(value);
+ });
+ $(document).trigger('refresh-handlers');
+ },
+
+ getFolders: function(name) {
+ var element_name = 'folder_select_' + name,
+ context = $('#' + element_name + '-destination').val(),
+ range = null;
+
+ if ($.inArray(context, ['courses']) > -1) {
+ range = $('#' + element_name + '-range-course > div > input')
+ .first()
+ .val();
+ } else if ($.inArray(context, ['institutes']) > -1) {
+ range = $('#' + element_name + '-range-inst > div > input')
+ .first()
+ .val();
+ } else if ($.inArray(context, ['myfiles']) > -1) {
+ range = $('#' + element_name + '-range-user_id').val();
+ }
+
+ if (range !== null) {
+ $.post(
+ STUDIP.URLHelper.getURL('dispatch.php/file/getFolders'),
+ { range: range },
+ function(data) {
+ if (data) {
+ $('#' + element_name + '-subfolder select').empty();
+ $.each(data, function(index, value) {
+ $.each(value, function(label, folder_id) {
+ $('#' + element_name + '-subfolder select').append(
+ '<option value="' + folder_id + '">' + label + '</option>'
+ );
+ });
+ });
+ }
+ },
+ 'json'
+ ).done(() => {
+ $(`#${element_name}-subfolder`).show();
+ });
+ }
+ },
+
+ changeFolderSource: function(name) {
+ var element_name = `folder_select_${name}`;
+ var elem = $(`#${element_name}-destination`);
+
+ $(`#${element_name}-range-course`).toggle(elem.val() === 'courses');
+ $(`#${element_name}-range-inst`).toggle(elem.val() === 'institutes');
+ $(`#${element_name}-subfolder`).toggle(elem.val() === 'myfiles');
+
+ if (elem.val() === 'myfiles') {
+ $(`#${element_name}-subfolder select`).empty();
+ Files.getFolders(name);
+ }
+ },
+
+ updateTermsOfUseDescription: function(e) {
+ //make all descriptions invisible:
+ $('div.terms_of_use_description_container > section').addClass('invisible');
+
+ var selected_id = $(this).val();
+
+ $(`#terms_of_use_description-${selected_id}`).removeClass('invisible');
+ },
+
+ openGallery: function () {
+ $(".lightbox-image").first().click();
+ },
+
+ // Upload constraints
+ uploadConstraints: false,
+
+ setUploadConstraints (constraints) {
+ Files.uploadConstraints = constraints;
+ }
+};
+
+export default Files; \ No newline at end of file
diff --git a/resources/assets/javascripts/lib/files_dashboard.js b/resources/assets/javascripts/lib/files_dashboard.js
new file mode 100644
index 0000000..5fc41c5
--- /dev/null
+++ b/resources/assets/javascripts/lib/files_dashboard.js
@@ -0,0 +1,19 @@
+import Table from './table.js';
+
+const FilesDashboard = {
+ /**
+ * Diese Methode wird aufgerufen, sobald ein Dashboard-Widget
+ * maximiert wurde. Die dort enthaltene Tabelle wird dann
+ * sortierbar gemacht.
+ * Die `elementId` bezieht sich auf die widget_element_id des Widgets.
+ */
+ enhanceList: function(elementId) {
+ $(document).on('dialog-open', function() {
+ $('.ui-dialog table[data-element-id="' + elementId + '"]').each(function(index, element) {
+ Table.enhanceSortableTable(element);
+ });
+ });
+ }
+};
+
+export default FilesDashboard;
diff --git a/resources/assets/javascripts/lib/folders.js b/resources/assets/javascripts/lib/folders.js
new file mode 100644
index 0000000..76dfadf
--- /dev/null
+++ b/resources/assets/javascripts/lib/folders.js
@@ -0,0 +1,87 @@
+import { $gettext } from './gettext.js';
+import Dialog from './dialog.js';
+
+const Folders = {
+ openAddFoldersWindow: function(folder_id, range_id) {
+ Dialog.fromURL(
+ STUDIP.ABSOLUTE_URI_STUDIP +
+ 'dispatch.php/folder/new?rangeId=' +
+ range_id +
+ '&parent_folder_id=' +
+ folder_id +
+ '&js=1',
+ {
+ title: $gettext('Dokument hinzufügen')
+ }
+ );
+ },
+
+ sendNewFolderForm: function() {
+ var new_folder_form = jQuery('#new_folder_form');
+
+ //get form fields to check if the required fields are set:
+ var folder_name = jQuery(new_folder_form)
+ .find('input[name="name"]')
+ .val();
+ var folder_type = jQuery(new_folder_form)
+ .find('input[name="folder_type"]')
+ .val();
+ var parent_folder_id = jQuery(new_folder_form)
+ .find('input[name="parent_folder_id"]')
+ .val();
+
+ if (folder_name && folder_type && parent_folder_id) {
+ jQuery.ajax({
+ method: 'POST',
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/folder/new',
+ data: new_folder_form.serialize(),
+ cache: false,
+ success: function(data) {
+ Folders.updateFolderListEntry(data.folder_id, data.tr);
+ Dialog.close();
+ }
+ });
+ }
+ },
+
+ updateFolderListEntry: function(folder_id, html, delay) {
+ //updates the folder entry in the folder list
+ var documents_table = jQuery('.documents[data-folder_id]');
+
+ if (jQuery('#row_folder_' + folder_id).length > 0) {
+ //row with folder-ID was found:
+ jQuery('#row_folder_' + folder_id).replaceWith(html);
+ } else {
+ jQuery(documents_table).append(html);
+ }
+ },
+
+ removeFolderListEntry: function(folder_id) {
+ //removes a row from the folder list:
+ if (jQuery('#row_folder_' + folder_id).length > 0) {
+ //row with folder-ID was found:
+ jQuery('#row_folder_' + folder_id).remove();
+ }
+ },
+
+ delete: function(folder_id) {
+ if (!folder_id) {
+ return false;
+ }
+
+ jQuery.ajax({
+ method: 'GET',
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/folder/delete/' + folder_id,
+ data: null,
+ cache: false,
+ success: function(data) {
+ if ($(data).hasClass('messagebox_success')) {
+ Folders.removeFolderListEntry(folder_id);
+ }
+ $('#layout_content').prepend(data);
+ }
+ });
+ }
+};
+
+export default Folders;
diff --git a/resources/assets/javascripts/lib/forms.js b/resources/assets/javascripts/lib/forms.js
new file mode 100644
index 0000000..8e6a3eb
--- /dev/null
+++ b/resources/assets/javascripts/lib/forms.js
@@ -0,0 +1,59 @@
+/* ------------------------------------------------------------------------
+ * Forms
+ * ------------------------------------------------------------------------ */
+
+const Forms = {
+ initialized: false,
+ initialize: function(scope) {
+ if (scope === undefined) {
+ scope = document;
+ }
+
+ $('input[required],textarea[required]', scope).attr('aria-required', true);
+ $('input[pattern][title],textarea[pattern][title]', scope).each(function() {
+ $(this).data('message', $(this).attr('title'));
+ });
+
+ if (!Forms.initialized) {
+ // add invalid-handler to every input and textarea on the page
+ $(document).on('invalid', 'input, textarea', function() {
+ $(this)
+ .attr('aria-invalid', 'true')
+ .change(function() {
+ $(this).removeAttr('aria-invalid');
+ });
+
+ // get the fieldset that contains the invalid input
+ var fieldset = $(this).closest('fieldset');
+ // toggle the collapsed class if the fieldset is currently collapsed
+ if (fieldset.hasClass('collapsed')) {
+ fieldset.toggleClass('collapsed');
+ }
+ });
+
+ $(document).on('change', 'form.default label.file-upload input[type=file]', function(ev) {
+ var selected_file = ev.target.files[0],
+ filename;
+ if (
+ $(this)
+ .closest('label')
+ .find('.filename').length
+ ) {
+ filename = $(this)
+ .closest('label')
+ .find('.filename');
+ } else {
+ filename = $('<span class="filename"/>');
+ $(this)
+ .closest('label')
+ .append(filename);
+ }
+ filename.text(selected_file.name + ' ' + Math.ceil(selected_file.size / 1024) + 'KB');
+ });
+ }
+
+ Forms.initialized = true;
+ }
+};
+
+export default Forms;
diff --git a/resources/assets/javascripts/lib/fullcalendar.js b/resources/assets/javascripts/lib/fullcalendar.js
new file mode 100644
index 0000000..8c3c123
--- /dev/null
+++ b/resources/assets/javascripts/lib/fullcalendar.js
@@ -0,0 +1,601 @@
+/*jslint esversion: 6*/
+
+/**
+ * This class contains Stud.IP specific code for the fullcalendar package.
+ */
+
+import { Calendar } from '@fullcalendar/core';
+import deLocale from '@fullcalendar/core/locales/de';
+import enLocale from '@fullcalendar/core/locales/en-gb';
+import interactionPlugin from '@fullcalendar/interaction';
+import { Draggable } from '@fullcalendar/interaction';
+import dayGridPlugin from '@fullcalendar/daygrid';
+import timeGridPlugin from '@fullcalendar/timegrid';
+import resourceCommonPlugin from '@fullcalendar/resource-common';
+import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
+import resourceTimelinePlugin from '@fullcalendar/resource-timeline';
+
+import jsPDF from 'jspdf-yworks';
+import html2canvas from 'html2canvas';
+
+Date.prototype.getWeekNumber = function () {
+ var d = new Date(Date.UTC(this.getFullYear(), this.getMonth(), this.getDate()));
+ var dayNum = d.getUTCDay() || 7;
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
+ var yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
+ return Math.ceil((((d - yearStart) / 86400000) + 1)/7);
+};
+
+function pad(what, length = 2, char = '0') {
+ let padding = new Array(length + 1).join(char);
+ return `${padding}${what}`.substr(-length);
+}
+
+class Fullcalendar
+{
+ /**
+ * The initialisation method. It loads the JS files for fullcalendar
+ * in case they are not loaded and sets up a fullcalendar instance
+ * for the nodes specified in the parameter node.
+ *
+ * @param DOMElement|string node The node which shall have a full calendar.
+ * This must either be a DOMElement or a string
+ * containing a CSS selector.
+ */
+ static init(node, fullcalendar_options = null)
+ {
+ // Convert css selector to actual dom element
+ node = $(node)[0];
+
+ if (!node) {
+ //We need a CSS selector or a node!
+ return;
+ }
+
+ if (document.getElementById('external-events')) {
+ new Draggable(document.getElementById('external-events'), {
+ itemSelector: '.fc-event',
+ eventData (eventEl) {
+ return {
+ title: eventEl.dataset.eventTitle,
+ duration: eventEl.dataset.eventDuration,
+ course_id: eventEl.dataset.eventCourse,
+ tooltip: eventEl.dataset.eventTooltip,
+ studip_api_urls: {drop: eventEl.dataset.eventDropUrl},
+ studip_view_urls: {edit: eventEl.dataset.eventDetailsUrl}
+ };
+ }
+ });
+ }
+
+ var calendar = new Calendar(node, fullcalendar_options);
+ node.calendar = calendar;
+ calendar.render();
+
+ return calendar;
+ }
+
+ /**
+ * Converts semester events to the default fullcalendar event format.
+ * The begin and end date are converted to fit into the current week.
+ */
+ static convertSemesterEvents(event_data, fake_week_start = Date())
+ {
+ if (!event_data) {
+ return {};
+ }
+
+ var start = String(event_data.start).split('T');
+ var end = String(event_data.end).split('T');
+
+ //start and end must be transformed to the current week.
+ //Therefore, we need the ISO weekdays for begin and end.
+ var fake_start = new Date(fake_week_start);
+ fake_start.setHours(12);
+ fake_start.setMinutes(0);
+ fake_start.setSeconds(0);
+ var fake_end = new Date(fake_week_start);
+ fake_end.setHours(12);
+ fake_end.setMinutes(0);
+ fake_end.setSeconds(0);
+
+ //Calculcate the week day of the current week for the event
+ //from the current day and convert sunday to ISO format
+ var start_day_diff = fake_start.getDay() || 7;
+ var end_day_diff = fake_end.getDay() || 7;
+
+ start_day_diff = start_day_diff - event_data.studip_weekday_begin;
+ end_day_diff = end_day_diff - event_data.studip_weekday_end;
+
+ fake_start = new Date(
+ fake_start.getTime() - start_day_diff * 24 * 60 * 60 * 1000
+ );
+ fake_end = new Date(
+ fake_end.getTime() - end_day_diff * 24 * 60 * 60 * 1000
+ );
+
+ //Output the modified begin and end date in the correct ISO format:
+ event_data.start =`${fake_start.getFullYear()}-${pad(fake_start.getMonth() + 1)}-${pad(fake_start.getDate())}T${start[1]}`;
+ event_data.end = `${fake_end.getFullYear()}-${pad(fake_end.getMonth() + 1)}-${pad(fake_end.getDate())}T${end[1]}`;
+
+ return event_data;
+ }
+
+
+ static createSemesterCalendarFromNode(node, additional_config = {})
+ {
+ if (!node) {
+ //Ain't no fullcalendar when the node's gone!
+ return;
+ }
+
+ var config = $.extend(
+ {},
+ $(node).data('config') || {},
+ additional_config
+ );
+
+ if (Array.isArray(config.eventSources)) {
+ config.eventSources = config.eventSources.map((s) => {
+ if (s.hasOwnProperty('url')) {
+ return s;
+ }
+ });
+ }
+
+ return this.createFromNode(node, config);
+ }
+
+
+ static defaultResizeEventHandler(info)
+ {
+ if (!info.event.durationEditable || !info.view.viewSpec.options.editable) {
+ //Read-only events cannot be resized!
+ info.revert();
+ return;
+ }
+
+ if (info.event.extendedProps.studip_api_urls.resize) {
+ $.post({
+ url: info.event.extendedProps.studip_api_urls.resize,
+ async: false,
+ data: {
+ begin: this.toRFC3339String(info.event.start),
+ end: this.toRFC3339String(info.event.end)
+ }
+ }).fail(info.revert);
+ }
+ }
+
+ static downloadPDF(format = 'landscape', withWeekend = false)
+ {
+ $('*[data-fullcalendar="1"]').each(function () {
+ if (this.calendar != undefined) {
+ $(this).addClass('print-view').toggleClass('without-weekend', !withWeekend);
+
+ var title = $(this).data('title');
+ let print_title = $('<h1>').text(title).prependTo(this);
+
+ window.scrollTo(0, 0);
+
+ html2canvas(this).then(canvas => {
+ var imgData = canvas.toDataURL('image/jpeg');
+ var pdf = new jsPDF({
+ orientation: format === 'landscape' ? 'landscape' : 'portrait'
+ });
+ if (format === 'landscape') {
+ pdf.addImage(imgData, 'JPEG', 20, 20, 250, 250, 'i1', 'NONE', 0);
+ } else {
+ pdf.addImage(imgData, 'JPEG', 25, 20, 160, 190, 'i1', 'NONE', 0);
+ }
+ pdf.save(title + '.pdf');
+ });
+
+ print_title.remove();
+ $(this).removeClass('print-view without-weekend');
+ }
+ });
+ }
+
+ static toRFC3339String(date)
+ {
+ var timezone_offset_min = date.getTimezoneOffset();
+ var offset_hrs = parseInt(Math.abs(timezone_offset_min / 60), 10);
+ var offset_min = Math.abs(timezone_offset_min%60);
+ var timezone_standard;
+
+ offset_hrs = pad(offset_hrs);
+ offset_min = pad(offset_min);
+
+ // Add an opposite sign to the offset
+ // If offset is 0, it means timezone is UTC
+ if (timezone_offset_min < 0) {
+ timezone_standard = `+${offset_hrs}:${offset_min}`;
+ } else if (timezone_offset_min > 0) {
+ timezone_standard = `-${offset_hrs}:${offset_min}`;
+ } else {
+ timezone_standard = '+00:00';
+ }
+
+ var current_date = pad(date.getDate());
+ var current_month = pad(date.getMonth() + 1);
+ var current_year = date.getFullYear();
+ var current_hrs = pad(date.getHours());
+ var current_mins = pad(date.getMinutes());
+ var current_secs = pad(date.getSeconds());
+ var current_datetime;
+
+ // Current datetime
+ // String such as 2016-07-16T19:20:30
+ current_datetime = `${current_year}-${current_month}-${current_date}T${current_hrs}:${current_mins}:${current_secs}`;
+
+ return current_datetime + timezone_standard;
+ }
+
+ static defaultDropEventHandler(info)
+ {
+ // The logic from fullcalendar is inversed here:
+ // If the calendar isn't editable, the event isn't either.
+ if (!info.event.startEditable || !info.view.viewSpec.options.editable) {
+ //Read-only events cannot be dragged and dropped!
+ info.revert();
+ return;
+ }
+
+ var drop_resource_id = info.newResource ? info.newResource.id : info.event.extendedProps.studip_range_id;
+
+ if (info.event.extendedProps.studip_api_urls.move) {
+ if (info.event.allDay) {
+ $.post({
+ async: false,
+ url: info.event.extendedProps.studip_api_urls.move,
+ data: {
+ resource_id: drop_resource_id,
+ begin: this.toRFC3339String(info.event.start.setHours(0,0,0)),
+ end: this.toRFC3339String(info.event.start.setHours(23,59,59))
+ }
+ }).fail(info.revert);
+ } else if (info.event.end === null) {
+ var real_end = new Date();
+ real_end.setTime(info.event.start.getTime());
+ real_end.setHours(info.event.start.getHours()+2);
+ $.post({
+ async: false,
+ url: info.event.extendedProps.studip_api_urls.move,
+ data: {
+ resource_id: drop_resource_id,
+ begin: this.toRFC3339String(info.event.start),
+ end: this.toRFC3339String(real_end)
+ }
+ }).fail(info.revert);
+ } else {
+ $.post({
+ async: false,
+ url: info.event.extendedProps.studip_api_urls.move,
+ data: {
+ resource_id: drop_resource_id,
+ begin: this.toRFC3339String(info.event.start),
+ end: this.toRFC3339String(info.event.end)
+ }
+ }).fail(info.revert);
+ }
+ }
+ }
+
+ static institutePlanDropEventHandler(info)
+ {
+ //The logic from fullcalendar is inversed here:
+ if (info.newResource) {
+ $.post({
+ async: false,
+ url: info.event.extendedProps.studip_api_urls.move,
+ data: {
+ cycle_id: info.event.id,
+ resource_id: info.newResource.id,
+ begin: this.toRFC3339String(info.event.start),
+ end: this.toRFC3339String(info.event.end)
+ }
+ }).fail(info.revert);
+ } else {
+ //If the calendar isn't editable, the event isn't either.
+ if (!info.event.startEditable || !info.view.viewSpec.options.editable) {
+ //Read-only events cannot be dragged and dropped!
+ info.revert();
+ return;
+ }
+
+ $.post({
+ async: false,
+ url: info.event.extendedProps.studip_api_urls.move,
+ data: {
+ cycle_id: info.event.id,
+ begin: this.toRFC3339String(info.event.start),
+ end: this.toRFC3339String(info.event.end)
+ }
+ }).fail(info.revert);
+ }
+ }
+
+ static institutePlanExternalDropEventHandler(info)
+ {
+ var resourceIds = info.event.getResources().map(resource => resource.id);
+
+ $.post({
+ async: false,
+ url: info.event.extendedProps.studip_api_urls.drop,
+ data: {
+ course_id: info.event.extendedProps.course_id,
+ begin: this.toRFC3339String(info.event.start),
+ end: this.toRFC3339String(info.event.end),
+ resource_id: resourceIds[0]
+ }
+ }).done(data => {
+ if (data) {
+ info.view.context.calendar.addEvent(JSON.parse(data));
+ info.event.remove();
+ }
+ });
+ }
+
+ static createFromNode(node, additional_config = {})
+ {
+ if (!node) {
+ //No node? No fullcalendar!
+ return;
+ }
+
+ var config = $(node).data('config');
+
+ //Make sure the default values are set, if they are not found
+ //in the additional_config object:
+ config = $.extend({
+ plugins: [ interactionPlugin, dayGridPlugin, timeGridPlugin, resourceCommonPlugin, resourceTimeGridPlugin, resourceTimelinePlugin ],
+ schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
+ defaultView: 'timeGridWeek',
+ header: {
+ left: 'dayGridMonth,timeGridWeek,timeGridDay'
+ },
+ minTime: '08:00:00',
+ maxTime: '20:00:00',
+ height: 'auto',
+ contentHeight: 'auto',
+ firstDay: 1,
+ weekNumberCalculation: 'ISO',
+ locales: [enLocale, deLocale ],
+ locale: String.locale === 'de-DE' ? 'de' : 'en-gb',
+ timeFormat: 'H:mm',
+ nowIndicator: true,
+ timeZone: 'local',
+ studip_functions: [],
+ resourceAreaWidth: '20%',
+ select (selectionInfo) {
+ if (!selectionInfo.view.viewSpec.options.editable || !selectionInfo.view.viewSpec.options.studip_urls) {
+ //The calendar isn't editable.
+ return;
+ }
+ if (selectionInfo.view.viewSpec.options.studip_urls.add) {
+ if (selectionInfo.resource) {
+ STUDIP.Dialog.fromURL( selectionInfo.view.viewSpec.options.studip_urls.add, {
+ data: {
+ begin: selectionInfo.start.getTime()/1000,
+ end: selectionInfo.end.getTime()/1000,
+ ressource_id: selectionInfo.resource.id
+ }
+ });
+ } else {
+ STUDIP.Dialog.fromURL(selectionInfo.view.viewSpec.options.studip_urls.add, {
+ data: {
+ begin: selectionInfo.start.getTime()/1000,
+ end: selectionInfo.end.getTime()/1000
+ }
+ });
+ }
+ }
+ },
+ eventClick (eventClickInfo) {
+ var event = eventClickInfo.event;
+ var extended_props = event.extendedProps;
+ if ($(eventClickInfo.jsEvent.target).hasClass('event-colorpicker')) {
+ STUDIP.Dialog.fromURL(
+ STUDIP.URLHelper.getURL('dispatch.php/admin/courseplanning/pick_color/' + extended_props.metadate_id + '/' + config.actionCalled),
+ {'size': '400x400'}
+ );
+ return false;
+ }
+
+
+ if ($(eventClickInfo.event._calendar.el).hasClass('request-plan')) {
+ if (extended_props.request_id && extended_props.studip_view_urls.edit) {
+ STUDIP.Dialog.fromURL(
+ STUDIP.URLHelper.getURL(extended_props.studip_view_urls.edit)
+ );
+ } else if(extended_props.studip_parent_object_class == 'ResourceBooking' && $.inArray('for-course', event._def.ui.classNames) != -1) {
+ STUDIP.Dialog.fromURL(
+ STUDIP.URLHelper.getURL('dispatch.php/resources/room_request/request_booking/' + extended_props.studip_parent_object_id)
+ );
+ }
+ return false;
+ }
+
+ if (extended_props.studip_view_urls === undefined) {
+ return;
+ }
+ if (!event.startEditable && extended_props.studip_view_urls.show) {
+ STUDIP.Dialog.fromURL(
+ STUDIP.URLHelper.getURL(extended_props.studip_view_urls.show)
+ );
+ } else if (event.startEditable && extended_props.studip_view_urls.edit) {
+ STUDIP.Dialog.fromURL(
+ STUDIP.URLHelper.getURL(extended_props.studip_view_urls.edit),
+ {'size': 'big'}
+ );
+ }
+ return false;
+ },
+ eventResize (info) {
+ // The logic from fullcalendar is inversed here:
+ // If the calendar isn't editable, the event isn't either.
+ if (info.view.viewSpec.options.studip_functions.resize_event) {
+ info.view.viewSpec.options.studip_functions.resize_event(info);
+ } else {
+ STUDIP.Fullcalendar.defaultResizeEventHandler(info);
+ }
+ info.event.source.refetch();
+ },
+ eventDrop (info) {
+ if ($(info.view.context.calendar.el).hasClass('institute-plan')) {
+ var start = info.event.start;
+ var cal_start = info.view.activeStart;
+ if ((start.getHours() - cal_start.getHours()) % 2 === 1) {
+ info.event.moveDates('-01:00');
+ }
+ STUDIP.Fullcalendar.institutePlanDropEventHandler(info);
+ } else {
+ if (info.view.viewSpec.options.studip_functions.drop_event) {
+ info.view.viewSpec.options.studip_functions.drop_event(info);
+ } else {
+ STUDIP.Fullcalendar.defaultDropEventHandler(info);
+ }
+ info.event.source.refetch();
+ }
+ },
+ eventRender (info) {
+ var event = info.event;
+ var eventElement = info.el;
+ var iconColor = event.textColor == '#000000' ? 'black' : 'white';
+
+ if ($(info.view.context.calendar.el).hasClass('institute-plan')) {
+ $(eventElement).attr('title', event.extendedProps.tooltip);
+ $(eventElement).find('.fc-title').html(
+ $('<div>').css({
+ width: 'calc(100% - 21px)',
+ height: '100%',
+ wordBreak: 'break-word'
+ }).text(eventElement.text)
+ );
+ $(eventElement).find('.fc-title').append(
+ $('<button class="event-colorpicker">').addClass(iconColor)
+ );
+ } else {
+ $(eventElement).attr('title', event.title);
+ }
+
+ if (event.extendedProps.icon) {
+ $(eventElement).find('.fc-title').prepend(
+ $('<img>').attr('src', `${STUDIP.ASSETS_URL}images/icons/${iconColor}/${event.extendedProps.icon}.svg`)
+ .css({
+ verticalAlign: 'text-bottom',
+ marginRight: '3px',
+ width: 14,
+ height: 14
+ })
+ );
+ }
+ },
+ eventSourceSuccess: function(content, xhr) {
+ if ($(node).hasClass('semester-plan')) {
+ $(content).each(function(i, event_data){
+ STUDIP.Fullcalendar.convertSemesterEvents(event_data, config.defaultDate);
+ });
+ }
+ return content;
+ },
+ loading (isLoading) {
+ if (isLoading) {
+ if (!$('#loading-spinner').length) {
+ jQuery('#layout_content').append(
+ $('<div id="loading-spinner" style="position: absolute; top: calc(50% - 55px); left: calc(50% + 135px); z-index: 9001;">').html(
+ $('<img>').attr('src', STUDIP.ASSETS_URL + 'images/ajax-indicator-black.svg')
+ .css({
+ width: 64,
+ height: 64
+ })
+ )
+ );
+ }
+ } else {
+ $('#loading-spinner').remove();
+ this.updateSize();
+ }
+ },
+ datesRender (info) {
+ var activeRange = info.view.props.dateProfile.activeRange;
+ var start = activeRange.start;
+ var end = activeRange.end;
+
+ if ($(info.el).hasClass('institute-plan')) {
+ $('.fc-slats tr:odd .fc-widget-content:not(.fc-axis)').remove();
+ }
+
+ if ($('.booking-plan-header').length) {
+ end.setDate(end.getDate());
+ var sem_start = $('.booking-plan-header').data('semester-begin');
+ var sem_end = $('.booking-plan-header').data('semester-end');
+
+ if (sem_start && (start.getTime() / 1000 < sem_start || start.getTime() / 1000 > sem_end)) {
+ sem_start = null;
+ sem_end = null;
+ } else if(sem_start) {
+ var sem_week = Math.floor((end.getTime() / 1000 - 10800 - sem_start) / (7 * 24 * 60 * 60)) + 1;
+ $("#booking-plan-header-semweek-part").text("Vorlesungswoche".toLocaleString());
+ $('#booking-plan-header-semweek').text(sem_week);
+ }
+ $('#booking-plan-header-calweek').text(start.getWeekNumber());
+ $('#booking-plan-header-calbegin').text(start.toLocaleDateString('de-DE', {weekday: 'short'}) + ' ' + start.toLocaleDateString('de-DE'));
+ $('#booking-plan-header-calend').text(end.toLocaleDateString('de-DE', {weekday: 'short'}) + ' ' + end.toLocaleDateString('de-DE'));
+ if (!sem_start || !sem_end) {
+ STUDIP.Resources.updateBookingPlanSemesterByView(activeRange);
+ }
+ }
+ },
+ resourceRender (renderInfo) {
+ if ($(renderInfo.view.context.calendar.el).hasClass('room-group-booking-plan')) {
+ var action = $(renderInfo.view.context.calendar.el).hasClass('semester-plan') ? 'semester' : 'booking';
+ var url = STUDIP.URLHelper.getURL(`dispatch.php/resources/room_planning/${action}_plan/${renderInfo.resource.id}`);
+ $(renderInfo.el).find('.fc-cell-text').html(
+ $('<a>').attr('href', url).text(renderInfo.el.innerText)
+ );
+ } else if ($("*[data-fullcalendar='1']").hasClass('institute-plan') && renderInfo.resource.id > 0) {
+ var icon = '<img class="text-bottom icon-role-clickable icon-shape-edit" width="16" height="16" src="' + STUDIP.URLHelper.getURL('assets/images/icons/blue/edit.svg') + '" alt="edit">';
+ $(renderInfo.el).append(
+ '<a href="'
+ + STUDIP.URLHelper.getURL('dispatch.php/admin/courseplanning/rename_column/'
+ + renderInfo.resource.id
+ +'/'
+ + renderInfo.view.activeStart.getDay())
+ + '" data-dialog="size=auto"> '
+ + icon
+ + '</a>'
+ );
+ }
+ },
+ drop (dropInfo) {
+ $(dropInfo.draggedEl).remove();
+ },
+ eventReceive (info) {
+ if ($(info.view.context.calendar.el).hasClass('institute-plan')) {
+ STUDIP.Fullcalendar.institutePlanExternalDropEventHandler(info);
+ }
+ }
+ }, config);
+
+ //Special treatment: If a general column header format is set,
+ //in the configuration, it shall be used for all columns in all views
+ //by using a special columnHeaderHtml function.
+ if (config.columnHeaderFormat) {
+ config.columnHeaderHtml = function (date) {
+ if ($("*[data-fullcalendar='1']").hasClass('institute-plan')) {
+ return '<a href="' + STUDIP.URLHelper.getURL('dispatch.php/admin/courseplanning/weekday/' + date.getDay()) + '">' + date.toLocaleDateString('de-DE', config.columnHeaderFormat) + '</a>';
+ } else {
+ return date.toLocaleDateString('de-DE', config.columnHeaderFormat);
+ }
+ };
+ }
+
+ config = $.extend({}, config, additional_config);
+
+ return this.init(node, config);
+ }
+}
+
+export default Fullcalendar;
diff --git a/resources/assets/javascripts/lib/fullscreen.js b/resources/assets/javascripts/lib/fullscreen.js
new file mode 100644
index 0000000..10ff69e
--- /dev/null
+++ b/resources/assets/javascripts/lib/fullscreen.js
@@ -0,0 +1,60 @@
+/*jslint esversion: 6*/
+const Fullscreen = {
+ toggle () {
+ if (sessionStorage.getItem('studip-fullscreen') === 'on') {
+ STUDIP.Fullscreen.leave();
+ } else {
+ STUDIP.Fullscreen.enter();
+ }
+ },
+
+ enter (immediate = false) {
+ // Set appropriate class on html element to trigger fullscreen mode and
+ // transisitions
+ $('html').addClass('is-fullscreen').toggleClass('is-fullscreen-immediately', immediate);
+
+ // Move toggle element into viewport
+ $('.fullscreen-toggle').prependTo('#layout_content');
+
+ // Attach key handler that allows keypress on escape to leave fullscreen
+ $(document).on('keydown.key27', (event) => {
+ if (event.key === 'Escape') {
+ STUDIP.Fullscreen.leave();
+ }
+ });
+
+ // Store indicator in session
+ sessionStorage.setItem('studip-fullscreen', 'on');
+ },
+
+ leave () {
+ // Remove indicator from session
+ sessionStorage.removeItem('studip-fullscreen');
+
+ // Deactivate key handler
+ $(document).off('keydown.key27');
+
+ // Move toggle element into secondary navigation
+ $('.fullscreen-toggle').insertBefore('.helpbar-container');
+
+ //
+ (new Promise((resolve, reject) => {
+ var timeout = setTimeout(() => {
+ $('#layout-sidebar').off('transitionend');
+ resolve();
+ }, 500);
+ $('#layout-sidebar').one('transitionend', () => {
+ clearTimeout(timeout);
+ resolve();
+ });
+ })).then(() => {
+ $(document.body).trigger('sticky_kit:recalc');
+ });
+
+
+ // Remove classes on html element
+ $('html').removeClass('is-fullscreen is-fullscreen-immediately');
+ }
+};
+
+export default Fullscreen;
diff --git a/resources/assets/javascripts/lib/gettext.js b/resources/assets/javascripts/lib/gettext.js
new file mode 100644
index 0000000..dea7f01
--- /dev/null
+++ b/resources/assets/javascripts/lib/gettext.js
@@ -0,0 +1,92 @@
+import { translate } from 'vue-gettext';
+import defaultTranslations from '../../../locales/de.json';
+import eventBus from './event-bus.js';
+
+const DEFAULT_LANG = 'de_DE';
+const DEFAULT_LANG_NAME = 'Deutsch';
+
+const state = getInitialState();
+
+const $gettext = translate.gettext.bind(translate);
+
+export { $gettext, translate, getLocale, setLocale, getVueConfig };
+
+function getLocale() {
+ return state.locale;
+}
+
+async function setLocale(locale = getInitialLocale()) {
+ if (!(locale in getInstalledLanguages())) {
+ throw new Error('Invalid locale: ' + locale);
+ }
+
+ state.locale = locale;
+ if (state.translations[state.locale] === null) {
+ state.translations[state.locale] = await getTranslations(state.locale);
+ }
+
+ translate.initTranslations(state.translations, {
+ getTextPluginMuteLanguages: [DEFAULT_LANG],
+ getTextPluginSilent: false,
+ language: state.locale,
+ silent: false,
+ });
+
+ eventBus.emit('studip:set-locale', state.locale);
+}
+
+function getVueConfig() {
+ const availableLanguages = Object.entries(getInstalledLanguages()).reduce((memo, [lang, { name }]) => {
+ memo[lang] = name;
+
+ return memo;
+ }, {});
+
+ return {
+ availableLanguages,
+ defaultLanguage: DEFAULT_LANG,
+ muteLanguages: [DEFAULT_LANG],
+ silent: false,
+ translations: state.translations,
+ };
+}
+
+function getInitialState() {
+ const translations = Object.entries(getInstalledLanguages()).reduce((memo, [lang]) => {
+ memo[lang] = lang === DEFAULT_LANG ? defaultTranslations : null;
+
+ return memo;
+ }, {});
+
+ return {
+ locale: DEFAULT_LANG,
+ translations,
+ };
+}
+
+function getInitialLocale() {
+ for (const [lang, { selected }] of Object.entries(getInstalledLanguages())) {
+ if (selected) {
+ return lang;
+ }
+ }
+
+ return DEFAULT_LANG;
+}
+
+function getInstalledLanguages() {
+ return window?.STUDIP?.INSTALLED_LANGUAGES ?? { [DEFAULT_LANG]: { name: DEFAULT_LANG_NAME, selected: true } };
+}
+
+async function getTranslations(locale) {
+ try {
+ const language = locale.split(/[_-]/)[0];
+ const translation = await import(`../../../locales/${language}.json`);
+
+ return translation;
+ } catch (exception) {
+ console.error('Could not load locale: "' + locale + '"', exception);
+
+ return {};
+ }
+}
diff --git a/resources/assets/javascripts/lib/global_search.js b/resources/assets/javascripts/lib/global_search.js
new file mode 100644
index 0000000..49b3e03
--- /dev/null
+++ b/resources/assets/javascripts/lib/global_search.js
@@ -0,0 +1,234 @@
+const GlobalSearch = {
+ lastSearch: null,
+
+ /**
+ * Toggles visibility of search input field and hints.
+ * @param visible boolean indicating whether shown or not
+ * @param cleanup boolean whether to clear search term and results
+ * @returns {boolean}
+ */
+ toggleSearchBar: function(visible, cleanup) {
+ $('#globalsearch-searchbar').toggleClass('is-visible', visible);
+ $('#globalsearch-input').toggleClass('hidden-small-down', !visible);
+ $('#globalsearch-icon').toggleClass('hidden-small-down', visible);
+ $('#globalsearch-clear').toggleClass('hidden-small-down', !visible);
+
+ if (!visible && cleanup) {
+ GlobalSearch.lastSearch = null;
+ $('#globalsearch-searchbar').removeClass('has-value');
+ $('#globalsearch-results').html('');
+ $('#globalsearch-input').blur().val('');
+ }
+
+ $('html:not(.size-large)').toggleClass('globalsearch-visible', visible);
+
+ return false;
+ },
+
+ /**
+ * Performs the actual search.
+ */
+ doSearch: function() {
+ var searchterm = $('#globalsearch-input').val().trim();
+ var hasValue = searchterm.length >= 3;
+ var results = $();
+ var resultsDiv = $('#globalsearch-results');
+ var resultsPerType = resultsDiv.data('results-per-type');
+ var moreResultsText = resultsDiv.data('more-results');
+ var limit = resultsPerType * 3;
+ var currentSemester = resultsDiv.data('current-semester');
+ var wrapper = $('#globalsearch-searchbar');
+
+ if (searchterm === '') {
+ return;
+ }
+
+ wrapper.toggleClass('has-value', hasValue);
+
+ if (!hasValue || GlobalSearch.lastSearch === searchterm) {
+ return;
+ }
+
+ GlobalSearch.lastSearch = searchterm;
+
+ // Display spinner symbol, user should always see something is happening.
+ wrapper.addClass('is-searching');
+
+ // Call AJAX endpoint and get search results.
+ $.getJSON(STUDIP.URLHelper.getURL('dispatch.php/globalsearch/find/' + limit, {}, true), {
+ search: searchterm,
+ filters: '{"category":"show_all_categories","semester":"' + currentSemester + '"}'
+ }).done(function(json) {
+ resultsDiv.empty();
+
+ // No results found...
+ if (!$.isPlainObject(json) || $.isEmptyObject(json)) {
+ wrapper.removeClass('is-searching');
+ resultsDiv.html(resultsDiv.data('no-result'));
+ return;
+ }
+
+ // Iterate over each result category.
+ $.each(json, function(name, value) {
+ // Create an <article> for category.
+ var category = $(`<article id="globalsearch-${name}">`),
+ header = $('<header>').appendTo(category),
+ counter = 0;
+
+ // Create header name
+ $('<a href="#">')
+ .text(value.name)
+ .wrap('<div class="globalsearch-category">')
+ .parent() // Element is now the wrapper
+ .data('category', name)
+ .appendTo(header);
+
+ // We have more search results than shown, provide link to
+ // full search if available.
+ if (value.more && value.fullsearch !== '') {
+ $('<a>')
+ .attr('href', value.fullsearch)
+ .text(moreResultsText)
+ .wrap('<div class="globalsearch-more-results">')
+ .parent() // Element is now the wrapper
+ .appendTo(header);
+ }
+
+ // Process results and create corresponding entries.
+ $.each(value.content, function(index, result) {
+ // Create single result entry.
+ var single = $('<section>'),
+ data = $('<div class="globalsearch-result-data">'),
+ details = $('<div class="globalsearch-result-details">');
+
+ if (counter >= resultsPerType) {
+ single.addClass('globalsearch-extended-result');
+ }
+
+ // 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);
+
+ // Optional image...
+ if (result.img !== null) {
+ $(`<img src="${result.img}">`)
+ .wrap('<div class="globalsearch-result-img">')
+ .parent() // Element is now the wrapper
+ .appendTo(link);
+ }
+
+ link.append(data);
+
+ // Name/title
+ $('<div class="globalsearch-result-title">')
+ .html(result.name)
+ .appendTo(data);
+
+ // Details: Descriptional text
+ if (result.description !== null) {
+ $('<div class="globalsearch-result-description">')
+ .html(result.description)
+ .appendTo(details);
+ }
+
+ // Details: Additional information
+ if (result.additional !== null) {
+ $('<div class="globalsearch-result-additional">')
+ .html(result.additional)
+ .appendTo(details);
+ }
+
+ data.append(details);
+
+ // Date/Time of entry
+ if (result.date !== null) {
+ $('<div class="globalsearch-result-time">')
+ .html(result.date)
+ .appendTo(link);
+ }
+
+ // "Expand" attribute for further, result-related search
+ // (e.g. search in course of found forum entry)
+ if (result.expand !== null && result.expand !== value.fullsearch && value.more) {
+ $(`<a href="${result.expand}" title="${result.expandtext}">`)
+ .wrap('<div class="globalsearch-result-expand">')
+ .parent() // Element is now the wrapper
+ .appendTo(single);
+ }
+ category.append(single);
+
+ counter += 1;
+ });
+ results = results.add(category);
+ });
+
+ resultsDiv.html(results);
+ wrapper.removeClass('is-searching');
+ }).fail(function(xhr, status, error) {
+ if (error) {
+ window.alert(error);
+ }
+ });
+ },
+
+ /**
+ * Clear search term and remove results for previous search term.
+ */
+ resetSearch: function() {
+ GlobalSearch.lastSearch = null;
+
+ $('#globalsearch-searchbar').removeClass('is-visible has-value');
+ $('#globalsearch-input').val('');
+ $('#globalsearch-results').html('');
+ $('#globalsearch-input').focus();
+ },
+
+ /**
+ * Expand a single category, showing more results, and hide other
+ * categories.
+ * @param category
+ * @returns {boolean}
+ */
+ expandCategory: function(category) {
+ // Hide other categories.
+ $(`#globalsearch-results article:not([id="globalsearch-${category}"])`).hide();
+ // Show all results.
+ $(`#globalsearch-${category} section.globalsearch-extended-result`).removeClass(
+ 'globalsearch-extended-result'
+ );
+ $(`article#globalsearch-${category}`).get(0).scrollIntoView();
+ // Reassign category click to closing extended view.
+ $(`#globalsearch-results article#globalsearch-${category} header div.globalsearch-category a`)
+ .off('click')
+ .on('click', function() {
+ GlobalSearch.showAllCategories(category);
+ return false;
+ });
+ return false;
+ },
+
+ /**
+ * Close expanded view of a single category, showing normal view with
+ * all categories again.
+ * @param currentCategory
+ */
+ showAllCategories: function(currentCategory) {
+ $(`#globalsearch-results article#globalsearch-${currentCategory} header div.globalsearch-category a`)
+ .off('click')
+ .on('click', function() {
+ GlobalSearch.expandCategory(currentCategory);
+ return false;
+ });
+ var resultCount = $('#globalsearch-results').data('results-per-type') - 1;
+ $(`#globalsearch-${currentCategory} section:gt(${resultCount})`).addClass(
+ 'globalsearch-extended-result'
+ );
+ $('#globalsearch-results')
+ .children(`article:not([id="globalsearch-${currentCategory}"])`)
+ .show();
+ return false;
+ }
+};
+
+export default GlobalSearch;
diff --git a/resources/assets/javascripts/lib/header_magic.js b/resources/assets/javascripts/lib/header_magic.js
new file mode 100644
index 0000000..f960d92
--- /dev/null
+++ b/resources/assets/javascripts/lib/header_magic.js
@@ -0,0 +1,49 @@
+import NavigationShrinker from './navigation_shrinker.js';
+import Scroll from './scroll.js';
+
+let fold;
+let was_below_the_fold = false;
+
+const scroll = function(scrolltop) {
+ var is_below_the_fold = scrolltop > fold,
+ menu;
+ if (is_below_the_fold !== was_below_the_fold) {
+ $('body').toggleClass('fixed', is_below_the_fold);
+
+ menu = $('#barTopMenu').remove();
+ if (is_below_the_fold) {
+ menu.append(
+ $('.action-menu-list li', menu)
+ .remove()
+ .addClass('from-action-menu')
+ );
+ menu.appendTo('#barBottomLeft');
+ } else {
+ $('.action-menu-list', menu).append(
+ $('.from-action-menu', menu)
+ .remove()
+ .removeClass('from-action-menu')
+ );
+ menu.prependTo('#flex-header');
+
+ NavigationShrinker();
+
+ $('#barTopMenu-toggle').prop('checked', false);
+ }
+
+ was_below_the_fold = is_below_the_fold;
+ }
+};
+
+const HeaderMagic = {
+ enable() {
+ fold = $('#flex-header').height();
+ Scroll.addHandler('header', scroll);
+ },
+ disable() {
+ Scroll.removeHandler('header');
+ $('body').removeClass('fixed');
+ }
+};
+
+export default HeaderMagic;
diff --git a/resources/assets/javascripts/lib/i18n.js b/resources/assets/javascripts/lib/i18n.js
new file mode 100644
index 0000000..694716f
--- /dev/null
+++ b/resources/assets/javascripts/lib/i18n.js
@@ -0,0 +1,43 @@
+const i18n = {
+ init: function(root) {
+ $('.i18n_group', root).each(function() {
+ var languages = $(this).children('.i18n'),
+ select = $('<select tabindex="-1">')
+ .addClass('i18n')
+ .css(
+ 'background-image',
+ $(languages)
+ .first()
+ .data('icon')
+ );
+ select.change(function() {
+ var opt = $(this).find('option:selected'),
+ index = opt.index();
+ languages.not(':eq(' + index + ')').hide();
+ languages
+ .eq(index)
+ .show()
+ .find(':input')
+ .trigger('focus');
+ $(this).css('background-image', opt.css('background-image'));
+ });
+ languages.each(function(id, lang) {
+ select.append(
+ $('<option>', { text: $(lang).data('lang') }).css('background-image', $(lang).data('icon'))
+ );
+ });
+ $(this).append(select);
+ languages.not(':eq(0)').hide();
+
+ $('div.i18n input[required], div.i18n textarea[required]', this).on('invalid', function() {
+ var element = $(this).closest('.i18n');
+ element
+ .siblings('select')
+ .val($(element).data('lang'))
+ .change();
+ });
+ });
+ }
+};
+
+export default i18n;
diff --git a/resources/assets/javascripts/lib/inline-editing.js b/resources/assets/javascripts/lib/inline-editing.js
new file mode 100644
index 0000000..a86442d
--- /dev/null
+++ b/resources/assets/javascripts/lib/inline-editing.js
@@ -0,0 +1,138 @@
+class InlineEditing
+{
+ static init(element) {
+ if (!element) {
+ return;
+ }
+
+ var text = jQuery(element).text().trim();
+
+ var icon_path = STUDIP.ASSETS_URL + '/images/icons/blue/NAME.svg';
+ var input_type = jQuery(element).data('input-type').toLowerCase();
+ var input_name = jQuery(element).data('input-name');
+ var icon_size = jQuery(element).data('icon-size');
+ if (!icon_size) {
+ icon_size = '20px';
+ }
+
+ //Build the display container:
+ var text_container = jQuery('<span class="text"></span>');
+ jQuery(text_container).text(text);
+ var icon_container = jQuery('<div></div>');
+ var icon_element = jQuery('<img class="edit-button"></img>');
+ jQuery(icon_element).attr('width', icon_size);
+ jQuery(icon_element).attr('height', icon_size);
+ jQuery(icon_element).attr('src', icon_path.replace('NAME', 'edit'));
+ jQuery(icon_container).append(icon_element);
+ var display_container = jQuery(
+ '<div class="display-container"></div>'
+ );
+ jQuery(display_container).append(text_container);
+ jQuery(display_container).append(icon_container);
+
+ var input_field = undefined;
+ var edit_icons_container = undefined;
+ var accept_icon = jQuery('<img class="save-button"></img>');
+ jQuery(accept_icon).attr('width', icon_size);
+ jQuery(accept_icon).attr('height', icon_size);
+ jQuery(accept_icon).attr('src', icon_path.replace('NAME', 'accept'));
+ var abort_icon = jQuery('<img class="abort-button"></img>');
+ jQuery(abort_icon).attr('width', icon_size);
+ jQuery(abort_icon).attr('height', icon_size);
+ jQuery(abort_icon).attr('src', icon_path.replace('NAME', 'decline'));
+
+ if (input_type == 'textarea') {
+ input_field = jQuery('<textarea class="input-field"></textarea>');
+ jQuery(input_field).attr('name', input_name);
+ jQuery(input_field).text(text);
+ edit_icons_container = jQuery('<div></div>');
+ } else {
+ input_field = jQuery('<input class="input-field">');
+ jQuery(input_field).attr('type', input_type);
+ jQuery(input_field).attr('name', input_name);
+ jQuery(input_field).val(text);
+ edit_icons_container = jQuery('<span></span>');
+ }
+ jQuery(edit_icons_container).append(accept_icon);
+ jQuery(edit_icons_container).append(abort_icon);
+
+ var edit_container = jQuery('<div class="edit-container invisible"></div>');
+ jQuery(edit_container).append(input_field);
+ jQuery(edit_container).append(edit_icons_container);
+
+ jQuery(element).empty();
+ jQuery(element).append(display_container);
+ jQuery(element).append(edit_container);
+ };
+
+
+ static activate(element) {
+ var container = jQuery(element).parents('[data-inline-editing]');
+ if (!container) {
+ return;
+ }
+
+ jQuery(container).children('.display-container').addClass('invisible');
+ jQuery(container).children('.edit-container').removeClass('invisible');
+ };
+
+
+ static save(element) {
+ var container = jQuery(element).parents('[data-inline-editing]');
+ if (!container) {
+ return;
+ }
+ var ajax_url = jQuery(container).data('inline-editing');
+
+ var text_field = jQuery(container).find('.text')[0];
+ if (!text_field) {
+ return;
+ }
+ var input_field = jQuery(container).find('.input-field')[0];
+ if (!input_field) {
+ return;
+ }
+ var input_name = jQuery(container).data('input-name');
+ var input_value = jQuery(input_field).val();
+ var data = {
+ quiet: 1
+ };
+ data[input_name] = input_value;
+
+ jQuery.ajax(
+ {
+ url: ajax_url,
+ method: 'POST',
+ data: data
+ }
+ ).done(
+ function() {
+ jQuery(text_field).text(input_value);
+ jQuery(container).find('.edit-container').addClass('invisible');
+ jQuery(container).find('.display-container').removeClass('invisible');
+ }
+ ).fail(
+ function(data) {
+ jQuery(input_field).css('border-color', 'red');
+ if (data) {
+ jQuery(container).find('.error-message').val(data);
+ }
+ }
+ );
+ };
+
+
+ static abort(element) {
+ var container = jQuery(element).parents('[data-inline-editing]');
+ if (!container) {
+ return;
+ }
+
+ jQuery(container).children('.edit-container').addClass('invisible');
+ jQuery(container).children('.display-container').removeClass('invisible');
+
+ };
+}
+
+
+export default InlineEditing;
diff --git a/resources/assets/javascripts/lib/instschedule.js b/resources/assets/javascripts/lib/instschedule.js
new file mode 100644
index 0000000..af438c2
--- /dev/null
+++ b/resources/assets/javascripts/lib/instschedule.js
@@ -0,0 +1,19 @@
+import { $gettext } from './gettext.js';
+import Dialog from './dialog.js';
+
+const Instschedule = {
+ /**
+ * show the details of a grouped-entry in the isntitute-calendar, containing several seminars
+ *
+ * @param string the id of the grouped-entry to be displayed
+ */
+ showInstituteDetails: function(id) {
+ jQuery.get(STUDIP.URLHelper.getURL('dispatch.php/calendar/instschedule/groupedentry/' + id), function(data) {
+ Dialog.show(data, {
+ title: $gettext('Detaillierte Veranstaltungsliste')
+ });
+ });
+ }
+};
+
+export default Instschedule;
diff --git a/resources/assets/javascripts/lib/jsonapi.js b/resources/assets/javascripts/lib/jsonapi.js
new file mode 100644
index 0000000..bcd954f
--- /dev/null
+++ b/resources/assets/javascripts/lib/jsonapi.js
@@ -0,0 +1,27 @@
+import AbstractAPI from './abstract-api.js';
+
+// Actual JSONAPI object
+class JSONAPI extends AbstractAPI
+{
+ constructor(version = 1) {
+ super(`jsonapi.php/v${version}`);
+ }
+
+ encodeData (data) {
+ data = super.encodeData(data);
+
+ if (Object.keys(data).length === 0) {
+ return null;
+ }
+
+ return JSON.stringify(data);
+ }
+
+ request (url, options = {}) {
+ options.contentType = 'application/vnd.api+json';
+ return super.request(url, options);
+ }
+}
+
+export default JSONAPI;
+export const jsonapi = new JSONAPI();
diff --git a/resources/assets/javascripts/lib/jsupdater.js b/resources/assets/javascripts/lib/jsupdater.js
new file mode 100644
index 0000000..cb75540
--- /dev/null
+++ b/resources/assets/javascripts/lib/jsupdater.js
@@ -0,0 +1,233 @@
+/* ------------------------------------------------------------------------
+ * JSUpdater - periodically polls for new data from server
+ * ------------------------------------------------------------------------
+ * Exposes the following method on the global STUDIP.JSUpdater object:
+ *
+ * - start()
+ * - stop()
+ * - register(index, callback, data)
+ * - unregister(index)
+ *
+ * Refer to the according function definitions for further info.
+ * ------------------------------------------------------------------------ */
+import { $gettext } from './gettext.js';
+
+let active = false;
+let lastAjaxDuration = 200; //ms of the duration of an ajax-call
+let currentDelayFactor = 0;
+let lastJsonResult = null;
+let dateOfLastCall = +new Date(); // Get milliseconds of date object
+let serverTimestamp = STUDIP.server_timestamp;
+let ajaxRequest = null;
+let timeout = null;
+let registeredHandlers = {};
+
+// Reset json memory, used to delay polling if consecutive requests always
+// return the same result
+function resetJSONMemory(json) {
+ if (json.hasOwnProperty('server_timestamp')) {
+ delete json.server_timestamp;
+ }
+ json = JSON.stringify(json);
+ if (json !== lastJsonResult) {
+ currentDelayFactor = 0;
+ }
+ lastJsonResult = json;
+}
+
+// Process returned json object by calling registered handlers
+function process(json) {
+ for (const [index, value] of Object.entries(json)) {
+ // Set timestamp
+ if (index === 'server_timestamp') {
+ serverTimestamp = value;
+ } else {
+ // Call registered handler callback by index
+ if (index in registeredHandlers) {
+ registeredHandlers[index].callback(value);
+ }
+ }
+ }
+
+ // Reset json memory
+ resetJSONMemory(json);
+}
+
+// Registers next poll
+function registerNextPoll() {
+ // Calculate smallest registered polling interval (but no more than 60 seconds)
+ let interval = 60000;
+ for (const [index, handler] of Object.entries(registeredHandlers)) {
+ if (handler.interval < interval) {
+ interval = handler.interval;
+ }
+ }
+
+ // Define delay by last poll request (respond to load on server) and
+ // current delay factor (respond to user activity)
+ var delay = (interval || lastAjaxDuration * 15) * Math.pow(1.33, currentDelayFactor);
+
+ // Clear any previously scheduled polling
+ window.clearTimeout(timeout);
+ timeout = window.setTimeout(poll, delay);
+
+ // Increase current delay factor
+ currentDelayFactor += 1;
+}
+
+// Collect data for polling
+function collectData() {
+ var data = {};
+ // Pull data from all registered handlers, either by collecting the data
+ // itself or by calling the appropriate function
+ for (const [index, handler] of Object.entries(registeredHandlers)) {
+ if (handler.data) {
+ const thisData = $.isFunction(handler.data) ? handler.data() : handler.data;
+ if (thisData !== null && !$.isEmptyObject(thisData)) {
+ data[index] = thisData;
+ }
+ }
+ }
+
+ return data;
+}
+
+// User activity handler
+function userActivityHandler() {
+ currentDelayFactor = 0;
+ if (+new Date() - dateOfLastCall > 5000) {
+ poll(true);
+ }
+}
+
+// Window activity handler
+function windowActivityHandler(event) {
+ if (event.type === 'blur') {
+ // Increase delay factor and reschedule next polling
+ currentDelayFactor += 10;
+ registerNextPoll();
+ } else if (event.type === 'focus') {
+ // Reset delay factor and start polling if neccessary
+ userActivityHandler();
+ }
+}
+
+// Actually poll data
+function poll(forced) {
+ // Skip polling if an ajax request is already running, unless forced
+ if (!forced && ajaxRequest) {
+ registerNextPoll();
+ return false;
+ }
+
+ // If forced, abort potential current ajax request
+ if (ajaxRequest) {
+ ajaxRequest.abort();
+ ajaxRequest = null;
+ }
+ // Abort potentially scheduled polling
+ window.clearTimeout(timeout);
+
+ // Store current timestamp
+ dateOfLastCall = +new Date();
+
+ // Prepare variables
+ var url = STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/jsupdater/get',
+ page = window.location.href.replace(STUDIP.ABSOLUTE_URI_STUDIP, '');
+
+ // Actual poll request, uses promises
+ ajaxRequest = $.ajax(url, {
+ data: {
+ page: page,
+ page_info: collectData(),
+ server_timestamp: serverTimestamp
+ },
+ type: 'POST',
+ dataType: 'json',
+ timeout: 5000
+ })
+ .done(function(json) {
+ process(json);
+ })
+ .fail(function(jqXHR, textStatus, errorThrown) {
+ resetJSONMemory({
+ text: textStatus,
+ error: errorThrown
+ });
+ })
+ .always(function() {
+ ajaxRequest = null;
+ lastAjaxDuration = +new Date() - dateOfLastCall;
+
+ // If logged out
+ if (arguments.length === 3 && arguments[1] === 'error' && arguments[0].status === 403) {
+ // Stop updater
+ JSUpdater.stop();
+
+ // Present appropriate message in dialog
+ var message = $gettext('Bitte laden Sie die Seite neu, um fortzufahren'),
+ buttons = {};
+ buttons[$gettext('Neu laden')] = function() {
+ location.reload();
+ };
+ buttons[$gettext('Schließen')] = function() {
+ $(this).dialog('close');
+ };
+
+ $('<div>')
+ .html(message)
+ .css({
+ textAlign: 'center',
+ padding: '2em 0'
+ })
+ .dialog({
+ width: '50%',
+ modal: true,
+ buttons: buttons,
+ title: $gettext('Sie sind nicht mehr im System angemeldet.')
+ });
+ } else {
+ registerNextPoll();
+ }
+ });
+}
+
+// Register global object
+const JSUpdater = {
+ // Starts the updater, also registers the activity handlers
+ start() {
+ if (!active) {
+ $(document).on('mousemove', userActivityHandler);
+ $(window).on('blur focus', windowActivityHandler);
+ registerNextPoll();
+ }
+ active = true;
+ },
+
+ // Stops the updater, also unregisters the activity handlers
+ stop() {
+ if (active) {
+ $(document).off('mousemove', userActivityHandler);
+ $(window).off('blur focus', windowActivityHandler);
+ if (ajaxRequest) {
+ ajaxRequest.abort();
+ ajaxRequest = null;
+ }
+ window.clearTimeout(timeout);
+ }
+ active = false;
+ },
+
+ // Registers a new handler by an index, a callback and an optional data
+ // object or function
+ register(index, callback, data = null, interval = 0) {
+ registeredHandlers[index] = { callback, data, interval };
+ },
+
+ // Unregisters/removes a previously registered handler
+ unregister(index) {
+ delete registeredHandlers[index];
+ }
+}
+
+export default JSUpdater;
diff --git a/resources/assets/javascripts/lib/lightbox.js b/resources/assets/javascripts/lib/lightbox.js
new file mode 100644
index 0000000..134cfca
--- /dev/null
+++ b/resources/assets/javascripts/lib/lightbox.js
@@ -0,0 +1,148 @@
+import { $gettext } from './gettext.js';
+import Dialog from './dialog.js';
+
+function sprintf(string) {
+ var args = arguments,
+ index = 1;
+ return string.replace(/%(s|u)/g, function(match, modifier) {
+ if (index > args.length) {
+ throw 'Invalid sprintf usage - not enough arguments';
+ }
+ var value = args[index];
+
+ if (modifier === 'u') {
+ value = parseInt(value, 10);
+ }
+
+ index += 1;
+
+ return String(value);
+ });
+}
+
+const Lightbox = {
+ max_width: false,
+ max_height: false,
+ extra_height: 55, // TODO: While this seems to work, hardcoded values suck
+ images: [],
+ current: false,
+ show: function(index) {
+ this.current = index || 0;
+
+ var image = new Image();
+ image.onload = $.proxy(this, 'onload', image);
+ image.src = this.getImage().src;
+ },
+ onload: function(image) {
+ var wrapper = $('<div class="wrapper">');
+ $('<a href="#" class="previous">').appendTo(wrapper);
+ $('<a href="#" class="next">').appendTo(wrapper);
+
+ wrapper.addClass(this.getClasses()).css({
+ backgroundImage: sprintf('url(%s)', this.getImage().src)
+ });
+
+ $(document).one('dialog-open.lightbox', $.proxy(this, 'registerEvents'));
+
+ Dialog.show(wrapper, {
+ buttons: false,
+ dialogClass: 'studip-lightbox',
+ id: 'lightbox',
+ resize: false,
+ size: this.getSize(image),
+ title: this.getTitle(),
+ wikilink: false
+ });
+ },
+ getImage: function() {
+ return this.images[this.current];
+ },
+ getTitle: function() {
+ var img = this.images[this.current],
+ title = [];
+ if (img.title) {
+ title.push(img.title);
+ }
+
+ if (this.images.length > 1) {
+ title.unshift(sprintf($gettext('Bild %u von %u'), this.current + 1, this.images.length));
+ }
+ return title.join(': ');
+ },
+ getClasses: function() {
+ var classes = [];
+ if (this.current === 0) {
+ classes.push('first');
+ }
+ if (this.current === this.images.length - 1) {
+ classes.push('last');
+ }
+ return classes.join(' ');
+ },
+ getSize: function(image) {
+ var width = image.width,
+ height = image.height;
+
+ if (width > this.max_width) {
+ height *= this.max_width / width;
+ width = this.max_width;
+ }
+ if (height > this.max_height) {
+ width *= this.max_height / height;
+ height = this.max_height;
+ }
+
+ return Math.floor(width) + 'x' + Math.floor(height + this.extra_height);
+ },
+ setImages: function(images) {
+ if (typeof images === 'string') {
+ images = $(images);
+ }
+ if (images instanceof jQuery) {
+ images = images.map(function() {
+ return {
+ src: $(this).attr('href'),
+ title: $(this).data().title || $(this).attr('title')
+ };
+ });
+ }
+ this.images = images;
+ },
+ init: function() {
+ // Values should match the ones in studip-dialog.js (this should be more generic)
+ this.max_width = $(window).width() * 0.95;
+ this.max_height = $(window).height() * 0.9 - Lightbox.extra_height;
+ },
+ registerEvents: function() {
+ $('.studip-lightbox')
+ .on('click', 'a.previous', function() {
+ Lightbox.show(Lightbox.current - 1);
+ return false;
+ })
+ .on('click', 'a.next', function() {
+ Lightbox.show(Lightbox.current + 1);
+ return false;
+ });
+
+ $(document)
+ .on('keyup.lightbox', function(event) {
+ if (event.keyCode === 37) {
+ $('.studip-lightbox .previous:visible').click();
+ } else if (event.keyCode === 39) {
+ $('.studip-lightbox .next:visible').click();
+ } else if (event.keyCode === 27) {
+ Dialog.close({id: 'lightbox'});
+ } else {
+ return;
+ }
+
+ return false;
+ })
+ .one('dialog-close', $.proxy(this, 'unregisterEvents'));
+ },
+ unregisterEvents: function() {
+ $(document).off('.lightbox');
+ }
+};
+
+export default Lightbox;
diff --git a/resources/assets/javascripts/lib/markup.js b/resources/assets/javascripts/lib/markup.js
new file mode 100644
index 0000000..76cff88
--- /dev/null
+++ b/resources/assets/javascripts/lib/markup.js
@@ -0,0 +1,41 @@
+/* ------------------------------------------------------------------------
+ * Javascript-spezifisches Markup
+ * ------------------------------------------------------------------------ */
+
+const Markup = {
+ element: function (selector) {
+ var elements;
+ if (typeof selector === 'string' && document.getElementById(selector)) {
+ elements = $('#' + selector);
+ } else {
+ elements = $(selector);
+ }
+ elements.each((index, element) => {
+ $.each(Markup.callbacks, (index, func) => {
+ if (index !== 'element' || typeof func === 'function') {
+ func(element);
+ }
+ });
+ });
+ },
+ callbacks: {
+ math_jax: function (element) {
+ $('span.math-tex:not(:has(.MathJax)),.formatted-content:contains("[tex]")', element).each((index, block) => {
+ STUDIP.loadChunk('mathjax').then((MathJax) => {
+ if (typeof MathJax.typeset === "function") {
+ MathJax.typeset([block]);
+ }
+ });
+ });
+ },
+ codehighlight: function (element) {
+ $('pre.usercode:not(.hljs)', element).each(function (index, block) {
+ STUDIP.loadChunk('code-highlight').then((hljs) => {
+ hljs.highlightBlock(block);
+ });
+ });
+ }
+ }
+};
+
+export default Markup;
diff --git a/resources/assets/javascripts/lib/members.js b/resources/assets/javascripts/lib/members.js
new file mode 100644
index 0000000..3fbdfb9
--- /dev/null
+++ b/resources/assets/javascripts/lib/members.js
@@ -0,0 +1,24 @@
+const Members = {
+ addPersonToSelection: function(userId, name) {
+ var target = $('#persons-to-add'),
+ newEl = $('<li>').html(
+ $('<span>')
+ .html(name)
+ .text()
+ ),
+ input = $('<input type="hidden" name="users[]">').val(userId),
+ remove = $('<img>').attr('src', STUDIP.ASSETS_URL + 'images/icons/blue/trash.svg');
+
+ remove.on('click', function() {
+ $(this)
+ .parent()
+ .remove();
+ });
+
+ newEl.append(input, remove).appendTo(target);
+
+ return false;
+ }
+};
+
+export default Members;
diff --git a/resources/assets/javascripts/lib/messages.js b/resources/assets/javascripts/lib/messages.js
new file mode 100644
index 0000000..3bd1656
--- /dev/null
+++ b/resources/assets/javascripts/lib/messages.js
@@ -0,0 +1,304 @@
+import { $gettext } from './gettext.js';
+import Markup from './markup.js';
+
+const Messages = {
+ init() {
+ STUDIP.JSUpdater.register('messages', Messages.newMessages, Messages.getParamsForPolling, 60000);
+ },
+
+ /*********** AJAX-reload function for overview ***********/
+
+ getParamsForPolling() {
+ if (jQuery('#messages').length && jQuery('#since').val()) {
+ return {
+ since: jQuery('#since').val(),
+ received: jQuery('#received').val(),
+ tag: jQuery('#tag').val()
+ };
+ }
+ },
+ newMessages: function(response) {
+ jQuery.each(response.messages, function(message_id, message) {
+ if (jQuery('#message_' + message_id).length === 0) {
+ jQuery('#messages > tbody').prepend(message);
+ }
+ });
+ jQuery('#since').val(Math.floor(new Date().getTime() / 1000));
+ },
+
+ /*********** helper for the overview site ***********/
+
+ whenMessageIsShown: function(lightbox) {
+ jQuery(lightbox)
+ .closest('tr')
+ .removeClass('unread');
+ },
+
+ /*********** helper for the composer-site ***********/
+
+ add_adressee: function(user_id, name) {
+ var new_adressee = jQuery('#template_adressee').clone();
+ new_adressee.find('input').val(user_id);
+ new_adressee
+ .find('.visual')
+ .text(name)
+ .find('b')
+ .replaceWith(function() {
+ return jQuery(this).contents();
+ });
+ new_adressee.find('img.avatar-medium').remove();
+ new_adressee.find('br').replaceWith(' ');
+ new_adressee
+ .removeAttr('id')
+ .appendTo('#adressees')
+ .fadeIn();
+ return false;
+ },
+
+ add_adressees: function(form) {
+ jQuery(form)
+ .find('#add_adressees_selectbox option:selected')
+ .each(function() {
+ var user_id = jQuery(this).val(),
+ name = jQuery(this).text();
+
+ var new_adressee = jQuery('#template_adressee').clone();
+ new_adressee.find('input').val(user_id);
+ new_adressee.find('.visual').text(name);
+ new_adressee
+ .removeAttr('id')
+ .appendTo('#adressees')
+ .fadeIn();
+ });
+ jQuery(form)
+ .closest('.ui-dialog-content')
+ .dialog('close');
+ return false;
+ },
+
+ remove_adressee: function() {
+ jQuery(this)
+ .closest('li')
+ .fadeOut(300, function() {
+ jQuery(this).remove();
+ });
+ },
+
+ remove_attachment: function() {
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/messages/delete_attachment',
+ data: {
+ document_id: jQuery(this)
+ .closest('li')
+ .data('document_id'),
+ message_id: jQuery(this)
+ .closest('form')
+ .find('input[name=message_id]')
+ .val()
+ },
+ type: 'POST'
+ });
+ jQuery(this)
+ .closest('li')
+ .fadeOut(300, function() {
+ jQuery(this).remove();
+ });
+ },
+
+ upload_from_input: function(input) {
+ Messages.upload_files(input.files);
+ jQuery(input).val('');
+ },
+ fileIDQueue: 1,
+ upload_files: function(files) {
+ for (var i = 0; i < files.length; i++) {
+ var fd = new FormData();
+ fd.append('file', files[i], files[i].name);
+ var statusbar = jQuery('#statusbar_container .statusbar')
+ .first()
+ .clone()
+ .show();
+ statusbar.appendTo('#statusbar_container');
+ fd.append('message_id', jQuery('#message_id').val());
+ Messages.upload_file(fd, statusbar);
+ }
+ },
+ upload_file: function(formdata, statusbar) {
+ $(".ui-dialog-buttonset button:first-child, footer[data-dialog-button] button:first-child").attr("disabled", "disabled");
+ $.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
+ );
+ }
+ return xhrobj;
+ },
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/messages/upload_attachment',
+ type: 'POST',
+ contentType: false,
+ processData: false,
+ cache: false,
+ data: formdata,
+ dataType: 'json'
+ })
+ .done(function(data) {
+ $(".ui-dialog-buttonset button:first-child, footer[data-dialog-button] button:first-child").removeAttr("disabled");
+ statusbar.find('.progress').css({ 'min-width': '100%', 'max-width': '100%' });
+ var file = jQuery('#attachments .files > .file')
+ .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.data('document_id', data.document_id);
+ file.appendTo('#attachments .files');
+ file.fadeIn(300);
+ statusbar.find('.progresstext').text(jQuery('#upload_received_data').text());
+ statusbar.delay(1000).fadeOut(300, function() {
+ 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();
+ });
+ });
+ });
+ },
+ checkAdressee: function() {
+ // Check if recipients added (one element is always there -> template)
+ var quicksearch = jQuery('form[name="write_message"] input[name="user_id_parameter"]');
+ if (jQuery('li.adressee').children('input[name^="message_to"]').length <= 1) {
+ quicksearch.attr('required', 'required').attr('value', '');
+ quicksearch[0].setCustomValidity(
+ $gettext('Sie haben nicht angegeben, wer die Nachricht empfangen soll!')
+ );
+ return true;
+ } else {
+ quicksearch.removeAttr('required');
+ quicksearch[0].setCustomValidity('');
+ return true;
+ }
+ },
+ setTags: function(message_id, tags) {
+ var container = jQuery('#message_' + message_id)
+ .find('.tag-container')
+ .empty(),
+ template = _.template('<a href="<%- url %>" class="message-tag"><%- tag %></a>');
+
+ jQuery.each(tags, function(index, tag) {
+ var html = template({
+ url: STUDIP.URLHelper.getURL('dispatch.php/messages/overview', { tag: tag }),
+ tag: tag.charAt(0).toUpperCase() + tag.slice(1) // ucfirst
+ });
+ jQuery(container)
+ .append(html)
+ .append(' ');
+ });
+ },
+ setAllTags: function(tags) {
+ var container = $('#messages-tags ul');
+ var template = _.template('<li><a href="<%- url %>" class="tag"><%- tag %></a></li>');
+
+ container.children('li:not(:has(.all-tags))').remove();
+
+ jQuery.each(tags, (index, tag) => {
+ let html = template({
+ url: STUDIP.URLHelper.getURL('dispatch.php/messages/overview', { tag: tag }),
+ tag: tag.charAt(0).toUpperCase() + tag.slice(1) // ucfirst
+ });
+ $(container).append(html);
+ });
+ $('#messages-tags')
+ .toggle(tags.length !== 0)
+ .find('li:has(.tag):not(.ui-droppable)')
+ .each(Messages.createDroppable);
+ },
+ createDroppable: function(element) {
+ jQuery(arguments.length === 1 ? element : this).droppable({
+ hoverClass: 'dropping',
+ drop: function(event, ui) {
+ var message_id = ui.draggable.attr('id').substr(ui.draggable.attr('id').lastIndexOf('_') + 1),
+ tag = jQuery(this)
+ .text()
+ .trim();
+ jQuery
+ .post(STUDIP.URLHelper.getURL('dispatch.php/messages/tag/' + message_id), {
+ add_tag: tag
+ })
+ .then(function(response, status, xhr) {
+ var tags = jQuery.parseJSON(xhr.getResponseHeader('X-Tags'));
+ Messages.setTags(message_id, tags);
+ });
+ }
+ });
+ },
+ toggleSetting: function(name) {
+ jQuery('#' + name).toggle('fade');
+ if (jQuery('#' + name).is(':visible')) {
+ jQuery('#' + name)[0].scrollIntoView(false);
+ }
+ },
+ previewComposedMessage: function() {
+ var old_written_text = '',
+ written_text = jQuery('textarea[name=message_body]').val();
+ var updatePreview = function() {
+ written_text = jQuery('textarea[name=message_body]').val();
+ if (old_written_text !== written_text) {
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/messages/preview',
+ data: {
+ text: STUDIP.editor_enabled ? STUDIP.wysiwyg.markAsHtml(written_text) : written_text
+ },
+ type: 'POST',
+ success: function(html) {
+ jQuery('#preview .message_body').html(html);
+ Markup.element('#preview .message_body');
+ }
+ });
+ old_written_text = written_text;
+ }
+ if (jQuery('#preview .message_body').is(':visible')) {
+ window.setTimeout(updatePreview, 1000);
+ }
+ };
+ updatePreview();
+ }
+};
+
+export default Messages;
diff --git a/resources/assets/javascripts/lib/multi_person_search.js b/resources/assets/javascripts/lib/multi_person_search.js
new file mode 100644
index 0000000..c3fd617
--- /dev/null
+++ b/resources/assets/javascripts/lib/multi_person_search.js
@@ -0,0 +1,160 @@
+import { $gettext } from './gettext.js';
+
+const MultiPersonSearch = {
+ init: function() {
+ $('.multi_person_search_link').each(function() {
+ // init js form
+ $(this).attr('href', $(this).data('js-form'));
+ // init form if it is loaded via ajax
+ $(this).on('dialog-open', function(event, parameters) {
+ MultiPersonSearch.dialog(
+ $(parameters.dialog)
+ .find('.mpscontainer')
+ .data('dialogname')
+ );
+ });
+ });
+ },
+
+ dialog: function(name) {
+ var count_template = _.template($gettext('Sie haben <%= count %> Personen ausgewählt'));
+
+ this.name = name;
+
+ $('#' + name + '_selectbox').multiSelect({
+ selectableHeader: '<div>' + $gettext('Suchergebnisse') + '</div>',
+ selectionHeader:
+ '<div>' + count_template({ count: "<span id='" + this.name + "_count'>0</span>" }) + '.</div>',
+ selectableFooter:
+ '<a href="javascript:STUDIP.MultiPersonSearch.selectAll();">' +
+ $gettext('Alle hinzufügen') +
+ '</a>',
+ selectionFooter:
+ '<a href="javascript:STUDIP.MultiPersonSearch.unselectAll();">' +
+ $gettext('Alle entfernen') +
+ '</a>'
+ });
+
+ $('#' + this.name).on('keyup keypress', function(e) {
+ var code = e.keyCode || e.which;
+ if (code == 13) {
+ e.preventDefault();
+ MultiPersonSearch.search();
+ return false;
+ }
+ });
+
+ $('#' + this.name + '_selectbox').change(function() {
+ MultiPersonSearch.count();
+ });
+
+ $('#' + this.name + ' .quickfilter').click(function() {
+ MultiPersonSearch.loadQuickfilter($(this).data('quickfilter'));
+ return false;
+ });
+ },
+
+ loadQuickfilter: function(title) {
+ MultiPersonSearch.removeAllNotSelected();
+
+ var count = 0;
+ $('#' + this.name + '_quickfilter_' + title + ' option').each(function() {
+ count += MultiPersonSearch.append(
+ $(this).val(),
+ $(this).text(),
+ MultiPersonSearch.isAlreadyMember($(this).val())
+ );
+ });
+
+ if (count == 0) {
+ MultiPersonSearch.append('--', $gettext(' Dieser Filter enthält keine (neuen) Personen.'), true);
+ }
+
+ MultiPersonSearch.refresh();
+ },
+
+ isAlreadyMember: function(user_id) {
+ if ($('#' + this.name + '_selectbox_default option[value="' + user_id + '"]').length > 0) {
+ return true;
+ } else {
+ return false;
+ }
+ },
+
+ search: function() {
+ var searchterm = $('#' + this.name + '_searchinput').val(),
+ name = this.name,
+ not_found_template = _.template(
+ $gettext('Es wurden keine neuen Ergebnisse für "<%= needle %>" gefunden.')
+ );
+ $.getJSON(
+ STUDIP.URLHelper.getURL('dispatch.php/multipersonsearch/ajax_search/' + this.name, { s: searchterm }),
+ function(data) {
+ MultiPersonSearch.removeAllNotSelected();
+ var searchcount = 0;
+ $.each(data, function(i, item) {
+ searchcount += MultiPersonSearch.append(
+ item.user_id,
+ item.avatar + ' -- ' + item.text,
+ item.member
+ );
+ });
+ MultiPersonSearch.refresh();
+
+ if (searchcount == 0) {
+ MultiPersonSearch.append('--', not_found_template({ needle: searchterm }), true);
+ MultiPersonSearch.refresh();
+ }
+ }
+ );
+ return false;
+ },
+
+ selectAll: function() {
+ $('#' + this.name + '_selectbox').multiSelect('select_all');
+ this.count();
+ },
+
+ unselectAll: function() {
+ $('#' + this.name + '_selectbox').multiSelect('deselect_all');
+ this.count();
+ },
+
+ removeAll: function() {
+ $('#' + this.name + '_selectbox option').remove();
+ this.refresh();
+ },
+
+ removeAllNotSelected: function() {
+ $('#' + this.name + '_selectbox option:not(:selected)').remove();
+ this.refresh();
+ },
+
+ resetSearch: function() {
+ $('#' + this.name + '_searchinput').val('');
+ MultiPersonSearch.removeAllNotSelected();
+ },
+
+ append: function(value, text, selected) {
+ if ($('#' + this.name + '_selectbox option[value=' + value + ']').length == 0) {
+ $('#' + this.name + '_selectbox').multiSelect('addOption', {
+ value: value,
+ text: text,
+ disabled: selected
+ });
+ return 1;
+ }
+ return 0;
+ },
+
+ refresh: function() {
+ $('#' + this.name + '_selectbox').multiSelect('refresh');
+ MultiPersonSearch.count();
+ },
+
+ count: function() {
+ $('#' + this.name + '_count').text($('#' + this.name + '_selectbox option:enabled:selected').length);
+ }
+};
+
+export default MultiPersonSearch;
diff --git a/resources/assets/javascripts/lib/multi_select.js b/resources/assets/javascripts/lib/multi_select.js
new file mode 100644
index 0000000..3a447fb
--- /dev/null
+++ b/resources/assets/javascripts/lib/multi_select.js
@@ -0,0 +1,48 @@
+/*jslint esversion:6*/
+import { $gettext } from './gettext.js';
+
+/**
+ * Turns a select-box into an easy to use multiple select-box
+ */
+
+const MultiSelect = {
+ create: function (id, itemName, options = {}) {
+ const count = $(id).find('option:selected').length;
+ const count_template = _.template(_('<%= count %> ausgewählt'));
+ const update_counter = function () {
+ const count = $(id).find('option:selected').length;
+ $(id).next().find('.counter').text(count_template({count: count}));
+ };
+
+ if (!$(id).attr('multiple')) {
+ $(id).attr('multiple', 'multiple').css('height', '6em');
+ }
+ $(id).multiSelect({
+ selectableHeader:
+ `<div class="header">
+ <a href="#" class="button select-all">${$gettext('Alle hinzufügen')}</a>
+ </div>`,
+ selectionHeader:
+ `<div class="header">
+ <div class="counter">${count_template({count: count})}.</div>
+ <a href="#" class="button deselect-all">${$gettext('Alle entfernen')}</a>
+ </div>`,
+ keepOrder: true,
+ cssClass: ['studip-multi-select', options.cssClass || ''].join(' ').trim(),
+ afterInit: function () {
+ $(id).next().find('.ms-elem-selectable,.ms-elem-selection').find('br').remove();
+ },
+ afterSelect: update_counter,
+ afterDeselect: update_counter
+ });
+
+ $(id).next().find('.select-all').click(function () {
+ $(id).multiSelect('select_all');
+ });
+ $(id).next().find('.deselect-all').click(function () {
+ $(id).multiSelect('deselect_all');
+ });
+ }
+};
+
+export default MultiSelect;
diff --git a/resources/assets/javascripts/lib/navigation_shrinker.js b/resources/assets/javascripts/lib/navigation_shrinker.js
new file mode 100644
index 0000000..dde8618
--- /dev/null
+++ b/resources/assets/javascripts/lib/navigation_shrinker.js
@@ -0,0 +1,51 @@
+import Cookie from './cookie.js';
+
+// Enable shrinking of navigation
+var shrinker = function() {
+ var main = $('#barTopMenu'),
+ sink = $('li.overflow', main),
+ x = 0,
+ index = false,
+ total = 0;
+ if (main.length === 0 || sink.length === 0) {
+ return;
+ }
+
+ // Reset sink (hide and lose all content)
+ main.removeClass('overflown');
+ $('> label > a', sink).removeAttr('data-badge');
+ $('li', sink)
+ .remove()
+ .insertBefore(sink);
+
+ if ($('html').is('.responsive-display')) {
+ return;
+ }
+
+ $('li:not(.overflow)', main).each(function(idx) {
+ var this_x = $(this).position().left;
+ if (this_x > x) {
+ x = this_x;
+ } else {
+ index = idx;
+ return false;
+ }
+ });
+
+ if (index !== false) {
+ $('li:not(.overflow)', main)
+ .slice(index - 2)
+ .detach()
+ .prependTo($('ul', sink))
+ .each(function() {
+ total += parseInt($('a', this).data().badge, 10) || 0;
+ });
+
+ main.addClass('overflown');
+ $('> label > a', sink).attr('data-badge', total);
+ }
+
+ Cookie.set('navigation-length', main.children(':not(.overflow)').length, 30);
+};
+
+export default shrinker;
diff --git a/resources/assets/javascripts/lib/news.js b/resources/assets/javascripts/lib/news.js
new file mode 100644
index 0000000..5319ed7
--- /dev/null
+++ b/resources/assets/javascripts/lib/news.js
@@ -0,0 +1,147 @@
+import { $gettext } from '../lib/gettext.js';
+
+/*jslint browser: true, unparam: true */
+/*global jQuery, STUDIP */
+const News = {
+ /**
+ * (Re-)initialise news-page, f.e. to stay in dialog
+ */
+ init (id) {
+ $('.add_toolbar').addToolbar();
+ STUDIP.i18n.init(`#${id}`);
+
+ // prevent forms within dialog from reloading whole page, and reload dialog instead
+ $(`#${id} form`).on('click', function (event) {
+ $(this).data('clicked', $(event.target));
+ }).on('submit', function (event) {
+ event.preventDefault();
+
+ var textarea, button, form_route, form_data;
+ if (STUDIP.editor_enabled) {
+ textarea = $('textarea.news_body');
+ // wysiwyg is active, ensure HTML markers are set
+ textarea.each(function () {
+ $(this).val(STUDIP.wysiwyg.markAsHtml($(this).val()));
+ });
+ }
+
+ button = $(this).data('clicked').attr('name');
+ form_route = $(this).attr('action');
+ form_data = $(this).serialize() + '&' + button + '=1';
+
+ $(this).find(`input[name=${button}]`).showAjaxNotification('left');
+ News.update_dialog(id, form_route, form_data);
+ });
+
+ $(document).on('change', `#${id} form .news_date`, function () {
+ // This is neccessary since datepickers are initialiszed on focus
+ // which might not have occured yet
+ STUDIP.UI.Datepicker.init();
+
+ var start = $('#news_startdate').blur().datepicker('getDate'),
+ duration,
+ end,
+ result;
+ if ($(this).is('#news_duration')) {
+ // datepicker assumes beginning of day (00:00), but the duration includes the end date (until 23:59)
+ duration = window.parseInt(this.value, 10) - 1;
+ result = new Date(start);
+ result.setDate(result.getDate() + duration);
+
+ $('#news_enddate').datepicker('setDate', result);
+ } else {
+ start = $('#news_startdate').datepicker('getDate');
+ end = $('#news_enddate').datepicker('getDate');
+ // datepicker assumes beginning of day (see above) and we need to add a day to the duration
+ duration = Math.round((end - start) / (24 * 60 * 60 * 1000)) + 1;
+ duration = Math.max(0, duration);
+
+ $('#news_duration').val(duration);
+ }
+ });
+ },
+
+ get_dialog (id, route) {
+ // initialize dialog
+ $('body').append(`<div id="${id}"></div>`);
+ $(`#${id}`).dialog({
+ modal: true,
+ height: News.dialog_height,
+ title: $gettext('Dialog wird geladen...'),
+ width: News.dialog_width,
+ close () {
+ $(`#${id}`).remove();
+ }
+ });
+
+ // load actual dialog content
+ $.get(route, 'html').done(function (html, status, xhr) {
+ $(`#${id}`).dialog('option', 'title', decodeURIComponent(xhr.getResponseHeader('X-Title')));
+ $(`#${id}`).html(html);
+ $(`#${id}_content`).css({
+ height : (News.dialog_height - 120) + 'px',
+ maxHeight: (News.dialog_height - 120) + 'px'
+ });
+
+ News.init(id);
+ }).fail(function () {
+ window.alert($gettext('Fehler beim Aufruf des News-Controllers'));
+ });
+ },
+
+ update_dialog (id, route, form_data) {
+ if (!News.pending_ajax_request) {
+ News.pending_ajax_request = true;
+
+ $.post(route, form_data, 'html').done(function (html) {
+ var obj;
+
+ News.pending_ajax_request = false;
+ if (html.length > 0) {
+ $(`#${id}`).html(html);
+ $(`#${id}_content`).css({
+ height : (News.dialog_height - 120) + 'px',
+ maxHeight: (News.dialog_height - 120) + 'px'
+ });
+ // scroll to anker
+ obj = $('a[name=anker]');
+ if (obj.length > 0) {
+ $(`#${id}_content`).scrollTop(obj.position().top);
+ }
+ } else {
+ $(`#${id}`).dialog('close');
+ obj = $('#admin_news_form');
+ if (obj.length > 0) {
+ $('#admin_news_form').submit();
+ } else {
+ location.replace(STUDIP.URLHelper.getURL(location.href, {nsave: 1}));
+ }
+ }
+
+ News.init(id);
+ }).fail(function () {
+ News.pending_ajax_request = false;
+ window.alert($gettext('Fehler beim Aufruf des News-Controllers'));
+ });
+ }
+ },
+
+ toggle_category_view (id) {
+ if ($(`input[name=${id}_js]`).val() === 'toggle') {
+ $(`input[name=${id}_js]`).val('');
+ } else {
+ $(`input[name=${id}_js]`).val('toggle');
+ }
+ if ($(`#${id}_content`).is(':visible')) {
+ $(`#${id}_content`).slideUp(400);
+ $(`#${id} input[type=image]:first`)
+ .attr('src', STUDIP.ASSETS_URL + 'images/icons/blue/arr_1right.svg');
+ } else {
+ $(`#${id}_content`).slideDown(400);
+ $(`#${id} input[type=image]:first`)
+ .attr('src', STUDIP.ASSETS_URL + 'images/icons/blue/arr_1down.svg');
+ }
+ }
+};
+
+export default News;
diff --git a/resources/assets/javascripts/lib/oer.js b/resources/assets/javascripts/lib/oer.js
new file mode 100755
index 0000000..02f1d16
--- /dev/null
+++ b/resources/assets/javascripts/lib/oer.js
@@ -0,0 +1,230 @@
+import { $gettext } from '../lib/gettext.js';
+
+const OER = {
+ periodicalPushData: function () {
+ if (jQuery(".comments").length) {
+ return {
+ 'review_id': jQuery("[name=comment]").data("review_id")
+ };
+ }
+ },
+ update: function (output) {
+ if (output.comments) {
+ for (var i = 0; i < output.comments.length; i++) {
+ if (jQuery("#comment_" + output.comments[i].comment_id).length === 0) {
+ jQuery(".comments").append(output.comments[i].html).find(":last-child").hide().fadeIn(300);
+ }
+ }
+ }
+ },
+ requestFullscreen: function (selector) {
+ var player = jQuery(selector)[0];
+ if (!player) {
+ window.alert($gettext('Kein passendes Element für Vollbildmodus.'));
+ return;
+ }
+ if (player.requestFullscreen) {
+ player.requestFullscreen();
+ } else if (player.msRequestFullscreen) {
+ player.msRequestFullscreen();
+ } else if (player.mozRequestFullScreen) {
+ player.mozRequestFullScreen();
+ } else if (player.webkitRequestFullscreen) {
+ player.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
+ }
+ },
+ initSearch: function () {
+ STUDIP.Vue.load().then(({createApp}) => {
+ STUDIP.OER.Search = createApp({
+ el: ".oer_search",
+ data: {
+ browseMode: false,
+ tags: $(".oer_search").data("tags"),
+ tagHistory: [],
+ searchtext: "",
+ activeFilterPanel: false,
+ difficulty: [1, 12],
+ category: null,
+ results: false,
+ material_select_url_template: $(".oer_search").data("material_select_url_template")
+ },
+ methods: {
+ sync_search_text: function () {
+ this.searchtext = $(".oer_search input[name=search]").val();
+ },
+ triggerFilterPanel: function () {
+ this.activeFilterPanel = !this.activeFilterPanel;
+ },
+ showFilterPanel: function () {
+ this.activeFilterPanel = true;
+ },
+ hideFilterPanel: function () {
+ this.activeFilterPanel = false;
+ },
+ clearAllFilters: function (keep_results) {
+ this.clearCategory();
+ this.clearDifficulty();
+ if (this.searchtext != '') {
+ this.searchtext = '';
+ }
+ $(".oer_search input[name=search]").val('');
+ if (keep_results !== true) {
+ this.results = false;
+ }
+ },
+ clearDifficulty: function () {
+ if ((this.difficulty[0] != 1) && (this.difficulty[1] != 12)) {
+ this.difficulty = [1, 12];
+ }
+ jQuery("#difficulty_slider").slider("values", this.difficulty);
+ },
+ clearCategory: function () {
+ if (this.category != null) {
+ this.category = null;
+ }
+ },
+ getIconShape: function (result) {
+ if (result.category === "video") {
+ return "video";
+ }
+ if (result.category === "audio") {
+ return "file-audio";
+ }
+ if (result.category === "presentation") {
+ return "file-pdf";
+ }
+ if (result.category === "elearning") {
+ return "learnmodule";
+ }
+ if (result.content_type === "application/zip") {
+ return "archive3";
+ }
+ return "file";
+ },
+ search: function () {
+ let v = this;
+ this.browseMode = false;
+ $.ajax({
+ url: STUDIP.URLHelper.getURL("dispatch.php/oer/market/search"),
+ data: {
+ type: this.category,
+ difficulty: this.difficulty.join(","),
+ search: this.searchtext
+ },
+ dataType: "json",
+ success: function (output) {
+ $("#new_ones").hide();
+ v.results = output.materials;
+ v.activeFilterPanel = false;
+ $(".material_navigation").toggle(v.results.length == 0);
+ $(".mainlist").toggle(v.results.length == 0);
+ $(".new_ones").toggle(v.results.length == 0);
+ }
+ });
+ return false;
+ },
+ browseTag: function (tag_hash, name) {
+ let v = this;
+ this.clearAllFilters(true);
+ let tags = [];
+ for (let i in this.tagHistory) {
+ tags.push(this.tagHistory[i].tag_hash);
+ }
+ if (tag_hash && (tags.indexOf(tag_hash) === -1)) {
+ tags.push(tag_hash);
+ }
+ let p = new Promise(function (resolve, reject) {
+ $.ajax({
+ url: STUDIP.URLHelper.getURL("dispatch.php/oer/market/get_tags"),
+ data: {
+ tags: tags
+ },
+ dataType: "json",
+ success: function (output) {
+ v.results = output.results.materials;
+ v.tags = output.tags;
+ if (tag_hash) {
+ v.tagHistory.push({
+ tag_hash: tag_hash,
+ name: name
+ });
+ }
+ if (v.tagHistory.length > 0) {
+ $("#new_ones").hide();
+ }
+ resolve();
+ },
+ error: function () {
+ reject();
+ }
+ });
+ });
+ return p;
+ },
+ backInCloud: function () {
+ this.tagHistory.pop();
+ let tag_hash = null;
+ let tag_name = null;
+ if (this.tagHistory.length > 0) {
+ tag_hash = this.tagHistory[this.tagHistory.length - 1].tag_hash;
+ tag_name = this.tagHistory[this.tagHistory.length - 1].name;
+ }
+ let v = this;
+ this.tagHistory.pop();
+ this.browseTag(tag_hash, tag_name).then(function () {
+ if (v.tagHistory.length === 0) {
+ $("#new_ones").show();
+ }
+ });
+
+ },
+ getTagStyle: function (tag_hash) {
+ return "position: relative; top: " + Math.floor(Math.random() * 15 - 15) + "px";
+ },
+ capitalizeFirstLetter: function (string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+ },
+ getMaterialURL: function (material_id) {
+ return this.material_select_url_template.replace("__material_id__", material_id);
+ }
+ },
+ mounted: function () {
+ this.results = $(this.$el).data('searchresults');
+ if (this.results !== null) {
+ $("#new_ones").hide();
+ }
+ if ($(this.$el).data('filteredcategory')) {
+ this.category = $(this.$el).data('filteredcategory');
+ }
+ },
+ updated: function () {
+ this.$nextTick(function () {
+ if (!jQuery("#difficulty_slider.ui-slider").length) { //to prevent an endless loop
+ let v = this;
+ jQuery("#difficulty_slider").slider({
+ range: true,
+ min: 1,
+ max: 12,
+ values: [v.difficulty[0], v.difficulty[1]],
+ change: function (event, ui) {
+ v.difficulty = ui.values;
+ }
+ });
+ }
+ });
+ }
+ });
+ });
+
+
+ jQuery(document).on("click", function (evnt) {
+ if (!jQuery(evnt.target).is(".searchform *") && STUDIP.OER.Search) {
+ STUDIP.OER.Search.hideFilterPanel();
+ }
+ });
+
+ }
+};
+
+
+export default OER;
diff --git a/resources/assets/javascripts/lib/old_upload.js b/resources/assets/javascripts/lib/old_upload.js
new file mode 100644
index 0000000..7c8a084
--- /dev/null
+++ b/resources/assets/javascripts/lib/old_upload.js
@@ -0,0 +1,64 @@
+const OldUpload = {
+ upload: false,
+ msg_window: null,
+ upload_end: function() {
+ if (OldUpload.upload) {
+ OldUpload.msg_window.close();
+ }
+ return;
+ },
+ upload_start: function(form_name) {
+ var file_name = jQuery(form_name)
+ .find('input[type=file]')
+ .val();
+ var ende, file_only;
+ if (!file_name) {
+ alert(jQuery('#upload_select_file_message').text());
+ jQuery(form_name)
+ .find('input[type=file]')
+ .focus();
+ return false;
+ }
+
+ if (file_name.charAt(file_name.length - 1) === '"') {
+ ende = file_name.length - 1;
+ } else {
+ ende = file_name.length;
+ }
+ var ext = file_name.substring(file_name.lastIndexOf('.') + 1, ende).toLowerCase();
+ file_only = file_name;
+ if (file_name.lastIndexOf('/') > 0) {
+ file_only = file_name.substring(file_name.lastIndexOf('/') + 1, ende);
+ }
+ if (file_name.lastIndexOf('\\') > 0) {
+ file_only = file_name.substring(file_name.lastIndexOf('\\') + 1, ende);
+ }
+
+ var permission = jQuery.parseJSON(jQuery('#upload_file_types').html());
+ if (
+ (permission.allow && jQuery.inArray(ext, permission.types) !== -1) ||
+ (!permission.allow && jQuery.inArray(ext, permission.types) === -1)
+ ) {
+ alert(jQuery('#upload_error_message_wrong_type').text());
+ jQuery(form_name)
+ .find('input[type=file]')
+ .focus();
+ return false;
+ }
+
+ OldUpload.msg_window = window.open(
+ '',
+ 'messagewindow',
+ 'height=250,width=200,left=20,top=20,scrollbars=no,resizable=no,toolbar=no'
+ );
+ OldUpload.msg_window.document.write(jQuery('#upload_window_template').text());
+ jQuery(OldUpload.msg_window.document)
+ .find('b')
+ .text(file_only);
+
+ OldUpload.upload = true;
+ return true;
+ }
+};
+
+export default OldUpload;
diff --git a/resources/assets/javascripts/lib/overlapping.js b/resources/assets/javascripts/lib/overlapping.js
new file mode 100644
index 0000000..73ab32f
--- /dev/null
+++ b/resources/assets/javascripts/lib/overlapping.js
@@ -0,0 +1,94 @@
+import { $gettext } from './gettext.js';
+
+const Overlapping = {
+
+ /**
+ * Initialize Select2 select boxes.
+ * @returns {undefined}
+ */
+ init: function () {
+ $('#base-version-select').select2({
+ placeholder: $gettext('Studiengangteil suchen'),
+ minimumInputLength: 3,
+ ajax: {
+ url: STUDIP.URLHelper.getURL('dispatch.php/admin/overlapping/base_version'),
+ dataType: 'json'
+ }
+ });
+
+ $('#comp-versions-select').select2({
+ placeholder: $gettext('Optional weitere Studiengangteile (max. 5)'),
+ minimumInputLength: 3,
+ ajax: {
+ url: STUDIP.URLHelper.getURL('dispatch.php/admin/overlapping/comp_versions'),
+ dataType: 'json'
+ }
+ });
+
+ $('#fachsem-select').select2({
+ placeholder: $gettext('Fachsemester auswählen (optional)')
+ });
+ $('#semtype-select').select2({
+ placeholder: $gettext('Veranstaltungstyp auswählen (optional)')
+ });
+ $('#base-version-select').on('select2:select', function (e) {
+ $('#comp-versions-select').val(null).trigger('change');
+ $.ajax({
+ url: STUDIP.URLHelper.getURL('dispatch.php/admin/overlapping/comp_versions'),
+ dataType: 'json',
+ data: {
+ version_id: $('#base-version-select').select2('data')[0].id
+ },
+ success: function(data) {
+ if (data.results.length) {
+ var inputlength = 3;
+ if (data.results.length < 4) {
+ inputlength = 0;
+ }
+ $('#comp-versions-select').select2({
+ placeholder: $gettext('Optional weitere Studiengangteile (max. 5)'),
+ minimumInputLength: inputlength,
+ ajax: {
+ url: STUDIP.URLHelper.getURL('dispatch.php/admin/overlapping/comp_versions',
+ {'version_id': $('#base-version-select').select2('data')[0].id}),
+ dataType: 'json'
+ }
+ });
+ } else {
+ $('#comp-versions-select').select2({
+ placeholder: $gettext('Keine weitere Auswahl möglich')
+ });
+ $('#comp-versions-select').prop('disabled', true).trigger('change');
+ }
+ }
+ });
+ });
+
+ $('span.mvv-overlapping-exclude').on('click', function () {
+ var course_id = $(this).data('mvv-ovl-course');
+ var selection_id = $(this).data('mvv-ovl-selection');
+ $.ajax({
+ method: 'post',
+ url: STUDIP.URLHelper.getURL('dispatch.php/admin/overlapping/set_exclude'),
+ data: {
+ 'excluded': $(this).is('.mvv-overlapping-invisible') ? 1 : 0,
+ 'course_id': course_id,
+ 'selection_id': selection_id
+ },
+ success: function(data, textStatus, jqXHR) {
+ $('.mvv-overlapping-exclude').each(function () {
+ if ($(this).data('mvv-ovl-course') == course_id) {
+ $(this).toggleClass('mvv-overlapping-invisible');
+ }
+ });
+ $('.mvv-overlapping-exclude').attr('title', $gettext('Veranstaltung berücksichtigen'));
+ $('.mvv-overlapping-invisible').attr('title', $gettext('Veranstaltung nicht berücksichtigen'));
+
+ }
+ })
+ return false;
+ });
+ }
+};
+
+export default Overlapping; \ No newline at end of file
diff --git a/resources/assets/javascripts/lib/overlay.js b/resources/assets/javascripts/lib/overlay.js
new file mode 100644
index 0000000..b4c6d4d
--- /dev/null
+++ b/resources/assets/javascripts/lib/overlay.js
@@ -0,0 +1,108 @@
+import { $gettext } from './gettext.js';
+
+const Overlay = {
+ delay: 300,
+ element: null,
+ selector: '.ui-front.modal-overlay',
+ timeout: null
+};
+
+Overlay.reset = function() {
+ if (this.timeout !== null) {
+ clearTimeout(this.timeout);
+ this.timeout = null;
+ }
+};
+
+Overlay.schedule = function(callback, delay) {
+ this.reset();
+ if (delay !== undefined && !delay) {
+ callback.call(this);
+ } else {
+ this.timeout = setTimeout(callback.bind(this), this.delay);
+ }
+};
+
+Overlay.show = function(ajax, containment, secure, callback, delay) {
+ this.schedule(function() {
+ if (this.element === null) {
+ containment = containment || 'body';
+
+ this.element = $('<div class="ui-front modal-overlay">');
+ if (ajax) {
+ this.element.addClass('modal-overlay-ajax');
+ if (ajax === 'dark') {
+ this.element.addClass('modal-overlay-dark');
+ }
+ }
+ if (containment !== 'body') {
+ this.element.addClass('modal-overlay-local');
+ } else {
+ // Blur background
+ $('#layout_wrapper').addClass('has-overlay');
+ }
+ this.element.appendTo(containment);
+ }
+
+ if (secure) {
+ $(window).on('beforeunload.overlay', Overlay.securityHandler);
+ }
+ if ($.type(callback) === 'function') {
+ callback.call(this);
+ }
+ }, delay);
+};
+
+Overlay.hide = function(delay) {
+ this.schedule(function() {
+ if (this.element !== null) {
+ this.element.remove();
+ this.element = null;
+ }
+
+ $('#layout_wrapper').removeClass('has-overlay');
+ $(window).off('beforeunload.overlay');
+ }, delay);
+};
+
+// Secure the overlay
+Overlay.securityHandler = function(event) {
+ event = event || window.event || {};
+ event.returnValue = $gettext('Ihre Eingaben wurden bislang noch nicht gespeichert.');
+ return event.returnValue;
+};
+
+// Allows progress information
+Overlay.showProgress = function(title, ajax, secure, delay) {
+ this.show(
+ ajax,
+ null,
+ secure,
+ function() {
+ if ($('h1', this.selector).length === 0) {
+ $(this.selector)
+ .append($('<h1>').text(title))
+ .append('<progress max="100" value="0">')
+ .append('<ul class="overlay-progress-log">');
+ }
+ },
+ delay
+ );
+};
+
+Overlay.updateProgress = function(percent, message) {
+ $('progress', this.selector).val(percent);
+ if (message) {
+ this.progressInfo(message);
+ }
+};
+
+Overlay.progressInfo = function(message) {
+ var li = $('<li>').text(message);
+ $('.overlay-progress-log', this.selector).prepend(li);
+ li.delay(1000).hide('fade', 300, function() {
+ $(this).remove();
+ });
+};
+
+export default Overlay;
diff --git a/resources/assets/javascripts/lib/page_layout.js b/resources/assets/javascripts/lib/page_layout.js
new file mode 100644
index 0000000..9509518
--- /dev/null
+++ b/resources/assets/javascripts/lib/page_layout.js
@@ -0,0 +1,29 @@
+/*jslint esversion: 6*/
+let options = {
+ title: document.title,
+ prefix: ''
+};
+
+export default {
+ get title () {
+ return options.title;
+ },
+
+ set title (title) {
+ options.title = title;
+ this.displayTitle();
+ },
+
+ get title_prefix () {
+ return options.prefix;
+ },
+
+ set title_prefix (prefix) {
+ options.prefix = prefix;
+ this.displayTitle();
+ },
+
+ displayTitle () {
+ document.title = `${options.prefix}${options.title}`;
+ }
+};
diff --git a/resources/assets/javascripts/lib/parse_options.js b/resources/assets/javascripts/lib/parse_options.js
new file mode 100644
index 0000000..8160f37
--- /dev/null
+++ b/resources/assets/javascripts/lib/parse_options.js
@@ -0,0 +1,89 @@
+/**
+ * Parses a given string "foo needle[option1;option2=value;option3=42;option4=false] bar"
+ * into the following structure:
+ *
+ * {option1: true, option2: "value", option3: 42, option4: false}
+ */
+function parseOptions(string, needle) {
+ var temp = needle ? string.match(/\w+\[(.*?)\]/g) || [] : [string],
+ options = {};
+
+ temp.forEach(function(slice) {
+ if (needle && (slice.indexOf(needle) !== 0 || slice === needle)) {
+ return;
+ }
+ var split = needle ? slice.replace(/^\w+\[(.*)\]$/, '$1') : slice,
+ index = '',
+ value = '',
+ inval = false,
+ escaped = 0,
+ inquotes = false,
+ l = split.length,
+ token,
+ write,
+ skip,
+ i;
+ for (i = 0; i < l; i += 1) {
+ token = split[i];
+ write = false;
+ skip = false;
+ if (inval && token === '\\' && escaped <= 0) {
+ escaped = 2;
+ } else if (!inval && token === '=') {
+ inval = true;
+ skip = true;
+ } else if (inval && value.length === 0 && (token === '"' || token === "'")) {
+ inquotes = token;
+ } else if (inval && inquotes && escaped <= 0 && token === inquotes) {
+ inquotes = false;
+ } else if (!inquotes && token === ';') {
+ write = true;
+ skip = true;
+ }
+ if (!skip && escaped <= 0) {
+ if (inval) {
+ value += token;
+ } else {
+ index += token;
+ }
+ }
+ escaped -= 1;
+
+ if (write || i === split.length - 1) {
+ if (i === split.length - 1 && inquotes) {
+ throw 'Invalid data, missing closing quote';
+ }
+ if (index.trim().length > 0) {
+ options[index.trim()] = inval ? parseValue(value) : true;
+ }
+ inval = false;
+ inquotes = false;
+ index = '';
+ value = '';
+ }
+ }
+ });
+ return options;
+}
+
+/**
+ * Tries to parse a given string into it's appropriate type.
+ * Supports boolean, int and float.
+ */
+function parseValue(value) {
+ if (value.toLowerCase() === 'true') {
+ return true;
+ }
+ if (value.toLowerCase() === 'false') {
+ return false;
+ }
+ if (/^[+\-]\d+$/.test(value)) {
+ return parseInt(value, 10);
+ }
+ if (/^[+\-]\d+\.\d+$/.test(value)) {
+ return parseFloat(value, 10);
+ }
+ return value.replace(/^(["'])(.*)\1$/, '$2');
+}
+
+export default parseOptions;
diff --git a/resources/assets/javascripts/lib/personal_notifications.js b/resources/assets/javascripts/lib/personal_notifications.js
new file mode 100644
index 0000000..4a0a6e3
--- /dev/null
+++ b/resources/assets/javascripts/lib/personal_notifications.js
@@ -0,0 +1,219 @@
+import Favico from 'favico.js';
+import Cache from './cache.js';
+import PageLayout from './page_layout.js';
+
+var stack = {};
+var audio_notification = false;
+var directlydeleted = [];
+var favicon = null;
+
+function updateFavicon(text) {
+ if (favicon === null) {
+ var valid = $('head')
+ .find('link[rel=icon]')
+ .first();
+ $('head')
+ .find('link[rel*=icon]')
+ .not(valid)
+ .remove();
+
+ favicon = new Favico({
+ bgColor: '#d60000',
+ textColor: '#fff',
+ fontStyle: 'normal',
+ fontFamily: 'Lato',
+ position: 'right',
+ type: 'rectangle'
+ });
+ }
+ favicon.badge(text);
+}
+
+// Wrapper function that creates a desktop notification from given data
+function create_desktop_notification(data) {
+ var notification = new Notification(STUDIP.STUDIP_SHORT_NAME, {
+ body: data.text,
+ icon: data.avatar,
+ tag: data.id
+ });
+ notification.addEventListener('click', () => {
+ location.href = STUDIP.URLHelper.getURL(`dispatch.php/jsupdater/mark_notification_read/${notification.tag}`);
+ });
+}
+
+// Handler for all notifications received by an ajax request
+function process_notifications({ notifications }) {
+ var cache = Cache.getInstance('desktop.notifications');
+ var ul = $('<ul/>');
+ var changed = false;
+ var new_stack = {};
+
+ notifications.forEach(notification => {
+ if (directlydeleted.indexOf(notification.personal_notification_id) !== -1) {
+ return;
+ }
+
+ ul.append(notification.html);
+
+ var id = $('.notification:last', ul).data().id;
+ new_stack[id] = notification;
+ if (notification.html_id) {
+ $(`#${notification.html_id}`).on('mouseenter', PersonalNotifications.isVisited);
+ }
+
+ changed = changed || !stack.hasOwnProperty(id);
+
+ // Check if notifications should be sent (depends on the
+ // Notification itself and session storage)
+ if (
+ !window.hasOwnProperty('Notification')
+ || Notification.permission !== 'granted'
+ || cache.has(notification.id)
+ ) {
+ return;
+ }
+
+ // If it's okay let's create a notification
+ create_desktop_notification(notification);
+
+ cache.set(id, true);
+ });
+
+ // Anything changed? Replace stack and display
+ if (changed || Object.keys(stack).length !== Object.keys(new_stack).length) {
+ stack = new_stack;
+ $('#notification_list > ul').replaceWith(ul);
+ }
+
+ PersonalNotifications.update();
+ directlydeleted = [];
+}
+
+const PersonalNotifications = {
+ initialize () {
+ if ($('#notification_marker').length > 0) {
+ $('#notification_list .notification').map(function() {
+ var data = $(this).data();
+ stack[data.id] = data;
+ });
+
+ STUDIP.JSUpdater.register(
+ 'personalnotifications',
+ process_notifications,
+ null,
+ 60000
+ );
+
+ if ($('#audio_notification').length > 0) {
+ audio_notification = $('#audio_notification').get(0);
+ audio_notification.load();
+ }
+
+ if ('Notification' in window) {
+ $('#notification_list .enable-desktop-notifications')
+ .toggle(Notification.permission === 'default')
+ .click(STUDIP.PersonalNotifications.activate);
+ }
+ }
+ },
+ activate () {
+ Promise.resolve(Notification.requestPermission()).then(permission => {
+ $('#notification_list .enable-desktop-notifications')
+ .toggle(permission === 'default');
+ });
+ },
+ markAsRead (event) {
+ var notification = $(this).closest('.notification'),
+ id = notification.data().id;
+ PersonalNotifications.sendReadInfo(id, notification);
+ return false;
+ },
+ markAllAsRead (event) {
+ var notifications = $(this)
+ .parent()
+ .find('.notification');
+ PersonalNotifications.sendReadInfo('all', notifications);
+ return false;
+ },
+ sendReadInfo (id, notification) {
+ $.get(STUDIP.URLHelper.getURL(`dispatch.php/jsupdater/mark_notification_read/${id}`)).done(() => {
+ if (notification) {
+ var count = notification.length;
+ notification.toggle('blind', 'fast', function() {
+ var data = $(this).data();
+ delete stack[data.id];
+ $(this).remove();
+
+ count -= 1;
+ if (count === 0) {
+ PersonalNotifications.update();
+ }
+ });
+ }
+ });
+ },
+ update () {
+ var count = _.values(stack).length;
+ var old_count = parseInt($('#notification_marker').text(), 10);
+ var really_new = 0;
+ $('#notification_list > ul > li').each(function() {
+ if (parseInt($(this).data('timestamp'), 10) > parseInt($('#notification_marker').data('lastvisit'), 10)) {
+ really_new += 1;
+ }
+ });
+ if (really_new > 0) {
+ $('#notification_marker')
+ .data('seen', false)
+ .addClass('alert');
+ PageLayout.title_prefix = '(!) ';
+ } else {
+ $('#notification_marker').removeClass('alert');
+ PageLayout.title_prefix = '';
+ }
+ if (count) {
+ $('#notification_container').addClass('hoverable');
+ if (count > old_count && audio_notification !== false) {
+ audio_notification.play();
+ }
+ } else {
+ $('#notification_container').removeClass('hoverable');
+ }
+ if (old_count !== count) {
+ $('#notification_marker').text(count);
+ updateFavicon(count);
+ $('#notification_container .mark-all-as-read').toggleClass('notification_hidden', count < 2);
+ }
+ },
+ isVisited () {
+ const id = this.id;
+ $.each(stack, (index, notification) => {
+ if (notification.html_id === id) {
+ PersonalNotifications.sendReadInfo(notification.personal_notification_id);
+
+ delete stack[index];
+
+ $(`.notification[data-id=${notification.personal_notification_id}]`).fadeOut(function () {
+ $(this).remove();
+ });
+
+ directlydeleted.push(notification.personal_notification_id);
+
+ PersonalNotifications.update();
+ }
+ });
+ },
+ setSeen () {
+ if ($('#notification_marker').data('seen')) {
+ return;
+ }
+ $('#notification_marker').data('seen', true);
+
+ $.get(STUDIP.URLHelper.getURL('dispatch.php/jsupdater/notifications_seen')).then(time => {
+ $('#notification_marker')
+ .removeClass('alert')
+ .data('lastvisit', time);
+ });
+ }
+};
+
+export default PersonalNotifications;
diff --git a/resources/assets/javascripts/lib/plus.js b/resources/assets/javascripts/lib/plus.js
new file mode 100644
index 0000000..0d447fd
--- /dev/null
+++ b/resources/assets/javascripts/lib/plus.js
@@ -0,0 +1,23 @@
+const Plus = {
+ setModule: function () {
+ $.ajax({
+ "url": STUDIP.URLHelper.getURL("dispatch.php/course/plus/trigger"),
+ "data": {
+ "moduleclass": $(this).data("moduleclass"),
+ "key": $(this).data("key"),
+ "active": $(this).is(":checked") ? 1 : 0
+ },
+ "dataType": "json",
+ "type": "post",
+ "success": function (output) {
+ if (output.tabs) {
+ $(".tabs_wrapper").replaceWith(output.tabs);
+ }
+ }
+ });
+ }
+};
+
+
+
+export default Plus;
diff --git a/resources/assets/javascripts/lib/qr_code.js b/resources/assets/javascripts/lib/qr_code.js
new file mode 100644
index 0000000..781d1aa
--- /dev/null
+++ b/resources/assets/javascripts/lib/qr_code.js
@@ -0,0 +1,60 @@
+import QRCodeGenerator from "../vendor/qrcode-04f46c6.js"
+
+const QRCode = {
+ show: function() {
+ jQuery('#qr_code').remove();
+ jQuery("<div id='qr_code'/>").appendTo('body');
+ var title = jQuery(this).data('qr-title');
+ if (title) {
+ jQuery('#qr_code').append('<h1 class="title">' + title + '</h1>');
+ } else {
+ jQuery('#qr_code').append("<div class='header'/>");
+ }
+ jQuery('#qr_code').append("<div class='code'/>");
+ jQuery('#qr_code').append("<div class='url'/>");
+ jQuery('#qr_code').append("<div class='description'/>");
+
+ var code = new QRCodeGenerator(jQuery('#qr_code .code')[0], {
+ text: jQuery(this).attr('href'),
+ width: 1280,
+ height: 1280,
+ correctLevel: 3
+ });
+
+ jQuery('#qr_code .url').text(jQuery(this).attr('href'));
+ jQuery('#qr_code .description').text(jQuery(this).data('qr-code'));
+ var print_button_enabled = jQuery(this).data('qr-code-print');
+ if (print_button_enabled) {
+ var icon_path = STUDIP.URLHelper.getURL(
+ 'assets/images/icons/blue/print.svg'
+ );
+ var print_element = jQuery('<img></img>');
+ jQuery(print_element).attr('src', icon_path);
+ jQuery(print_element).addClass('PrintAction');
+ jQuery('#qr_code').append(print_element);
+ }
+
+ //jQuery("#qr_code .code").html(jQuery(this).find(".qrcode_image").clone());
+ var qr = jQuery('#qr_code')[0];
+ if (qr.requestFullscreen) {
+ qr.requestFullscreen();
+ } else if (qr.msRequestFullscreen) {
+ qr.msRequestFullscreen();
+ } else if (qr.mozRequestFullScreen) {
+ qr.mozRequestFullScreen();
+ } else if (qr.webkitRequestFullscreen) {
+ qr.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
+ }
+ return false;
+ },
+ generate: function (element, text, options = {}) {
+ options.text = text;
+ if (!options.hasOwnProperty('correctLevel')) {
+ options.correctLevel = 3;
+ }
+
+ var qrcode = new QRCodeGenerator(element, options);
+ }
+};
+
+export default QRCode;
diff --git a/resources/assets/javascripts/lib/questionnaire.js b/resources/assets/javascripts/lib/questionnaire.js
new file mode 100644
index 0000000..199a98a
--- /dev/null
+++ b/resources/assets/javascripts/lib/questionnaire.js
@@ -0,0 +1,233 @@
+import { $gettext } from '../lib/gettext.js';
+
+const Questionnaire = {
+ lastUpdate: null,
+ initialize() {
+ STUDIP.JSUpdater.register(
+ 'questionnaire',
+ Questionnaire.updateQuestionnaireResults,
+ Questionnaire.getParamsForPolling,
+ 15000
+ );
+ },
+ getParamsForPolling: function() {
+ var questionnaires = {
+ questionnaire_ids: [],
+ last_update: Questionnaire.lastUpdate
+ };
+ Questionnaire.lastUpdate = Math.floor(Date.now() / 1000);
+ jQuery('.questionnaire_results').each(function() {
+ questionnaires.questionnaire_ids.push(jQuery(this).data('questionnaire_id'));
+ });
+ if (questionnaires.questionnaire_ids.length > 0) {
+ return questionnaires;
+ }
+ },
+ updateQuestionnaireResults: function(data) {
+ for (var questionnaire_id in data) {
+ if (data[questionnaire_id].html) {
+ var new_view = jQuery(data[questionnaire_id].html);
+ jQuery('.questionnaire_results.questionnaire_' + questionnaire_id).replaceWith(new_view);
+ jQuery(document).trigger('dialog-open');
+ }
+ }
+ },
+ updateOverviewQuestionnaire: function(data) {
+ if (jQuery('#questionnaire_overview tr#questionnaire_' + data.questionnaire_id).length > 0) {
+ jQuery('#questionnaire_overview tr#questionnaire_' + data.questionnaire_id).replaceWith(data.overview_html);
+ } else {
+ if (jQuery('#questionnaire_overview').length > 0) {
+ jQuery(data.overview_html)
+ .hide()
+ .insertBefore('#questionnaire_overview > tbody > :first-child')
+ .delay(300)
+ .fadeIn();
+ jQuery('#questionnaire_overview .noquestionnaires').remove();
+ }
+ if (data.message) {
+ jQuery('.messagebox').hide();
+ jQuery('#layout_content').prepend(data.message);
+ }
+ }
+ if (jQuery('.questionnaire_widget .widget_questionnaire_' + data.questionnaire_id).length > 0) {
+ if (data.widget_html) {
+ jQuery('.questionnaire_widget .widget_questionnaire_' + data.questionnaire_id).replaceWith(
+ data.widget_html
+ );
+ } else {
+ jQuery('.questionnaire_widget .widget_questionnaire_' + data.questionnaire_id).remove();
+ }
+ } else {
+ if (jQuery('.questionnaire_widget').length > 0 && data.widget_html) {
+ jQuery('.ui-dialog-content').dialog('close');
+ if (jQuery('.questionnaire_widget > article').length > 0) {
+ jQuery(data.widget_html)
+ .hide()
+ .insertBefore(
+ '.questionnaire_widget > article:first-of-type, .questionnaire_widget > section:first-of-type'
+ )
+ .delay(300)
+ .fadeIn();
+ } else {
+ jQuery('.questionnaire_widget .noquestionnaires')
+ .replaceWith(data.widget_html)
+ .hide()
+ .delay(300)
+ .fadeIn();
+ }
+ } else {
+ if (data.message) {
+ jQuery('.messagebox').hide();
+ jQuery('#layout_content').prepend(data.message);
+ jQuery.scrollTo('#layout_content', 400);
+ }
+ }
+ }
+ jQuery(document).trigger('dialog-open');
+ },
+ updateWidgetQuestionnaire: function(html) {
+ //update the results of a questionnaire
+ var questionnaire_id = jQuery(html).data('questionnaire_id');
+ jQuery('.questionnaire_widget .questionnaire_' + questionnaire_id).replaceWith(html);
+ jQuery(document).trigger('dialog-open');
+ },
+ beforeAnswer: function() {
+ var form = jQuery(this).closest('form')[0];
+ var questionnaire_id = jQuery(form)
+ .closest('article')
+ .data('questionnaire_id');
+ let validated = true;
+
+ //validation
+ $(form).find("input, select, textarea").each(function () {
+ if ($(this).is(":invalid")) {
+ validated = false;
+ }
+ });
+
+ $(form).find(".questionnaire_answer > article").each(function () {
+ let question_type = $(this).data("question_type");
+ if (typeof STUDIP.Questionnaire[question_type] !== "undefined"
+ && typeof STUDIP.Questionnaire[question_type].validator === "function") {
+ if (!STUDIP.Questionnaire[question_type].validator.call(this)) {
+ validated = false;
+ }
+ }
+ });
+
+ if (!validated) {
+ $(form).addClass("show_validation_hints");
+ STUDIP.Report.warning($gettext("Noch nicht komplett ausgefüllt."), $gettext("Füllen Sie noch die rot markierten Stellen korrekt aus."));
+ return false;
+ }
+
+ if (jQuery(form).is('.questionnaire_widget form')) {
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/questionnaire/submit/' + questionnaire_id,
+ data: new FormData(form),
+ cache: false,
+ processData: false,
+ contentType: false,
+ type: 'POST',
+ success: function(output) {
+ jQuery(form).replaceWith(output);
+ jQuery(document).trigger('dialog-open');
+ }
+ });
+ jQuery(form).css('opacity', '0.5');
+ return false;
+ } else {
+ return true;
+ }
+ },
+ Test: {
+ updateCheckboxValues: function() {
+ jQuery('.questionnaire_edit .question.test').each(function() {
+ jQuery(this)
+ .find('.options > li')
+ .each(function(index, li) {
+ jQuery(li)
+ .find('input[type=checkbox]')
+ .val(index + 1);
+ });
+ });
+ }
+ },
+ Vote: {
+ validator: function () {
+ if ($(this).find(".mandatory").length > 0) {
+ if ($(this).find(":selected, :checked").length === 0) {
+ $(this).find(".invalidation_notice").addClass("invalid");
+ return false;
+ } else {
+ $(this).find(".invalidation_notice").removeClass("invalid");
+ }
+ }
+ return true;
+ }
+ },
+ addQuestion: function(questiontype) {
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/questionnaire/add_question',
+ data: {
+ questiontype: questiontype
+ },
+ dataType: 'json',
+ success: function(output) {
+ var order = JSON.parse(jQuery("input[name=order]").val());
+ order.push(output.question_id);
+ jQuery("input[name=order]").val(JSON.stringify(order));
+ jQuery(output.html)
+ .hide()
+ .appendTo('.questionnaire_edit .all_questions')
+ .show('fade');
+ }
+ });
+ },
+ moveQuestionUp: function () {
+ let thisquestion = jQuery(this).closest(".question");
+ let upper = thisquestion.prev();
+ thisquestion.insertBefore(upper);
+ upper.hide().fadeIn();
+ thisquestion.hide().fadeIn();
+ },
+ moveQuestionDown: function () {
+ let thisquestion = jQuery(this).closest(".question");
+ let downer = thisquestion.next();
+ thisquestion.insertAfter(downer);
+ downer.hide().fadeIn();
+ thisquestion.hide().fadeIn();
+ },
+ initVoteEvaluation: async function (el, data, isAjax, isMultiple) {
+
+ const Chartist = await STUDIP.loadChunk('chartist');
+
+ if (isAjax) {
+ jQuery(document).add(".questionnaire_results").one("dialog-open", enhance);
+ } else {
+ jQuery(enhance);
+ }
+
+ function enhance() {
+ if (isMultiple) {
+ new Chartist.Bar(
+ el,
+ data,
+ { onlyInteger: true, axisY: { onlyInteger: true } }
+ );
+ } else {
+ data.series = data.series[0];
+ new Chartist.Pie(
+ el,
+ data,
+ { labelPosition: 'outside' }
+ );
+ }
+ };
+ },
+ initTestEvaluation: async function (el, data, isAjax, isMultiple) {
+ this.initVoteEvaluation(el, data, isAjax, isMultiple);
+ },
+};
+
+export default Questionnaire;
diff --git a/resources/assets/javascripts/lib/quick_search.js b/resources/assets/javascripts/lib/quick_search.js
new file mode 100644
index 0000000..d8fda74
--- /dev/null
+++ b/resources/assets/javascripts/lib/quick_search.js
@@ -0,0 +1,172 @@
+/*jslint esversion: 6*/
+import { $gettext } from './gettext.js';
+
+/* ------------------------------------------------------------------------
+ * QuickSearch inputs
+ * ------------------------------------------------------------------------ */
+
+const QuickSearch = {
+ /**
+ * the function to be called from the QuickSearch class template
+ * @param name string: ID of input
+ * @param url string: URL of AJAX-response
+ * @param func string: name of a possible function executed
+ * when user has selected something
+ * @return: void
+ */
+ autocomplete: function(name, url, func, disabled) {
+ if (disabled === undefined || disabled !== true) {
+ var appendTo = 'body';
+ if (jQuery(`#${name}_frame`).length > 0) {
+ appendTo = `#${name}_frame`;
+ } else if ($(`#${name}`).closest('.ui-dialog').length > 0) {
+ appendTo = $(`#${name}`).closest('.ui-dialog');
+ }
+ jQuery('#' + name).quicksearch({
+ delay: 500,
+ minLength: 3,
+ appendTo: appendTo,
+ create: function() {
+ if ($(this).is('[autofocus]')) {
+ $(this).focus();
+ }
+ },
+ position: $('#' + name).is('.expand-to-left')
+ ? {
+ my: 'right top',
+ at: 'right bottom',
+ collision: 'none'
+ }
+ : {
+ my: 'left top',
+ at: 'left bottom',
+ collision: 'none'
+ },
+ source: function(input, add) {
+ //get the variables that should be sent:
+ var send_vars = jQuery('#' + name)
+ .closest('form')
+ .serializeArray();
+ send_vars.push({
+ name: 'request',
+ value: input.term
+ });
+
+ jQuery
+ .ajax({
+ url: url,
+ type: 'post',
+ data: send_vars
+ })
+ .done(function(data) {
+ var stripTags = /<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi;
+ //an array of possible selections
+
+ if (!data.length) {
+ add([{
+ value: '',
+ label: $gettext('Kein Ergebnis gefunden.'),
+ disabled: true
+ }]);
+ return;
+ }
+
+ var suggestions = _.map(data, function(val) {
+ //adding a label and a hidden item_id - don't use "value":
+ var label_text = val.item_name;
+ if (val.item_description !== undefined) {
+ label_text += '<br>' + val.item_description;
+ }
+
+ return {
+ //what is displayed in the drop down box
+ label: label_text,
+ //the hidden ID of the item
+ item_id: val.item_id,
+ //what is inserted in the visible input box
+ value:
+ val.item_search_name !== null
+ ? val.item_search_name
+ : jQuery('<div/>')
+ .html((val.item_name || '').replace(stripTags, ''))
+ .text()
+ };
+ });
+ //pass it to the function of UI-widget:
+ add(suggestions);
+ })
+ .fail(function(jqxhr, textStatus) {
+ add([
+ {
+ value: '',
+ label: $gettext('Fehler') + ': ' + jqxhr.responseText,
+ disabled: true
+ }
+ ]);
+ });
+ },
+ select: function(event, ui) {
+ if (ui.item.disabled) {
+ return;
+ }
+
+ //inserts the ID of the selected item in the hidden input:
+ jQuery('#' + name + '_realvalue').attr('value', ui.item.item_id);
+ //and execute a special function defined before by the programmer:
+ if (func) {
+ var proceed = func.bind(event.target)(ui.item.item_id, ui.item.value);
+ if (!proceed) {
+ jQuery(this).val('');
+ return false;
+ }
+ }
+ }
+ });
+
+ if (jQuery('#' + name + '_frame').length) {
+ // trigger search on button click
+ jQuery('#' + name + '_frame input[type="submit"]').click(function(e) {
+ e.preventDefault();
+ QuickSearch.triggerSearch(name);
+ });
+
+ // trigger search on enter key down
+ jQuery('#' + name).keydown(function(e) {
+ if (e.keyCode == 13) {
+ e.preventDefault();
+ QuickSearch.triggerSearch(name);
+ }
+ });
+ }
+
+ var input = jQuery('#' + name);
+ var hidden = jQuery('#' + name + '_realvalue');
+ if (input.is('[required]')) {
+ input.closest('form').submit(function (event) {
+ if (hidden.val() === '') {
+ input[0].setCustomValidity($gettext('Bitte wählen Sie einen gültigen Wert aus!'));
+ event.preventDefault();
+ }
+ });
+ }
+ }
+ },
+
+ // start searching now
+ triggerSearch: function(name) {
+ var term = jQuery('#' + name).val();
+ jQuery('#' + name).quicksearch({ minLength: 1 });
+ jQuery('#' + name).quicksearch('search', term);
+ jQuery('#' + name).quicksearch({ minLength: 3 });
+ },
+
+ reset: function(form_name, quick_search_name) {
+ if (!form_name || !quick_search_name) {
+ return;
+ }
+ document.forms[form_name].elements[quick_search_name].value = '';
+ document.forms[form_name].elements[quick_search_name + '_parameter'].value = '';
+ }
+};
+
+export default QuickSearch;
diff --git a/resources/assets/javascripts/lib/raumzeit.js b/resources/assets/javascripts/lib/raumzeit.js
new file mode 100644
index 0000000..5cd5e55
--- /dev/null
+++ b/resources/assets/javascripts/lib/raumzeit.js
@@ -0,0 +1,19 @@
+import { $gettext } from './gettext.js';
+
+const Raumzeit = {
+ disableBookableRooms: function(icon) {
+ var select = $(icon).prev('select')[0];
+ var me = $(icon);
+ select.title = '';
+ $(select)
+ .children('option')
+ .each(function() {
+ $(this).prop('disabled', false);
+ });
+
+ me.attr('data-state', false);
+ me.attr('title', $gettext('Nur buchbare Räume anzeigen'));
+ }
+};
+
+export default Raumzeit;
diff --git a/resources/assets/javascripts/lib/ready.js b/resources/assets/javascripts/lib/ready.js
new file mode 100644
index 0000000..3464ec6
--- /dev/null
+++ b/resources/assets/javascripts/lib/ready.js
@@ -0,0 +1,60 @@
+/*jslint esversion: 6*/
+
+function ready(callback, top = false) {
+ if (top) {
+ ready.handlers.unshift({
+ type: false,
+ callback: callback
+ });
+ } else {
+ ready.handlers.push({
+ type: false,
+ callback: callback
+ });
+ }
+ return this; // = STUDIP
+}
+ready.handlers = [];
+ready.trigger = function (type, context) {
+ ready.handlers.filter(handler => !handler.type || handler.type === type).forEach(handler => {
+ handler.callback({
+ target: context || document
+ });
+ });
+
+ let event = $.Event('studip-ready');
+ event.target = context || document;
+ $(document).trigger(event);
+};
+
+function domReady(callback, top = false) {
+ if (top) {
+ ready.handlers.unshift({
+ type: 'dom',
+ callback: callback
+ });
+ } else {
+ ready.handlers.push({
+ type: 'dom',
+ callback: callback
+ });
+ }
+ return this; // = STUDIP
+}
+
+function dialogReady(callback, top = false) {
+ if (top) {
+ ready.handlers.unshift({
+ type: 'dialog',
+ callback: callback
+ });
+ } else {
+ ready.handlers.push({
+ type: 'dialog',
+ callback: callback
+ });
+ }
+ return this; // = STUDIP
+}
+
+export { ready, domReady, dialogReady };
diff --git a/resources/assets/javascripts/lib/register.js b/resources/assets/javascripts/lib/register.js
new file mode 100644
index 0000000..da81132
--- /dev/null
+++ b/resources/assets/javascripts/lib/register.js
@@ -0,0 +1,134 @@
+import { $gettext } from './gettext.js';
+
+const register = {
+ re_username: null,
+ re_name: null,
+
+ clearErrors: function(field) {
+ jQuery('input[name=' + field + ']')
+ .parent()
+ .find('div.error')
+ .remove();
+ },
+
+ addError: function(field, error) {
+ jQuery('input[name=' + field + ']')
+ .parent()
+ .append('<div class="error">' + error + '</div>');
+ jQuery('div[class=error]').show();
+ },
+
+ checkusername: function() {
+ register.clearErrors('username');
+
+ if (jQuery('input[name=username]').val().length < 4) {
+ register.addError(
+ 'username',
+ $gettext('Der Benutzername ist zu kurz, er sollte mindestens 4 Zeichen lang sein.')
+ );
+ document.login.username.focus();
+ return false;
+ }
+
+ if (register.re_username.test(jQuery('input[name=username]').val()) === false) {
+ register.addError(
+ 'username',
+ $gettext('Der Benutzername enthält unzulässige Zeichen, er darf keine Sonderzeichen oder Leerzeichen enthalten.')
+ );
+ document.login.username.focus();
+ return false;
+ }
+
+ return true;
+ },
+
+ checkpassword: function() {
+ register.clearErrors('password');
+
+ var checked = true;
+ if (jQuery('input[name=password]').val().length < 8) {
+ register.addError(
+ 'password',
+ $gettext('Das Passwort ist zu kurz. Es sollte mindestens 8 Zeichen lang sein.')
+ );
+ document.login.password.focus();
+ checked = false;
+ }
+ return checked;
+ },
+
+ checkpassword2: function() {
+ register.clearErrors('password2');
+
+ var checked = true;
+ if (jQuery('input[name=password]').val() !== jQuery('input[name=password2]').val()) {
+ register.addError(
+ 'password2',
+ $gettext('Das Passwort stimmt nicht mit dem Bestätigungspasswort überein!')
+ );
+ document.login.password2.focus();
+ checked = false;
+ }
+ return checked;
+ },
+
+ checkVorname: function() {
+ register.clearErrors('Vorname');
+
+ var checked = true;
+ if (register.re_name.test(jQuery('input[name=Vorname]').val()) === false) {
+ register.addError('Vorname', $gettext('Bitte geben Sie Ihren tatsächlichen Vornamen an.'));
+ document.login.Vorname.focus();
+ checked = false;
+ }
+ return checked;
+ },
+
+ checkNachname: function() {
+ register.clearErrors('Nachname');
+
+ var checked = true;
+ if (register.re_name.test(jQuery('input[name=Nachname]').val()) === false) {
+ register.addError('Nachname', $gettext('Bitte geben Sie Ihren tatsächlichen Nachnamen an.'));
+ document.login.Nachname.focus();
+ checked = false;
+ }
+ return checked;
+ },
+
+ checkEmail: function() {
+ register.clearErrors('Email');
+
+ var email = jQuery('input[name=Email]').val();
+ var domain = jQuery('select[name=emaildomain]').val();
+ var checked = false;
+
+ if (domain) {
+ email += '@' + domain;
+ }
+
+ checked = $('<input type="email">')
+ .val(email)[0]
+ .checkValidity();
+
+ if (!checked) {
+ register.addError('Email', $gettext('Die E-Mail-Adresse ist nicht korrekt!'));
+ $('#Email').focus();
+ }
+
+ return checked;
+ },
+
+ checkdata: function() {
+ return (
+ this.checkusername() &&
+ this.checkpassword() &&
+ this.checkpassword2() &&
+ this.checkVorname() &&
+ this.checkNachname() &&
+ this.checkEmail()
+ );
+ }
+};
+
+export default register;
diff --git a/resources/assets/javascripts/lib/report.js b/resources/assets/javascripts/lib/report.js
new file mode 100644
index 0000000..b81e0c4
--- /dev/null
+++ b/resources/assets/javascripts/lib/report.js
@@ -0,0 +1,48 @@
+/**
+ * Message reporting
+ *
+ * @author Viktoria Wiebe
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @version 1.0
+ * @since Stud.IP 4.5
+ * @license GLP2 or any later version
+ * @copyright 2019 Stud.IP Core Group
+ */
+
+import Dialog from './dialog.js';
+
+let counter = 0;
+
+function reportMessage(type, title, content, options) {
+ options.id = `report-${type}-${counter++}`;
+ options.title = title;
+ options.size = 'fit';
+ options.wikilink = false;
+ options.dialogClass = `report-${type}`;
+
+ Dialog.show(content, options);
+}
+
+const Report = {
+ // Info message
+ info (title, content, options = {}) {
+ reportMessage('info', title, content, options);
+ },
+
+ // Success message
+ success (title, content, options = {}) {
+ reportMessage('success', title, content, options);
+ },
+
+ // Warning message
+ warning (title, content, options = {}) {
+ reportMessage('warning', title, content, options);
+ },
+
+ // Error message
+ error (title, content, options = {}) {
+ reportMessage('error', title, content, options);
+ }
+};
+
+export default Report;
diff --git a/resources/assets/javascripts/lib/resources.js b/resources/assets/javascripts/lib/resources.js
new file mode 100644
index 0000000..126a55e
--- /dev/null
+++ b/resources/assets/javascripts/lib/resources.js
@@ -0,0 +1,907 @@
+import { $gettext } from '../lib/gettext.js';
+
+class Resources
+{
+
+ static addUserToPermissionList(user_id, table_element)
+ {
+ if (!user_id || !table_element) {
+ return;
+ }
+
+ var is_temporary_table = false;
+ var table_id = jQuery(table_element).attr('id');
+ if (table_id === 'TemporaryPermissionList') {
+ is_temporary_table = true;
+ }
+
+ var template_row = jQuery(table_element).find('tr.resource-permission-list-template')[0];
+ if (!template_row) {
+ //Something is wrong with the HTML
+ return;
+ }
+ var temp_perms_row = false;
+
+ if (jQuery(template_row).attr('data-temp-perms') === '1') {
+ temp_perms_row = true;
+ }
+
+ if (!is_temporary_table) {
+ //Check if the user is already in the list:
+ var trs = jQuery(table_element).find('tr');
+ for (var tr of trs) {
+ if (jQuery(tr).data('user_id') === user_id) {
+ //We have found a table entry for the user specified by
+ //user_id. Nothing to do here.
+ return;
+ }
+ }
+ }
+ var insert_function = function(user_id = null, username = null) {
+ var new_row = jQuery(template_row).clone(true);
+ jQuery(new_row).removeClass('invisible');
+ jQuery(new_row).removeClass('resource-permission-list-template');
+
+ jQuery(new_row).attr('data-user_id', user_id);
+
+ var row_tds = jQuery(new_row).children('td');
+
+ //Set the name-TD's content:
+ var user_td_index = 1;
+ jQuery(row_tds[user_td_index]).children('input').removeAttr('disabled');
+
+ if (username) {
+ jQuery(row_tds[user_td_index]).append(username);
+ } else {
+ jQuery(row_tds[user_td_index]).append('ID ' + user_id);
+ }
+ var user_id_input = jQuery(row_tds[user_td_index]).children('input')[0];
+ if (!user_id_input) {
+ return;
+ }
+ 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:
+
+ var begin = new Date();
+ begin.setHours(begin.getHours() + 1);
+
+ var begin_month = (begin.getMonth() + 1).toString();
+ if (begin_month.length === 1) {
+ begin_month = '0' + begin_month;
+ }
+ var begin_date = begin.getDate()
+ + '.'
+ + begin_month
+ + '.'
+ + begin.getFullYear();
+
+ var begin_time = begin.getHours() + ':00';
+ if (begin.getHours() < 10) {
+ begin_time = '0' + begin_time;
+ }
+
+ var end = new Date();
+ end.setDate(end.getDate() + 14);
+ var end_month = (end.getMonth() + 1).toString();
+ if (end_month.length === 1) {
+ end_month = '0' + end_month;
+ }
+
+ var end_date = end.getDate()
+ + '.'
+ + end_month
+ + '.'
+ + end.getFullYear();
+
+ var end_time = end.getHours() + ':00';
+ if (end.getHours() < 10) {
+ end_time = '0' + end_time;
+ }
+
+ var begin_td_inputs = jQuery(row_tds[user_td_index + 2]).children();
+
+ jQuery(begin_td_inputs[0]).addClass('has-date-picker');
+ jQuery(begin_td_inputs[1]).addClass('has-time-picker');
+ jQuery(begin_td_inputs[1]).timepicker({timeFormat: 'HH:mm'});
+ jQuery(begin_td_inputs[0]).val(begin_date);
+ jQuery(begin_td_inputs[1]).val(begin_time);
+
+ var end_td_inputs = jQuery(row_tds[user_td_index + 3]).children();
+ jQuery(end_td_inputs[0]).addClass('has-date-picker');
+ jQuery(end_td_inputs[1]).addClass('has-time-picker');
+ jQuery(end_td_inputs[1]).timepicker({timeFormat: 'HH:mm'});
+ jQuery(end_td_inputs[0]).val(end_date);
+ jQuery(end_td_inputs[1]).val(end_time);
+
+ }
+
+ var last_tr = jQuery(table_element).find('tr:last')[0];
+ if (!last_tr) {
+ //Something is wrong with the HTML.
+ return;
+ }
+ jQuery(last_tr).parent().append(new_row);
+
+ //Make the empty permission list message box
+ //invisible if it is still visible:
+ jQuery('#ResourceEmptyPermissionListMessage').addClass('invisible');
+
+ //Trigger a table update so that the tablesorter will re-sort
+ //the table:
+ 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;
+ }
+ if (data.name.suffix) {
+ username += ' ' + data.name.suffix;
+ }
+ username += ' (' + data.name.username +')'
+ + ' (' + data.perms + ')';
+ insert_function(user_id, username);
+ }).fail(function() {
+ insert_function(user_id);
+ });
+ }
+
+
+ static addCourseUsersToPermissionList(course_id, table_element)
+ {
+ if (!course_id || !table_element) {
+ return;
+ }
+
+ STUDIP.api.GET(
+ `course/${course_id}/members`,
+ {
+ //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.Resources.addUserToPermissionList(
+ user_id,
+ table_element
+ );
+ }
+ });
+ }
+
+
+ static removeUserFromPermissionList(html_node)
+ {
+ if (!html_node) {
+ return;
+ }
+
+ var row = jQuery(html_node).parent().parent();
+ var tbody = jQuery(row).parent();
+
+ STUDIP.Dialog.confirm(
+ $gettext('Soll die ausgewählte Berechtigung wirklich entfernt werden?')
+ ).done(function () {
+ jQuery(row).remove();
+ if (jQuery(tbody).children().length < 3) {
+ //No special permissions available: show the empty permission list
+ //message box:
+ jQuery('#ResourceEmptyPermissionListMessage').removeClass('invisible');
+ }
+ });
+ }
+
+
+ //Room search related methods:
+
+
+ static addSearchCriteriaToRoomSearchWidget(select_node)
+ {
+ if (!select_node) {
+ return;
+ }
+
+ var selected_option = jQuery(select_node).find(":selected")[0];
+ if (!selected_option) {
+ return;
+ }
+
+ var option_value = jQuery(selected_option).val();
+ if (!option_value) {
+ //The first option which is left blank intentionally
+ //has been selected.
+ return;
+ }
+ var option_title = jQuery(selected_option).attr('data-title');
+ var option_type = jQuery(selected_option).attr('data-type');
+ var option_select_options = jQuery(selected_option).attr('data-select_options').split(';;');
+ var option_range_search = jQuery(selected_option).attr('data-range-search');
+
+ var template = undefined;
+ if (option_type === 'bool') {
+ template = jQuery(select_node).parent().parent().find(
+ '.criteria-list .template[data-template-type="'
+ + option_type
+ + '"]'
+ )[0];
+ } else if (option_type === 'select') {
+ template = jQuery(select_node).parent().parent().find(
+ '.criteria-list .template[data-template-type="select"]'
+ )[0];
+ } else if (option_type === 'date') {
+ if (option_range_search) {
+ template = jQuery(select_node).parent().parent().find(
+ '.criteria-list .template[data-template-type="date_range"]'
+ )[0];
+ } else {
+ template = jQuery(select_node).parent().parent().find(
+ '.criteria-list .template[data-template-type="date"]'
+ )[0];
+ }
+ } else if (option_type === 'num') {
+ if (option_range_search) {
+ template = jQuery(select_node).parent().parent().find(
+ '.criteria-list .template[data-template-type="range"]'
+ )[0];
+ } else {
+ template = jQuery(select_node).parent().parent().find(
+ '.criteria-list .template[data-template-type="num"]'
+ )[0];
+ }
+ } else {
+ template = jQuery(select_node).parent().parent().find(
+ '.criteria-list .template[data-template-type="other"]'
+ )[0];
+ }
+
+ if (!template) {
+ return;
+ }
+
+ var criteria_list = jQuery(template).parent();
+
+ var new_criteria = jQuery(template).clone();
+ jQuery(new_criteria).attr('class', 'item');
+ jQuery(new_criteria).attr('data-criteria', option_value);
+
+ var new_criteria_text_field = jQuery(new_criteria).find('span')[0];
+ jQuery(new_criteria_text_field).text(option_title);
+
+ if (option_type === 'bool') {
+ var new_criteria_input = jQuery(new_criteria).find('input');
+ jQuery(new_criteria_input).attr('name', option_value);
+ } else if (option_type === 'select') {
+ var new_criteria_select = jQuery(new_criteria).find('select')[0];
+ jQuery(new_criteria_select).attr('name', option_value);
+ //Build the option elements from the data-options field:
+ if (!option_select_options) {
+ //Something is wrong.
+ return;
+ }
+ var options_html = '';
+ for (option of option_select_options) {
+ var splitted_option = option.split('~~');
+ options_html += '<option value="' + splitted_option[0] + '">'
+ + splitted_option[1]
+ + '</option>';
+ }
+ jQuery(new_criteria_select).html(options_html);
+ } else if (option_type === 'date') {
+ var time_inputs = jQuery(new_criteria).find('input[data-time="yes"]');
+ var date_inputs = jQuery(new_criteria).find('input[type="date"]');
+
+ if (time_inputs.length < 2) {
+ //Something is wrong with the HTML.
+ return;
+ }
+ var now = new Date();
+
+ jQuery(time_inputs[0]).attr('name', option_value + '_begin_time');
+ jQuery(time_inputs[1]).attr('name', option_value + '_end_time');
+ jQuery(time_inputs[0]).val(
+ now.getHours() + ':00'
+ );
+ jQuery(time_inputs[1]).val(
+ (now.getHours() + 2) + ':00'
+ );
+
+ if (option_range_search) {
+ //We must fill two date fields.
+ if (date_inputs.length < 2) {
+ //Something is wrong with the HTML.
+ return;
+ }
+
+ jQuery(date_inputs[0]).attr('name', option_value + '_begin_date');
+ jQuery(date_inputs[1]).attr('name', option_value + '_end_date');
+ jQuery(date_inputs[0]).val(
+ now.getFullYear() + '-'
+ + (now.getMonth() + 1) + '-'
+ + (now.getDate() + 1)
+ );
+ jQuery(date_inputs[1]).val(
+ now.getFullYear() + '-'
+ + (now.getMonth() + 1) + '-'
+ + (now.getDate() + 2)
+ );
+ } else {
+ //One date field, two time fields.
+ if (date_inputs.length < 1) {
+ //Something is wrong with the HTML.
+ return;
+ }
+ jQuery(date_inputs[0]).attr('name', option_value + '_date');
+ jQuery(date_inputs[0]).val(
+ now.getFullYear() + '-'
+ + (now.getMonth() + 1) + '-'
+ + (now.getDate() + 1)
+ );
+ }
+
+ } else {
+ if (option_type === 'num' && option_range_search) {
+ var new_criteria_inputs = jQuery(new_criteria).find('input');
+ jQuery(new_criteria_inputs[0]).attr('name', option_value);
+ var min_input = new_criteria_inputs[1];
+ var max_input = new_criteria_inputs[2];
+ jQuery(min_input).attr('name', option_value + '_min');
+ jQuery(min_input).attr('type', 'number');
+ jQuery(max_input).attr('name', option_value + '_max');
+ jQuery(max_input).attr('type', 'number');
+ jQuery(min_input).val(Math.round(parseInt(min_input) * 1.25));
+ jQuery(max_input).val(Math.round(parseInt(max_input) * 0.75));
+ } else {
+ var new_criteria_input = jQuery(new_criteria).find('input')[0];
+ jQuery(new_criteria_input).attr('name', option_value);
+ if (option_type === 'num') {
+ jQuery(new_criteria_input).attr('type', 'number');
+ } else {
+ jQuery(new_criteria_input).attr('type', 'text');
+ }
+ }
+ }
+
+ jQuery(criteria_list).append(new_criteria);
+
+ //hide the criteria in the select list:
+ jQuery(selected_option).addClass('invisible');
+ //set the select field to the first option:
+ jQuery(select_node).val('');
+ }
+
+
+ static removeSearchCriteriaFromRoomSearchWidget(icon_node)
+ {
+ if (!icon_node) {
+ return;
+ }
+
+ var input = jQuery(icon_node).parent().find('input');
+ var criteria_name = jQuery(input).attr('name');
+
+ var form = jQuery(icon_node).parents('form')[0];
+
+ if (!form) {
+ return;
+ }
+
+ var select_element = jQuery(form).find('select.criteria-selector');
+
+ jQuery(icon_node).parent().remove();
+
+ //enable the option in the select field:
+ var disabled_option = jQuery(select_element).find(
+ 'option[value="' + criteria_name + '"]'
+ )[0];
+
+ jQuery(disabled_option).removeClass('invisible');
+
+ //Trigger change event:
+ jQuery(form).find('.room-search-widget_criteria-list_input').trigger('change');
+ }
+
+
+ static submitRoomSearchWidgetForm(input_node)
+ {
+ if (!input_node) {
+ return;
+ }
+
+ //find the form element:
+ var form = jQuery(input_node).parents('form')[0];
+ if (!form) {
+ return;
+ }
+
+ jQuery(form).submit();
+ }
+
+
+ //Resource request related methods:
+
+
+ static addPropertyToRequest(event)
+ {
+ var select = jQuery(event.target).siblings('select.requestable-properties-select')[0];
+ if (!select) {
+ return;
+ }
+
+ var table = jQuery(event.target).parents('.resource-request-properties-table')[0];
+ if (!table) {
+ return;
+ }
+ var tbody = jQuery(table).find('tbody')[0];
+ if (!tbody) {
+ }
+
+ var selected_option = jQuery(select).find(':selected')[0];
+ if (!selected_option) {
+ return;
+ }
+
+ var property_id = jQuery(selected_option).val();
+ var option_html = jQuery(selected_option).data('input-html');
+ if (!property_id || !option_html) {
+ return;
+ }
+
+ var template = jQuery(tbody).find('tr.template')[0];
+ if (!template) {
+ return;
+ }
+
+ var new_row = jQuery(template).clone();
+ if (!new_row) {
+ return;
+ }
+
+ jQuery(new_row).removeClass('template');
+ jQuery(new_row).removeClass('invisible');
+ jQuery(new_row).attr('data-property_id', property_id);
+ var row_cells = jQuery(new_row).find('td');
+ jQuery(row_cells[0]).text(jQuery(selected_option).text());
+ jQuery(row_cells[1]).html(option_html);
+
+ jQuery(tbody).append(new_row);
+ jQuery(tbody).find('.empty-table-message').addClass('invisible');
+ jQuery(selected_option).attr('disabled', 'disabled');
+ jQuery(selected_option).removeAttr('selected');
+ jQuery(select).val([]);
+ }
+
+
+ //ResourceBookingInterval methods:
+
+
+ static toggleBookingIntervalStatus(event)
+ {
+ var li = jQuery(event.target).parents('tr')[0];
+ if (!li) {
+ //Something is wrong with the HTML.
+ return;
+ }
+ var interval_id = jQuery(li).data('interval_id');
+ if (!interval_id) {
+ return;
+ }
+
+ STUDIP.api.POST(
+ `resources/booking_interval/${interval_id}/toggle_takes_place`
+ ).done(function(data) {
+ if (data['takes_place'] === undefined) {
+ //Something went wrong: do nothing.
+ return;
+ }
+
+ if (data['takes_place'] === '1') {
+ //Switch on the icons and text for the "takes place"
+ //status and switch off the other ones:
+ jQuery(li).find('.takes-place-revive').addClass('invisible');
+ jQuery(li).find('.takes-place-delete').removeClass('invisible');
+ jQuery(li).find('.booking-list-interval-date').removeClass('not-taking-place');
+ } else {
+ //Do the opposite of the if-block above:
+ jQuery(li).find('.takes-place-delete').addClass('invisible');
+ jQuery(li).find('.takes-place-revive').removeClass('invisible');
+ jQuery(li).find('.booking-list-interval-date').addClass('not-taking-place');
+ }
+ });
+ }
+
+
+ //Methods for the resource category form:
+
+
+ static addResourcePropertyToTable(event)
+ {
+ var select = jQuery(event.target).siblings('select')[0];
+ if (!select) {
+ //Something is wrong with the HTML
+ return;
+ }
+
+ var selected_property_id = jQuery(select).val();
+ var selected_property = jQuery(select).children(
+ 'option:selected'
+ )[0];
+ if (!selected_property) {
+ return;
+ }
+ var selected_property_name = jQuery(selected_property).text();
+
+ if (!selected_property_id || !selected_property_name) {
+ //Invalid option
+ return;
+ }
+
+ var table = jQuery(event.target).parents(
+ 'table'
+ )[0];
+ if (!table) {
+ return;
+ }
+
+ var template = jQuery(table).find('tr.template')[0];
+ if (!template) {
+ return;
+ }
+
+ var new_row = jQuery(template).clone();
+ if (!new_row) {
+ return;
+ }
+
+ var columns = jQuery(new_row).find('td');
+ var text_field = jQuery(new_row).find('.name');
+ jQuery(text_field).text(selected_property_name);
+ var set_input = jQuery(new_row).find('.property-input');
+ jQuery(set_input).attr(
+ 'name',
+ 'prop[' + selected_property_id + ']'
+ );
+ var value_input = jQuery(new_row).find('.value-input');
+ jQuery(value_input).attr(
+ 'name',
+ 'prop_value[' + selected_property_id + ']'
+ );
+ var requestable_input = jQuery(new_row).find('.requestable-input');
+ jQuery(requestable_input).attr(
+ 'name',
+ 'prop_requestable[' + selected_property_id + ']'
+ );
+ var protected_input = jQuery(new_row).find('.protected-input');
+ jQuery(protected_input).attr(
+ 'name',
+ 'prop_protected[' + selected_property_id + ']'
+ );
+
+ var tbody = jQuery(table).find('tbody')[0];
+ if (!tbody) {
+ return;
+ }
+
+ jQuery(new_row).removeClass('invisible');
+ jQuery(new_row).removeClass('template');
+ jQuery(new_row).data('property_id', selected_property_id);
+ jQuery(tbody).append(new_row);
+ jQuery(selected_property).attr('disabled', 'disabled');
+ jQuery(selected_property).removeAttr('selected');
+ jQuery(select).val([]);
+ }
+
+
+ //Methods for opening or closing of ressource tree elements:
+
+
+ static toggleTreeNode(treenode)
+ {
+ var arr = treenode.children("img");
+ if (arr.hasClass('rotated')) {
+ arr.attr('style', 'transform: rotate(0deg)');
+ } else {
+ arr.attr('style', 'transform: rotate(90deg)');
+ }
+ arr.toggleClass('rotated') ;
+ treenode.children(".resource-tree").children("li").toggle();
+ }
+
+
+ static moveTimeOptions(bookingtype_val)
+ {
+ if(bookingtype_val === 'single') {
+ $(".time-option-container").hide();
+ $(".block-booking-item").hide();
+ $(".repetition-booking-item").hide();
+ $("#BookingStartDateInput").show();
+ $(".semester-selector").parent().hide();
+ $(".manual-time-option").prop('checked', true).trigger('change');
+ } else {
+ var time_options = $(".time-option-container");
+ $(".time-option-container").detach();
+ if(bookingtype_val === 'block') {
+ $("#BlockBookingFieldset").prepend(time_options);
+
+ $("#BlockEndLabel").show();
+ $("#RepetitionEndLabel").hide();
+
+ $(".block-booking-item").show();
+ $(".repetition-booking-item").hide();
+ } else {
+ $("#RepetitionBookingFieldset").prepend(time_options);
+
+ $("#RepetitionEndLabel").show();
+ $("#BlockEndLabel").hide();
+
+ $(".repetition-booking-item").show();
+ $(".block-booking-item").hide();
+ }
+ $(".time-option-container").show();
+ }
+ };
+
+
+ //Fullcalendar specialisations:
+
+
+ static updateEventUrlsInCalendar(calendar_event)
+ {
+ if (!calendar_event) {
+ return;
+ }
+
+ STUDIP.api.GET(
+ `resources/booking/${calendar_event.extendedProps.studip_parent_object_id}/intervals`,
+ {
+ data: {
+ begin: STUDIP.Fullcalendar.toRFC3339String(calendar_event.start),
+ end: STUDIP.Fullcalendar.toRFC3339String(calendar_event.end)
+ }
+ }
+ ).done(function (data) {
+ if (!data || (data.length == 0)) {
+ return;
+ }
+ var new_interval_id = data[0].interval_id;
+ calendar_event.setExtendedProp('studip_object_id', new_interval_id);
+ if (new_interval_id) {
+ var move_url = calendar_event.extendedProps.studip_api_urls['move'];
+ var resize_url = calendar_event.extendedProps.studip_api_urls['resize'];
+ move_url = move_url.replace(
+ /\&interval_id=([0-9a-f]{32})/,
+ '&interval_id=' + new_interval_id
+ );
+ resize_url = resize_url.replace(
+ /\&interval_id=([0-9a-f]{32})/,
+ '&interval_id=' + new_interval_id
+ );
+ var studip_api_urls = calendar_event.extendedProps.studip_api_urls;
+ studip_api_urls['move'] = move_url;
+ studip_api_urls['resize'] = resize_url;
+ calendar_event.setExtendedProp('studip_api_urls', studip_api_urls);
+ }
+ });
+ }
+
+
+ static resizeEventInRoomGroupBookingPlan(info)
+ {
+ STUDIP.Fullcalendar.defaultResizeEventHandler(info);
+ STUDIP.Resources.updateEventUrlsInCalendar(info.event);
+ }
+
+ static dropEventInRoomGroupBookingPlan(info)
+ {
+ STUDIP.Fullcalendar.defaultDropEventHandler(info);
+ STUDIP.Resources.updateEventUrlsInCalendar(info.event);
+ }
+
+
+ static updateBookingPlanSemesterByView(activeRange, api_url = 'api.php/semesters') {
+ var semester = null;
+ jQuery.ajax(
+ STUDIP.URLHelper.getURL(api_url),
+ {
+ method: 'get',
+ dataType: 'json',
+ success: function(data) {
+ if (data) {
+ var start = activeRange.start;
+ var end = activeRange.end;
+ Object.values(data.collection).forEach(item => {
+ if (start.getTime()/1000 >= item.seminars_begin && start.getTime()/1000 < item.seminars_end) {
+ semester = item;
+ }
+ });
+ if (semester) {
+ $(".booking-plan-header")
+ .data('semester-begin', semester.seminars_begin)
+ .data('semester-end', semester.seminars_end);
+ $("#booking-plan-header-semrow").show();
+ $("#booking-plan-header-semname").text(semester.title);
+ var sem_week = Math.floor((end.getTime()/1000 - 10800 - semester.seminars_begin) / 604800)+1;
+ $("#booking-plan-header-semweek-part").text("Vorlesungswoche".toLocaleString());
+ $("#booking-plan-header-semweek").text(sem_week);
+ } else {
+ if (data.pagination && data.pagination.links && data.pagination.links.next != api_url) {
+ semester = STUDIP.Resources.updateBookingPlanSemesterByView(activeRange, data.pagination.links.next);
+ } else {
+ $(".booking-plan-header")
+ .data('semester-begin', '')
+ .data('semester-end', '');
+ }
+ }
+
+ $('#booking-plan-header-calweek').text(start.getWeekNumber());
+ $('#booking-plan-header-calbegin').text(start.toLocaleDateString('de-DE', {weekday: 'short'}) + ' ' + start.toLocaleDateString('de-DE'));
+ $('#booking-plan-header-calend').text(end.toLocaleDateString('de-DE', {weekday: 'short'}) + ' ' + end.toLocaleDateString('de-DE'));
+ }
+ }
+ }
+ );
+ }
+
+
+ static toggleRequestMarked(source_node)
+ {
+ if (!source_node) {
+ return;
+ }
+
+ var request_id = jQuery(source_node).data('request_id');
+ if (!request_id) {
+ return;
+ }
+
+ STUDIP.api.POST(
+ `resources/request/${request_id}/toggle_marked`
+ ).done(function(data) {
+ jQuery(source_node).attr('data-marked', data.marked);
+ jQuery(source_node).parent().attr('data-sort-value', data.marked);
+ jQuery(source_node).parents('table.request-list').trigger('update');
+ });
+ }
+
+ static bookAllCalendarRequests()
+ {
+ var calendarSektion = $('*[data-resources-fullcalendar="1"]')[0];
+ if (calendarSektion) {
+ var calendar = calendarSektion.calendar;
+ if (calendar) {
+ if (!$('#loading-spinner').length) {
+ jQuery('#layout_content').append(
+ $('<div id="loading-spinner" style="position: absolute; top: calc(50% - 55px); left: calc(50% + 135px); z-index: 9001;">').html(
+ $('<img>').attr('src', STUDIP.ASSETS_URL + 'images/ajax-indicator-black.svg')
+ .css({
+ width: 64,
+ height: 64
+ })
+ )
+ );
+ }
+ $('.fc-request-event').each(function(){
+ var objectData = $(this).data();
+ var existingRequestEvent = calendar.getEventById(objectData.eventId);
+ if (existingRequestEvent) {
+ var bookingURL = 'dispatch.php/resources/room_request/quickbook/'
+ + objectData.eventRequest +'/'
+ + objectData.eventResource +'/'
+ + objectData.eventMetadate;
+ jQuery.ajax(
+ STUDIP.URLHelper.getURL(bookingURL),
+ {
+ method: 'get',
+ dataType: 'json',
+ async: false,
+ success: function(data) {
+ if (data) {
+ }
+ }
+ }
+ );
+ }
+ });
+ document.location.reload(true);
+ }
+ }
+ }
+
+};
+
+
+//Class properties:
+
+
+Resources.definedResourceClasses = [
+ 'Resource', 'Room', 'Building', 'Location'
+];
+
+
+class Messages
+{
+ static selectRoom(room_id, room_name)
+ {
+ if (!room_id) {
+ return;
+ }
+
+ var selection_area = jQuery('.resources_messages-form .selection-area')[0];
+ if (!selection_area) {
+ return;
+ }
+
+ var template = jQuery(selection_area).find('.template')[0];
+ if (!template) {
+ return;
+ }
+
+ var new_room = jQuery(template).clone();
+ jQuery(new_room).removeClass('template');
+ jQuery(new_room).removeClass('invisible');
+ jQuery(new_room).find('span').text(room_name);
+ jQuery(new_room).find('input[type="hidden"]').val(room_id);
+ jQuery(selection_area).append(new_room);
+ }
+}
+Resources.Messages = Messages;
+
+
+class BookingPlan
+{
+ static insertEntry(new_entry, date, begin_hour, end_hour)
+ {
+ //Get the resource-ID from the current URL:
+ var results = window.location.href.match(
+ /dispatch.php\/resources\/resource\/booking_plan\/([a-z0-9]{1,32})/
+ );
+ if (results.length === 0) {
+ //No resource-ID found.
+ jQuery(new_entry).remove();
+ return;
+ }
+ var resource_id = results[1];
+
+ //Now we re-format the time from begin_hour and end_hour.
+ //In case the data-dragged attribute is set for the
+ //calendar entry we just add two hours to the start time
+ //to get the end time.
+
+ var dragged = jQuery(new_entry).data('dragged');
+ if (dragged) {
+ end_hour = begin_hour + 2;
+ }
+ begin_hour += ':00';
+ if (end_hour > 23) {
+ end_hour = '23:59';
+ } else {
+ end_hour += ':00';
+ }
+
+ var result = STUDIP.Dialog.fromURL(
+ STUDIP.URLHelper.getURL(
+ 'dispatch.php/resources/booking/add/' + resource_id,
+ {
+ 'begin_date': date,
+ 'begin_time': begin_hour,
+ 'end_date': date,
+ 'end_time': end_hour
+ }
+ ), {size: 'auto'}
+ );
+ }
+}
+Resources.BookingPlan = BookingPlan;
+
+
+export default Resources;
diff --git a/resources/assets/javascripts/lib/responsive.js b/resources/assets/javascripts/lib/responsive.js
new file mode 100644
index 0000000..09712dd
--- /dev/null
+++ b/resources/assets/javascripts/lib/responsive.js
@@ -0,0 +1,156 @@
+/*jslint esversion: 6*/
+
+import HeaderMagic from './header_magic.js';
+import Sidebar from './sidebar.js';
+
+const Responsive = {
+ media_query: window.matchMedia('(max-width: 767px)'),
+
+ // Builds a dom element from a navigation object
+ buildMenu (navigation, id, activated) {
+ var list = $('<ul>');
+
+ if (id) {
+ list.attr('id', id);
+ }
+
+ // TODO: Templating?
+ _.forEach(navigation, (nav, node) => {
+ nav.url = STUDIP.URLHelper.getURL(nav.url, {}, true);
+ let li = $('<li class="navigation-item">');
+ let title = $('<div class="nav-title">').appendTo(li);
+ let link = $(`<a href="${nav.url}">`).text(nav.title).appendTo(title);
+
+ if (nav.icon) {
+ if (!nav.icon.match(/^https?:\/\//)) {
+ nav.icon = STUDIP.ASSETS_URL + nav.icon;
+ }
+ $(link).prepend(`<img class="icon" src="${nav.icon}">`);
+ }
+
+ if (nav.children) {
+ let active = activated.indexOf(node) !== -1;
+ $(`<input type="checkbox" id="resp/${node}">`)
+ .prop('checked', active)
+ .appendTo(li);
+ li.append(
+ `<label class="nav-label" for="resp/${node}"> </label>`,
+ Responsive.buildMenu(nav.children, false, activated)
+ );
+ }
+
+ list.append(li);
+ });
+
+ return list;
+ },
+
+ // Adds the responsive menu to the dom
+ addMenu () {
+ let wrapper = $('<div id="responsive-container">').append(
+ '<label for="responsive-toggle">',
+ '<input type="checkbox" id="responsive-toggle">',
+ Responsive.buildMenu(
+ STUDIP.Navigation.navigation,
+ 'responsive-navigation',
+ STUDIP.Navigation.activated
+ ),
+ '<label for="responsive-toggle">'
+ );
+
+ $('<li>', { html: wrapper }).prependTo('#barBottomright > ul');
+ },
+
+ // Responsifies the layout. Builds the responsive menu from existing
+ // STUDIP.Navigation object
+ responsify () {
+ Responsive.media_query.removeListener(Responsive.responsify);
+
+ $('html').addClass('responsified');
+
+ Responsive.addMenu();
+
+ if ($('#layout-sidebar > section').length > 0) {
+ $('<li id="sidebar-menu">')
+ .on('click', () => Sidebar.open())
+ .appendTo('#barBottomright > ul');
+
+ $('<label id="sidebar-shadow-toggle">')
+ .on('click', () => Sidebar.close())
+ .prependTo('#layout-sidebar');
+
+ $('#responsive-toggle').on('change', function() {
+ $('#layout-sidebar').removeClass('visible-sidebar');
+ $('#responsive-navigation').toggleClass('visible', this.checked);
+ });
+ } else {
+ $('#responsive-toggle').on('change', function() {
+ $('#responsive-navigation').toggleClass('visible', this.checked);
+ });
+ }
+
+ $('#responsive-navigation :checkbox').on('change', function () {
+ let li = $(this).closest('li');
+ if ($(this).is(':checked')) {
+ li.siblings().find(':checkbox:checked').prop('checked', false);
+ }
+
+ // Force redraw of submenu (at least ios safari/chrome would
+ // not show it without a forced redraw)
+ $(this).siblings('ul').hide(0, function () {
+ $(this).show();
+ });
+ }).reverse().trigger('change');
+
+ var sidebar_avatar_menu = $('<div class="sidebar-widget sidebar-avatar-menu">');
+ var avatar_menu = $('#header_avatar_menu');
+ var title = $('.action-menu-title', avatar_menu).text();
+ var list = $('<ul class="widget-list widget-links">');
+ $('<div class="sidebar-widget-header">').text(title).appendTo(sidebar_avatar_menu);
+
+ $('.action-menu-item', avatar_menu).each(function() {
+ var src = $('img', this).attr('src');
+ var link = $('a', this).clone();
+
+ link.find('img').remove();
+
+ $('<li>').append(link).css({
+ backgroundSize: '16px',
+ backgroundImage: `url(${src})`
+ }).appendTo(list);
+ });
+
+ $('<div class="sidebar-widget-content">')
+ .append(list)
+ .appendTo(sidebar_avatar_menu);
+
+ $('#layout-sidebar > .sidebar').prepend(sidebar_avatar_menu);
+ },
+
+ setResponsiveDisplay (state = true) {
+ $('html').toggleClass('responsive-display', state);
+
+ if (state) {
+ Sidebar.disableSticky();
+ HeaderMagic.disable();
+ } else {
+ Sidebar.enableSticky();
+ HeaderMagic.enable();
+ }
+ },
+
+ engage () {
+ if (Responsive.media_query.matches) {
+ Responsive.responsify();
+ Responsive.setResponsiveDisplay();
+ } else {
+ Responsive.media_query.addListener(Responsive.responsify);
+ }
+
+ Responsive.media_query.addListener(() => {
+ Responsive.setResponsiveDisplay(Responsive.media_query.matches);
+ });
+ }
+};
+
+export default Responsive;
diff --git a/resources/assets/javascripts/lib/restapi.js b/resources/assets/javascripts/lib/restapi.js
new file mode 100644
index 0000000..b6e31df
--- /dev/null
+++ b/resources/assets/javascripts/lib/restapi.js
@@ -0,0 +1,12 @@
+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/schedule.js b/resources/assets/javascripts/lib/schedule.js
new file mode 100644
index 0000000..b7c9d37
--- /dev/null
+++ b/resources/assets/javascripts/lib/schedule.js
@@ -0,0 +1,254 @@
+import { $gettext } from './gettext.js';
+import Calendar from './calendar.js';
+import Dialog from './dialog.js';
+
+const Schedule = {
+ inst_changed: false,
+
+ /**
+ * this function is called, when an entry shall be created in the calendar
+ *
+ * @param object the empty entry in the calendar
+ * @param int the day that has been clicked
+ * @param int the start-hour that has been clicked
+ * @param int the end-hour that has been chosen
+ */
+ newEntry: function(entry, day, start_hour, end_hour) {
+ /*
+ // do not allow creation of new entry, if one of the following popups is visible!
+ if (jQuery('#edit_sem_entry').is(':visible') ||
+ jQuery('#edit_entry').is(':visible') ||
+ jQuery('#edit_inst_entry').is(':visible')) {
+ jQuery(entry).remove();
+ return;
+ }
+ */
+
+ // if there is already an entry set, kick him first before showing a new one
+ if (this.entry) {
+ jQuery(this.entry).fadeOut('fast');
+ jQuery(this.entry).remove();
+ }
+
+ this.entry = entry;
+
+ if (!Schedule.new_entry_template) {
+ jQuery.get(STUDIP.URLHelper.getURL('dispatch.php/calendar/schedule/entry'), function(data) {
+ Schedule.new_entry_template = data;
+ Schedule.showEntryDialog(Schedule.new_entry_template, day, start_hour, end_hour);
+ });
+ } else {
+ Schedule.showEntryDialog(Schedule.new_entry_template, day, start_hour, end_hour);
+ }
+ },
+
+ /**
+ * this function is called, when an entry shall be created in the calendar
+ * and the template-data has been loaded
+ *
+ * @param string the html for the new-entry dialog
+ * @param int the day that has been clicked
+ * @param int the start-hour that has been clicked
+ * @param int the end-hour that has been chosen
+ */
+ showEntryDialog: function(template, day, start_hour, end_hour) {
+ // do not open dialog, if no new-entry-marker is present
+ if ($('#schedule_entry_new').length === 0) return;
+
+ Dialog.show(template, {
+ title: $gettext('Neuen Termin eintragen'),
+ origin: this
+ });
+
+ $(this).on('dialog-close', function() {
+ $('#schedule_entry_new').remove();
+ });
+
+ // fill values of overlay
+ jQuery('input[name=entry_start]').val(start_hour + ':00');
+ jQuery('input[name=entry_end]').val(end_hour + ':00');
+ jQuery('select[name=entry_day]').val(parseInt(day) + 1);
+ },
+
+ /**
+ * this function morphs from the quick-add box for adding a new entry to the schedule
+ * to the larger box with more details to edit
+ *
+ * @return: void
+ */
+ showDetails: function() {
+ // set the values for detailed view
+ jQuery('select[name=entry_day]').val(Number(jQuery('#new_entry_day').val()) + 1);
+ jQuery('input[name=entry_start_hour]').val(parseInt(jQuery('#new_entry_start_hour').val(), 10));
+ jQuery('input[name=entry_start_minute]').val('00');
+ jQuery('input[name=entry_end_hour]').val(parseInt(jQuery('#new_entry_end_hour').val(), 10));
+ jQuery('input[name=entry_end_minute]').val('00');
+
+ jQuery('input[name=entry_title]').val(jQuery('#entry_title').val());
+ jQuery('textarea[name=entry_content]').val(jQuery('#entry_content').val());
+
+ jQuery('#edit_entry_drag').html(jQuery('#new_entry_drag').html());
+
+ // morph to the detailed view
+ jQuery('#schedule_new_entry').animate(
+ {
+ left: Math.floor(jQuery(window).width() / 4), // for safari
+ width: '50%',
+ top: '180px'
+ },
+ 500,
+ function() {
+ jQuery('#edit_entry').fadeIn(400, function() {
+ // reset the box
+ jQuery('#schedule_new_entry').css({
+ display: 'none',
+ left: 0,
+ width: '400px',
+ top: 0,
+ height: '230px',
+ 'margin-left': 0
+ });
+ });
+ }
+ );
+ },
+
+ /**
+ * show a popup conatining the details of the passed seminar
+ * at the passed cycle
+ *
+ * @param string the seminar to be shown
+ * @param string the cycle-id of the regular time-entry to be shown
+ * (a seminar can have multiple of these
+ */
+ showSeminarDetails: function(seminar_id, cycle_id) {
+ jQuery.get(
+ STUDIP.URLHelper.getURL('dispatch.php/calendar/schedule/entryajax/' + seminar_id + '/' + cycle_id),
+ function(data) {
+ Dialog.show(data, {
+ title: $gettext('Veranstaltungsdetails')
+ });
+ }
+ );
+
+ Calendar.click_in_progress = false;
+ },
+
+ /**
+ * show a popup with the details of a regular schedule entry with passed id
+ *
+ * @param string the id of the schedule-entry
+ */
+ showScheduleDetails: function(id) {
+ jQuery.get(STUDIP.URLHelper.getURL('dispatch.php/calendar/schedule/entry/' + id), function(data) {
+ Dialog.show(data, {
+ title: $gettext('Termindetails bearbeiten')
+ });
+ });
+
+ Calendar.click_in_progress = false;
+ },
+
+ /**
+ * show a popup with the details of a group entry, containing several seminars
+ *
+ * @param string the id of the grouped entry to be displayed
+ */
+ showInstituteDetails: function(id) {
+ jQuery.get(STUDIP.URLHelper.getURL('dispatch.php/calendar/schedule/groupedentry/' + id + '/true'), function(
+ data
+ ) {
+ Dialog.show(data, {
+ title: $gettext('Veranstaltungsdetails')
+ });
+ });
+
+ Calendar.click_in_progress = false;
+ },
+
+ /**
+ * hide a seminar-entry in the schedule (admin-version)
+ *
+ * @param string the seminar to be shown
+ * @param string the cycle-id of the regular time-entry to be shown
+ * (a seminar can have multiple of these
+ */
+ instSemUnbind: function(seminar_id, cycle_id) {
+ Schedule.inst_changed = true;
+ jQuery.ajax({
+ type: 'GET',
+ url: STUDIP.URLHelper.getURL(
+ 'dispatch.php/calendar/schedule/adminbind/' + seminar_id + '/' + cycle_id + '/0/true'
+ )
+ });
+
+ jQuery('#' + seminar_id + '_' + cycle_id + '_hide').fadeOut('fast', function() {
+ jQuery('#' + seminar_id + '_' + cycle_id + '_show').fadeIn('fast');
+ });
+ },
+
+ /**
+ * make a hidden seminar-entry visible in the schedule again
+ *
+ * @param string the seminar to be shown
+ * @param string the cycle-id of the regular time-entry to be shown
+ * (a seminar can have multiple of these
+ */
+ instSemBind: function(seminar_id, cycle_id) {
+ Schedule.inst_changed = true;
+ jQuery.ajax({
+ type: 'GET',
+ url: STUDIP.URLHelper.getURL(
+ 'dispatch.php/calendar/schedule/adminbind/' + seminar_id + '/' + cycle_id + '/1/true'
+ )
+ });
+
+ jQuery('#' + seminar_id + '_' + cycle_id + '_show').fadeOut('fast', function() {
+ jQuery('#' + seminar_id + '_' + cycle_id + '_hide').fadeIn('fast');
+ });
+ },
+
+ /**
+ * hide the popup of grouped-entry, containing a list of seminars.
+ * returns true if the visiblity of one of the entries has been changed,
+ * false otherwise
+ *
+ * @param object the element to be hidden
+ *
+ * @return bool true if the visibility of one seminar hase changed, false otherwise
+ */
+ hideInstOverlay: function(element) {
+ if (Schedule.inst_changed) {
+ return true;
+ }
+ jQuery(element).fadeOut('fast');
+
+ Calendar.click_in_progress = false;
+
+ return false;
+ },
+
+ /**
+ * calls Calendar.checkTimeslot to check that the time is valid
+ *
+ * @param bool returns true if the time is valid, false otherwise
+ */
+ checkFormFields: function() {
+ if (
+ !Calendar.checkTimeslot(
+ jQuery('#schedule_entry_hours > input[name=entry_start_hour]'),
+ jQuery('#schedule_entry_hours > input[name=entry_start_minute]'),
+ jQuery('#schedule_entry_hours > input[name=entry_end_hour]'),
+ jQuery('#schedule_entry_hours > input[name=entry_end_minute]')
+ )
+ ) {
+ jQuery('#schedule_entry_hours').addClass('invalid');
+ jQuery('#schedule_entry_hours > span[class=invalid_message]').show();
+ return false;
+ }
+
+ return true;
+ }
+};
+
+export default Schedule;
diff --git a/resources/assets/javascripts/lib/scroll.js b/resources/assets/javascripts/lib/scroll.js
new file mode 100644
index 0000000..a4d24d5
--- /dev/null
+++ b/resources/assets/javascripts/lib/scroll.js
@@ -0,0 +1,59 @@
+/**
+ * Provides means to hook into the scroll event. Registered callbacks are
+ * called with the current scroll top and scroll left position so both
+ * vertical and horizontal scroll events can be handled.
+ *
+ * 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;
+
+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();
+}
+
+function refresh() {
+ var hasHandlers = !$.isEmptyObject(handlers);
+ if (!hasHandlers && animId !== false) {
+ window.cancelAnimationFrame(animId);
+ animId = false;
+ } else if (hasHandlers && animId === false) {
+ animId = window.requestAnimationFrame(scrollHandler);
+ }
+}
+
+function engageScrollTrigger() {
+ $(window).off('scroll.studip-handler');
+ $(window).one('scroll.studip-handler', refresh);
+}
+
+const Scroll = {
+ addHandler(index, handler) {
+ handlers[index] = handler;
+ engageScrollTrigger();
+ },
+ removeHandler(index) {
+ delete handlers[index];
+ engageScrollTrigger();
+ }
+};
+
+export default Scroll;
diff --git a/resources/assets/javascripts/lib/scroll_to_top.js b/resources/assets/javascripts/lib/scroll_to_top.js
new file mode 100644
index 0000000..2a75402
--- /dev/null
+++ b/resources/assets/javascripts/lib/scroll_to_top.js
@@ -0,0 +1,38 @@
+import Scroll from './scroll.js';
+
+let fold;
+let was_below_the_fold = false;
+
+const back_to_top = function(scrolltop) {
+ var is_below_the_fold = scrolltop > fold;
+ if (is_below_the_fold !== was_below_the_fold) {
+ $('#scroll-to-top').toggleClass('hide', !is_below_the_fold);
+ was_below_the_fold = is_below_the_fold;
+ }
+};
+
+const ScrollToTop = {
+ enable() {
+ var minScrollHeight = Math.min(
+ document.body.scrollHeight, document.documentElement.scrollHeight,
+ document.body.offsetHeight, document.documentElement.offsetHeight,
+ document.body.clientHeight, document.documentElement.clientHeight
+ );
+ fold = minScrollHeight - (minScrollHeight / 5); // top of fifth portion!
+ Scroll.addHandler('back-to-top', back_to_top);
+ },
+ disable() {
+ Scroll.removeHandler('header');
+ $('#scroll-to-top').addClass('hide');
+ },
+ moveBack() {
+ $('#scroll-to-top').on('click', function(e) {
+ $('html, body').stop().animate({
+ scrollTop: (0)
+ }, 1000, 'easeInOutExpo');
+ e.preventDefault();
+ });
+ }
+};
+
+export default ScrollToTop;
diff --git a/resources/assets/javascripts/lib/search.js b/resources/assets/javascripts/lib/search.js
new file mode 100644
index 0000000..38894f7
--- /dev/null
+++ b/resources/assets/javascripts/lib/search.js
@@ -0,0 +1,566 @@
+var cache = null;
+
+const Search = {
+ lastSearch: null,
+ lastSearchFilter: null,
+ resultsInCategory: false,
+ searchTermLength: 3,
+
+ getCache: function () {
+ if (cache === null) {
+ let prefix = '';
+ if ($('meta[name="studip-cache-prefix"]').length > 0) {
+ prefix = $('meta[name="studip-cache-prefix"]').attr('content');
+ }
+ cache = STUDIP.Cache.getInstance(prefix);
+ }
+ return cache;
+ },
+
+ /**
+ * This function starts the actual search via AJAX call.
+ *
+ * @param {Object} filter object with filter information (e.g. 'category', 'semester', etc.)
+ * that is set by the filter selects in the sidebar.
+ */
+ doSearch: function (filter) {
+
+ var cache = STUDIP.Search.getCache();
+ var searchterm = $('#search-input').val().trim() || cache.get('searchterm');
+ var hasValue = searchterm && searchterm.length >= STUDIP.Search.searchTermLength;
+ var resultsDiv = $('#search-results');
+ var wrapper = $('#search');
+ const data = resultsDiv.data();
+ const limit = 100;
+
+ if (searchterm === '') {
+ return;
+ }
+
+ if (!hasValue) {
+ $('#search-term-invalid .searchtermlen').text(STUDIP.Search.searchTermLength);
+ $('#search-term-invalid').show();
+ } else {
+ $('#search-term-invalid').hide();
+ }
+
+ if (!hasValue || STUDIP.Search.lastSearch === searchterm
+ && JSON.stringify(STUDIP.Search.lastSearchFilter) === JSON.stringify(filter)) {
+ return;
+ }
+
+ STUDIP.Search.resultsInCategory = false;
+
+ $('#search-no-result').hide();
+ $('#reset-search').show();
+
+ STUDIP.Search.resetSearchCategories();
+ STUDIP.Search.greyOutSearchCategories();
+
+ cache.set('searchterm', searchterm);
+ STUDIP.Search.lastSearch = searchterm;
+ STUDIP.Search.lastSearchFilter = filter;
+
+ // Display spinner symbol, user should always see something is happening.
+ wrapper.addClass('is-searching');
+
+ // Call AJAX endpoint and get search results.
+ $.getJSON(STUDIP.URLHelper.getURL('dispatch.php/globalsearch/find/' + limit), {
+ search: searchterm,
+ filters: JSON.stringify(filter)
+ }).done(function (json) {
+ // Trigger searched event (regardless of successful or not)
+ $(document).trigger('searched.studip', {
+ needle: searchterm,
+ category: STUDIP.Search.getActiveCategory()
+ });
+
+ resultsDiv.empty();
+
+ // No results found...
+ if (!$.isPlainObject(json) || $.isEmptyObject(json)) {
+ wrapper.removeClass('is-searching');
+ $('#search-no-result .searchterm').text(searchterm);
+ $('#search-no-result').show();
+ STUDIP.Search.setActiveCategory('show_all_categories');
+ return;
+ }
+
+ // Iterate over each result category.
+ $.each(json, function (name, value) {
+ var category = STUDIP.Search.printCategory(name, value, data);
+ resultsDiv.append(category);
+ });
+
+ if (STUDIP.Search.getActiveCategory()
+ && STUDIP.Search.getActiveCategory() !== 'show_all_categories')
+ {
+ STUDIP.Search.expandCategory(STUDIP.Search.getActiveCategory());
+ if (!STUDIP.Search.resultsInCategory) {
+ $('#search-no-result .searchterm').text(searchterm);
+ $('#search-no-result').show();
+ }
+ }
+
+ wrapper.removeClass('is-searching');
+ }).fail(function (xhr, status, error) {
+ if (error) {
+ window.alert(error);
+ }
+ });
+ },
+
+ printCategory: function (name, value, data) {
+ // Create an <article> for category.
+ var allResultsText = data.allResults;
+ var category = $(`<article id="search-${name}" class="studip padding-less">`);
+ var header = $('<header>').appendTo(category);
+ var categoryBodyDiv = $(`<div id="${name}-body">`).appendTo(category);
+ var counter = 0;
+ var isActive = STUDIP.Search.getActiveCategory() === name;
+
+ if (isActive) {
+ STUDIP.Search.resultsInCategory = true;
+ }
+
+ // Create header name
+ $(`<h1 class="search-category" data-category="${name}">`)
+ .append(`<a href="#">${value.name}</a>`)
+ .appendTo(header);
+
+ if (value.more) {
+ $(`<div id="show-all-categories-${name}" class="search-more-results">`)
+ .append(`<a href="#">${allResultsText}</a>`)
+ .toggle(isActive)
+ .appendTo(header);
+ }
+
+ // Process results and create corresponding entries.
+ $.each(value.content, function (index, result) {
+ STUDIP.Search.printSingleResult(name, data, result, counter, value.fullsearch, categoryBodyDiv);
+ counter += 1;
+ });
+
+ $(`a#search_category_${name}`)
+ .removeClass('no-result')
+ .text(`${value.name} (${counter}${value.plus ? '+' : ''})`);
+
+ // We have more search results than shown, provide link to
+ // full search if available.
+ if (value.more) {
+ var footer = $('<footer class="search-more-results">');
+ $(`<a id="link_all_results_${name}" href="#">`).text(`alle ${counter} ${value.name} anzeigen`)
+ .click(function() {
+ STUDIP.Search.toggleLinkText(name);
+ STUDIP.Search.expandCategory(name);
+ STUDIP.Search.setActiveCategory(name);
+ })
+ .toggle(!isActive)
+ .appendTo(footer);
+ $(`<a id="link_results_${name}" href="#">`).text(allResultsText).hide()
+ .click(function() {
+ STUDIP.Search.toggleLinkText(name);
+ STUDIP.Search.showAllCategories(name);
+ STUDIP.Search.setActiveCategory(name);
+ })
+ .toggle(isActive)
+ .appendTo(footer);
+ footer.appendTo(category);
+ }
+
+ return category;
+ },
+
+ printSingleResult: function(categoryName, data, result, counter, fullsearch, categoryBodyDiv) {
+ var resultsPerType = data.resultsPerType;
+ var hasSubcourses = (categoryName === 'GlobalSearchMyCourses' || categoryName === 'GlobalSearchCourses') && result.has_children;
+ var addIcon = data.imgAdd;
+ var removeIcon = data.imgRemove;
+ // Create single result entry.
+ var single = $('<section>');
+ var data = $('<div class="search-result-data">');
+ var details = $('<div class="search-result-details">');
+ var information = $('<div class="search-result-information">');
+
+ if (counter >= resultsPerType) {
+ single.addClass('search-extended-result');
+ }
+ // Which result types should be opened via dialog?
+ const openInDialog = ['GlobalSearchFiles', 'GlobalSearchMessages'];
+ var dataDialog = (openInDialog.indexOf(categoryName) >= 0 ? dataDialog = 'data-dialog' : dataDialog = '');
+ var link = $(`<a href="${result.url}" ${dataDialog}>`)
+ .appendTo(single);
+
+ // Optional image...
+ if (result.img !== null) {
+ $('<div class="search-result-img hidden-tiny-down">')
+ .append(`<img src="${result.img}">`)
+ .appendTo(link);
+ }
+
+ link.append(data);
+
+ // add/remove icon for courses with sub courses
+ if (hasSubcourses) {
+ // initially show the 'add' icon
+ $(`<a href="#" id="show-subcourses-${result.id}" class="search-has-subcourses">`)
+ .click(function(e) {
+ STUDIP.Search.showSubcourses(result.id);
+ e.preventDefault();
+ })
+ .html(addIcon)
+ .appendTo(data);
+ // initially hide the 'remove' icon
+ $(`<a href="#" id="hide-subcourses-${result.id}" class="search-has-subcourses">`)
+ .click(function(e) {
+ STUDIP.Search.hideSubcourses(result.id);
+ e.preventDefault();
+ })
+ .html(removeIcon)
+ .appendTo(data)
+ .hide();
+ }
+
+ // Name/title
+ $('<div class="search-result-title">')
+ .html(result.name)
+ .appendTo(data);
+
+ if (result.number !== null) {
+ $('<div class="search-result-number">')
+ .html(result.number)
+ .appendTo(details);
+ }
+
+ // Details: Descriptional text
+ if (result.description !== null) {
+ $('<div class="search-result-description">')
+ .html(result.description)
+ .appendTo(details);
+ }
+
+ if (result.dates !== null) {
+ $('<div class="search-result-dates">')
+ .html(result.dates)
+ .appendTo(details);
+ }
+
+ data.append(details);
+
+ // Date/Time of entry
+ if (result.date !== null) {
+ $('<div class="search-result-time">')
+ .html(result.date)
+ .appendTo(information);
+ }
+
+ // Course Admission State as Img
+ if (result.admission_state !== null) {
+ $('<div class="search-result-admission-state">')
+ .html(result.admission_state)
+ .appendTo(information);
+ }
+
+
+ // Details: Additional information
+ var additional = $('<div class="search-result-additional">');
+ if (result.additional !== null) {
+ additional.html(result.additional);
+
+ // "Expand" attribute for further, result-related search
+ // (e.g. search in course of found forum entry)
+ if (result.expand !== null && result.expand !== fullsearch) {
+ additional.wrapInner(`<a href="${result.expand}" title="${result.expandtext}">`);
+ }
+ additional.appendTo(information);
+ }
+
+ link.append(information);
+
+ categoryBodyDiv.append(single);
+
+ if (hasSubcourses) {
+ $.each(result.children, function(key, child) {
+ var subcourse = STUDIP.Search.printSingleResult(name, data, child, counter, fullsearch, categoryBodyDiv);
+ subcourse.addClass('search-is-subcourse');
+ subcourse.addClass(`search-subcourse-${result.id}`);
+ subcourse.hide();
+ });
+ }
+
+ return single;
+ },
+
+ /**
+ * Clear search term and category from the cache,
+ * reload the page and reset the active category.
+ */
+ resetSearch: function () {
+ var cache = STUDIP.Search.getCache();
+ STUDIP.Search.lastSearch = null;
+ cache.remove('searchterm');
+ cache.remove('search_category');
+ // reload without parameters
+ if (location.href.includes('?')) {
+ location = location.href.split('?')[0];
+ } else {
+ location.reload();
+ }
+ STUDIP.Search.setActiveCategory('show_all_categories');
+ },
+
+ /**
+ * Show all possible categories in the sidebar without result numbers.
+ */
+ resetSearchCategories: function () {
+ $('a[id^="search_category_"]').each(function () {
+ var category = $(this).text();
+ if (category.includes('(')) {
+ category = category.substr(0, category.indexOf('(') - 1);
+ $(this).text(category);
+ }
+ }).show();
+ },
+
+ /**
+ * Grey out all categories in the sidebar with no results.
+ */
+ greyOutSearchCategories: function () {
+ $('a[id^="search_category_"]').addClass('no-result');
+ },
+
+ /**
+ * Hide all select filters in the sidebar.
+ */
+ hideAllFilters: function () {
+ $('div[id$="_filter"]').hide();
+ },
+
+ /**
+ * Show the select filters for a given category in the sidebar. Default: semester filter.
+ *
+ * @param {string} category Given category for which specific select filters should be shown.
+ */
+ showFilter: function (category) {
+ var filters = $('#search-results').data('filters');
+ STUDIP.Search.hideAllFilters();
+ if (filters && filters.hasOwnProperty(category) && category != 'show_all_categories') {
+ for (let i = 0; i < filters[category].length; i++) {
+ $(`#${filters[category][i]}_filter`).show();
+ }
+ } else if (category === 'show_all_categories') {
+ $('#semester_filter').show();
+ }
+ },
+
+ /**
+ * Set the specified category active (highlighted) in the sidebar.
+ * <li class="active">
+ *
+ * @param {string} category Given category which should be highlighted in the sidebar.
+ */
+ setActiveCategory: function (category) {
+ var cache = STUDIP.Search.getCache();
+ cache.set('search_category', category);
+ // remove all active classes
+ $('#show_all_categories').closest('li').removeClass('active');
+ $('a[id^="search_category_"]').closest('li').removeClass('active');
+
+ // set clicked class active
+ if (category === 'show_all_categories') {
+ $('#show_all_categories').closest('li').addClass('active');
+ } else {
+ $(`#search_category_${category}`).closest('li').addClass('active');
+ }
+ STUDIP.Search.showFilter(category);
+
+ $(document).trigger('search-category-change.studip', {category: category});
+ },
+
+ /**
+ * Get the current values from the filter selects in the sidebar that are relevant.
+ *
+ * @return {Object} filter object with the filter values set by the user.
+ */
+ getFilter: function () {
+ var filters = $('#search-results').data('filters');
+ var category = STUDIP.Search.getActiveCategory();
+ var filter = {category: category};
+ if (filters !== undefined) {
+ var active_filters = filters[category];
+ $('select[id$="_select"]').each(function () {
+ var selected = this.id.substr(0, this.id.lastIndexOf('_'));
+ if ($.inArray(selected, active_filters) !== -1) {
+ filter[selected] = $('option:selected', this).val();
+ }
+ });
+ }
+ return filter;
+ },
+
+ /**
+ * Set a specific sidebar filter select to the given value.
+ *
+ * @param {string} filter filter that should be set.
+ * @param {string} value value that the filter should be set to.
+ */
+ setFilter: function (filter, value) {
+ $(`#${filter}_select`).val(value);
+ },
+
+ /**
+ * Reset all sidebar filters except for the semester filter to their default value ('all').
+ */
+ resetFilters: function () {
+ $('select[id$="_select"]').not('#semester_select').val('').change();
+ },
+
+ /**
+ * Getter for the selected (active) category.
+ *
+ * @return {string} The active (currently selected) category in the sidebar widget.
+ */
+ getActiveCategory: function () {
+ var cache = STUDIP.Search.getCache();
+ return cache.get('search_category');
+ },
+
+ /**
+ * Toggle the link text for 'show all' results of one category and 'show all categories'
+ * with max. 3 results each.
+ *
+ * @param {string} category Category for which the link text should be toggled
+ */
+ toggleLinkText: function (category) {
+ var visible = $(`a#link_all_results_${category}`).is(':visible');
+ $(`a#link_all_results_${category}`).toggle(!visible);
+ $(`a#link_results_${category}`).toggle(visible);
+ $(`div#show-all-categories-${category}`).toggle(visible);
+ },
+
+ /**
+ * When clicked on toggle the icon ('+' -> '-' and vice versa)
+ * belonging to a parent course which has sub courses.
+ *
+ * @param {string} id parent course ID with add/remove Icon
+ */
+ toggleParentCourseIcon: function (id) {
+ var visible = $(`a#show-subcourses-${id}`).is(':visible');
+ $(`a#show-subcourses-${id}`).toggle(!visible);
+ $(`a#hide-subcourses-${id}`).toggle(visible);
+ },
+
+ /**
+ * Shows all sub courses for a specific parent course.
+ *
+ * @param {string} id parent course ID with sub courses
+ */
+ showSubcourses: function (id) {
+ STUDIP.Search.toggleParentCourseIcon(id);
+ $(`section.search-subcourse-${id}`).show();
+ },
+
+ /**
+ * Hides all sub courses for a specific parent course.
+ *
+ * @param {string} id parent course ID with sub courses
+ */
+ hideSubcourses: function (id) {
+ STUDIP.Search.toggleParentCourseIcon(id);
+ $(`section.search-subcourse-${id}`).hide();
+ },
+
+ /**
+ * Expand a single category, showing more results, and hide other categories.
+ *
+ * @param {string} category Category that should be expanded.
+ * @returns {boolean} false
+ */
+ expandCategory: function (category) {
+ // Hide other categories.
+ $(`#search-results article:not([id="search-${category}"])`).hide();
+ $('#search-no-result').hide();
+ // Show all results.
+ $(`#search-${category} section.search-extended-result`)
+ .removeClass('search-extended-result');
+ // Reassign category click to closing extended view.
+ var selector = [
+ `#search-results article#search-${category} header a`,
+ `#link_all_results_${category}`,
+ `#link_results_${category}`,
+ `#show-all-categories-${category}`
+ ].join(',');
+ $(selector).off('click').on('click', function () {
+ STUDIP.Search.toggleLinkText(category);
+ STUDIP.Search.showAllCategories(category);
+ return false;
+ });
+ return false;
+ },
+
+ /**
+ * Close expanded view of a single category, showing normal view with
+ * all categories again.
+ *
+ * @param {string} currentCategory Category that was previously selected.
+ * @return {boolean} false
+ */
+ showAllCategories: function (currentCategory) {
+ var selector = [
+ `#search-results article#search-${currentCategory} header a`,
+ `#link_all_results_${currentCategory}`,
+ `#link_results_${currentCategory}`
+ ].join(',');
+ $(selector).off('click').on('click', function () {
+ STUDIP.Search.toggleLinkText(currentCategory);
+ STUDIP.Search.expandCategory(currentCategory);
+ STUDIP.Search.setActiveCategory(currentCategory);
+ return false;
+ });
+ var resultCount = $('#search-results').data('results-per-type') - 1;
+ $(`#search-${currentCategory} section:gt(${resultCount})`)
+ .addClass('search-extended-result');
+ $('#search-results').children(`article:not([id="search-${currentCategory}"])`).show();
+ STUDIP.Search.setActiveCategory('show_all_categories');
+ $('#search-no-result').hide();
+ return false;
+ },
+
+ /**
+ * Show active filters based on active category
+ *
+ * @param {Object} filter object with filter information (e.g. 'category', 'semester', etc.)
+ * @return {boolean} false
+ */
+ showActiveFilters: function (filter) {
+ var container = $('#search-active-filters').find('.filter-items');
+ container.empty();
+ var emptyFilter = true;
+ for ( var item in filter) {
+ if (item != 'category') {
+ var value = filter[item];
+ if (value.trim()) {
+ var name = $(`#${item}_filter .sidebar-widget-header`).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 () {
+ var filter_name = $(this).data('filter-name');
+ $(`#${filter_name}_select`).val("");
+ $(`#${filter_name}_select`).trigger('change');
+ return false;
+ });
+ container.append(filterItem);
+ emptyFilter = false;
+ }
+ }
+ }
+ if (emptyFilter) {
+ $('#search-active-filters').hide();
+ } else {
+ $('#search-active-filters').show();
+ }
+ return false;
+ }
+};
+
+export default Search;
diff --git a/resources/assets/javascripts/lib/sidebar.js b/resources/assets/javascripts/lib/sidebar.js
new file mode 100644
index 0000000..6768a86
--- /dev/null
+++ b/resources/assets/javascripts/lib/sidebar.js
@@ -0,0 +1,75 @@
+import Scroll from './scroll.js';
+
+const Sidebar = {
+ stickyEnabled: true,
+ enableSticky() {
+ this.stickyEnabled = true;
+ this.setSticky();
+ },
+ disableSticky() {
+ this.stickyEnabled = false;
+ this.setSticky(false);
+ },
+ open () {
+ this.toggle(true);
+ },
+ close () {
+ this.toggle(false);
+ },
+ toggle (visible = null) {
+ visible = visible ?? !$('#layout-sidebar').hasClass('visible-sidebar');
+
+ // Hide navigation
+ $('#responsive-toggle').prop('checked', false);
+ $('#responsive-navigation').removeClass('visible');
+
+ $('#layout-sidebar').toggleClass('visible-sidebar', visible);
+ }
+};
+
+// This function inits the sticky sidebar by using the StickyKit lib
+// <http://leafo.net/sticky-kit/>
+Sidebar.setSticky = function(is_sticky) {
+ if (!this.stickyEnabled) {
+ return;
+ }
+
+ if (is_sticky === undefined || is_sticky) {
+ $('#layout-sidebar .sidebar')
+ .stick_in_parent({
+ offset_top: $('#barBottomContainer').outerHeight(true) + 15,
+ inner_scrolling: true
+ })
+ .on('sticky_kit:stick sticky_kit:unbottom', function() {
+ var stuckHandler = function(top, left) {
+ $('#layout-sidebar .sidebar').css('margin-left', -left);
+ };
+ Scroll.addHandler('sticky.horizontal', stuckHandler);
+ stuckHandler(0, $(window).scrollLeft());
+ })
+ .on('sticky_kit:unstick sticky_kit:bottom', function() {
+ Scroll.removeHandler('sticky.horizontal');
+ $(this).css('margin-left', 0);
+ });
+ } else {
+ Scroll.removeHandler('sticky.horizontal');
+ $('#layout-sidebar .sidebar')
+ .trigger('sticky_kit:unstick')
+ .trigger('sticky_kit:detach');
+ }
+};
+
+Sidebar.checkActiveLineHeight = () => {
+ $('#layout-sidebar .sidebar .sidebar-widget-content .widget-links li.active a.active').each(function() {
+ var link = $(this);
+ var actual_text = link.text();
+ link.text('tmp');
+ var default_height = link.outerHeight();
+ link.text(actual_text);
+ var actual_height = link.outerHeight();
+ if (actual_height > default_height) { //it is rendered in more lines
+ link.css('line-height', '20px');
+ }
+ });
+}
+export default Sidebar;
diff --git a/resources/assets/javascripts/lib/skip_links.js b/resources/assets/javascripts/lib/skip_links.js
new file mode 100644
index 0000000..2d17534
--- /dev/null
+++ b/resources/assets/javascripts/lib/skip_links.js
@@ -0,0 +1,143 @@
+const SkipLinks = {
+ activeElement: null,
+ navigationStatus: 0,
+
+ /**
+ * Displays the skip link navigation after first hitting the tab-key
+ * @param event: event-object of type keyup
+ */
+ showSkipLinkNavigation: function(event) {
+ if (event.keyCode === 9) {
+ //tab-key
+ SkipLinks.moveSkipLinkNavigationIn();
+ jQuery('.focus_box').removeClass('focus_box');
+ }
+ },
+
+ /**
+ * shows the skiplink-navigation window by moving it from the left
+ */
+ moveSkipLinkNavigationIn: function() {
+ if (SkipLinks.navigationStatus === 0) {
+ var VpWidth = jQuery(window).width();
+ jQuery('#skip_link_navigation li:first a').focus();
+ jQuery('#skip_link_navigation').css({ left: VpWidth / 2, opacity: 0 });
+ jQuery('#skip_link_navigation').animate({ opacity: 1.0 }, 500);
+ SkipLinks.navigationStatus = 1;
+ }
+ },
+
+ /**
+ * removes the skiplink-navigation window by moving it out of viewport
+ */
+ moveSkipLinkNavigationOut: function() {
+ if (SkipLinks.navigationStatus === 1) {
+ jQuery(SkipLinks.box).hide();
+ jQuery('#skip_link_navigation').animate({ opacity: 0 }, 500, function() {
+ jQuery(this).css('left', '-600px');
+ });
+ }
+ SkipLinks.navigationStatus = 2;
+ },
+
+ getFragment: function() {
+ var fragmentStart = document.location.hash.indexOf('#');
+ if (fragmentStart < 0) {
+ return '';
+ }
+ return document.location.hash.substring(fragmentStart);
+ },
+
+ /**
+ * Inserts the list with skip links
+ */
+ insertSkipLinks: function() {
+ jQuery('#skip_link_navigation').prepend(jQuery('#skiplink_list'));
+ jQuery('#skiplink_list').show();
+ jQuery('#skip_link_navigation').attr('aria-busy', 'false');
+ jQuery('#skip_link_navigation').attr('tabindex', '-1');
+ SkipLinks.insertHeadLines();
+ return false;
+ },
+
+ /**
+ * sets the area (of the id) as the current area for tab-navigation
+ * and highlights it
+ */
+ setActiveTarget: function(id) {
+ var fragment = null;
+ // set active area only if skip links are activated
+ if (!jQuery('*').is('#skip_link_navigation')) {
+ return false;
+ }
+ if (id) {
+ fragment = id;
+ } else {
+ fragment = SkipLinks.getFragment();
+ }
+ if (jQuery('*').is(fragment) && fragment.length > 0 && fragment !== SkipLinks.activeElement) {
+ SkipLinks.moveSkipLinkNavigationOut();
+ jQuery('.focus_box').removeClass('focus_box');
+ jQuery(fragment).addClass('focus_box');
+ jQuery(fragment)
+ .attr('tabindex', '-1')
+ .click()
+ .focus();
+ SkipLinks.activeElement = fragment;
+ return true;
+ } else {
+ jQuery('#skip_link_navigation li a')
+ .first()
+ .focus();
+ }
+ return false;
+ },
+
+ injectAriaRoles: function() {
+ jQuery('#main_content').attr({
+ role: 'main',
+ 'aria-labelledby': 'main_content_landmark_label'
+ });
+ jQuery('#layout_content').attr({
+ role: 'main',
+ 'aria-labelledby': 'layout_content_landmark_label'
+ });
+ jQuery('#layout_infobox').attr({
+ role: 'complementary',
+ 'aria-labelledby': 'layout_infobox_landmark_label'
+ });
+ },
+
+ insertHeadLines: function() {
+ var target = null;
+ jQuery('#skip_link_navigation a').each(function() {
+ target = jQuery(this).attr('href');
+ if (jQuery(target).is('li,td')) {
+ jQuery(target).prepend(
+ '<h2 id="' +
+ jQuery(target).attr('id') +
+ '_landmark_label" class="skip_target">' +
+ jQuery(this).text() +
+ '</h2>'
+ );
+ } else {
+ jQuery(target).before(
+ '<h2 id="' +
+ jQuery(target).attr('id') +
+ '_landmark_label" class="skip_target">' +
+ jQuery(this).text() +
+ '</h2>'
+ );
+ }
+ jQuery(target).attr('aria-labelledby', jQuery(target).attr('id') + '_landmark_label');
+ });
+ },
+
+ initialize: function() {
+ SkipLinks.insertSkipLinks();
+ SkipLinks.injectAriaRoles();
+ SkipLinks.setActiveTarget();
+ }
+};
+
+export default SkipLinks;
diff --git a/resources/assets/javascripts/lib/smiley_picker.js b/resources/assets/javascripts/lib/smiley_picker.js
new file mode 100644
index 0000000..8fd85ca
--- /dev/null
+++ b/resources/assets/javascripts/lib/smiley_picker.js
@@ -0,0 +1,128 @@
+/**
+ * smiley-picker.js - Smiley Picker
+ *
+ * Creates a SmileyPicker object in the global STUDIP namespace with
+ * the methods show, hide and toggle.
+ * show and toggle accept two arguments "triggerElement, onSelect":
+ * - triggerElement is the element that triggered the event
+ * - onSelect is a function to be executed once a smiley is selected
+ *
+ * The picker requires a php based backend under the route
+ * "smileys/picker" which renders the html for the picker.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ */
+import { $gettext } from './gettext.js';
+import Dialog from './dialog.js';
+
+var initialized = false,
+ picker_element = $('<div/>'),
+ select_handler = function() {};
+
+// Loads a url
+function loadURL(url, callback) {
+ $.get(url, function(response) {
+ response = $(response);
+
+ // Add a preload icon for each smiley to avoid a potential flash
+ // of the alternative text
+ $('.smileys img', response).each(function() {
+ var that = this,
+ src = this.src,
+ image = new Image();
+ this.src = STUDIP.ASSETS_URL + 'images/ajax_indicator_small.gif';
+
+ image.onload = image.onerror = function() {
+ that.src = src;
+ };
+ image.src = src;
+ });
+
+ picker_element.html(response);
+
+ if ($.isFunction(callback)) {
+ callback();
+ }
+ });
+}
+
+// Create smiley picker object and bind it to global STUDIP namespace
+const SmileyPicker = {
+ // Show smiley picker, triggered by a specific element and handle
+ // a selected smiley by the passed function
+ show: function(triggerElement, onSelect) {
+ select_handler = onSelect;
+
+ if (!initialized) {
+ // Setup picker dialog
+ picker_element.dialog({
+ autoOpen: false,
+ width: 420, // needs to be hardcoded, unfortunately.
+ dialogClass: 'smiley-picker-dialog',
+ resizable: false,
+ title: $gettext('Smileys'),
+ show: 'fade',
+ hide: 'fade',
+ buttons: [
+ {
+ text: $gettext('Zur Gesamtübersicht'),
+ click: function() {
+ var url = STUDIP.URLHelper.getURL('dispatch.php/smileys');
+ picker_element.dialog('close');
+ Dialog.fromURL(url);
+ }
+ },
+ {
+ text: $gettext('Schliessen'),
+ click: function() {
+ picker_element.dialog('close');
+ }
+ }
+ ]
+ });
+
+ // Initial load with spinner next to trigger element
+ $(triggerElement).showAjaxNotification();
+ loadURL(STUDIP.URLHelper.getURL('dispatch.php/smileys/picker'), function() {
+ $(triggerElement).hideAjaxNotification();
+ picker_element.dialog('open');
+ });
+
+ initialized = true;
+ } else {
+ picker_element.dialog('open');
+ }
+ },
+ // Hide smiley picker
+ hide: function() {
+ picker_element.dialog('close');
+ },
+ // Toggle smiley picker display (pass the same arguments as for show)
+ toggle: function(triggerElement, onSelect) {
+ if (initialized && picker_element.dialog('isOpen')) {
+ SmileyPicker.hide();
+ } else {
+ SmileyPicker.show(triggerElement, onSelect);
+ }
+ },
+
+ handleNavigationClick: function(event) {
+ loadURL(this.href);
+ return false;
+ },
+
+ handleSmileyClick: function(event) {
+ select_handler($(this).data().code);
+ picker_element.dialog('close');
+ return false;
+ }
+};
+
+export default SmileyPicker;
diff --git a/resources/assets/javascripts/lib/startpage.js b/resources/assets/javascripts/lib/startpage.js
new file mode 100644
index 0000000..15b2c59
--- /dev/null
+++ b/resources/assets/javascripts/lib/startpage.js
@@ -0,0 +1,64 @@
+const startpage = {
+ init: function() {
+ $('.start-widgetcontainer .portal-widget-list').sortable({
+ handle: '.widget-header',
+ connectWith: 'ul.portal-widget-list',
+ start: function() {
+ $(this)
+ .closest('.start-widgetcontainer')
+ .find('.portal-widget-list')
+ .addClass('ui-sortable move');
+ },
+ stop: function(event, ui) {
+ $.get(STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/start/storeNewOrder', {
+ widget: $(ui.item).attr('id'),
+ position: $(ui.item).index(),
+ column: $(ui.item)
+ .parent()
+ .index()
+ });
+ $(this)
+ .closest('.start-widgetcontainer')
+ .find('.portal-widget-list')
+ .removeClass('move');
+ }
+ });
+ },
+
+ init_edit: function(perm) {
+ $('.edit-widgetcontainer .portal-widget-list').sortable({
+ handle: '.widget-header',
+ connectWith: '.edit-widgetcontainer .portal-widget-list',
+ start: function() {
+ $(this)
+ .closest('.edit-widgetcontainer')
+ .find('.portal-widget-list')
+ .addClass('ui-sortable move');
+ },
+ stop: function() {
+ // store the whole widget constellation
+ var widgets = {
+ left: {},
+ right: {}
+ };
+
+ $('.edit-widgetcontainer .start-widgetcontainer .portal-widget-list:first-child > li').each(function() {
+ widgets.left[$(this).attr('id')] = $(this).index();
+ });
+
+ $('.edit-widgetcontainer .start-widgetcontainer .portal-widget-list:last-child > li').each(function() {
+ widgets.right[$(this).attr('id')] = $(this).index();
+ });
+
+ $.post(STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/start/update_defaults/' + perm, widgets);
+
+ $(this)
+ .closest('.edit-widgetcontainer')
+ .find('.portal-widget-list')
+ .removeClass('move');
+ }
+ });
+ }
+};
+
+export default startpage;
diff --git a/resources/assets/javascripts/lib/statusgroups.js b/resources/assets/javascripts/lib/statusgroups.js
new file mode 100644
index 0000000..ea6b6bd
--- /dev/null
+++ b/resources/assets/javascripts/lib/statusgroups.js
@@ -0,0 +1,77 @@
+const Statusgroups = {
+ ajax_endpoint: false,
+ apply: function() {
+ $('.movable tbody').sortable({
+ axis: 'y',
+ handle: '.dragHandle',
+ helper: function(event, ui) {
+ ui.children().each(function() {
+ $(this).width($(this).width());
+ });
+ return ui;
+ },
+ start: function(event, ui) {
+ $(this)
+ .closest('table')
+ .addClass('nohover');
+ },
+ stop: function(event, ui) {
+ var table = $(this).closest('table'),
+ group = table.attr('id'),
+ user = ui.item.data('userid'),
+ position = $(ui.item).prevAll().length;
+
+ table.removeClass('nohover');
+
+ $.ajax({
+ type: 'POST',
+ url: Statusgroups.ajax_endpoint,
+ dataType: 'html',
+ data: { group: group, user: user, pos: position },
+ async: false
+ }).done(function(data) {
+ $('tbody', table).html(data);
+ Statusgroups.apply();
+ });
+ }
+ });
+ },
+
+ initInputs: function() {
+ //$('input[name="selfassign_start"]').datetimepicker();
+ if (!$('input[name="selfassign"]').attr('checked')) {
+ $('input[name="exclusive"]')
+ .closest($('section'))
+ .hide();
+ $('input[name="selfassign_start"]')
+ .closest($('section'))
+ .hide();
+ $('input[name="selfassign_end"]')
+ .closest($('section'))
+ .hide();
+ }
+ //$('input[name="selfassign_end"]').datetimepicker();
+ $('input[name="selfassign"]').on('click', function() {
+ $('input[name="exclusive"]')
+ .closest($('section'))
+ .toggle();
+ $('input[name="selfassign_start"]')
+ .closest($('section'))
+ .toggle();
+ $('input[name="selfassign_end"]')
+ .closest($('section'))
+ .toggle();
+ });
+
+ $('input[name="numbering_type"]').on('click', function() {
+ var type = $('input[name="numbering_type"]:checked').val(),
+ disabled = parseInt(type, 10) === 2;
+
+ $('input[name="startnumber"]')
+ .prop('disabled', disabled)
+ .toggle(!disabled);
+ });
+ }
+};
+
+export default Statusgroups;
diff --git a/resources/assets/javascripts/lib/studip-vue.js b/resources/assets/javascripts/lib/studip-vue.js
new file mode 100644
index 0000000..eac6679
--- /dev/null
+++ b/resources/assets/javascripts/lib/studip-vue.js
@@ -0,0 +1,15 @@
+const load = async function () {
+ return await STUDIP.loadChunk('vue');
+};
+
+const on = async function (...args) {
+ const { eventBus } = await load();
+ eventBus.on(...args);
+};
+
+const emit = async function (...args) {
+ const { eventBus } = await load();
+ eventBus.emit(...args);
+};
+
+export default { load, on, emit };
diff --git a/resources/assets/javascripts/lib/study_area_selection.js b/resources/assets/javascripts/lib/study_area_selection.js
new file mode 100644
index 0000000..b973424
--- /dev/null
+++ b/resources/assets/javascripts/lib/study_area_selection.js
@@ -0,0 +1,120 @@
+/* ------------------------------------------------------------------------
+ * study area selection for courses
+ * ------------------------------------------------------------------------ */
+const study_area_selection = {
+ initialize: function() {
+ // Ein bisschen hässlich im Sinne von "DRY", aber wie sonst?
+ jQuery(document).on('click', 'input[name^="study_area_selection[add]"]', function() {
+ var parameters = jQuery(this).data();
+ if (!(parameters && parameters.id)) {
+ return;
+ }
+ study_area_selection.add(parameters.id, parameters.course_id || '-');
+ return false;
+ });
+ jQuery(document).on('click', 'input[name^="study_area_selection[remove]"]', function() {
+ var parameters = jQuery(this).data();
+ if (!(parameters && parameters.id)) {
+ return;
+ }
+ study_area_selection.remove(parameters.id, parameters.course_id || '-');
+ return false;
+ });
+ jQuery(document).on('click', 'a.study_area_selection_expand', function() {
+ var parameters = jQuery(this).data();
+ if (!(parameters && parameters.id)) {
+ return;
+ }
+ study_area_selection.expandSelection(parameters.id, parameters.course_id || '-');
+ return false;
+ });
+ },
+
+ url: function(/* action, args...*/) {
+ return STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/course/study_areas/' + jQuery.makeArray(arguments).join('/');
+ },
+
+ add: function(id, course_id) {
+ // may not be visible at the current
+ jQuery('.study_area_selection_add_' + id)
+ .prop('disabled', true)
+ .fadeTo('slow', 0);
+
+ jQuery.ajax({
+ type: 'POST',
+ url: study_area_selection.url('add', course_id || '-'),
+ data: { id: id },
+ dataType: 'html',
+ async: false, // Critical request thus synchronous
+ success: function(data) {
+ jQuery('#study_area_selection_none').fadeOut();
+ jQuery('#study_area_selection_selected').replaceWith(data);
+ study_area_selection.refreshSelection();
+ }
+ });
+ },
+
+ remove: function(id, course_id) {
+ var jQueryselection = jQuery('#study_area_selection_' + id);
+
+ if (jQueryselection.siblings().length === 0) {
+ jQuery('#study_area_selection_at_least_one')
+ .fadeIn()
+ .delay(5000)
+ .fadeOut();
+ jQueryselection.effect('bounce', 'fast');
+ return;
+ }
+
+ jQuery.ajax({
+ type: 'POST',
+ url: study_area_selection.url('remove', course_id || '-'),
+ data: { id: id },
+ dataType: 'html',
+ async: false, // Critical request thus synchronous
+ success: function() {
+ jQueryselection.fadeOut(function() {
+ jQuery(this).remove();
+ });
+ if (jQuery('#study_area_selection_selected li').length === 0) {
+ jQuery('#study_area_selection_none').fadeIn();
+ }
+ jQuery('.study_area_selection_add_' + id)
+ .css({
+ visibility: 'visible',
+ opacity: 0
+ })
+ .fadeTo('slow', 1, function() {
+ jQuery(this).prop('disabled', false);
+ });
+
+ study_area_selection.refreshSelection();
+ },
+ error: function() {
+ jQueryselection.fadeIn();
+ }
+ });
+ },
+
+ expandSelection: function(id, course_id) {
+ jQuery.post(
+ study_area_selection.url('expand', course_id || '-', id),
+ function(data) {
+ jQuery('#study_area_selection_selectables ul').replaceWith(data);
+ },
+ 'html'
+ );
+ },
+
+ refreshSelection: function() {
+ // "even=odd && odd=even ??" - this may seem strange but jQuery and Stud.IP differ in odd/even
+ jQuery('#study_area_selection_selected li:odd')
+ .removeClass('odd')
+ .addClass('even');
+ jQuery('#study_area_selection_selected li:even')
+ .removeClass('even')
+ .addClass('odd');
+ }
+};
+
+export default study_area_selection;
diff --git a/resources/assets/javascripts/lib/table-of-contents.js b/resources/assets/javascripts/lib/table-of-contents.js
new file mode 100644
index 0000000..09cbd74
--- /dev/null
+++ b/resources/assets/javascripts/lib/table-of-contents.js
@@ -0,0 +1,11 @@
+const TableOfContents = {
+ toggle_toc() {
+ $('.transform').toggleClass('transform-active');
+ }
+};
+
+export default TableOfContents;
+
+
+
+
diff --git a/resources/assets/javascripts/lib/table.js b/resources/assets/javascripts/lib/table.js
new file mode 100644
index 0000000..0325fbe
--- /dev/null
+++ b/resources/assets/javascripts/lib/table.js
@@ -0,0 +1,54 @@
+function enhanceSortableTable(table) {
+ var headers = {};
+ $('thead tr:last th', table).each(function(index, element) {
+ headers[index] = {
+ sorter: $(element).data().sort || false
+ };
+ });
+
+ if ($('tbody tr[data-sort-fixed]', table).length > 0) {
+ $('tbody tr[data-sort-fixed]', table).each(function() {
+ $(this).data('sort-fixed', {
+ index: $(this).index(),
+ tbody: $(this).closest('table').find('tbody').index($(this).parent())
+ });
+ });
+ $(table)
+ .on('sortStart', function() {
+ $('tbody tr[data-sort-fixed]', table).each(function() {
+ var hidden = $(this).is(':hidden');
+ $(this).data('sort-hidden', hidden);
+ });
+ })
+ .on('sortEnd', function() {
+ $('tbody tr[data-sort-fixed]', table)
+ .detach()
+ .each(function() {
+ var pos = $(this).data('sort-fixed');
+ if ($(`tbody:eq(${pos.tbody}) tr:eq(${pos.index})`, table).length > 0) {
+ $(`tbody:eq(${pos.tbody}) tr:eq(${pos.index})`, table).before(this);
+ } else {
+ $(`tbody:eq(${pos.tbody})`, table).append(this);
+ }
+
+ if ($(this).data('sort-hidden')) {
+ setTimeout(() => $(this).hide(), 100);
+ }
+ });
+ });
+ }
+
+ $(table).tablesorter({
+ headers: headers,
+ sortLocaleCompare : true,
+ sortRestart: true
+ });
+}
+
+const Table = {
+ enhanceSortableTable: function (table) {
+ STUDIP.loadChunk('tablesorter').then(() => enhanceSortableTable(table));
+ }
+};
+
+export default Table;
diff --git a/resources/assets/javascripts/lib/toolbar.js b/resources/assets/javascripts/lib/toolbar.js
new file mode 100644
index 0000000..3251065
--- /dev/null
+++ b/resources/assets/javascripts/lib/toolbar.js
@@ -0,0 +1,135 @@
+import { $gettext } from './gettext.js';
+import ToolbarButtonset from './toolbar_buttonset.js';
+import Dialog from './dialog.js';
+
+function getElementWidth(element) {
+ var proxy = null;
+
+ // Special case: Handle i18n hidden textareas
+ // - Hidden textareas have no dimensions thus we need to proxy the
+ // width from the first visible element in the i18n group
+ if ($(element).is(':hidden') && $(element).closest('.i18n_group').length > 0) {
+ proxy = $(element)
+ .closest('.i18n_group')
+ .find('div.i18n:visible')
+ .children()
+ .first();
+ if (proxy.length > 0) {
+ element = proxy;
+ }
+ }
+
+ return $(element).css('width') || $(element).outerWidth(true);
+}
+
+const Toolbar = {
+ buttonSet: ToolbarButtonset,
+
+ // Initializes (adds) a toolbar the passed textarea element
+ initialize: function(element, button_set) {
+ var $element = $(element),
+ wrap,
+ toolbar;
+
+ // don't initialize toolbar for wysiwyg textareas
+ if (STUDIP.editor_enabled && $element.hasClass('wysiwyg')) {
+ return;
+ }
+
+ // Bail out if the element is not a tetarea or a toolbar has already
+ // been applied
+ if (!$element.is('textarea') || $element.data('toolbar-added')) {
+ return;
+ }
+
+ button_set = button_set || Toolbar.buttonSet;
+
+ // if WYSIWYG is globally enabled then add a button so
+ // the user can activate it
+ if (STUDIP.wysiwyg_enabled && $element.hasClass('wysiwyg')) {
+ button_set.right.wysiwyg = {
+ label: 'WYSIWYG',
+ evaluate: function() {
+ var question = [
+ $gettext('Soll der WYSIWYG Editor aktiviert werden?'),
+ '',
+ $gettext('Die Seite muss danach neu geladen werden, um den WYSIWYG Editor zu laden.')
+ ].join('\n');
+ Dialog.confirm(question, function() {
+ var url = STUDIP.URLHelper.resolveURL('dispatch.php/wysiwyg/settings/users/current');
+
+ $.ajax({
+ url: url,
+ type: 'PUT',
+ contentType: 'application/json',
+ data: JSON.stringify({ disabled: false })
+ }).fail(function(xhr) {
+ window.alert(
+ [
+ $gettext('Das Aktivieren des WYSIWYG Editors ist fehlgeschlagen.'),
+ '',
+ $gettext('URL') + ': ' + url,
+ $gettext('Status') + ': ' + xhr.status + ' ' + xhr.statusText,
+ $gettext('Antwort') + ': ' + xhr.responseText
+ ].join('\n')
+ );
+ });
+ });
+ }
+ };
+ }
+
+ // Add flag so one element will never have more than one toolbar
+ $element.data('toolbar-added', true);
+
+ // Create toolbar element
+ toolbar = $('<div class="buttons">');
+
+ // Assemble toolbar
+ ['left', 'right'].forEach(function(position) {
+ var buttons = $('<span>').addClass(position);
+ $.each(button_set[position], function(name, format) {
+ var button = $('<span>').addClass(name),
+ label = format.label || name;
+
+ if (format.icon) {
+ label = $('<img>', {
+ alt: format.label || name,
+ src: STUDIP.ASSETS_URL + 'images/icons/blue/' + format.icon + '.svg'
+ });
+ }
+
+ button
+ .html(label)
+ .button()
+ .click(function() {
+ var selection = $element.getSelection(),
+ result = format.evaluate(selection, $element, this) || selection,
+ replacement = $.isPlainObject(result)
+ ? result.replacement
+ : result === undefined
+ ? selection
+ : result,
+ offset = $.isPlainObject(result) ? result.offset : (result || '').length;
+ $element.replaceSelection(replacement, offset).change();
+ return false;
+ });
+
+ buttons.append(button);
+ });
+ toolbar.append(buttons);
+ });
+
+ // Attach toolbar to the specified element
+ wrap = $('<div class="editor_toolbar">').css({
+ width: getElementWidth($element),
+ display: $element.css('display')
+ });
+ $element
+ .css('width', '100%')
+ .wrap(wrap)
+ .before(toolbar);
+ }
+};
+
+export default Toolbar;
diff --git a/resources/assets/javascripts/lib/toolbar_buttonset.js b/resources/assets/javascripts/lib/toolbar_buttonset.js
new file mode 100644
index 0000000..5bb211f
--- /dev/null
+++ b/resources/assets/javascripts/lib/toolbar_buttonset.js
@@ -0,0 +1,77 @@
+import SmileyPicker from './smiley_picker.js';
+
+// Creates a wrapper function that wraps the passed string using the
+// passed prefix and suffix. If the suffix is omitted, it will be replaced
+// by the prefix.
+// Be aware that the wrap function will not wrap a string twice.
+function createWrap(prefix, suffix) {
+ if (suffix === undefined) {
+ suffix = prefix;
+ }
+ return function(string) {
+ if (string.substr(0, prefix.length) === prefix && string.substr(-suffix.length) === suffix) {
+ return string;
+ }
+ if (string) {
+ return prefix + string + suffix;
+ }
+ return {
+ replacement: prefix + suffix,
+ offset: prefix.length
+ };
+ };
+}
+
+// Define default stud.ip button set
+const buttonSet = {
+ left: {
+ bold: { label: '<strong>B</strong>', evaluate: createWrap('**') },
+ italic: { label: '<em>i</em>', evaluate: createWrap('%%') },
+ underline: { label: '<u>u</u>', evaluate: createWrap('__') },
+ strikethrough: { label: '<del>u</del>', evaluate: createWrap('{-', '-}') },
+ code: { label: '<code>code</code>', evaluate: createWrap('[code]', '[/code]') },
+ larger: { label: 'A+', evaluate: createWrap('++') },
+ smaller: { label: 'A-', evaluate: createWrap('--') },
+ signature: { label: 'signature', evaluate: createWrap('', '\u2013~~~') },
+ link: {
+ label: 'link',
+ evaluate: function(string) {
+ string = string || window.prompt('Text:') || '';
+ if (string.length === 0) {
+ return string;
+ }
+
+ var url = window.prompt('URL:') || '';
+ return url.length === 0 ? string : '[' + string + ']' + url;
+ }
+ },
+ image: {
+ label: 'img',
+ evaluate: function(string) {
+ var url = window.prompt('URL:') || '';
+ return url.length === 0 ? string : '[img]' + url;
+ }
+ }
+ },
+ right: {
+ smilies: {
+ label: ':)',
+ evaluate: function(string, textarea, button) {
+ SmileyPicker.toggle(button, function(code) {
+ textarea.replaceSelection(code + ' ');
+ });
+ }
+ },
+ help: {
+ label: '?',
+ evaluate: function() {
+ var url = $('link[rel=help].text-format').attr('href'),
+ win;
+ win = window.open(url, '_blank');
+ win.opener = null;
+ }
+ }
+ }
+};
+
+export default buttonSet;
diff --git a/resources/assets/javascripts/lib/tooltip.js b/resources/assets/javascripts/lib/tooltip.js
new file mode 100644
index 0000000..ae812b5
--- /dev/null
+++ b/resources/assets/javascripts/lib/tooltip.js
@@ -0,0 +1,198 @@
+/*jslint esversion: 6*/
+
+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.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) {
+ CSS.removeRule(`#${this.id}::before`);
+ CSS.removeRule(`#${this.id}::after`);
+
+ if (x !== 0 || y !== 0) {
+ const rule = `translate(${x}px, ${y}px);`;
+ CSS.addRule(`#${this.id}::before`, { transform: rule }, ['-ms-', '-webkit-']);
+ CSS.addRule(`#${this.id}::after`, { transform: 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();
+ let x = this.x - width / 2;
+ let y = this.y - height;
+ let arrowOffset = 0;
+
+ if (x < Tooltip.threshold) {
+ arrowOffset = x - Tooltip.threshold;
+ x = Tooltip.threshold;
+ } else if (x + width > maxWidth - Tooltip.threshold) {
+ arrowOffset = x + width - maxWidth + Tooltip.threshold;
+ x = maxWidth - width - Tooltip.threshold;
+ }
+ this.translateArrows(arrowOffset, 0);
+
+ 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/tour.js b/resources/assets/javascripts/lib/tour.js
new file mode 100644
index 0000000..44b170e
--- /dev/null
+++ b/resources/assets/javascripts/lib/tour.js
@@ -0,0 +1,655 @@
+import { $gettext } from './gettext.js';
+
+/* ------------------------------------------------------------------------
+ * Stud.IP Tour
+ * ------------------------------------------------------------------------
+ *
+ * @author Arne Schröder, schroeder@data-quest.de
+ * @description Studip Tour
+ *
+ * Parts of this script are a modified version of:
+ *
+ * jQuery aSimpleTour
+ * @author alvaro.veliz@gmail.com
+ * @servedby perkins (http://p.erkins.com)
+ *
+ * Dependencies :
+ * - jQuery scrollTo
+ *
+ */
+
+const Tour = {
+ show_helpcenter: function() {
+ jQuery('#helpbar-sticky').prop('checked', true);
+ },
+ hide_helpcenter: function() {
+ jQuery('#helpbar-sticky').prop('checked', false);
+ },
+ init: function(tour_id, step_nr) {
+ Tour.direction = 'f';
+ if (!Tour.started && !Tour.pending_ajax_request) {
+ Tour.pending_ajax_request = true;
+ Tour.started = true;
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/tour/get_data/' + tour_id + '/' + step_nr,
+ type: 'POST',
+ data: { route: window.location.href },
+ dataType: 'json',
+ success: function(json) {
+ jQuery(document).trigger('tourstart.studip');
+
+ Tour.pending_ajax_request = false;
+ Tour.options = json;
+ if (Tour.options.redirect) {
+ window.location.href = Tour.options.redirect;
+ }
+ Tour.id = tour_id;
+ Tour.step = 0;
+ Tour.steps = Tour.options.data.length;
+ jQuery('body').prepend(Tour.options.tour_html);
+ if (!Tour.steps) {
+ Tour.started = false;
+ Tour.show_helpcenter();
+ } else if (Tour.options.last_run) {
+ Tour.hide_helpcenter();
+ if (Tour.options.tour_type === 'tour' && !Tour.options.edit_mode) {
+ jQuery('body').prepend('<div id="tour_overlay"></div>');
+ }
+ jQuery('#tour_title').html(Tour.options.last_run);
+ jQuery('#tour_end').show();
+ jQuery('#tour_next').hide();
+ jQuery('#tour_prev').hide();
+ jQuery('#tour_controls').show();
+ jQuery('#tour_reset').show();
+ jQuery('#tour_reset').on('click', function() {
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/tour/set_status/' + Tour.id + '/1/on'
+ });
+ jQuery('#tour_reset').hide();
+ jQuery('#tour_proceed').hide();
+ Tour.step = -1;
+ Tour.next();
+ });
+ jQuery('#tour_proceed').show();
+ jQuery('#tour_proceed').on('click', function() {
+ if (Tour.options.last_run_href) {
+ jQuery.ajax({
+ url:
+ STUDIP.ABSOLUTE_URI_STUDIP +
+ 'dispatch.php/tour/set_status/' +
+ Tour.id +
+ '/' +
+ Tour.options.last_run_step +
+ '/on',
+ success: function() {
+ window.location.href = STUDIP.URLHelper.getURL(Tour.options.last_run_href);
+ }
+ });
+ }
+ });
+ } else {
+ Tour.hide_helpcenter();
+ if (Tour.options.tour_type === 'tour' && !Tour.options.edit_mode) {
+ jQuery('body').prepend('<div id="tour_overlay"></div>');
+ }
+ Tour.step = step_nr - Tour.options.route_step_nr - 1;
+ Tour.next();
+ if (Tour.options.edit_mode) {
+ Tour.startEditor();
+ }
+ }
+ },
+ fail: function() {
+ Tour.pending_ajax_request = false;
+ alert($gettext('Fehler beim Aufruf des Tour-Controllers'));
+ }
+ });
+ }
+ },
+
+ showControlButtons: function() {
+ jQuery('#tour_tip').hide();
+ jQuery('#tour_tip_interactive').hide();
+ jQuery('.tour_focus_box').removeClass('tour_focus_box');
+ jQuery('#tour_reset').hide();
+ jQuery('#tour_proceed').hide();
+ jQuery('#tour_end').show();
+ if (Tour.step > 0 || Tour.options.back_link) {
+ jQuery('#tour_prev').show();
+ } else {
+ jQuery('#tour_prev').hide();
+ }
+ if (Tour.step < Tour.steps - 1 || Tour.options.proceed_link) {
+ jQuery('#tour_next').show();
+ } else {
+ jQuery('#tour_next').hide();
+ }
+ jQuery('#tour_controls').show();
+ },
+
+ next: function() {
+ Tour.direction = 'f';
+ Tour.step++;
+
+ if (Tour.step >= Tour.steps) {
+ if (Tour.options.proceed_link) {
+ window.location.href = STUDIP.URLHelper.getURL(Tour.options.proceed_link);
+ } else {
+ this.destroy();
+ }
+ } else {
+ if (Tour.options.data[Tour.step].action_next) {
+ jQuery(Tour.options.data[Tour.step].action_next).click();
+ }
+ Tour.showControlButtons();
+ Tour.setTooltip(Tour.options.data[Tour.step]);
+ }
+ },
+
+ prev: function() {
+ Tour.direction = 'b';
+ Tour.step--;
+
+ if (Tour.step < 0 && Tour.options.back_link) {
+ jQuery.ajax({
+ url:
+ STUDIP.ABSOLUTE_URI_STUDIP +
+ 'dispatch.php/tour/set_status/' +
+ Tour.id +
+ '/' +
+ (Tour.options.route_step_nr - 1) +
+ '/on',
+ success: function() {
+ window.location.href = STUDIP.URLHelper.getURL(Tour.options.back_link);
+ }
+ });
+ } else {
+ if (Tour.options.data[Tour.step].action_prev) {
+ jQuery(Tour.options.data[Tour.step].action_prev).click();
+ }
+ Tour.showControlButtons();
+ Tour.setTooltip(Tour.options.data[Tour.step]);
+ }
+ },
+
+ setTooltip: function(stepData) {
+ jQuery('.tour_focus_box').removeClass('tour_focus_box');
+ var tip_id = 'tour_tip';
+ if (stepData.interactive) {
+ if (
+ Tour.step === Tour.steps - 1 &&
+ parseInt(Tour.options.route_step_nr, 10) + Tour.step !== Tour.options.step_count
+ ) {
+ jQuery('#tour_interactive_text').show();
+ }
+ tip_id = 'tour_tip_interactive';
+ }
+ jQuery('#tour_title').html(
+ Tour.options.tour_title +
+ ' (' +
+ (parseInt(Tour.options.route_step_nr, 10) + Tour.step) +
+ '/' +
+ Tour.options.step_count +
+ ')'
+ );
+ if (stepData.controlsPosition) {
+ Tour.setControlsPosition(stepData.controlsPosition);
+ }
+ if (stepData.title || stepData.tip) {
+ jQuery('#' + tip_id + ' #tour_tip_title').html(stepData.title);
+ jQuery('#' + tip_id + ' #tour_tip_content').html(stepData.tip);
+
+ var tooltipPos = typeof stepData.orientation === 'undefined' ? 'B' : stepData.orientation;
+ Tour.setTooltipPosition(tooltipPos, stepData.element, tip_id);
+ if (stepData.interactive && stepData.element) {
+ jQuery(stepData.element).addClass('tour_focus_box');
+ }
+ }
+ },
+
+ setControlsPosition: function(pos) {
+ var position = Tour.getControlPosition(pos);
+ jQuery('#tour_controls').css(position);
+ },
+
+ setTooltipPosition: function(pos, element, tip_id) {
+ jQuery('.tourArrow').remove();
+ if (element && !jQuery(element).length) {
+ //alert('Das Element wurde nicht gefunden, Tooltip konnte nicht positioniert werden.');
+ element = '';
+ }
+ var tw =
+ jQuery('#' + tip_id).width() +
+ parseInt(jQuery('#' + tip_id).css('padding-left'), 10) +
+ parseInt(jQuery('#' + tip_id).css('padding-right'), 10);
+ var th =
+ jQuery('#' + tip_id).height() +
+ parseInt(jQuery('#' + tip_id).css('padding-top'), 10) +
+ parseInt(jQuery('#' + tip_id).css('padding-bottom'), 10);
+ if (Tour.options.edit_mode) {
+ Tour.setSelectorOverlay();
+ if (jQuery('#tour_edit').length) {
+ jQuery('#tour_edit').attr(
+ 'href',
+ STUDIP.ABSOLUTE_URI_STUDIP +
+ 'dispatch.php/tour/edit_step/' +
+ Tour.id +
+ '/' +
+ (parseInt(Tour.options.route_step_nr, 10) + Tour.step) +
+ '?hide_route=1'
+ );
+ jQuery('#tour_new_step').attr(
+ 'href',
+ STUDIP.ABSOLUTE_URI_STUDIP +
+ 'dispatch.php/tour/edit_step/' +
+ Tour.id +
+ '/' +
+ (parseInt(Tour.options.route_step_nr, 10) + Tour.step + 1) +
+ '/new?hide_route=1'
+ );
+ jQuery('#tour_new_page').attr(
+ 'href',
+ STUDIP.ABSOLUTE_URI_STUDIP +
+ 'dispatch.php/tour/edit_step/' +
+ Tour.id +
+ '/' +
+ (parseInt(Tour.options.route_step_nr, 10) + Tour.step + 1) +
+ '/new'
+ );
+ }
+ }
+ if (!element || !pos) {
+ jQuery('#' + tip_id).css({
+ top: window.innerHeight / 2 - th / 2 + 'px',
+ left: window.innerWidth / 2 - tw / 2 + 'px',
+ position: 'fixed'
+ });
+ jQuery('#' + tip_id).show('fast');
+ return;
+ }
+ var ew = jQuery(element).outerWidth();
+ var eh = jQuery(element).outerHeight();
+ var el = jQuery(element).offset().left;
+ var et = jQuery(element).offset().top;
+
+ var tbg = jQuery('#' + tip_id).css('background-color');
+ var $upArrow = $('<div class="tourArrow"></div>').css({
+ 'border-left': '16px solid transparent',
+ 'border-right': '16px solid transparent',
+ 'border-bottom': '16px solid ' + tbg
+ });
+ var $downArrow = $('<div class="tourArrow"></div>').css({
+ 'border-left': '16px solid transparent',
+ 'border-right': '16px solid transparent',
+ 'border-top': '16px solid ' + tbg
+ });
+ var $rightArrow = $('<div class="tourArrow"></div>').css({
+ 'border-top': '16px solid transparent',
+ 'border-bottom': '16px solid transparent',
+ 'border-left': '16px solid ' + tbg
+ });
+ var $leftArrow = $('<div class="tourArrow"></div>').css({
+ 'border-top': '16px solid transparent',
+ 'border-bottom': '16px solid transparent',
+ 'border-right': '16px solid ' + tbg
+ });
+ var position;
+ switch (pos) {
+ case 'BL':
+ position = { left: el - 10, top: et + eh + 20 };
+ $upArrow.css({ top: '-16px', left: '10px' });
+ jQuery('#' + tip_id).prepend($upArrow);
+ break;
+
+ case 'BR':
+ position = { left: el + ew - tw + 10, top: et + eh + 20 };
+ $upArrow.css({ top: '-16px', right: '10px' });
+ jQuery('#' + tip_id).prepend($upArrow);
+ break;
+
+ case 'TL':
+ position = { left: el - 10, top: et - th - 20 };
+ $downArrow.css({ top: th, left: '10px' });
+ jQuery('#' + tip_id).append($downArrow);
+ break;
+
+ case 'TR':
+ position = { left: el + ew - tw + 10, top: et - th - 20 };
+ $downArrow.css({ top: th, right: '10px' });
+ jQuery('#' + tip_id).append($downArrow);
+ break;
+
+ case 'RT':
+ position = { left: el + ew + 20, top: et - 10 };
+ $leftArrow.css({ left: '-16px' });
+ jQuery('#' + tip_id).prepend($leftArrow);
+ break;
+
+ case 'RB':
+ position = { left: el + ew + 20, top: et + eh - th + 10 };
+ $leftArrow.css({ left: '-16px' });
+ jQuery('#' + tip_id).prepend($leftArrow);
+ break;
+
+ case 'LT':
+ position = { left: el - tw - 20, top: et - 10 };
+ $rightArrow.css({ right: '-16px' });
+ jQuery('#' + tip_id).prepend($rightArrow);
+ break;
+
+ case 'LB':
+ position = { left: el - tw - 20, top: et + eh - th + 10 };
+ $rightArrow.css({ right: '-16px' });
+ jQuery('#' + tip_id).prepend($rightArrow);
+ break;
+
+ case 'B':
+ position = { left: el + ew / 2 - tw / 2, top: et + eh + 20 };
+ $upArrow.css({ top: '-16px', left: tw / 2 - 16 + 'px' });
+ jQuery('#' + tip_id).prepend($upArrow);
+ break;
+
+ case 'T':
+ position = { left: el + ew / 2 - tw / 2, top: et - th - 20 };
+ $downArrow.css({ top: th, left: tw / 2 - 16 + 'px' });
+ jQuery('#' + tip_id).append($downArrow);
+ break;
+
+ case 'L':
+ position = { left: el - tw - 20, top: et + eh / 2 - th / 2 };
+ $rightArrow.css({ right: '-16px', top: th / 2 - 16 + 'px' });
+ jQuery('#' + tip_id).prepend($rightArrow);
+ break;
+
+ case 'R':
+ position = { left: el + ew + 20, top: et + eh / 2 - th / 2 };
+ $leftArrow.css({ left: '-16px', top: th / 2 - 16 + 'px' });
+ jQuery('#' + tip_id).prepend($leftArrow);
+ break;
+ }
+
+ jQuery('#' + tip_id).css({ top: position.top + 'px', left: position.left + 'px', position: 'absolute' });
+ jQuery('#' + tip_id).show('fast');
+ jQuery.scrollTo(jQuery('#' + tip_id), 400, { offset: -100 });
+ },
+
+ destroy: function() {
+ jQuery(document).trigger('tourend.studip');
+
+ jQuery('#tour_overlay').remove();
+ if (jQuery('#tour_selector_overlay').length) {
+ jQuery('#tour_selector_overlay').hide();
+ }
+ if (!jQuery('#tour_proceed').is(':visible')) {
+ jQuery.ajax({
+ url:
+ STUDIP.ABSOLUTE_URI_STUDIP +
+ 'dispatch.php/tour/set_status/' +
+ Tour.id +
+ '/' +
+ (parseInt(Tour.options.route_step_nr, 10) + Tour.step) +
+ '/off'
+ });
+ }
+ jQuery('#tour_controls').hide();
+ jQuery('#tour_tip').hide();
+ jQuery('#tour_tip_interactive').hide();
+ jQuery('.tour_focus_box').removeClass('tour_focus_box');
+ Tour.show_helpcenter();
+ Tour.step = -1;
+ Tour.started = false;
+ },
+
+ setSelectorOverlay: function() {
+ if (jQuery(Tour.options.data[Tour.step].element).length) {
+ jQuery('#tour_selector_overlay').css({
+ display: 'block',
+ width: jQuery(Tour.options.data[Tour.step].element).outerWidth() + 'px',
+ height: jQuery(Tour.options.data[Tour.step].element).outerHeight() + 'px',
+ top: jQuery(Tour.options.data[Tour.step].element).offset().top + 'px',
+ left: jQuery(Tour.options.data[Tour.step].element).offset().left + 'px'
+ });
+ } else {
+ jQuery('#tour_selector_overlay').hide();
+ }
+ },
+
+ getSelector: function(target) {
+ var element = jQuery(target).prop('tagName');
+ if (jQuery(target).attr('id')) {
+ element = '#' + jQuery(target).attr('id');
+ } else if (jQuery(target).attr('name')) {
+ element = element + '[name=' + jQuery(target).attr('name') + ']';
+ } else {
+ if (jQuery(target).parent().length) {
+ element = Tour.getSelector(jQuery(target).parent()) + ' ' + element;
+ element = element + ':eq(' + jQuery(target).index(element) + ') ';
+ }
+ }
+ return element;
+ },
+
+ deleteStep: function(tour_id, step_nr, button) {
+ button = typeof button !== 'undefined' ? button : 'question';
+ if (!Tour.pending_ajax_request) {
+ Tour.pending_ajax_request = true;
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/tour/delete_step/' + tour_id + '/' + step_nr,
+ data: jQuery('.modaloverlay form').serialize() + '&' + button + '=1',
+ success: function(html, status, xhr) {
+ Tour.pending_ajax_request = false;
+ if (xhr.getResponseHeader('X-Action') === 'question') {
+ if (Tour.started) {
+ jQuery('#tour_controls').hide();
+ jQuery('#tour_tip').hide();
+ jQuery('#tour_tip_interactive').hide();
+ jQuery('#tour_selector_overlay').hide();
+ jQuery('.tour_focus_box').removeClass('tour_focus_box');
+ }
+ jQuery('body').prepend(html);
+ jQuery('.modaloverlay form').on('click', function(event) {
+ jQuery(this).data('clicked', jQuery(event.target));
+ });
+ jQuery('.modaloverlay form').on('submit', function(event) {
+ event.preventDefault();
+ Tour.deleteStep(
+ jQuery('.modaloverlay form input[name=tour_id]').val(),
+ jQuery('.modaloverlay form input[name=step_nr]').val(),
+ jQuery(this)
+ .data('clicked')
+ .attr('name')
+ );
+ jQuery('.modaloverlay').remove();
+ });
+ } else if (xhr.getResponseHeader('X-Action') === 'complete') {
+ if (Tour.started) {
+ Tour.showControlButtons();
+ Tour.setTooltip(Tour.options.data[Tour.step]);
+ Tour.started = false;
+ if (step_nr > 1 && step_nr - Tour.options.route_step_nr >= Tour.steps - 1) {
+ Tour.init(tour_id, step_nr - 1);
+ } else {
+ Tour.init(tour_id, step_nr);
+ }
+ }
+ }
+ },
+ fail: function() {
+ Tour.pending_ajax_request = false;
+ alert('Fehler beim Aufruf des Tour-Controllers');
+ }
+ });
+ }
+ },
+
+ saveStep: function(tour_id, step_nr) {
+ if (!Tour.pending_ajax_request) {
+ Tour.pending_ajax_request = true;
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/tour/edit_step/' + tour_id + '/' + step_nr + '/save',
+ type: 'POST',
+ data: jQuery('#edit_tour_form').serialize(),
+ dataType: 'html',
+ success: function(html, status, xhr) {
+ Tour.pending_ajax_request = false;
+ if (xhr.getResponseHeader('X-Action') === 'close') {
+ jQuery('#edit_tour_step')
+ .parent()
+ .dialog('close');
+ if (Tour.started) {
+ Tour.started = false;
+ Tour.init(tour_id, step_nr);
+ } else {
+ window.location.replace(window.location.href);
+ }
+ } else {
+ jQuery('#edit_tour_step').replaceWith(html);
+ }
+ },
+ fail: function() {
+ Tour.pending_ajax_request = false;
+ alert('Fehler beim Aufruf des Tour-Controllers');
+ }
+ });
+ }
+ },
+
+ saveStepPosition: function(tour_id, step_nr, element, mode) {
+ mode = typeof mode !== 'undefined' ? mode : 'save_position';
+ Tour.options.data[Tour.step].element = element;
+ if (!Tour.pending_ajax_request) {
+ Tour.pending_ajax_request = true;
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/tour/edit_step/' + tour_id + '/' + step_nr + '/' + mode,
+ type: 'POST',
+ data: { position: element },
+ success: function(html, status, xhr) {
+ Tour.pending_ajax_request = false;
+ },
+ fail: function() {
+ Tour.pending_ajax_request = false;
+ alert('Fehler beim Aufruf des Tour-Controllers');
+ }
+ });
+ }
+ },
+
+ startEditor: function() {
+ jQuery('#tour_editor').show();
+ if (Tour.options.step_count > 1) {
+ jQuery('#tour_delete_step').show();
+ } else {
+ jQuery('#tour_delete_step').hide();
+ }
+
+ jQuery('#tour_delete_step').on('click', function(event) {
+ Tour.deleteStep(Tour.id, parseInt(Tour.options.route_step_nr, 10) + Tour.step);
+ event.preventDefault();
+ });
+
+ jQuery('#tour_no_css').on('click', function() {
+ jQuery('#tour_selector_overlay').hide();
+ if (jQuery('#tour_overlay').length) {
+ jQuery('#tour_overlay').hide();
+ }
+ Tour.saveStepPosition(Tour.id, parseInt(Tour.options.route_step_nr, 10) + Tour.step, '');
+ Tour.setTooltip(Tour.options.data[Tour.step]);
+ });
+
+ jQuery('#tour_select_css').on('click', function() {
+ jQuery('#tour_controls').hide();
+ jQuery('#tour_tip').hide();
+ jQuery('#tour_tip_interactive').hide();
+ jQuery('#tour_selector_overlay').hide();
+ if (jQuery('#tour_overlay').length) {
+ jQuery('#tour_overlay').hide();
+ }
+ Tour.options.edit_mode = 'select_css';
+ });
+
+ jQuery('#tour_select_action_next').on('click', function() {
+ jQuery('#tour_controls').hide();
+ jQuery('#tour_tip').hide();
+ jQuery('#tour_tip_interactive').hide();
+ jQuery('#tour_selector_overlay').hide();
+ if (jQuery('#tour_overlay').length) {
+ jQuery('#tour_overlay').hide();
+ }
+ Tour.options.edit_mode = 'select_action_next';
+ });
+
+ jQuery('#tour_select_action_prev').on('click', function() {
+ jQuery('#tour_controls').hide();
+ jQuery('#tour_tip').hide();
+ jQuery('#tour_tip_interactive').hide();
+ jQuery('#tour_selector_overlay').hide();
+ if (jQuery('#tour_overlay').length) {
+ jQuery('#tour_overlay').hide();
+ }
+ Tour.options.edit_mode = 'select_action_prev';
+ });
+
+ if (!jQuery('#tour_selector_overlay').length) {
+ jQuery('body').prepend('<div id="tour_selector_overlay" style="z-index:20000;"></div>');
+ }
+ jQuery('body').on('click', function(event) {
+ var clicked_element;
+ if (Tour.options.edit_mode === 'select_css') {
+ clicked_element = Tour.getSelector(event.target);
+ event.preventDefault();
+ if (clicked_element !== '#tour_select_css') {
+ Tour.options.edit_mode = 1;
+ Tour.saveStepPosition(
+ Tour.id,
+ parseInt(Tour.options.route_step_nr, 10) + Tour.step,
+ clicked_element,
+ 'save_position'
+ );
+ Tour.setTooltip(Tour.options.data[Tour.step]);
+ if (jQuery('#tour_overlay').length) {
+ jQuery('#tour_overlay').show();
+ }
+ Tour.showControlButtons();
+ }
+ }
+ if (Tour.options.edit_mode === 'select_action_next') {
+ clicked_element = Tour.getSelector(event.target);
+ event.preventDefault();
+ if (clicked_element !== '#tour_select_action_next') {
+ Tour.options.edit_mode = 1;
+ Tour.saveStepPosition(
+ Tour.id,
+ parseInt(Tour.options.route_step_nr, 10) + Tour.step,
+ clicked_element,
+ 'save_action_next'
+ );
+ Tour.setTooltip(Tour.options.data[Tour.step]);
+ if (jQuery('#tour_overlay').length) {
+ jQuery('#tour_overlay').show();
+ }
+ Tour.showControlButtons();
+ }
+ }
+ if (Tour.options.edit_mode === 'select_action_prev') {
+ clicked_element = Tour.getSelector(event.target);
+ event.preventDefault();
+ if (clicked_element !== '#tour_select_action_prev') {
+ Tour.options.edit_mode = 1;
+ Tour.saveStepPosition(
+ Tour.id,
+ parseInt(Tour.options.route_step_nr, 10) + Tour.step,
+ clicked_element,
+ 'save_action_prev'
+ );
+ Tour.setTooltip(Tour.options.data[Tour.step]);
+ if (jQuery('#tour_overlay').length) {
+ jQuery('#tour_overlay').show();
+ }
+ Tour.showControlButtons();
+ }
+ }
+ });
+ }
+};
+
+export default Tour;
diff --git a/resources/assets/javascripts/lib/url_helper.js b/resources/assets/javascripts/lib/url_helper.js
new file mode 100644
index 0000000..61a5a62
--- /dev/null
+++ b/resources/assets/javascripts/lib/url_helper.js
@@ -0,0 +1,89 @@
+/**
+ * This class helps to handle URLs of hyperlinks and change their parameters.
+ * For example a javascript-page may open an item and the user expects other links
+ * on the same page to "know" that this item is now open. But because we don't use
+ * PHP session-variables here, this is difficult to use. This class can help. You
+ * can overwrite the href-attribute of the link by:
+ *
+ * [code]
+ * link.href = STUDIP.URLHelper.getURL("adresse.php?hello=world#anchor");
+ * [/code]
+ * Returns something like:
+ * "http://uni-adresse.de/studip/adresse.php?hello=world&mandatory=parameter#anchor"
+ */
+
+class URLHelper {
+ constructor(base_url = null, parameters = {}) {
+ //the base url for all links
+ this.base_url = base_url;
+
+ // bound link parameters
+ this.parameters = parameters;
+ }
+
+ /**
+ * method to extend short URLs like "dispatch.php/profile" to "http://.../dispatch.php/profile"
+ */
+ resolveURL(url) {
+ if (!_.isString(this.base_url) || url.match(/^[a-zA-Z][a-zA-Z0-9+-.]*:/) !== null || url.charAt(0) === '?') {
+ //this method cannot do any more:
+ return url;
+ }
+ var base_url = this.base_url;
+ if (url.charAt(0) === '/') {
+ var host = this.base_url.match(/^[a-zA-Z][a-zA-Z0-9+-.]*:\/\/[\w:.\-]+/);
+ base_url = host ? host[0] : '';
+ }
+ return base_url + url;
+ }
+
+ /**
+ * returns a readily encoded URL with the mandatory parameters and additionally passed
+ * parameters.
+ *
+ * @param url string: any url-string
+ * @param param_object map: associative object for extra values
+ * @param ignore_params boolean: ignore previously bound parameters
+ * @return: url with all necessary and additional parameters, encoded
+ */
+ getURL(url, param_object, ignore_params) {
+ var params = _.defaults(param_object || {}, ignore_params ? {} : this.parameters),
+ tmp,
+ fragment,
+ query;
+
+ tmp = url.split('#');
+ url = tmp[0];
+ fragment = tmp[1];
+
+ tmp = url.split('?');
+ url = tmp[0];
+ query = tmp[1] || '';
+
+ if (url !== '') {
+ url = this.resolveURL(url);
+ }
+ query = decodeURIComponent(query);
+ // split query string and merge with param_object
+ _.each((query && query.split('&')) || [], function(e) {
+ var pair = e.split('=');
+ if (!(pair[0] in params)) {
+ params[pair[0]] = pair[1];
+ }
+ });
+
+ if (_.keys(params).length || url === '') {
+ url += '?' + jQuery.param(params);
+ }
+
+ if (fragment) {
+ url += '#' + fragment;
+ }
+
+ return url;
+ }
+}
+
+export default function createURLHelper(config) {
+ return new URLHelper(config && config.base_url, config && config.parameters);
+}
diff --git a/resources/assets/javascripts/lib/user_filter.js b/resources/assets/javascripts/lib/user_filter.js
new file mode 100644
index 0000000..ee2d622
--- /dev/null
+++ b/resources/assets/javascripts/lib/user_filter.js
@@ -0,0 +1,172 @@
+/* ------------------------------------------------------------------------
+ * Bedingungen zur Auswahl von Stud.IP-Nutzern
+ * ------------------------------------------------------------------------ */
+import { $gettext } from './gettext.js';
+import Dialog from './dialog.js';
+
+const UserFilter = {
+ new_group_nr: 1,
+
+ configureCondition: function(targetId, targetUrl) {
+ Dialog.fromURL(targetUrl, {
+ title: $gettext('Bedingung konfigurieren'),
+ size: Math.min(Math.round(0.9 * $(window).width()), 850) + 'x400',
+ method: 'post',
+ id: 'configurecondition'
+ });
+ return false;
+ },
+
+ /**
+ * Adds a new user filter to the list of set filters.
+ * @param String containerId
+ * @param String targetUrl
+ */
+ addCondition: function(containerId, targetUrl) {
+ var children = $('.conditionfield');
+ var query = '';
+ $('.conditionfield').each(function() {
+ query +=
+ '&field[]=' +
+ encodeURIComponent(
+ $(this)
+ .children('.conditionfield_class:first')
+ .val()
+ ) +
+ '&compare_operator[]=' +
+ encodeURIComponent(
+ $(this)
+ .children('.conditionfield_compare_op:first')
+ .val()
+ ) +
+ '&value[]=' +
+ encodeURIComponent(
+ $(this)
+ .children('.conditionfield_value:first')
+ .val()
+ );
+ });
+ $.ajax({
+ type: 'post',
+ url: targetUrl,
+ data: query,
+ dataType: 'html',
+ success: function(data, textStatus, jqXHR) {
+ var result = '';
+ if ($('#' + containerId).children('.nofilter:visible').length > 0) {
+ $('#' + containerId)
+ .children('.nofilter')
+ .hide();
+ $('#' + containerId)
+ .children('.userfilter')
+ .show();
+ } else if ($('#' + containerId).children('.ungrouped_conditions .condition_list').length > 0) {
+ result += '<b>' + $gettext('oder') + '</b>';
+ }
+ result += data;
+ $('#' + containerId)
+ .find('.userfilter .ungrouped_conditions .condition_list')
+ .append(result);
+ if ($('#no_conditiongroups').length > 0) {
+ $('.userfilter .ungrouped_conditions .condition_list input[type=checkbox]').hide();
+ }
+ $('.userfilter .group_conditions').show();
+ }
+ });
+ Dialog.close({ id: 'configurecondition' });
+ },
+
+ /**
+ * groups selected conditions
+ */
+ groupConditions: function() {
+ var selected = $('.userfilter input:checked').parent('div');
+ var group_template = $('.grouped_conditions_template').clone();
+ if (selected.length > 0) {
+ $('.userfilter input[type=checkbox]:checked')
+ .prop('checked', false)
+ .hide();
+ $('.userfilter .group_conditions').after(group_template.show());
+ selected.find('input[name^=conditiongroup_]').prop('value', UserFilter.new_group_nr);
+ $('.grouped_conditions_template:last .condition_list').append(selected);
+ $('.grouped_conditions_template:last .condition_list input[name=quota]').prop(
+ 'name',
+ 'quota_' + UserFilter.new_group_nr
+ );
+ $('.grouped_conditions_template:last').prop('id', 'new_conditiongroup_' + UserFilter.new_group_nr);
+ $('.grouped_conditions_template:last').prop('class', 'grouped_conditions');
+ UserFilter.new_group_nr++;
+ }
+ if ($('.userfilter .ungrouped_conditions .condition_list .condition').length == 0) {
+ $('.userfilter .group_conditions').hide();
+ }
+ return false;
+ },
+
+ /**
+ * removes group for conditions
+ */
+ ungroupConditions: function(element) {
+ var selected = $(element)
+ .parents('.grouped_conditions')
+ .find('.condition');
+ var empty_group = $(element).parents('.grouped_conditions');
+ if (selected.length > 0) {
+ selected.find('input[name^=conditiongroup_]').prop('value', '');
+ $('.ungrouped_conditions .condition_list').append(selected);
+ $('.ungrouped_conditions input[type=checkbox]:not(:visible)').show();
+ empty_group.remove();
+ }
+ $('.userfilter .group_conditions').show();
+ return false;
+ },
+
+ getConditionFieldConfiguration: function(element, targetUrl) {
+ var target = $(element).parent();
+ $.ajax(targetUrl, {
+ url: targetUrl,
+ data: { fieldtype: $(element).val() },
+ success: function(data, textStatus, jqXHR) {
+ target.children('.conditionfield_compare_op').remove();
+ target.children('.conditionfield_value').remove();
+ target
+ .children('.conditionfield_delete')
+ .first()
+ .before(data);
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ alert('Status: ' + textStatus + '\nError: ' + errorThrown);
+ }
+ });
+ return false;
+ },
+
+ addConditionField: function(targetId, targetUrl) {
+ $.ajax({
+ url: targetUrl,
+ success: function(data, textStatus, jqXHR) {
+ $('#' + targetId).append(data);
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ alert('Status: ' + textStatus + '\nError: ' + errorThrown);
+ }
+ });
+ return false;
+ },
+
+ removeConditionField: function(element) {
+ element.remove();
+ Dialogs.closeConfirmDialog();
+ return false;
+ },
+
+ closeDialog: function(button) {
+ var dialog = $(button)
+ .parents('div[role=dialog]')
+ .first();
+ dialog.remove();
+ return false;
+ }
+};
+
+export default UserFilter;
diff --git a/resources/assets/javascripts/lib/wysiwyg.js b/resources/assets/javascripts/lib/wysiwyg.js
new file mode 100644
index 0000000..87929c9
--- /dev/null
+++ b/resources/assets/javascripts/lib/wysiwyg.js
@@ -0,0 +1,569 @@
+/**
+ * wysiwyg.js - Replace HTML textareas with WYSIWYG editor.
+ *
+ * Developer documentation can be found at
+ * http://docs.studip.de/develop/Entwickler/Wysiwyg.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author Robert Costa <zabbarob@gmail.com>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ */
+import parseOptions from './parse_options.js';
+
+const wysiwyg = {
+ disabled: !STUDIP.editor_enabled,
+ // NOTE keep this function in sync with Markup class
+ htmlMarker: '<!--HTML-->',
+ htmlMarkerRegExp: /^\s*<!--\s*HTML.*?-->/i,
+
+ isHtml: function isHtml(text) {
+ // NOTE keep this function in sync with
+ // Markup::isHtml in Markup.class.php
+ return this.hasHtmlMarker(text);
+ },
+ hasHtmlMarker: function hasHtmlMarker(text) {
+ // NOTE keep this function in sync with
+ // Markup::hasHtmlMarker in Markup.class.php
+ return this.htmlMarkerRegExp.test(text);
+ },
+ markAsHtml: function markAsHtml(text) {
+ // NOTE keep this function in sync with
+ // Markup::markAsHtml in Markup.class.php
+ if (this.hasHtmlMarker(text) || text.trim() == '') {
+ return text; // marker already set, don't set twice
+ }
+ return this.htmlMarker + '\n' + text;
+ },
+ // Create Stud.IP default configuration for editor
+ getDefaultConfig: function(textarea) {
+ // create new toolbar container
+ var textareaHeight = Math.max(textarea.height(), 200),
+ textareaWidth = (textarea.outerWidth() / textarea.parent().width()) * 100 + '%';
+
+ // fetch ckeditor configuration
+ var options = textarea.attr('data-editor'),
+ extraPlugins,
+ removePlugins;
+
+ if (options) {
+ options = parseOptions(options);
+ extraPlugins = options.extraPlugins;
+ removePlugins = options.removePlugins;
+ }
+
+ return {
+ allowedContent: {
+ // NOTE update the dev docs when changing ACF settings!!
+ // at http://docs.studip.de/develop/Entwickler/Wysiwyg
+ //
+ // note that changes here should also be reflected in
+ // HTMLPurifier's settings!!
+ a: {
+ // note that external links should always have
+ // class="link-extern", target="_blank" and rel="nofollow"
+ // and internal links should not have any attributes except
+ // for href, but this cannot be enforced here
+ attributes: ['href', 'target', 'rel', 'name', 'id'],
+ classes: ['link-extern', 'link-intern']
+ },
+ audio: {
+ attributes: ['controls', '!src', 'height', 'width'],
+ // only float:left and float:right should be allowed
+ styles: ['float', 'height', 'width']
+ },
+ big: {},
+ blockquote: {},
+ br: {},
+ caption: {},
+ code: {},
+ em: {},
+ div: {
+ classes: 'author', // needed for quotes
+ // only allow left margin and horizontal text alignment to
+ // be set in divs
+ // - margin-left should only be settable in multiples of
+ // 40 pixels
+ // - text-align should only be either "center", "right" or
+ // "justify"
+ // - note that maybe these two features will be removed
+ // completely in future versions
+ styles: ['margin-left', 'text-align']
+ },
+ h1: {},
+ h2: {},
+ h3: {},
+ h4: {},
+ h5: {},
+ h6: {},
+ hr: {},
+ img: {
+ attributes: ['alt', '!src', 'height', 'width'],
+ // only float:left and float:right should be allowed
+ styles: ['float']
+ },
+ li: {},
+ ol: {},
+ p: {
+ // - margin-left should only be settable in multiples of
+ // 40 pixels
+ // - text-align should only be either "center", "right" or
+ // "justify"
+ styles: ['margin-left', 'text-align']
+ },
+ pre: {
+ classes: ['usercode']
+ },
+ span: {
+ // note that 'wiki-links' are currently set as a span due
+ // to implementation difficulties, but probably this
+ // might be changed in future versions
+ classes: ['wiki-link', 'math-tex'],
+
+ // note that allowed (background-)colors should be further
+ // restricted
+ styles: ['color', 'background-color']
+ },
+ strong: {},
+ u: {},
+ ul: {},
+ s: {},
+ small: {},
+ sub: {},
+ sup: {},
+ table: {
+ // note that tables should always have the class "content"
+ // (it should not be optional)
+ classes: 'content'
+ },
+ tbody: {},
+ td: {
+ // attributes and styles should be the same
+ // as for <th>, except for 'scope' attribute
+ attributes: ['colspan', 'rowspan'],
+ styles: ['text-align', 'width', 'height', 'background-color']
+ },
+ thead: {},
+ th: {
+ // attributes and styles should be the same
+ // as for <td>, except for 'scope' attribute
+ //
+ // note that allowed scope values should be restricted to
+ // "col", "row" or "col row", if scope is set
+ attributes: ['colspan', 'rowspan', 'scope'],
+ styles: ['text-align', 'width', 'height']
+ },
+ tr: {},
+ tt: {},
+ video: {
+ attributes: ['controls', '!src', 'height', 'width'],
+ // only float:left and float:right should be allowed
+ styles: ['float', 'height', 'width']
+ }
+ },
+ height: textareaHeight,
+ width: textareaWidth,
+ skin: 'studip,' + STUDIP.ASSETS_URL + 'stylesheets/ckeditor-skin/',
+ // NOTE codemirror crashes when not explicitely loaded in CKEditor 4.4.7
+ extraPlugins:
+ 'emojione,studip-floatbar,studip-quote,studip-upload,studip-settings' +
+ (extraPlugins ? ',' + extraPlugins : ''),
+ removePlugins: removePlugins ? removePlugins : textarea.closest('.ui-dialog').length ? 'autogrow' : '',
+ enterMode: CKEDITOR.ENTER_BR,
+ mathJaxLib: STUDIP.URLHelper.getURL('assets/javascripts/mathjax/MathJax.js?config=TeX-AMS_HTML,default'),
+ studipUpload_url: STUDIP.URLHelper.getURL('dispatch.php/wysiwyg/upload'),
+ codemirror: {
+ autoCloseTags: false,
+ autoCloseBrackets: false,
+ showSearchButton: false,
+ showFormatButton: false,
+ showCommentButton: false,
+ showUncommentButton: false,
+ showAutoCompleteButton: false
+ },
+ autoGrow_onStartup: true,
+
+ // configure toolbar
+ toolbarGroups: [
+ { name: 'basicstyles', groups: ['undo', 'basicstyles', 'cleanup'] },
+ { name: 'paragraph', groups: ['list', 'indent', 'blocks', 'align', 'quote'] },
+ '/',
+ { name: 'styles', groups: ['styles', 'colors', 'tools', 'links', 'insert'] },
+ { name: 'others', groups: ['mode', 'settings'] }
+ ],
+ removeButtons: 'Font,FontSize',
+ toolbarCanCollapse: true,
+ toolbarStartupExpanded: textarea.width() > 420,
+
+ // configure dialogs
+ dialog_buttonsOrder: 'ltr',
+ removeDialogTabs: 'image:Link;image:advanced;' + 'link:target;link:advanced;' + 'table:advanced',
+
+ // convert special chars except latin ones to html entities
+ entities: false,
+ entities_latin: false,
+ entities_processNumerical: true,
+
+ // set WYSIWYG's menu language to the language set in Stud.IP
+ defaultLanguage: 'de', // use German if user language not available
+ language: String.locale, // override browser-stored preferences
+
+ // configure list of special characters
+ // NOTE 17 characters fit in one row of special characters dialog
+ specialChars: [].concat(
+ [
+ '&Agrave;',
+ '&Aacute;',
+ '&Acirc;',
+ '&Atilde;',
+ '&Auml;',
+ '&Aring;',
+ '&AElig;',
+ '&Egrave;',
+ '&Eacute;',
+ '&Ecirc;',
+ '&Euml;',
+ '&Igrave;',
+ '&Iacute;',
+ '&Iuml;',
+ '&Icirc;',
+ '',
+ '&Yacute;',
+
+ '&agrave;',
+ '&aacute;',
+ '&acirc;',
+ '&atilde;',
+ '&auml;',
+ '&aring;',
+ '&aelig;',
+ '&egrave;',
+ '&eacute;',
+ '&ecirc;',
+ '&euml;',
+ '&igrave;',
+ '&iacute;',
+ '&iuml;',
+ '&icirc;',
+ '',
+ '&yacute;',
+
+ '&Ograve;',
+ '&Oacute;',
+ '&Ocirc;',
+ '&Otilde;',
+ '&Ouml;',
+ '&Oslash;',
+ '&OElig;',
+ '&Ugrave;',
+ '&Uacute;',
+ '&Ucirc;',
+ '&Uuml;',
+ '',
+ '&Ccedil;',
+ '&Ntilde;',
+ '&#372;',
+ '',
+ '&#374',
+
+ '&ograve;',
+ '&oacute;',
+ '&ocirc;',
+ '&otilde;',
+ '&ouml;',
+ '&oslash;',
+ '&oelig;',
+ '&ugrave;',
+ '&uacute;',
+ '&ucirc;',
+ '&uuml;',
+ '',
+ '&ccedil;',
+ '&ntilde;',
+ '&#373',
+ '',
+ '&#375;',
+
+ '&szlig;',
+ '&ETH;',
+ '&eth;',
+ '&THORN;',
+ '&thorn;',
+ '',
+ '',
+ '`',
+ '&acute;',
+ '^',
+ '&uml;',
+ '',
+ '&cedil;',
+ '~',
+ '&asymp;',
+ '',
+ '&yuml;'
+ ],
+ (function() {
+ var greek = [];
+ for (var i = 913; i <= 929; i++) {
+ // 17 uppercase characters
+ greek.push('&#' + String(i));
+ }
+ for (var i = 945; i <= 962; i++) {
+ // 17 lowercase characters
+ greek.push('&#' + String(i));
+ }
+ // NOTE character #930 is not assigned!!
+ for (var i = 931; i <= 937; i++) {
+ // remaining upercase
+ greek.push('&#' + String(i));
+ }
+ greek.push('');
+ for (var i = 963; i <= 969; i++) {
+ // remaining lowercase
+ greek.push('&#' + String(i));
+ }
+ greek.push('');
+ return greek;
+ })(),
+ [
+ '&ordf;',
+ '&ordm;',
+ '&deg;',
+ '&sup1;',
+ '&sup2;',
+ '&sup3;',
+ '&frac14;',
+ '&frac12;',
+ '&frac34;',
+ '&lsquo;',
+ '&rsquo;',
+ '&ldquo;',
+ '&rdquo;',
+ '&laquo;',
+ '&raquo;',
+ '&iexcl;',
+ '&iquest;',
+
+ '@',
+ '&sect;',
+ '&para;',
+ '&micro;',
+ '[',
+ ']',
+ '{',
+ '}',
+ '|',
+ '&brvbar;',
+ '&ndash;',
+ '&mdash;',
+ '&macr;',
+ '&sbquo;',
+ '&#8219;',
+ '&bdquo;',
+ '&hellip;',
+
+ '&euro;',
+ '&cent;',
+ '&pound;',
+ '&yen;',
+ '&curren;',
+ '&copy;',
+ '&reg;',
+ '&trade;',
+
+ '&not;',
+ '&middot;',
+ '&times;',
+ '&divide;',
+
+ '&#9658;',
+ '&bull;',
+ '&rarr;',
+ '&rArr;',
+ '&hArr;',
+ '&diams;',
+
+ '&#x00B1', // ±
+ '&#x2229', // ∩ INTERSECTION
+ '&#x222A', // ∪ UNION
+ '&#x221E', // ∞ INFINITY
+ '&#x2107', // ℇ EULER CONSTANT
+ '&#x2200', // ∀ FOR ALL
+ '&#x2201', // ∁ COMPLEMENT
+ '&#x2202', // ∂ PARTIAL DIFFERENTIAL
+ '&#x2203', // ∃ THERE EXISTS
+ '&#x2204', // ∄ THERE DOES NOT EXIST
+ '&#x2205', // ∅ EMPTY SET
+ '&#x2206', // ∆ INCREMENT
+ '&#x2207', // ∇ NABLA
+ '&#x2282', // ⊂ SUBSET OF
+ '&#x2283', // ⊃ SUPERSET OF
+ '&#x2284', // ⊄ NOT A SUBSET OF
+ '&#x2286', // ⊆ SUBSET OF OR EQUAL TO
+ '&#x2287', // ⊇ SUPERSET OF OR EQUAL TO
+ '&#x2208', // ∈ ELEMENT OF
+ '&#x2209', // ∉ NOT AN ELEMENT OF
+ '&#x2227', // ∧ LOGICAL AND
+ '&#x2228', // ∨ LOGICAL OR
+ '&#x2264', // ≤ LESS-THAN OR EQUAL TO
+ '&#x2265', // ≥ GREATER-THAN OR EQUAL TO
+ '&#x220E', // ∎ END OF PROOF
+ '&#x220F', // ∏ N-ARY PRODUCT
+ '&#x2211', // ∑ N-ARY SUMMATION
+ '&#x221A', // √ SQUARE ROOT
+ '&#x222B', // ∫ INTEGRAL
+ '&#x2234', // ∴ THEREFORE
+ '&#x2235', // ∵ BECAUSE
+ '&#x2260', // ≠ NOT EQUAL TO
+ '&#x2262', // ≢ NOT IDENTICAL TO
+ '&#x2263', // ≣ STRICTLY EQUIVALENT TO
+ '&#x22A2', // ⊢ RIGHT TACK
+ '&#x22A3', // ⊣ LEFT TACK
+ '&#x22A4', // ⊤ DOWN TACK
+ '&#x22A5', // ⊥ UP TACK
+ '&#x22A7', // ⊧ MODELS
+ '&#x22A8', // ⊨ TRUE
+ '&#x22AC', // ⊬ DOES NOT PROVE
+ '&#x22AD', // ⊭ NOT TRUE
+ '&#x22EE', // ⋮ VERTICAL ELLIPSIS
+ '&#x22EF', // ⋯ MIDLINE HORIZONTAL ELLIPSIS
+ '&#x29FC', // ⧼ LEFT-POINTING CURVED ANGLE BRACKET
+ '&#x29FD', // ⧽ RIGHT-POINTING CURVED ANGLE BRACKET
+ '&#x207F', // ⁿ SUPERSCRIPT LATIN SMALL LETTER N
+ '&#x2295', // ⊕ CIRCLED PLUS
+ '&#x2297', // ⊗ CIRCLED TIMES
+ '&#x2299' // ⊙ CIRCLED DOT OPERATOR
+ ]
+ ),
+ on: { pluginsLoaded: onPluginsLoaded },
+ title: false
+ };
+ },
+
+ // for jquery dialogs, see toolbar.js
+ replace: replaceTextarea
+};
+
+export default wysiwyg;
+
+function replaceTextarea(textarea, config) {
+ // TODO support jQuery object with multiple textareas
+ if (!(textarea instanceof jQuery)) {
+ textarea = $(textarea);
+ }
+
+ // In Firefox the browser's window is not set active after a Drag and Drop action.
+ // So placeholders do not work correctly in Firefox and will be removed.
+ if (CKEDITOR.env.gecko) {
+ textarea.removeAttr('placeholder');
+ }
+
+ // create ID for textarea if it doesn't have one
+ if (!textarea.attr('id')) {
+ textarea.attr('id', createNewId('wysiwyg'));
+ }
+
+ // No custom config given, fetch default config
+ if (config == undefined) {
+ config = wysiwyg.getDefaultConfig(textarea);
+ }
+
+ // replace textarea with editor
+ CKEDITOR.replace(textarea[0], config);
+
+ CKEDITOR.on('instanceReady', function(event) {
+ var editor = event.editor,
+ $textarea = $(editor.element.$);
+
+ // auto-resize editor area in source view mode, and keep focus!
+ editor.on('mode', function(event) {
+ var editor = event.editor;
+ if (editor.mode === 'source') {
+ $(editor.container.$)
+ .find('.cke_source')
+ .focus();
+ } else {
+ editor.focus();
+ }
+ });
+
+ // fix for not pasting text from clipboard twice on firefox in a dialog
+ if (CKEDITOR.env.gecko && $textarea.closest('.ui-dialog').length) {
+ $(editor.container.$).on('paste', function(event) {
+ event.preventDefault();
+ });
+ }
+
+ // clean up HTML edited in source mode before submit
+ var form = $textarea.closest('form');
+ form.submit(function(event) {
+ // make sure HTML marker is always set, in
+ // case contents are cut-off by the backend
+ editor.setData(wysiwyg.markAsHtml(editor.getData()));
+ editor.updateElement(); // update textarea, in case it's accessed by other JS code
+ });
+
+ // update textarea on editor blur
+ editor.on('blur', function(event) {
+ event.editor.updateElement();
+ });
+ $(editor.container.$).on('blur', '.CodeMirror', function(event) {
+ editor.updateElement(); // also update in source mode
+ });
+
+ // blurDelay = 0 is an ugly hack to be faster than Stud.IP
+ // forum's save function; might produce "strange" behaviour
+ CKEDITOR.focusManager._.blurDelay = 0;
+
+ // display "focused"-effect when editor area is focused
+ editor.on('focus', function(event) {
+ event.editor.container.addClass('cke_chrome_focused');
+ });
+ editor.on('blur', function(event) {
+ event.editor.container.removeClass('cke_chrome_focused');
+ });
+
+ // keep the editor focused when a toolbar item gets selected
+ editor.on('blur', function(event) {
+ var toolbarContainer = $('#' + event.editor.config.sharedSpaces.top);
+ if (toolbarContainer.has(':focus').length > 0) {
+ event.editor.focus();
+ }
+ });
+
+ // Trigger load event for the editor event. Uses the underlying
+ // textarea element to ensure that the event will be catchable by
+ // jQuery.
+ $textarea.trigger('load.wysiwyg');
+
+ // focus the editor if requested
+ if ($textarea.is('[autofocus]')) {
+ editor.focus();
+ }
+ });
+}
+
+// editor events
+function onPluginsLoaded(event) {
+ // tell editor to always remove html comments
+ event.editor.dataProcessor.htmlFilter.addRules({
+ comment: function(element) {
+ if (!wysiwyg.hasHtmlMarker(decodeURIComponent(element).substring(18))) {
+ return false;
+ }
+ }
+ });
+}
+
+// create an unused id
+function createNewId(prefix) {
+ var i = 0;
+ while ($('#' + prefix + i).length > 0) {
+ i++;
+ }
+ return prefix + i;
+}