aboutsummaryrefslogtreecommitdiff
path: root/resources/assets/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'resources/assets/javascripts')
-rw-r--r--resources/assets/javascripts/bootstrap/actionmenu.js40
-rw-r--r--resources/assets/javascripts/bootstrap/admin-courses.js12
-rw-r--r--resources/assets/javascripts/bootstrap/admin_sem_classes.js10
-rw-r--r--resources/assets/javascripts/bootstrap/admission.js19
-rw-r--r--resources/assets/javascripts/bootstrap/application.js424
-rw-r--r--resources/assets/javascripts/bootstrap/article.js32
-rw-r--r--resources/assets/javascripts/bootstrap/avatar.js46
-rw-r--r--resources/assets/javascripts/bootstrap/big_image_handler.js2
-rw-r--r--resources/assets/javascripts/bootstrap/blubber.js4
-rw-r--r--resources/assets/javascripts/bootstrap/cache-admin.js22
-rw-r--r--resources/assets/javascripts/bootstrap/calendar_dialog.js11
-rw-r--r--resources/assets/javascripts/bootstrap/clipboard.js95
-rw-r--r--resources/assets/javascripts/bootstrap/consultations.js26
-rw-r--r--resources/assets/javascripts/bootstrap/contentbox.js17
-rw-r--r--resources/assets/javascripts/bootstrap/copyable_links.js43
-rw-r--r--resources/assets/javascripts/bootstrap/course_wizard.js14
-rwxr-xr-xresources/assets/javascripts/bootstrap/courseware.js34
-rw-r--r--resources/assets/javascripts/bootstrap/cronjobs.js33
-rw-r--r--resources/assets/javascripts/bootstrap/data_secure.js161
-rw-r--r--resources/assets/javascripts/bootstrap/dates.js75
-rw-r--r--resources/assets/javascripts/bootstrap/dialog.js3
-rw-r--r--resources/assets/javascripts/bootstrap/drag_and_drop_upload.js5
-rw-r--r--resources/assets/javascripts/bootstrap/files.js108
-rw-r--r--resources/assets/javascripts/bootstrap/forms.js316
-rw-r--r--resources/assets/javascripts/bootstrap/fullcalendar.js47
-rw-r--r--resources/assets/javascripts/bootstrap/fullscreen.js9
-rw-r--r--resources/assets/javascripts/bootstrap/global_search.js97
-rw-r--r--resources/assets/javascripts/bootstrap/gradebook.js13
-rw-r--r--resources/assets/javascripts/bootstrap/header_magic.js6
-rw-r--r--resources/assets/javascripts/bootstrap/header_navigation.js15
-rw-r--r--resources/assets/javascripts/bootstrap/i18n_input.js3
-rw-r--r--resources/assets/javascripts/bootstrap/inline-editing.js48
-rw-r--r--resources/assets/javascripts/bootstrap/installer.js124
-rw-r--r--resources/assets/javascripts/bootstrap/jsupdater.js10
-rw-r--r--resources/assets/javascripts/bootstrap/lightbox.js31
-rw-r--r--resources/assets/javascripts/bootstrap/members.js21
-rw-r--r--resources/assets/javascripts/bootstrap/messages.js100
-rw-r--r--resources/assets/javascripts/bootstrap/multi_person_search.js8
-rw-r--r--resources/assets/javascripts/bootstrap/multi_select.js11
-rw-r--r--resources/assets/javascripts/bootstrap/mvv_difflog.js200
-rw-r--r--resources/assets/javascripts/bootstrap/my-courses.js22
-rw-r--r--resources/assets/javascripts/bootstrap/news.js34
-rw-r--r--resources/assets/javascripts/bootstrap/oer.js141
-rw-r--r--resources/assets/javascripts/bootstrap/opengraph.js55
-rw-r--r--resources/assets/javascripts/bootstrap/personal_notifications.js10
-rw-r--r--resources/assets/javascripts/bootstrap/qr_code.js41
-rw-r--r--resources/assets/javascripts/bootstrap/questionnaire.js75
-rw-r--r--resources/assets/javascripts/bootstrap/quick_search.js21
-rw-r--r--resources/assets/javascripts/bootstrap/raumzeit.js163
-rw-r--r--resources/assets/javascripts/bootstrap/resource-tree-widget.js48
-rw-r--r--resources/assets/javascripts/bootstrap/resources.js999
-rw-r--r--resources/assets/javascripts/bootstrap/responsive.js39
-rw-r--r--resources/assets/javascripts/bootstrap/scroll_to_top.js7
-rw-r--r--resources/assets/javascripts/bootstrap/search.js146
-rw-r--r--resources/assets/javascripts/bootstrap/selection.js34
-rw-r--r--resources/assets/javascripts/bootstrap/settings.js76
-rw-r--r--resources/assets/javascripts/bootstrap/sidebar.js64
-rw-r--r--resources/assets/javascripts/bootstrap/skip_links.js8
-rw-r--r--resources/assets/javascripts/bootstrap/smiley.js19
-rw-r--r--resources/assets/javascripts/bootstrap/smiley_picker.js7
-rw-r--r--resources/assets/javascripts/bootstrap/startpage.js34
-rw-r--r--resources/assets/javascripts/bootstrap/statusgroups.js73
-rw-r--r--resources/assets/javascripts/bootstrap/studip_helper_attributes.js229
-rw-r--r--resources/assets/javascripts/bootstrap/subcourses.js73
-rw-r--r--resources/assets/javascripts/bootstrap/tabbable_widget.js31
-rw-r--r--resources/assets/javascripts/bootstrap/tables.js42
-rw-r--r--resources/assets/javascripts/bootstrap/tfa.js42
-rw-r--r--resources/assets/javascripts/bootstrap/tooltip.js68
-rw-r--r--resources/assets/javascripts/bootstrap/tour.js52
-rw-r--r--resources/assets/javascripts/bootstrap/vue.js59
-rw-r--r--resources/assets/javascripts/bootstrap/wiki.js146
-rw-r--r--resources/assets/javascripts/bootstrap/wysiwyg.js61
-rw-r--r--resources/assets/javascripts/chunk-loader.js88
-rw-r--r--resources/assets/javascripts/chunks/chartist.js4
-rw-r--r--resources/assets/javascripts/chunks/code-highlight.js18
-rw-r--r--resources/assets/javascripts/chunks/fullcalendar.js11
-rw-r--r--resources/assets/javascripts/chunks/tablesorter.js18
-rw-r--r--resources/assets/javascripts/chunks/vue.js54
-rw-r--r--resources/assets/javascripts/entry-admission.js2
-rw-r--r--resources/assets/javascripts/entry-base.js95
-rw-r--r--resources/assets/javascripts/entry-installer.js2
-rw-r--r--resources/assets/javascripts/entry-statusgroups.js4
-rw-r--r--resources/assets/javascripts/entry-wysiwyg.js4
-rw-r--r--resources/assets/javascripts/feedback.js169
-rw-r--r--resources/assets/javascripts/init.js175
-rw-r--r--resources/assets/javascripts/jquery-bundle.js182
-rw-r--r--resources/assets/javascripts/jquery/autoresize.jquery.min.js7
-rw-r--r--resources/assets/javascripts/jquery/jquery.filtertable-1.5.7.js402
-rw-r--r--resources/assets/javascripts/jquery/jstree/jquery.jstree.js4564
-rw-r--r--resources/assets/javascripts/jquery/jstree/themes/default/d.gifbin0 -> 2944 bytes
-rw-r--r--resources/assets/javascripts/jquery/jstree/themes/default/d.pngbin0 -> 7635 bytes
-rw-r--r--resources/assets/javascripts/jquery/jstree/themes/default/style.css76
-rw-r--r--resources/assets/javascripts/jquery/jstree/themes/default/throbber.gifbin0 -> 1849 bytes
-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
-rw-r--r--resources/assets/javascripts/mvv.js831
-rw-r--r--resources/assets/javascripts/mvv_course_wizard.js346
-rw-r--r--resources/assets/javascripts/public-path.js1
-rw-r--r--resources/assets/javascripts/studip-jquery-selection-helper.js107
-rw-r--r--resources/assets/javascripts/studip-jquery-tweaks.js67
-rw-r--r--resources/assets/javascripts/studip-jquery.multi-select.tweaks.js86
-rw-r--r--resources/assets/javascripts/studip-ui.js638
-rw-r--r--resources/assets/javascripts/vendor/qrcode-04f46c6.js1487
186 files changed, 27802 insertions, 0 deletions
diff --git a/resources/assets/javascripts/bootstrap/actionmenu.js b/resources/assets/javascripts/bootstrap/actionmenu.js
new file mode 100644
index 0000000..75542ce
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/actionmenu.js
@@ -0,0 +1,40 @@
+/*jslint esversion: 6 */
+
+(function ($) {
+ 'use strict';
+
+ var last = null;
+
+ // Open action menu on click on the icon
+ $(document).on('click', '.action-menu-icon', function (event) {
+ // Choose correct root element if menu was positioned absolutely
+ let root_element = $(this).closest('.action-menu');
+ if ($(this).closest('.action-menu-wrapper').length > 0) {
+ root_element = $(this).data('action-menu-element');
+ }
+
+ var position = root_element.data('action-menu-reposition');
+ if (position === undefined) {
+ position = true;
+ }
+ // Obtain unique id for the root element and close other menus if neccessary
+ const id = root_element.uniqueId().attr('id');
+ if (last !== id) {
+ STUDIP.ActionMenu.closeAll();
+ last = id;
+ }
+
+ STUDIP.ActionMenu.create(root_element, position).toggle();
+
+ // Stop event so the following close event will not be fired
+ return false;
+ });
+
+ // Close action menu on click outside
+ $(document).on('click', (event) => {
+ if ($(event.target).closest('.action-menu-content').length === 0) {
+ STUDIP.ActionMenu.closeAll();
+ }
+ });
+
+}(jQuery));
diff --git a/resources/assets/javascripts/bootstrap/admin-courses.js b/resources/assets/javascripts/bootstrap/admin-courses.js
new file mode 100644
index 0000000..3fa0511
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/admin-courses.js
@@ -0,0 +1,12 @@
+STUDIP.Dialog.registerHeaderHandler('X-Dialog-Notice', json => {
+ json = JSON.parse(json);
+
+ $(`#course-${json.id} td.actions .button`)
+ .removeClass('has-notice has-no-notice')
+ .addClass(json.notice.length > 0 ? 'has-notice' : 'has-no-notice')
+ .attr('title', json.notice);
+
+ STUDIP.Dialog.close();
+
+ return false;
+});
diff --git a/resources/assets/javascripts/bootstrap/admin_sem_classes.js b/resources/assets/javascripts/bootstrap/admin_sem_classes.js
new file mode 100644
index 0000000..7d004ee
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/admin_sem_classes.js
@@ -0,0 +1,10 @@
+STUDIP.domReady(() => {
+ $(document).on('click', '.sem_type_delete', STUDIP.admin_sem_class.delete_sem_type_question);
+ $(document).on('blur', '.name_input > input', STUDIP.admin_sem_class.rename_sem_type);
+ $(STUDIP.admin_sem_class.make_sortable);
+ $('div[container] > div.droparea > div.plugin select[name=sticky]').change(function() {
+ $(this)
+ .closest('div.plugin')
+ .toggleClass('sticky', this.value === 'sticky');
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/admission.js b/resources/assets/javascripts/bootstrap/admission.js
new file mode 100644
index 0000000..0734d39
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/admission.js
@@ -0,0 +1,19 @@
+/* ------------------------------------------------------------------------
+ * Anmeldeverfahren und -sets
+ * ------------------------------------------------------------------------ */
+
+STUDIP.domReady(function () {
+ $(document).on('change', 'tr.course input', function(i) {
+ STUDIP.Admission.toggleNotSavedAlert();
+ });
+
+ $('a.userlist-delete-user').on('click', function(event) {
+ $(this).closest('tr').remove();
+ return false;
+ });
+
+ $('#courseset-form .autosave').on('click', (event) => {
+ STUDIP.Admission.autosaveCourseset();
+ })
+
+});
diff --git a/resources/assets/javascripts/bootstrap/application.js b/resources/assets/javascripts/bootstrap/application.js
new file mode 100644
index 0000000..c9ac326
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/application.js
@@ -0,0 +1,424 @@
+import { $gettext } from '../lib/gettext.js';
+
+/*jslint browser: true, esversion: 6 */
+/*global window, $, jQuery, _ */
+/* ------------------------------------------------------------------------
+ * application.js
+ * This file is part of Stud.IP - http://www.studip.de
+ *
+ * Stud.IP 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.
+ *
+ * Stud.IP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Stud.IP; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301 USA
+ */
+
+ /* ------------------------------------------------------------------------
+ * add classes to html element according to horizontal screen size
+ * ------------------------------------------------------------------------ */
+(function ($) {
+ // These sizes must match the breakpoints defined in breakspoints.less
+ // TODO: use same webpack configuration for both
+ const sizes = {
+ tiny: '0px',
+ small: '576px',
+ medium: '768px',
+ large: '1200px'
+ };
+
+ const setScreensizeClasses = function () {
+ for (let size in sizes) {
+ if (window.matchMedia(`(min-width: ${sizes[size]})`).matches) {
+ $('html').addClass(`size-${size}`);
+ } else {
+ $('html').removeClass(`size-${size}`);
+ }
+ }
+ };
+
+ // Reset screen size classes on window resizes
+ $(window).resize(setScreensizeClasses);
+
+ // Set screen size classes initially
+ setScreensizeClasses();
+}(jQuery));
+
+/* ------------------------------------------------------------------------
+ * messages boxes
+ * ------------------------------------------------------------------------ */
+jQuery(document).on('click', '.messagebox .messagebox_buttons a', function () {
+ if (jQuery(this).is('.details')) {
+ jQuery(this).closest('.messagebox').toggleClass('details_hidden');
+ } else if (jQuery(this).is('.close')) {
+ jQuery(this).closest('.messagebox').hide('blind', 'fast', function () {
+ jQuery(this).remove();
+ });
+ }
+ return false;
+}).on('focus', '.messagebox .messagebox_buttons a', function () {
+ jQuery(this).blur(); // Get rid of the ugly "clicked border" due to the text-indent
+});
+
+
+/* ------------------------------------------------------------------------
+ * application wide setup
+ * ------------------------------------------------------------------------ */
+STUDIP.domReady(function () {
+ // AJAX Indicator
+ STUDIP.ajax_indicator = true;
+
+ STUDIP.study_area_selection.initialize();
+
+ // autofocus for all browsers
+ if (!("autofocus" in document.createElement("input"))) {
+ jQuery('[autofocus]').first().focus();
+ }
+
+ if (document.createElement('textarea').style.resize === undefined) {
+ jQuery('textarea.resizable').resizable({
+ handles: 's',
+ minHeight: 50,
+ zIndex: 1
+ });
+ }
+
+ jQuery.ajaxSetup({
+ beforeSend (jqXHR, settings) {
+ const requestUrl = new URL(settings.url, STUDIP.ABSOLUTE_URI_STUDIP);
+ const studipUrl = new URL(STUDIP.ABSOLUTE_URI_STUDIP);
+ if (requestUrl.hostname === studipUrl.hostname && requestUrl.protocol === studipUrl.protocol) {
+ jqXHR.setRequestHeader('X-CSRF-TOKEN', STUDIP.CSRF_TOKEN.value);
+ }
+ },
+ });
+});
+
+STUDIP.ready((event) => {
+ jQuery('.add_toolbar', event.target).addToolbar();
+
+ STUDIP.Forms.initialize(event.target);
+ STUDIP.Markup.element(event.target);
+});
+
+
+/* ------------------------------------------------------------------------
+ * application collapsable tablerows
+ * ------------------------------------------------------------------------ */
+STUDIP.domReady(function () {
+
+ $(document).on('focus', 'table.collapsable .toggler', function () {
+ $(this).blur();
+ }).on('click', 'table.collapsable .toggler', function () {
+ $(this).closest('tbody').toggleClass('collapsed')
+ .filter('.collapsed').find('.action-menu').removeClass('active');
+ return false;
+ });
+
+ $(document).on('click', 'a.load-in-new-row', function () {
+ if ($(this).data('busy')) {
+ return false;
+ }
+
+ if ($(this).closest('tr').next().hasClass('loaded-details')) {
+ $(this).closest('tr').next().remove();
+ return false;
+ }
+ $(this).showAjaxNotification().data('busy', true);
+
+ var that = this;
+ $.get($(this).attr('href'), function (response) {
+ var row = $('<tr />').addClass('loaded-details');
+ $('<td />')
+ .attr('colspan', $(that).closest('td').siblings().length + 1)
+ .html(response)
+ .appendTo(row);
+
+ $(that)
+ .hideAjaxNotification()
+ .closest('tr').after(row);
+
+ $(that).data('busy', false);
+ $('body').trigger('ajaxLoaded');
+ });
+
+ return false;
+ });
+
+ $(document).on('click', '.loaded-details a.cancel', function () {
+ $(this).closest('.loaded-details').prev().find('a.load-in-new-row').click();
+ return false;
+ });
+
+ var elements = $('.load-in-new-row-open');
+ elements.click();
+ if (elements.length > 0) {
+ $(window).scrollTo(elements.first());
+ }
+});
+
+/* ------------------------------------------------------------------------
+ * Toggle dates in seminar_main
+ * ------------------------------------------------------------------------ */
+(function ($) {
+ $(document).on('click', '.more-dates', function () {
+ $('.more-dates-infos').toggle();
+ $('.more-dates-digits').toggle();
+ if ($('.more-dates-infos').is(':visible')) {
+ $('.more-dates').text('(weniger)');
+ $('.more-dates').attr('title', $gettext('Blenden Sie die restlichen Termine aus'));
+ } else {
+ $('.more-dates').text('(mehr)');
+ $('.more-dates').attr('title', $gettext('Blenden Sie die restlichen Termine ein'));
+ }
+ });
+
+ $(document).on('click', '.more-location-dates', function () {
+ $(this).closest('div').prev().toggle();
+ $(this).prev().toggle();
+
+ if ($(this).closest('div').prev().is(':visible')) {
+ $(this).text('(weniger)');
+ $(this).attr('title', $gettext('Blenden Sie die restlichen Termine aus'));
+ } else {
+ $(this).text('(mehr)');
+ $(this).attr('title', $gettext('Blenden Sie die restlichen Termine ein'));
+ }
+ });
+}(jQuery));
+
+/* ------------------------------------------------------------------------
+ * additional jQuery (UI) settings for Stud.IP
+ * ------------------------------------------------------------------------ */
+jQuery.ui.accordion.prototype.options.icons = {
+ header: 'arrow_right',
+ activeHeader: 'arrow_down'
+};
+jQuery.extend(jQuery.ui.dialog.prototype.options, {
+ closeText: $gettext('Schließen')
+});
+
+
+/* ------------------------------------------------------------------------
+ * jQuery timepicker
+ * ------------------------------------------------------------------------ */
+
+/* German translation for the jQuery Timepicker Addon */
+/* Written by Marvin */
+(function ($) {
+ $.timepicker.regional.de = {
+ timeOnlyTitle: 'Zeit wählen',
+ timeText: 'Zeit',
+ hourText: 'Stunde',
+ minuteText: 'Minute',
+ secondText: 'Sekunde',
+ millisecText: 'Millisekunde',
+ microsecText: 'Mikrosekunde',
+ timezoneText: 'Zeitzone',
+ currentText: 'Jetzt',
+ closeText: 'Fertig',
+ timeFormat: "HH:mm",
+ amNames: ['vorm.', 'AM', 'A'],
+ pmNames: ['nachm.', 'PM', 'P'],
+ isRTL: false,
+ showTimezone: false
+ };
+ $.timepicker.setDefaults($.timepicker.regional.de);
+
+ $(document).on('focus', '.has-time-picker', function () {
+ $(this).removeClass('has-time-picker').timepicker();
+ });
+ $(document).on('focus', '.has-time-picker-select', function () {
+ $(this).removeClass('has-time-picker-select').timepicker({controlType: 'select'});
+ });
+}(jQuery));
+
+
+(function ($) {
+ $(document).on('focusout', '.studip-timepicker', function () {
+ var time = $(this).val();
+ if (time.length > 0 && time.length <= 2) {
+ $(this).val(time + ":00");
+ } else if (time.indexOf(':') === -1 && time.length > 2) {
+ var parts = time.split('');
+ parts.splice(-2, 0, ':');
+ time = parts.join('');
+ $(this).val(time);
+ }
+ });
+}(jQuery))
+
+
+STUDIP.domReady(function () {
+ $(document).on('click', 'a.print_action', function (event) {
+ var url_to_print = this.href;
+ $('<iframe/>', {
+ name: url_to_print,
+ src: url_to_print,
+ width: '1px',
+ height: '1px',
+ frameborder: 0
+ })
+ .css({top: '-99px', position: 'absolute'})
+ .appendTo('body')
+ .on('load', (function () {
+ this.contentWindow.focus();
+ this.contentWindow.print();
+ }));
+ return false;
+ });
+});
+
+/* Copies a value from a select to another element*/
+jQuery(document).on('change', 'select[data-copy-to]', function () {
+ var target = jQuery(this).data().copyTo,
+ value = jQuery(this).val() || jQuery(target).prop('defaultValue');
+ jQuery(target).val(value);
+});
+
+STUDIP.domReady(function () {
+ $('#checkAll').prop('checked', $('.sem_checkbox:checked').length !== 0);
+});
+
+// Fix horizontal scroll issue on domready, window load and window resize.
+// This also makes the header and footer sticky regarding horizontal scrolling.
+STUDIP.domReady(function () {
+ var page_margin = ($('#layout_page').outerWidth(true) - $('#layout_page').width()) / 2,
+ content_margin = $('#layout_content').outerWidth(true) - $('#layout_content').innerWidth(),
+ sidebar_width = $('#layout-sidebar').outerWidth(true);
+
+ function fixScrolling() {
+ $('#layout_page').removeClass('oversized').css({
+ minWidth: '',
+ marginRight: '',
+ paddingRight: ''
+ });
+
+ var max_width = 0,
+ fix_required = $('html').is(':not(.responsified)') && $('#layout_content').get(0).scrollWidth > $('#layout_content').width();
+
+ if (fix_required) {
+ $('#layout_content').children().each(function () {
+ var width = $(this).get(0).scrollWidth + ($(this).outerWidth(true) - $(this).innerWidth());
+ if (width > max_width) {
+ max_width = width;
+ }
+ });
+
+ $('#layout_page').addClass('oversized').css({
+ minWidth: sidebar_width + content_margin + max_width + page_margin,
+ marginRight: 0,
+ paddingRight: page_margin
+ });
+
+ STUDIP.Scroll.addHandler('horizontal-scroll', (function () {
+ var last_left = null;
+ return function (top, left) {
+ if (last_left !== left) {
+ $('#flex-header,#tabs,#layout_footer,#barBottomContainer').css({
+ transform: 'translate3d(' + left + 'px,0,0)'
+ });
+ }
+ last_left = left;
+ };
+ }()));
+ } else {
+ STUDIP.Scroll.removeHandler('horizontal-scroll');
+ }
+ };
+
+ if ($('.no-touch #layout_content').length > 0) {
+ window.matchMedia('screen').addListener(function() {
+ // Try to fix now
+ fixScrolling();
+
+ // and fix again on window load and resize
+ $(window).on('resize load', _.debounce(fixScrolling, 100));
+ });
+ }
+});
+
+jQuery(document).on('click', '.course-admin td .course-completion', function () {
+ var href = $(this).attr('href'),
+ timeout = window.setTimeout(function () {
+ $(this).addClass('ajaxing');
+ }.bind(this), 300);;
+
+ $.getJSON(href).done(function (completion) {
+ clearTimeout(timeout);
+
+ $(this).removeClass('ajaxing').attr('data-course-completion', completion);
+ }.bind(this));
+
+ return false;
+});
+
+// Global handler:
+// Toggle a table element. The url of the link will be called, an ajax
+// indicator will be shown instead of the element and the whole table row
+// will be replaced with the row with the same id from the response.
+// Thus, in your controller you only have to execute the appropriate
+// action and redraw the page with the new state.
+jQuery(document).on('click', 'a[data-behaviour~="ajax-toggle"]', function (event) {
+ var $that = jQuery(this),
+ href = $that.attr('href'),
+ id = $that.closest('tr').attr('id');
+
+ $that.prop('disabled', true).addClass('ajaxing');
+ jQuery.get(href).done(function (response) {
+ var row = jQuery('#' + id, response);
+ $that.closest('tr').replaceWith(row);
+ });
+
+ event.preventDefault();
+});
+
+/* Change open-variable on course-basicdata*/
+(function ($) {
+ $(document).on('click', 'form[name=course-details] fieldset legend', function () {
+ $('#open_variable').attr('value', $(this).parent('fieldset').data('open'));
+ });
+}(jQuery));
+
+// Detect high contrast mode
+// https://gist.github.com/ffoodd/78f99204b5806e183574
+$(window).on('load', () => {
+ function prefersContrast () {
+ if (window.matchMedia('prefers-contrast: more').matches || window.matchMedia('prefers-contrast: high').matches) {
+ return true;
+ }
+
+ const testColor = 'rgb(31,41,59)';
+ const testElement = document.createElement('a');
+ let strColor;
+
+ testElement.style.color = testColor;
+ document.documentElement.appendChild(testElement);
+ strColor = document.defaultView ? document.defaultView.getComputedStyle(testElement, null).color : testElement.currentStyle.color;
+ strColor = strColor.replace(/ /g, '');
+ document.documentElement.removeChild(testElement);
+ return strColor !== testColor;
+ }
+
+ document.querySelector('html').classList.toggle(
+ 'high-contrast-mode-activated',
+ prefersContrast()
+ );
+});
+
+
+// Trigger consuming mode on contentbar
+STUDIP.domReady(function () {
+ $(document).on("click", ".consuming_mode_trigger", function () {
+ $("body").toggleClass("consuming_mode");
+ return false;
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/article.js b/resources/assets/javascripts/bootstrap/article.js
new file mode 100644
index 0000000..5811575
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/article.js
@@ -0,0 +1,32 @@
+/*jslint browser: true */
+/*global jQuery, STUDIP */
+(function ($, STUDIP) {
+ 'use strict';
+
+ $(document).on('click', 'article.studip.toggle header h1 a', function (e) {
+ e.preventDefault();
+
+ var article = $(this).closest('article');
+
+ // If the contentbox article is new send an ajax request
+ if (article.hasClass('new') && article.data('visiturl')) {
+ $.post(STUDIP.URLHelper.getURL(decodeURIComponent(article.data('visiturl') + $(this).attr('href'))));
+ }
+
+ // Open the contentbox
+ article.toggleClass('open').removeClass('new');
+ });
+
+ // Open closed article contents when location hash matches
+ $(window).on('hashchange', (event) => {
+ const hash = location.hash.split('#').pop();
+ $(`article.studip.toggle:not(.open) header h1 a[name="${hash}"]`).click();
+ });
+
+ STUDIP.ready(() => {
+ const hash = location.hash.split('#').pop();
+ if (hash.length > 0) {
+ $(`article.studip.toggle:not(.open) header h1 a[name="${hash}"]`).click();
+ }
+ });
+}(jQuery, STUDIP));
diff --git a/resources/assets/javascripts/bootstrap/avatar.js b/resources/assets/javascripts/bootstrap/avatar.js
new file mode 100644
index 0000000..a164ca2
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/avatar.js
@@ -0,0 +1,46 @@
+/*global jQuery, STUDIP */
+STUDIP.domReady(() => {
+ STUDIP.Avatar.init('#avatar-upload');
+
+ // Get file data on drop
+ var dropZone = document.getElementById('avatar-overlay');
+
+ if (dropZone) {
+ dropZone.addEventListener('dragover', function(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ e.target.parentNode.classList.add("dragging");
+ });
+
+ dropZone.addEventListener('dragleave', function(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ e.target.parentNode.classList.remove("dragging");
+ });
+
+ dropZone.addEventListener('drop', function(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ e.target.parentNode.classList.remove("dragging");
+ var files = e.dataTransfer.files;
+ var div = e.target.parentNode;
+ var avatar_dialog = div.getElementsByTagName('a')[0];
+
+ if (!div.getAttribute('accept') || !div.getAttribute('accept').includes(files[0].type)) {
+ alert(div.getAttribute('data-message-unaccepted'));
+ return false;
+ }
+
+ if (!div.getAttribute('data-max-size') || files[0].size > div.getAttribute('data-max-size')) {
+ alert(div.getAttribute('data-message-too-large'));
+ return false;
+ }
+
+ avatar_dialog.click();
+ div.files = files;
+ STUDIP.dialogReady(() => {
+ STUDIP.Avatar.readFile(div);
+ });
+ });
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/big_image_handler.js b/resources/assets/javascripts/bootstrap/big_image_handler.js
new file mode 100644
index 0000000..001341c
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/big_image_handler.js
@@ -0,0 +1,2 @@
+// Engage by default
+STUDIP.BigImageHandler.enable();
diff --git a/resources/assets/javascripts/bootstrap/blubber.js b/resources/assets/javascripts/bootstrap/blubber.js
new file mode 100644
index 0000000..095ac77
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/blubber.js
@@ -0,0 +1,4 @@
+/*global jQuery, STUDIP */
+STUDIP.domReady(() => {
+ STUDIP.Blubber.init();
+}); \ No newline at end of file
diff --git a/resources/assets/javascripts/bootstrap/cache-admin.js b/resources/assets/javascripts/bootstrap/cache-admin.js
new file mode 100644
index 0000000..67cf7f9
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/cache-admin.js
@@ -0,0 +1,22 @@
+/**
+ * Stud.IP: Administration of available cache types, like database, Memcached, Redis etc.
+ *
+ * @author Thomas Hackl <studip@thomas-hackl.name>
+ * @license GPL2 or any later version
+ * @copyright Stud.IP core group
+ * @since Stud.IP 5.0
+ */
+
+/*global jQuery, STUDIP */
+import CacheAdministration from '../../../vue/components/CacheAdministration.vue'
+
+STUDIP.domReady(() => {
+ if (document.getElementById('cache-admin-container')) {
+ STUDIP.Vue.load().then(({ createApp }) => {
+ createApp({
+ el: '#cache-admin-container',
+ components: { CacheAdministration }
+ })
+ })
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/calendar_dialog.js b/resources/assets/javascripts/bootstrap/calendar_dialog.js
new file mode 100644
index 0000000..ee5ab4c
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/calendar_dialog.js
@@ -0,0 +1,11 @@
+jQuery(document).on('click', 'td.calendar-day-edit, td.calendar-day-event', function(event) {
+ var elem = jQuery(this)
+ .find('a')
+ .first();
+ if (_.isString(elem.attr('href'))) {
+ STUDIP.Dialog.fromURL(elem.attr('href'), { title: elem.attr('title') });
+ event.preventDefault();
+ } else {
+ return false;
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/clipboard.js b/resources/assets/javascripts/bootstrap/clipboard.js
new file mode 100644
index 0000000..0b6c271
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/clipboard.js
@@ -0,0 +1,95 @@
+STUDIP.domReady(function() {
+ jQuery('.clipboard-draggable-item').draggable(
+ {
+ cursorAt: {left: 28, top: 15},
+ appendTo: 'body',
+ helper: function () {
+ var dragged_item = jQuery('<div class="dragged-clipboard-item"></div>');
+ jQuery(dragged_item).data('id', jQuery(this).data('id'));
+ jQuery(dragged_item).data('range_type', jQuery(this).data('range_type'));
+ jQuery(dragged_item).text(jQuery(this).data('name'));
+ return dragged_item;
+ },
+ revert: true,
+ revertDuration: 0
+ }
+ );
+
+ jQuery('.clipboard-area').droppable(
+ {
+ drop: STUDIP.Clipboard.handleItemDrop
+ }
+ );
+
+ jQuery('.clipboard-selector').change(
+ STUDIP.Clipboard.switchClipboard
+ );
+
+ jQuery(document).on(
+ 'change',
+ '.clipboard-selector',
+ STUDIP.Clipboard.switchClipboard
+ );
+
+ jQuery(document).on(
+ 'dragend',
+ '.clipboard-draggable-item',
+ function(event) {
+ jQuery(event.target).css(
+ {
+ 'top': '0px',
+ 'left': '0px'
+ }
+ );
+ }
+ );
+
+ jQuery(document).on(
+ 'dragover',
+ '.clipboard-area',
+ function(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ );
+
+ jQuery(document).on(
+ 'dragenter',
+ '.clipboard-area',
+ function(event) {
+ //TODO:rrv2: use CSS classes!
+ event.target.style.backgroundColor = '#0F0';
+ }
+ );
+
+ jQuery(document).on(
+ 'dragleave',
+ '.clipboard-area',
+ function(event) {
+ //TODO:rrv2: use CSS classes!
+ event.target.style.backgroundColor = '#FFF';
+ }
+ );
+
+ jQuery(document).on(
+ 'click',
+ '.clipboard-remove-button',
+ STUDIP.Clipboard.confirmRemoveClick
+ );
+
+ jQuery(document).on(
+ 'click',
+ '.clipboard-item-remove-button',
+ STUDIP.Clipboard.confirmRemoveItemClick
+ );
+
+ jQuery('.clipboard-widget .new-clipboard-form').submit(
+ STUDIP.Clipboard.handleAddForm
+ );
+
+ jQuery(document).on(
+ 'click',
+ '.clipboard-add-item-button',
+ STUDIP.Clipboard.handleAddItemButtonClick
+ );
+});
diff --git a/resources/assets/javascripts/bootstrap/consultations.js b/resources/assets/javascripts/bootstrap/consultations.js
new file mode 100644
index 0000000..6897a09
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/consultations.js
@@ -0,0 +1,26 @@
+import { $gettext } from '../lib/gettext.js';
+
+$(document).on('click', '.consultation-delete-check:not(.ignore)', event => {
+ const form = $(event.target).closest('form');
+ const checkboxes = form.find(':checkbox[name="slot-id[]"]:checked');
+ const ids = checkboxes.map((index, element) => element.value.split('-').pop()).get();
+
+ if (!ids.length) {
+ return false;
+ }
+
+ STUDIP.api.GET('consultations/slots/bulk', {data: {ids: ids}}).done(slots => {
+ let bookings = 0;
+ slots.forEach(slot => bookings += slot.booking_count);
+ if (bookings === 0) {
+ STUDIP.Dialog.confirm($gettext('Wollen Sie diese Termine wirklich löschen?')).done(() => {
+ $('<input type="hidden" name="delete" value="1"/>').appendTo(form);
+ form.submit();
+ });
+ } else {
+ $(event.target).addClass('ignore').click().removeClass('ignore');
+ }
+ });
+
+ event.preventDefault();
+});
diff --git a/resources/assets/javascripts/bootstrap/contentbox.js b/resources/assets/javascripts/bootstrap/contentbox.js
new file mode 100644
index 0000000..42c5df1
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/contentbox.js
@@ -0,0 +1,17 @@
+$(document).on('click', 'section.contentbox article header h1 a', function(e) {
+ if (!$(this).hasClass('no-contentbox-link')) {
+ e.preventDefault();
+ var article = $(this).closest('article');
+
+ // If the contentbox article is new send an ajax request
+ if (article.hasClass('new')) {
+ $.ajax({
+ type: 'POST',
+ url: STUDIP.URLHelper.getURL(decodeURIComponent(article.data('visiturl') + $(this).attr('href')))
+ });
+ }
+
+ // Open the contentbox
+ article.toggleClass('open').removeClass('new');
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/copyable_links.js b/resources/assets/javascripts/bootstrap/copyable_links.js
new file mode 100644
index 0000000..1cd9c5f
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/copyable_links.js
@@ -0,0 +1,43 @@
+import { $gettext } from '../lib/gettext.js';
+
+/*jslint esversion: 6*/
+$(document).on('click', 'a.copyable-link', function (event) {
+ event.preventDefault();
+
+ // Create dummy element and position it off screen
+ // This element must be "visible" (as in "not hidden") or otherwise
+ // the copy command will fail
+ var dummy = $('<textarea>').val(this.href).css({
+ position: 'absolute',
+ left: '-9999px'
+ }).appendTo('body');
+
+ // Select text and copy it to clipboard
+ dummy[0].select();
+ document.execCommand('Copy');
+ dummy.remove();
+
+ // Show visual hint using a deferred (this way we don't need to
+ // duplicate the functionality in the done() handler)
+ (new Promise((resolve, reject) => {
+ var confirmation = $('<div class="copyable-link-confirmation">');
+ confirmation.text($gettext('Link wurde kopiert'));
+ confirmation.insertBefore(this);
+
+ $(this).parent().addClass('copyable-link-animation');
+
+ // Resolve deferred when animation has ended or after 2 seconds as a
+ // fail safe
+ var timeout = setTimeout(() => {
+ $(this).parent().off('animationend');
+ resolve(confirmation);
+ }, 1500);
+ $(this).parent().one('animationend', () => {
+ clearTimeout(timeout);
+ resolve(confirmation);
+ });
+ })).then((confirmation, parent) => {
+ confirmation.remove();
+ $(this).parent().removeClass('copyable-link-animation');
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/course_wizard.js b/resources/assets/javascripts/bootstrap/course_wizard.js
new file mode 100644
index 0000000..ef85e96
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/course_wizard.js
@@ -0,0 +1,14 @@
+STUDIP.domReady(function() {
+ if ($('.sem-tree-assigned-root > ul > li').length == 0) {
+ $('.sem-tree-assigned-root').addClass('hidden-js');
+ }
+});
+
+STUDIP.ready(function() {
+ $('.course-wizard-step-0 *:input:not(input[type=submit])').each(function (index) {
+ $(this).attr(
+ 'tabindex',
+ $(this).closest('section,footer').css('order')
+ );
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/courseware.js b/resources/assets/javascripts/bootstrap/courseware.js
new file mode 100755
index 0000000..f9a85fe
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/courseware.js
@@ -0,0 +1,34 @@
+STUDIP.domReady(() => {
+ if (document.getElementById('courseware-index-app')) {
+ STUDIP.Vue.load().then(({ createApp }) => {
+ import(
+ /* webpackChunkName: "courseware-index-app" */
+ '@/vue/courseware-index-app.js'
+ ).then(({ default: mountApp }) => {
+ return mountApp(STUDIP, createApp, '#courseware-index-app');
+ });
+ });
+ }
+
+ if (document.getElementById('courseware-dashboard-app')) {
+ STUDIP.Vue.load().then(({ createApp }) => {
+ import(
+ /* webpackChunkName: "courseware-dashboard-app" */
+ '@/vue/courseware-dashboard-app.js'
+ ).then(({ default: mountApp }) => {
+ return mountApp(STUDIP, createApp, '#courseware-dashboard-app');
+ });
+ });
+ }
+
+ if (document.getElementById('courseware-manager-app')) {
+ STUDIP.Vue.load().then(({ createApp }) => {
+ import(
+ /* webpackChunkName: "courseware-manager-app" */
+ '@/vue/courseware-manager-app.js'
+ ).then(({ default: mountApp }) => {
+ return mountApp(STUDIP, createApp, '#courseware-manager-app');
+ });
+ });
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/cronjobs.js b/resources/assets/javascripts/bootstrap/cronjobs.js
new file mode 100644
index 0000000..8a62247
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/cronjobs.js
@@ -0,0 +1,33 @@
+// Cron task: Change tbody class according to inherent input setting
+$(document).on('change', '.cron-task input', function() {
+ $(this)
+ .closest('tbody')
+ .addClass('selected')
+ .siblings()
+ .removeClass('selected');
+});
+
+// Cron item:
+// Display the following element and focus it's inherent input element
+// if no value from a select element has been chosen. Hide the following
+// element if a value has been chosen.
+$(document).on('change', '.cron-item select', function() {
+ var state = $(this).val().length > 0,
+ $next = $(this).next();
+
+ if (state) {
+ $next
+ .show()
+ .find('input')
+ .focus();
+ } else {
+ $next.hide();
+ }
+});
+
+// Active date and time picker as well as the Cron item selector on
+// document ready / page load.
+STUDIP.domReady(function() {
+ $('.cron-item select').change();
+ $('.cronjobs tfoot select').change();
+});
diff --git a/resources/assets/javascripts/bootstrap/data_secure.js b/resources/assets/javascripts/bootstrap/data_secure.js
new file mode 100644
index 0000000..3837df7
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/data_secure.js
@@ -0,0 +1,161 @@
+import { $gettext } from '../lib/gettext.js';
+
+/**
+ * Secure forms or form elements by displaying a warning on page unload if
+ * there are unsaved changes.
+ *
+ * Add the data-attribute "secure" to any <form> or :input element and when
+ * the page is reloaded or the surrounding dialog is closed, a confirmation
+ * dialog will appear.
+ *
+ * There are two config options that may be passed via the data-secure
+ * attribute.
+ *
+ * {
+ * always: Secures the element regardless of it's changed state. If a
+ * form should always be secured, use this. If you want to exclude
+ * an element from the security check, set always on that element
+ * to false (but you should use the shorthand `data-secure="false"`
+ * since the wording "always" is a little bit misleading in this
+ * case).
+ * exists: Dynamically added nodes cannot be detected and thus will
+ * never be taken into account when detecting whether the
+ * element's value has changed. Specify a css selector that
+ * precisely identifies elements that are only present when the
+ * element needs to be secured.
+ *
+ * These options may be passed as a json encoded array like this:
+ *
+ * <form data-secure='{always: false, exists: "#foo > .bar"}'>
+ *
+ * But since you will probably never need the two options at once, you may
+ * either pass just a boolean value to the data-secure attribute for setting
+ * the "always" option or any other non-object value as the "exists" option:
+ *
+ * <form data-secure="true">
+ *
+ * is equivalent to
+ *
+ * <form data-secure='{always: true}'>
+ *
+ * and
+ *
+ * <form data-secure="#foo .bar">
+ *
+ * is equivalent to
+ *
+ * <form data-secure='{exists: "#foo .bar"}'>
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license GPL2 or any later version
+ * @since Stud.IP 3.4
+ */
+
+/**
+ * Normalize arbitrary input to config option object
+ *
+ * @param mixed input Arbitrary input
+ * @return Object config
+ */
+function normalizeConfig(input) {
+ var config = {
+ always: null,
+ exists: false
+ };
+ if ($.isPlainObject(input)) {
+ config = $.extend(config, input);
+ } else if (input === false || input === true) {
+ config.always = input;
+ } else {
+ config.exists = input || false;
+ }
+ return config;
+}
+
+/**
+ * Detect any changes on elements with the data-secure attribute
+ * in a given context.
+ *
+ * @param mixed context Optional context in which the elements should be
+ * located
+ * @return bool indicating whether any changes have occured
+ */
+function detectChanges(context) {
+ var changed = false;
+
+ $('[data-secure]', context || document).each(function() {
+ if (
+ $(this)
+ .closest('form')
+ .data().secureSkip
+ ) {
+ return;
+ }
+
+ var data = $(this).data().secure;
+ var config = normalizeConfig(data);
+ var items = $(this).is('form') ? $(this).find(':input') : $(this);
+
+ if (config.always === true) {
+ changed = true;
+ } else if (config.always !== false && config.exists === false) {
+ items
+ .filter('[name]')
+ .filter(':not(:checkbox,:radio)')
+ .each(function() {
+ changed = changed || (this.defaultValue !== undefined && this.value !== this.defaultValue);
+ });
+ items
+ .filter('[name]')
+ .filter(':checkbox,:radio')
+ .each(function() {
+ changed = changed || (this.defaultChecked !== undefined && this.checked !== this.defaultChecked);
+ });
+ }
+
+ if (!changed && config.exists !== false) {
+ changed = $(config.exists, this).length > 0;
+ }
+ });
+
+ return changed;
+}
+
+// Secure browser window on refresh via the beforeunload event
+$(window).on('beforeunload', function(event) {
+ if (detectChanges() === false) {
+ return;
+ }
+
+ event = event || window.event || {};
+ event.returnValue = $gettext('Ihre Eingaben wurden bislang noch nicht gespeichert.');
+ return event.returnValue;
+});
+
+// Secure dialogs on close via the dialogbeforeclose event
+$(document).on('dialogbeforeclose', function(event) {
+ if (detectChanges(event.target) === false) {
+ return true;
+ }
+
+ if (!window.confirm($gettext('Ihre Eingaben wurden bislang noch nicht gespeichert.'))) {
+ event.preventDefault();
+ event.stopPropagation();
+ return false;
+ }
+
+ return true;
+});
+
+// Mark form on submit so it will be skipped during security check
+$(document)
+ .on('submit', 'form[data-secure],form:has([data-secure])', function() {
+ $(this)
+ .closest('form')
+ .data('secure-skip', true);
+ })
+ .on('change', 'form[data-secure],form *[data-secure]', function() {
+ $(this)
+ .closest('form')
+ .data('secure-skip', false);
+ });
diff --git a/resources/assets/javascripts/bootstrap/dates.js b/resources/assets/javascripts/bootstrap/dates.js
new file mode 100644
index 0000000..c6cc77e
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/dates.js
@@ -0,0 +1,75 @@
+$(document).on('click', '.remove_topic', STUDIP.Dates.removeTopicFromIcon);
+
+// Drag and drop support for topics in date list
+function createDraggable() {
+ $('.dates.has-access tbody tr:not(:only-child) .themen-list li > a.title:not(.draggable-topic)').each(function() {
+ var table_id = $(this)
+ .closest('table')
+ .data().tableId;
+
+ $(this)
+ .children()
+ .addClass('draggable-topic-handle');
+
+ $(this)
+ .closest('li')
+ .addClass('draggable-topic')
+ .data('table-id', table_id)
+ .attr('data-table-id', table_id)
+ .draggable({
+ axis: 'y',
+ containment: $(this).closest('tbody'),
+ handle: '.draggable-topic-handle',
+ revert: true
+ });
+ });
+}
+
+STUDIP.domReady(function () {
+ if ($('body#course-dates-index').length === 0) {
+ return;
+ }
+
+ $(document).ajaxComplete(createDraggable);
+
+ $('.themen-list').each(function() {
+ var table_id = $(this)
+ .closest('table')
+ .data().tableId;
+ $(this)
+ .closest('td')
+ .addClass('topic-droppable')
+ .droppable({
+ accept: '.draggable-topic[data-table-id="' + table_id + '"]',
+ activeClass: 'active',
+ hoverClass: 'hovered',
+ drop: function(event, ui) {
+ var context = $(ui.draggable),
+ topic = context.closest('li').data().issue_id,
+ source = context.closest('tr').data().terminId,
+ target = $(this)
+ .closest('tr')
+ .data().terminId,
+ path = ['dispatch.php/course/dates/move_topic', topic, source, target].join('/'),
+ url = STUDIP.URLHelper.getURL(path),
+ cell = $(this);
+
+ if (source === target) {
+ return;
+ }
+
+ ui.draggable.draggable('option', 'revert', false);
+
+ $.post(url).done(function(response) {
+ ui.draggable
+ .draggable('destroy')
+ .closest('li')
+ .remove();
+ $('ul', cell).append(response);
+ });
+ }
+ });
+ });
+
+ createDraggable();
+});
diff --git a/resources/assets/javascripts/bootstrap/dialog.js b/resources/assets/javascripts/bootstrap/dialog.js
new file mode 100644
index 0000000..f186307
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/dialog.js
@@ -0,0 +1,3 @@
+STUDIP.domReady(function () {
+ STUDIP.Dialog.initialize();
+});
diff --git a/resources/assets/javascripts/bootstrap/drag_and_drop_upload.js b/resources/assets/javascripts/bootstrap/drag_and_drop_upload.js
new file mode 100644
index 0000000..a89bc09
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/drag_and_drop_upload.js
@@ -0,0 +1,5 @@
+STUDIP.ready((event) => {
+ $('form.drag-and-drop:not(.files)', event.target).each(function() {
+ STUDIP.DragAndDropUpload.bind(this);
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/files.js b/resources/assets/javascripts/bootstrap/files.js
new file mode 100644
index 0000000..ae47af4
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/files.js
@@ -0,0 +1,108 @@
+/*jslint esversion: 6 */
+function searchMoreFiles (button) {
+ var table = $(button).closest('table');
+ var loading = $('<div class="loading" style="padding: 10px">').html(
+ $('<img>')
+ .attr('src', STUDIP.ASSETS_URL + 'images/ajax-indicator-black.svg')
+ .css('width', '24')
+ .css('height', '24')
+ );
+
+ $(button).replaceWith(loading);
+
+ $.get(button.href).done((output) => {
+ table.find('tbody').append($('tbody tr', output));
+ table.find('tfoot').replaceWith($('tfoot', output));
+ });
+
+ return false;
+}
+
+STUDIP.domReady(() => {
+
+ STUDIP.Files.init();
+
+ $('form.drag-and-drop.files').on('dragover dragleave', (event) => {
+ $(event.target).toggleClass('hovered', event.type === 'dragover');
+
+ event.preventDefault();
+ }).on('drop', (event) => {
+ var filelist = event.originalEvent.dataTransfer.files || {};
+ STUDIP.Files.upload(filelist);
+
+ event.preventDefault();
+ }).on('click', function() {
+ $('.file_selector input[type=file]').first().click();
+ });
+
+ $('table.documents.flat.filter').each(function () {
+ var ignored = [];
+ $('colgroup col', this).each((index, col) => {
+ if ($(col).is('[data-filter-ignore]')) {
+ ignored.push(index);
+ }
+ });
+ $(this).filterTable({
+ highlightClass: 'filter-match',
+ ignoreColumns: ignored,
+ inputSelector: '.sidebar .tablesorterfilter',
+ minChars: 1,
+ minRows: 1
+ });
+ });
+
+ $(document).trigger('refresh-handlers');
+
+ $(document).on(
+ 'click',
+ '#file_license_chooser_1 > input[type=radio]',
+ STUDIP.Files.updateTermsOfUseDescription
+ );
+
+ $(document).on('click', '.files-search-more', (event) => {
+ searchMoreFiles(event.target);
+
+ event.preventDefault();
+ });
+});
+
+
+jQuery(document).on('ajaxComplete', (event, xhr) => {
+ if (!xhr.getResponseHeader('X-Filesystem-Changes')) {
+ return;
+ }
+
+ var changes = JSON.parse(xhr.getResponseHeader('X-Filesystem-Changes'));
+ var payload = false;
+
+ function process(key, handler) {
+ if (!changes.hasOwnProperty(key)) {
+ return;
+ }
+
+ var values = changes[key];
+ if (values === null && xhr.getResponseHeader('Content-Type').match(/json/)) {
+ try {
+ if (payload === false) {
+ payload = JSON.parse(xhr.responseText);
+ }
+ if (payload.hasOwnProperty(key)) {
+ values = payload[key];
+ }
+ } catch (e) {
+ }
+ }
+
+ handler(values);
+ }
+
+ process('added_files', STUDIP.Files.addFileDisplay);
+ process('added_folders', STUDIP.Files.addFolderDisplay);
+ process('removed_files', STUDIP.Files.removeFileDisplay);
+ process('redirect', STUDIP.Dialog.fromURL);
+ process('message', (html) => {
+ $('.file_upload_window .uploadbar').hide().parent().append(html);
+ });
+ process('close_dialog', STUDIP.Dialog.close);
+
+}); \ No newline at end of file
diff --git a/resources/assets/javascripts/bootstrap/forms.js b/resources/assets/javascripts/bootstrap/forms.js
new file mode 100644
index 0000000..a6bb204
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/forms.js
@@ -0,0 +1,316 @@
+import { $gettext } from '../lib/gettext.js';
+
+// Allow fieldsets to collapse
+$(document).on(
+ 'click',
+ 'form.default fieldset.collapsable legend,form.default.collapsable fieldset legend',
+ function() {
+ $(this)
+ .closest('fieldset')
+ .toggleClass('collapsed');
+ }
+);
+
+// Display a visible hint that indicates how many characters the user may
+// input if the element has a maxlength restriction.
+
+$(document).on('focus', 'form.default [maxlength]:not(.no-hint)', function() {
+ if (!$(this).is('textarea,input') || $(this).data('length-hint') || $(this).is('[readonly],[disabled]')) {
+ return;
+ }
+
+ var width = $(this).outerWidth(true),
+ hint = $('<div class="length-hint">').hide(),
+ wrap = $('<div class="length-hint-wrapper">').width(width),
+ timeout = null;
+
+ $(this).wrap(wrap);
+
+ hint.text($gettext('Zeichen verbleibend: '));
+
+ hint.append('<span class="length-hint-counter">');
+ hint.insertBefore(this);
+
+ $(this)
+ .focus(function() {
+ clearTimeout(timeout);
+ timeout = setTimeout(function() {
+ hint.finish().show('slide', { direction: 'down' }, 300);
+ }, 200);
+ })
+ .blur(function() {
+ clearTimeout(timeout);
+ timeout = setTimeout(function() {
+ hint.finish().hide('slide', { direction: 'down' }, 300);
+ }, 200);
+ })
+ .on('focus propertychange change keyup', function() {
+ var count = $(this).val().length,
+ max = parseInt($(this).attr('maxlength'), 10);
+
+ hint.find('.length-hint-counter').text(max - count);
+ });
+
+ $(this).data('length-hint', true);
+
+ setTimeout(
+ function() {
+ $(this).focus();
+ }.bind(this),
+ 0
+ );
+});
+
+// Automatic form submission handler when a select has changed it's value.
+// Due to accessibility issues, an intuitive select[onchange=form.submit()]
+// leads to terrible behaviour when invoked not by mouse. The form is
+// submitted upon _every_ change, including key strokes.
+// Thus, we need to overwrite this behaviour. Breakdown of this solution:
+//
+// - Only submit when the value has actually changed
+// - Always submit when pressing enter (keycode 13)
+// - Always check for change on blur event
+//
+// - Store whether the element was activated by click event
+// - If so, submit upon next change event
+// - Otherwise submit when enter has been pressed
+//
+// Be aware: All select[onchange*="submit()"] will be rewritten to
+// select.submit-upon-select and have the onchange attribute removed.
+// This might lead to unexpected behaviour.
+
+// Ensure, every .submit-upon-select has an defaultSelected option.
+$(document)
+ .on('focus', 'select[onchange*="submit()"]', function() {
+ $(this)
+ .removeAttr('onchange')
+ .addClass('submit-upon-select');
+ })
+ .on('click mousedown', 'select.submit-upon-select', function(event) {
+ // Firefox and Chrome handle click events on selects differently,
+ // thus we need the mousedown event and the click event is needed for
+ // select2 elements. Please do not change!
+
+ $(this).data('wasClicked', true);
+ })
+ .on('change', 'select.submit-upon-select', function(event) {
+ // Trigger blur event if element was clicked in the beginning
+
+ if ($(this).data('wasClicked')) {
+ $(this).trigger('blur');
+ }
+ })
+ .on('focusout keyup keypress keydown select', 'select.submit-upon-select', function(event) {
+ var shouldSubmit = event.type === 'keyup' ? event.which === 13 : $(this).data('wasClicked'),
+ is_default = $('option:selected', this).prop('defaultSelected');
+
+ // Submit only if value has changed and either enter was pressed or
+ // select was opened by click
+ if (!is_default && shouldSubmit) {
+ $(this)
+ .closest('form')
+ .submit();
+ return false;
+ }
+ });
+
+STUDIP.ready((event) => {
+ $('.submit-upon-select', event.target).each(function() {
+ var has_default_selected =
+ $('option', this).filter(function() {
+ return this.defaultSelected;
+ }).length > 0;
+ if (!has_default_selected) {
+ $('option', this)
+ .first()
+ .prop('defaultSelected', true);
+ }
+ });
+});
+
+
+// simulate formaction attribute for input[type=image] in IE11
+$(document).on('click', 'input[type=image][formaction]', function() {
+ if ($(this).attr('data-confirm') === undefined) {
+ $(this)
+ .closest('form')
+ .attr('action', $(this).attr('formaction'));
+ }
+});
+
+// Use select2 for crossbrowser compliant select styling and
+// handling
+$.fn.select2.amd.define('select2/i18n/de', [], function() {
+ return {
+ inputTooLong: function(e) {
+ var t = e.input.length - e.maximum;
+ return $gettext('Bitte %u Zeichen weniger eingeben').replace('%u', t);
+ },
+ inputTooShort: function(e) {
+ var t = e.minimum - e.input.length;
+ return $gettext('Bitte %u Zeichen mehr eingeben').replace('%u', t);
+ },
+ loadingMore: function() {
+ return $gettext('Lade mehr Ergebnisse...');
+ },
+ maximumSelected: function(e) {
+ var t = [
+ $gettext('Sie können nur %u Eintrag auswählen'),
+ $gettext('Sie können nur %u Einträge auswählen')
+ ];
+ return t[e.maximum === 1 ? 0 : 1].replace('%u', e.maximum);
+ },
+ noResults: function() {
+ return $gettext('Keine Übereinstimmungen gefunden');
+ },
+ searching: function() {
+ return $gettext('Suche...');
+ }
+ };
+});
+$.fn.select2.defaults.set('language', 'de');
+
+function createSelect2(element) {
+ if ($(element).data('select2')) {
+ return;
+ }
+
+ var select_classes = $(element)
+ .removeClass('select2-awaiting')
+ .attr('class'),
+ option = $('<option>'),
+ width = $(element).outerWidth(true),
+ cloned = $(element)
+ .clone()
+ .css('opacity', 0)
+ .appendTo('body'),
+ wrapper = $('<div class="select2-wrapper">').css('display', cloned.css('display')),
+ placeholder;
+
+ cloned.remove();
+ $(wrapper)
+ .add(element)
+ .css('width', width);
+
+ if ($('.is-placeholder', element).length > 0) {
+ placeholder = $('.is-placeholder', element)
+ .text()
+ .trim();
+
+ option.attr('selected', $(element).val() === '');
+ $('.is-placeholder', element).replaceWith(option);
+ }
+
+ $(element).select2({
+ adaptDropdownCssClass: function() {
+ return select_classes;
+ },
+ allowClear: placeholder !== undefined,
+ minimumResultsForSearch: $(element).closest('.sidebar').length > 0 ? 15 : 10,
+ placeholder: placeholder,
+ dropdownParent: $(element).closest('.ui-dialog,body'),
+ templateResult: function(data, container) {
+ if (data.element) {
+ var option_classes = $(data.element).attr('class'),
+ element_data = $(data.element).data();
+ $(container).addClass(option_classes);
+
+ // Allow text color changes (calendar needs this)
+ if (element_data.textColor) {
+ $(container).css('color', element_data.textColor);
+ }
+ }
+ return data.text;
+ },
+ templateSelection: function(data, container) {
+ var result = $('<span class="select2-selection__content">').text(data.text),
+ element_data = $(data.element).data();
+ if (element_data && element_data.textColor) {
+ result.css('color', element_data.textColor);
+ }
+
+ if (element_data && element_data.colorClass) {
+ result.addClass(element_data.colorClass);
+ }
+
+ return result;
+ },
+ width: 'style'
+ });
+
+ $(element)
+ .next()
+ .addBack()
+ .wrapAll(wrapper);
+}
+
+STUDIP.ready(function () {
+ // Well, this is really nasty: Select2 can't determine the select
+ // element's width if it is hidden (by itself or by it's parent).
+ // This is due to the fact that elements are not rendered when hidden
+ // (which seems pretty obvious when you think about it) but elements
+ // only have a width when they are rendered (pretty obvious as well).
+ //
+ // Thus, we need to handle the visible elements first and apply
+ // select2 directly.
+ $('select.nested-select:not(:has(optgroup)):visible').each(function() {
+ createSelect2(this);
+ });
+
+ // The hidden need a little more love. The only, almost sane-ish
+ // solution seems to be to attach a mutation observer to the closest
+ // visible element from the requested select element and observe style,
+ // class and attribute changes in order to detect when the select
+ // element itself will become visible. Pretty straight forward, huh?
+ $('select.nested-select:not(:has(optgroup)):hidden:not(.select2-awaiting)').each(function() {
+ var observer = new window.MutationObserver(function(mutations) {
+ mutations.forEach(function(mutation) {
+ if ($('select.select2-awaiting', mutation.target).length > 0) {
+ $('select.select2-awaiting', mutation.target)
+ .removeClass('select2-awaiting')
+ .each(function() {
+ createSelect2(this);
+ });
+ observer.disconnect();
+ observer = null;
+ }
+ });
+ });
+ observer.observe($(this).closest(':visible')[0], {
+ attributeOldValue: true,
+ attributes: true,
+ attributeFilter: ['style', 'class'],
+ characterData: false,
+ childList: true,
+ subtree: false
+ });
+
+ $(this).addClass('select2-awaiting');
+ });
+
+ // Unfortunately, this code needs to be duplicated because jQuery
+ // namespacing kind of sucks. If the below change handler is namespaced
+ // and we trigger that namespaced event here, still all change handlers
+ // will execute (which is bad due to $(select).change(form.submit())).
+ $('select:not([multiple])').each(function() {
+ $(this)
+ .toggleClass('has-no-value', this.value === '')
+ .blur();
+ });
+});
+
+$(document)
+ .on('change', 'select:not([multiple])', function() {
+ $(this).toggleClass('has-no-value', this.value === '');
+ })
+ .on('dialog-close', function(event, data) {
+ $('select.nested-select:not(:has(optgroup))', data.dialog).each(function() {
+ if (!$(this).data('select2')) {
+ return;
+ }
+ $(this).select2('close');
+ });
+ })
+ .on('select2:open', 'select', function() {
+ $(this).click();
+ });
diff --git a/resources/assets/javascripts/bootstrap/fullcalendar.js b/resources/assets/javascripts/bootstrap/fullcalendar.js
new file mode 100644
index 0000000..6280b4c
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/fullcalendar.js
@@ -0,0 +1,47 @@
+/*jslint esversion: 6*/
+STUDIP.ready(function () {
+ //Check if fullcalendar instances are to be displayed:
+ $('*[data-fullcalendar="1"]').each(function () {
+ STUDIP.loadChunk('fullcalendar').then(() => {
+ if (this.calendar === undefined) {
+ let calendar;
+ if ($(this).hasClass('semester-plan')) {
+ calendar = STUDIP.Fullcalendar.createSemesterCalendarFromNode(this);
+ } else {
+ calendar = STUDIP.Fullcalendar.createFromNode(this);
+ }
+
+ let continuousRefresh = (ttl) => {
+ setTimeout(() => {
+ calendar.updateSize();
+ if (ttl > 0) {
+ continuousRefresh(ttl - 1);
+ }
+ }, 200);
+ };
+ continuousRefresh(10);
+ }
+ });
+ });
+
+ if ($('#event-color-picker > option').length <= 1) {
+ var selectedColor = $('#selected-color').val();
+ var colors = ['yellow', 'orange', 'red', 'violet', 'dark-violet', 'green', 'dark-green', 'petrol', 'brown'];
+
+ var style = window.getComputedStyle(document.body);
+ colors.forEach(color => {
+ let real_color = style.getPropertyValue(`--${color}`).trim();
+ $('#event-color-picker').append([
+ $('<input type="radio" name="event_color">').attr({
+ id: color,
+ value: real_color,
+ checked: selectedColor === real_color
+ }),
+ $('<label>').attr('for', color).css({
+ backgroundColor: `var(--${color})`
+ }),
+ ]);
+ });
+ }
+
+});
diff --git a/resources/assets/javascripts/bootstrap/fullscreen.js b/resources/assets/javascripts/bootstrap/fullscreen.js
new file mode 100644
index 0000000..1fa9b72
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/fullscreen.js
@@ -0,0 +1,9 @@
+STUDIP.domReady(function () {
+ $('.fullscreen-toggle').click(() => STUDIP.Fullscreen.toggle());
+
+ if (sessionStorage.getItem('studip-fullscreen') == 'on' && $('.fullscreen-toggle').length > 0) {
+ STUDIP.Fullscreen.enter(true);
+ } else {
+ $('.fullscreen-toggle').insertBefore('.helpbar-container');
+ }
+}, true);
diff --git a/resources/assets/javascripts/bootstrap/global_search.js b/resources/assets/javascripts/bootstrap/global_search.js
new file mode 100644
index 0000000..8d2b12c
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/global_search.js
@@ -0,0 +1,97 @@
+STUDIP.domReady(() => {
+ // Clear search term
+ $('#globalsearch-clear').on('click', function() {
+ var before = $('#globalsearch-input').val();
+ STUDIP.GlobalSearch.resetSearch();
+
+ if ($('html').is('.responsive-display') && before.length === 0) {
+ STUDIP.GlobalSearch.toggleSearchBar(false);
+ }
+
+ return false;
+ });
+
+ // Bind icon click to performing search.
+ $('#globalsearch-icon').on('click', function() {
+ STUDIP.GlobalSearch.doSearch();
+
+ if ($('html').hasClass('responsified')) {
+ var input = $('#globalsearch-input');
+ input.toggleClass('hidden-small-down', false);
+ input.focus();
+ }
+
+ return false;
+ });
+
+ // Enlarge search input on focus and show hints.
+ $('#globalsearch-input').on('focus', function() {
+ STUDIP.GlobalSearch.toggleSearchBar(true, false);
+ });
+
+ // Start search on Enter
+ $('#globalsearch-input').on('keypress', function(e) {
+ if (e.which === 13) {
+ STUDIP.GlobalSearch.doSearch();
+ return false;
+ }
+ });
+
+ // Close search on click on page.
+ $('div#flex-header, div#layout_page, div#layout_footer').on('click', function() {
+ if (!$('#globalsearch-input').hasClass('hidden-js')) {
+ STUDIP.GlobalSearch.toggleSearchBar(false, false);
+ }
+ });
+
+ // Show/hide hints on click.
+ $('#globalsearch-togglehints').on('click', function() {
+ var toggle = $('#globalsearch-togglehints'),
+ currentText = toggle.text();
+
+ toggle.text(toggle.data('toggle-text').trim());
+ toggle.data('toggle-text', currentText);
+
+ toggle.toggleClass('open');
+ });
+
+ // Delegate events on result container so we don't have to bind them
+ // one by one
+ $('#globalsearch-results').on('click', '.globalsearch-category a', function() {
+ var category = $(this)
+ .closest('.globalsearch-category')
+ .data('category');
+ STUDIP.GlobalSearch.expandCategory(category);
+ return false;
+ });
+
+ // Key bindings.
+ $(document).keydown(function(e) {
+ // Don't do anything if a dialog is open
+ if (STUDIP.Dialog.stack.length > 0) {
+ return;
+ }
+
+ // ctrl + space
+ if (e.which === 32 && e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) {
+ e.preventDefault();
+ if ($('#globalsearch-searchbar').hasClass('is-visible')) {
+ STUDIP.GlobalSearch.toggleSearchBar(false, false);
+ $('#globalsearch-input').blur();
+ } else {
+ $('#globalsearch-input').focus();
+ }
+ // escape
+ } else if (e.which === 27 && !e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) {
+ e.preventDefault();
+ STUDIP.GlobalSearch.toggleSearchBar(false, true);
+ }
+ });
+
+ // Start searching 750 ms after user stopped typing.
+ $('#globalsearch-input').keyup(
+ _.debounce(function() {
+ STUDIP.GlobalSearch.doSearch();
+ }, 750)
+ );
+});
diff --git a/resources/assets/javascripts/bootstrap/gradebook.js b/resources/assets/javascripts/bootstrap/gradebook.js
new file mode 100644
index 0000000..0e2e711
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/gradebook.js
@@ -0,0 +1,13 @@
+jQuery(function ($) {
+ let inputSel = 'form.gradebook-lecturer-weights input[type="number"]'
+ const inputs = document.querySelectorAll.bind(document, inputSel)
+ const adder = inputEls => [...inputEls].reduce((a, b) => a + parseInt(b.value, 10), 0)
+ const percenter = (sum, item) => sum ? (parseInt(item.value, 10) / sum * 100).toFixed(1) : 0
+
+ $(document).on('change blur', inputSel, function (event) {
+ const sum = adder(inputs())
+ inputs().forEach(input => {
+ input.parentElement.querySelector("output").value = percenter(sum, input)
+ })
+ })
+});
diff --git a/resources/assets/javascripts/bootstrap/header_magic.js b/resources/assets/javascripts/bootstrap/header_magic.js
new file mode 100644
index 0000000..f125179
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/header_magic.js
@@ -0,0 +1,6 @@
+STUDIP.domReady(() => {
+ // Test if the header is actually present
+ if ($('#barBottomContainer').length > 0) {
+ STUDIP.HeaderMagic.enable();
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/header_navigation.js b/resources/assets/javascripts/bootstrap/header_navigation.js
new file mode 100644
index 0000000..9b80619
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/header_navigation.js
@@ -0,0 +1,15 @@
+// Hide sink on touch elsewhere
+$(document).on('touchstart', function (event) {
+ if ($(event.target).closest('li.overflow').length === 0) {
+ $('#header-sink').prop('checked', false);
+ }
+ if ($(event.target).closest('li.has-subnavigation').length === 0) {
+ $('.responsive-toggle').prop('checked', false);
+ }
+});
+
+// Reshrink on resize
+$(window).on('resize', _.debounce(STUDIP.NavigationShrinker, 100));
+
+// Shrink on domready
+STUDIP.domReady(STUDIP.NavigationShrinker);
diff --git a/resources/assets/javascripts/bootstrap/i18n_input.js b/resources/assets/javascripts/bootstrap/i18n_input.js
new file mode 100644
index 0000000..6bb5c76
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/i18n_input.js
@@ -0,0 +1,3 @@
+STUDIP.ready(event => {
+ STUDIP.i18n.init(event.target);
+});
diff --git a/resources/assets/javascripts/bootstrap/inline-editing.js b/resources/assets/javascripts/bootstrap/inline-editing.js
new file mode 100644
index 0000000..b72e014
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/inline-editing.js
@@ -0,0 +1,48 @@
+jQuery(
+ function () {
+
+ jQuery(document).ready(
+ function() {
+ var elements = jQuery('[data-inline-editing]');
+ for (element of elements) {
+ STUDIP.InlineEditing.init(element);
+ }
+ }
+ );
+
+ jQuery(document).on(
+ 'dialog-update',
+ null,
+ function() {
+ var elements = jQuery('.ui-dialog [data-inline-editing]');
+ for (element of elements) {
+ STUDIP.InlineEditing.init(element);
+ }
+ }
+ );
+
+ jQuery(document).on(
+ 'click',
+ '[data-inline-editing] .edit-button',
+ function (event) {
+ STUDIP.InlineEditing.activate(event.target);
+ }
+ );
+
+ jQuery(document).on(
+ 'click',
+ '[data-inline-editing] .save-button',
+ function (event) {
+ STUDIP.InlineEditing.save(event.target);
+ }
+ );
+
+ jQuery(document).on(
+ 'click',
+ '[data-inline-editing] .abort-button',
+ function (event) {
+ STUDIP.InlineEditing.abort(event.target);
+ }
+ );
+ }
+);
diff --git a/resources/assets/javascripts/bootstrap/installer.js b/resources/assets/javascripts/bootstrap/installer.js
new file mode 100644
index 0000000..a60c1ed
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/installer.js
@@ -0,0 +1,124 @@
+/*jslint esversion: 6*/
+
+function domReady(fn) {
+ if (document.readyState === 'complete' || document.readyState === 'interactive') {
+ setTimeout(fn, 1);
+ } else {
+ document.addEventListener('DOMContentLoaded', fn);
+ }
+}
+
+domReady(() => {
+ if (!('fetch' in window) || !('Promise' in window)) {
+ const hidden_input = document.createElement('input');
+ hidden_input.setAttribute('type', 'hidden');
+ hidden_input.setAttribute('name', 'basic');
+ hidden_input.setAttribute('value', 1);
+ document.querySelector('form').append(hidden_input);
+
+ return;
+ }
+
+ var requests = [];
+ document.querySelectorAll('dl.requests > dt[data-request-url]').forEach((element) => {
+ requests.push({
+ element: element,
+ url: element.dataset.requestUrl,
+ event_source: element.dataset.eventSource !== undefined
+ });
+ });
+
+ const submit_button = document.querySelector('form button[type="submit"].button');
+ submit_button.disabled = true;
+
+ function next() {
+ if (requests.length === 0) {
+ submit_button.disabled = false;
+ return;
+ }
+ const current = requests.shift();
+ var promise;
+
+ current.element.classList.add('requesting');
+
+ if (current.event_source && 'EventSource' in window) {
+ const notifier = document.createElement('div');
+ notifier.setAttribute('data-percent', 0);
+
+ promise = new Promise((resolve, reject) => {
+ current.element.classList.add('event-sourced');
+
+ const progress = current.element.nextElementSibling.nextElementSibling.nextElementSibling;
+ var total = 0;
+
+ progress.insertAdjacentElement('afterend', notifier);
+ notifier.setAttribute(
+ 'style',
+ `left: ${progress.offsetLeft}px; top: ${progress.offsetTop}px`
+ );
+
+ const evtSource = new EventSource(current.url + '?evts=1', {
+ withCredentials: true
+ });
+ evtSource.addEventListener('total', (event) => {
+ total = parseInt(event.data, 10);
+ progress.setAttribute('max', total);
+ });
+ evtSource.addEventListener('file', (event) => {
+ notifier.setAttribute('data-file', event.data);
+ });
+ evtSource.addEventListener('current', (event) => {
+ let current = parseInt(event.data, 10);
+ progress.setAttribute('value', current);
+ notifier.setAttribute('data-percent', (100 * current / total).toFixed(2));
+ });
+ evtSource.addEventListener('error', (event) => {
+ evtSource.close();
+ reject(event.data || 'Fehler beim Installieren');
+ });
+ evtSource.addEventListener('close', (event) => {
+ evtSource.close();
+ resolve();
+ });
+ });
+
+ promise.finally(() => {
+ if (notifier.parentNode) {
+ notifier.parentNode.removeChild(notifier);
+ }
+ current.element.classList.remove('event-sourced');
+ });
+ } else {
+ promise = fetch(current.url, {
+ cache: 'no-cache',
+ credentials: 'same-origin'
+ }).then(response => {
+ if (!response.ok) {
+ return response.json().then(message => {
+ return Promise.reject(message);
+ });
+ }
+ });
+ }
+
+ promise.then(response => {
+ current.element.classList.add('succeeded');
+ next();
+ }).catch(error => {
+ current.element.classList.add('failed');
+
+ if (error !== null && error === Object(error)) {
+ current.element.nextElementSibling.nextElementSibling.querySelectorAll('.response').forEach((element) => {
+ let key = element.dataset.key;
+ element.value = error[key];
+ });
+ } else {
+ current.element.nextElementSibling.nextElementSibling.querySelector('.response').innerText = error;
+ }
+ }).finally(() => {
+ current.element.classList.remove('requesting');
+ });
+ }
+
+ next();
+});
diff --git a/resources/assets/javascripts/bootstrap/jsupdater.js b/resources/assets/javascripts/bootstrap/jsupdater.js
new file mode 100644
index 0000000..9fbcd0e
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/jsupdater.js
@@ -0,0 +1,10 @@
+// Start js updater if global settings says so
+$(window).on('load', function() {
+ if (STUDIP.jsupdate_enable) {
+ STUDIP.JSUpdater.start();
+ }
+});
+
+// Try to stop js updater if window is unloaded (might not work in all
+// browsers)
+$(window).on('unload', STUDIP.JSUpdater.stop);
diff --git a/resources/assets/javascripts/bootstrap/lightbox.js b/resources/assets/javascripts/bootstrap/lightbox.js
new file mode 100644
index 0000000..ff5b985
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/lightbox.js
@@ -0,0 +1,31 @@
+$(document)
+ .on('click', 'a[href][data-lightbox]', function() {
+ var gallery = $(this).data().lightbox,
+ elements = $(this),
+ images = [],
+ index = 0;
+
+ if (gallery) {
+ elements = $('a[href][data-lightbox="' + gallery + '"]');
+ index = elements.index(this);
+ }
+
+ elements.each(function() {
+ images.push({
+ src: $(this).attr('href'),
+ title: $(this).data().title || $(this).attr('title')
+ });
+ });
+
+ STUDIP.Lightbox.setImages(images);
+ STUDIP.Lightbox.show(index);
+
+ return false;
+ })
+ .on('resize', function() {
+ STUDIP.Lightbox.init();
+ });
+
+STUDIP.domReady(function () {
+ STUDIP.Lightbox.init();
+});
diff --git a/resources/assets/javascripts/bootstrap/members.js b/resources/assets/javascripts/bootstrap/members.js
new file mode 100644
index 0000000..b6a5191
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/members.js
@@ -0,0 +1,21 @@
+STUDIP.domReady(() => {
+ $('a.get-course-members').on('click', function() {
+ var dataEl = $('article#course-members-' + $(this).data('course-id')),
+ url;
+ if ($.trim(dataEl.html()).length === 0) {
+ url = $(this).data('get-members-url');
+
+ dataEl.html(
+ $('<img>').attr({
+ width: 32,
+ height: 32,
+ src: STUDIP.ASSETS_URL + 'images/ajax-indicator-black.svg'
+ })
+ );
+
+ $.get(url).done(function(html) {
+ dataEl.html(html);
+ });
+ }
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/messages.js b/resources/assets/javascripts/bootstrap/messages.js
new file mode 100644
index 0000000..f518bb4
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/messages.js
@@ -0,0 +1,100 @@
+jQuery(document).on('dialog-load', 'form#message-tags', function(event, data) {
+ var tags = jQuery.parseJSON(data.xhr.getResponseHeader('X-Tags')),
+ all_tags = jQuery.parseJSON(data.xhr.getResponseHeader('X-All-Tags')),
+ message_id = jQuery(this)
+ .closest('table')
+ .data().message_id;
+ STUDIP.Messages.setTags(message_id, tags);
+ STUDIP.Messages.setAllTags(all_tags);
+});
+
+jQuery(document).on('dialog-open', '#messages .title a', function() {
+ STUDIP.Messages.whenMessageIsShown(this);
+});
+
+STUDIP.domReady(() => {
+ /*********** infinity-scroll in the overview ***********/
+ if (jQuery('#messages').length > 0) {
+ STUDIP.Messages.init();
+ jQuery(window.document).on(
+ 'scroll',
+ _.throttle(function(event) {
+ if (
+ jQuery(window).scrollTop() + jQuery(window).height() > jQuery(window.document).height() - 500 &&
+ jQuery('#reloader').hasClass('more')
+ ) {
+ //nachladen
+ jQuery('#reloader')
+ .removeClass('more')
+ .addClass('loading');
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/messages/more',
+ data: {
+ received: jQuery('#received').val(),
+ offset: jQuery('#messages > tbody > tr').length - 1,
+ tag: jQuery('#tag').val(),
+ search: jQuery('#search').val(),
+ search_autor: jQuery('#search_autor').val(),
+ search_subject: jQuery('#search_subject').val(),
+ search_content: jQuery('#search_content').val(),
+ limit: 50
+ },
+ dataType: 'json',
+ success: function(response) {
+ var more_indicator = jQuery('#reloader').detach();
+
+ jQuery('#loaded').val(parseInt(jQuery('#loaded').val(), 10) + 1);
+ jQuery.each(response.messages, function(index, message) {
+ jQuery('#messages > tbody').append(message);
+ });
+
+ if (response.more) {
+ jQuery('#messages > tbody').append(
+ more_indicator.addClass('more').removeClass('loading')
+ );
+ }
+ }
+ });
+ }
+ }, 30)
+ );
+ }
+
+ /*********** dragging the messages to the tags ***********/
+
+ jQuery('#messages > tbody').on('mouseover touchstart', function() {
+ if ($('html').is('.responsive-display') || jQuery('#messages-tags ul > li').length === 0) {
+ jQuery('#messages > tbody > tr').draggable('disable');
+ } else {
+ jQuery('#messages > tbody > tr').draggable('enable');
+ }
+ });
+
+ jQuery('#messages > tbody > tr').draggable({
+ //cursor: "move",
+ distance: 10,
+ cursorAt: { left: 28, top: 15 },
+ helper: function() {
+ var title = jQuery(this)
+ .find('.title')
+ .text()
+ .trim();
+ return jQuery('<div id="message-move-handle">').text(title);
+ },
+ revert: true,
+ revertDuration: '200',
+ appendTo: 'body',
+ zIndex: 1000,
+ start: function() {
+ jQuery('#messages-tags').addClass('dragging');
+ },
+ stop: function() {
+ jQuery('#messages-tags').removeClass('dragging');
+ }
+ });
+ jQuery('#messages > tbody').trigger('touchstart');
+ jQuery('.widget-links li:has(.tag)').each(STUDIP.Messages.createDroppable);
+
+ jQuery(document).on('click', '.adressee .remove_adressee', STUDIP.Messages.remove_adressee);
+ jQuery(document).on('click', '.file .remove_attachment', STUDIP.Messages.remove_attachment);
+});
diff --git a/resources/assets/javascripts/bootstrap/multi_person_search.js b/resources/assets/javascripts/bootstrap/multi_person_search.js
new file mode 100644
index 0000000..3f80e69
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/multi_person_search.js
@@ -0,0 +1,8 @@
+STUDIP.domReady(() => {
+ STUDIP.MultiPersonSearch.init();
+
+ // init form if it is loaded without ajax
+ if ($('.mpscontainer').length) {
+ STUDIP.MultiPersonSearch.dialog($('.mpscontainer').data('dialogname'));
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/multi_select.js b/resources/assets/javascripts/bootstrap/multi_select.js
new file mode 100644
index 0000000..6e033af
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/multi_select.js
@@ -0,0 +1,11 @@
+import { $gettext } from '../lib/gettext.js';
+
+STUDIP.domReady(() => {
+ $.extend($.ui.multiselect, {
+ locale: {
+ addAll: $gettext('Alle hinzufügen'),
+ removeAll: $gettext('Alle entfernen'),
+ itemsCount: $gettext('ausgewählt')
+ }
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/mvv_difflog.js b/resources/assets/javascripts/bootstrap/mvv_difflog.js
new file mode 100644
index 0000000..a1c13e0
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/mvv_difflog.js
@@ -0,0 +1,200 @@
+STUDIP.domReady(() => {
+ $('del.diffdel').each(function() {
+ var mvv_field = '';
+
+ $(this)
+ .parentsUntil('div')
+ .each(function() {
+ if ($(this).attr('data-mvv-field')) {
+ mvv_field = $(this).attr('data-mvv-field');
+ return true;
+ }
+ });
+
+ if (mvv_field != '') {
+ $(this)
+ .parentsUntil('div')
+ .each(function() {
+ if ($(this).attr('data-mvv-id')) {
+ mvv_id = $(this).attr('data-mvv-id');
+ return true;
+ }
+ });
+ var mvv_debug = $(this).text();
+
+ var del = $(this);
+ var fields = mvv_field.split(' ');
+
+ for (var i = 0; i < fields.length; ++i) {
+ var obj_elements = fields[i].split('.');
+
+ if (obj_elements.length == 1) {
+ var senddata = { mvv_field: fields[i], mvv_debug: mvv_debug, log_action: 'del' };
+ } else {
+ var senddata = { mvv_field: fields[i], mvv_id: mvv_id, log_action: 'update' };
+ }
+
+ var url = STUDIP.URLHelper.getURL('dispatch.php/shared/log_event/get_log_autor');
+ $.post(
+ url,
+ senddata,
+ function(data) {
+ if (data) {
+ var info = 'Entfernt von ' + data.user + ' am ' + data.time;
+ del.attr('title', info);
+ del.after('<del class="difflog"> [' + info + '] </ins>');
+ }
+ },
+ 'json'
+ );
+ }
+ }
+ });
+
+ $('ins').each(function() {
+ var mvv_field = '';
+ var mvv_coid = '';
+ var mvv_id = '';
+
+ switch ($('ins').attr('class')) {
+ case 'diffins':
+ var mvv_log_action = 'new';
+ break;
+ case 'diffmod':
+ var mvv_log_action = 'update';
+ break;
+ default:
+ var mvv_log_action = null;
+ break;
+ }
+
+ $(this)
+ .parentsUntil('div')
+ .each(function() {
+ if ($(this).attr('data-mvv-field')) {
+ mvv_field = $(this).attr('data-mvv-field');
+ mvv_coid = $(this).attr('data-mvv-coid');
+ return false;
+ }
+ });
+
+ if (mvv_field != '') {
+ $(this)
+ .parentsUntil('div')
+ .each(function() {
+ if ($(this).attr('data-mvv-id')) {
+ mvv_id = $(this).attr('data-mvv-id');
+ return false;
+ }
+ });
+
+ var ins = $(this);
+ var fields = mvv_field.split(' ');
+ for (var i = 0; i < fields.length; ++i) {
+ var obj_elements = fields[i].split('.');
+ if (obj_elements.length == 1 && mvv_coid) {
+ var senddata = {
+ mvv_field: fields[i],
+ mvv_id: mvv_id,
+ mvv_coid: mvv_coid,
+ log_action: mvv_log_action
+ };
+ } else if (fields[i] == 'mvv_modulteil_stgteilabschnitt.differenzierung' && mvv_coid) {
+ var classes = $(this)
+ .parent()
+ .attr('class')
+ .split(' ');
+ if (classes.length > 1) {
+ var mvv_debug =
+ $(this)
+ .parent()
+ .attr('data-mvv-index') +
+ ';' +
+ classes[1];
+ var senddata = {
+ mvv_field: fields[i],
+ mvv_id: mvv_id,
+ mvv_coid: mvv_coid,
+ log_action: mvv_log_action,
+ mvv_debug: mvv_debug
+ };
+ } else {
+ return true;
+ }
+ } else {
+ var senddata = { mvv_field: fields[i], mvv_id: mvv_id, log_action: mvv_log_action };
+ }
+
+ var url = STUDIP.URLHelper.getURL('dispatch.php/shared/log_event/get_log_autor');
+ $.post(
+ url,
+ senddata,
+ function(data) {
+ if (data) {
+ var info = 'Änderung durch ' + data.user + ' am ' + data.time;
+ ins.attr('title', info);
+ ins.after('<ins class="difflog"> [' + info + '] </ins>');
+ }
+ },
+ 'json'
+ );
+ }
+ }
+ });
+
+ $('.mvv-diff-added').each(function() {
+ $(this)
+ .find('table')
+ .each(function() {
+ if ($(this).attr('data-mvv-type')) {
+ var mvv_type = $(this).attr('data-mvv-type');
+ var mvv_id = $(this).attr('data-mvv-id');
+ var curtable = $(this);
+ } else {
+ return true;
+ }
+
+ var url = STUDIP.URLHelper.getURL('dispatch.php/shared/log_event/get_log_autor');
+ $.post(
+ url,
+ { mvv_field: 'mvv_' + mvv_type, mvv_id: mvv_id, log_action: 'new' },
+ function(data) {
+ if (data) {
+ var info = 'Hinzugefügt von ' + data.user + ' am ' + data.time;
+ curtable.attr('title', info);
+ curtable.append('<tr><td><ins class="difflog"> [' + info + '] </ins><td></tr>');
+ }
+ },
+ 'json'
+ );
+ });
+ });
+
+ $('.mvv-diff-deleted').each(function() {
+ $(this)
+ .find('table')
+ .each(function() {
+ if ($(this).attr('data-mvv-type')) {
+ var mvv_type = $(this).attr('data-mvv-type');
+ var mvv_id = $(this).attr('data-mvv-id');
+ var curtable = $(this);
+ } else {
+ return true;
+ }
+
+ var url = STUDIP.URLHelper.getURL('dispatch.php/shared/log_event/get_log_autor');
+ $.post(
+ url,
+ { mvv_field: 'mvv_' + mvv_type, mvv_id: mvv_id, log_action: 'del' },
+ function(data) {
+ if (data) {
+ var info = 'Entfernt von ' + data.user + ' am ' + data.time;
+ curtable.attr('title', info);
+ curtable.append('<tr><td><del class="difflog"> [' + info + '] </del><td></tr>');
+ }
+ },
+ 'json'
+ );
+ });
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/my-courses.js b/resources/assets/javascripts/bootstrap/my-courses.js
new file mode 100644
index 0000000..40e0c24
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/my-courses.js
@@ -0,0 +1,22 @@
+import MyCourses from '../../../vue/components/MyCourses.vue';
+import storeConfig from '../../../vue/store/MyCoursesStore.js';
+
+STUDIP.domReady(async () => {
+ if ($('.my-courses-vue-app').length === 0) {
+ return;
+ }
+
+ const { createApp, store } = await STUDIP.Vue.load();
+
+ store.registerModule('mycourses', storeConfig);
+
+ store.commit('mycourses/setCourses', window.STUDIP.MyCoursesData['courses']);
+ store.commit('mycourses/setGroups', window.STUDIP.MyCoursesData['groups']);
+ store.commit('mycourses/setUserId', window.STUDIP.MyCoursesData['user_id']);
+ store.commit('mycourses/setConfig', window.STUDIP.MyCoursesData['config']);
+
+ const vm = createApp({
+ components: { MyCourses }
+ });
+ vm.$mount('.my-courses-vue-app');
+});
diff --git a/resources/assets/javascripts/bootstrap/news.js b/resources/assets/javascripts/bootstrap/news.js
new file mode 100644
index 0000000..6329f16
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/news.js
@@ -0,0 +1,34 @@
+STUDIP.domReady(() => {
+ STUDIP.News.dialog_width = window.innerWidth * (1 / 2);
+ STUDIP.News.dialog_height = window.innerHeight - 60;
+ if (STUDIP.News.dialog_width < 550) {
+ STUDIP.News.dialog_width = 550;
+ }
+ if (STUDIP.News.dialog_height < 400) {
+ STUDIP.News.dialog_height = 400;
+ }
+ STUDIP.News.pending_ajax_request = false;
+
+ $(document).on('click', 'a[rel~="get_dialog"]', function(event) {
+ event.preventDefault();
+ STUDIP.News.get_dialog('news_dialog', $(this).attr('href'));
+ });
+
+ $(document).on('click', 'a[rel~="close_dialog"]', function(event) {
+ event.preventDefault();
+ $('#news_dialog').dialog('close');
+ });
+
+ // open/close categories without ajax-request
+ $(document).on('click', '.news_category_header', function(event) {
+ event.preventDefault();
+ STUDIP.News.toggle_category_view(
+ $(this)
+ .parent('div')
+ .attr('id')
+ );
+ });
+ $(document).on('click', '.news_category_header input[type=image]', function(event) {
+ event.preventDefault();
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/oer.js b/resources/assets/javascripts/bootstrap/oer.js
new file mode 100644
index 0000000..c0f51fd
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/oer.js
@@ -0,0 +1,141 @@
+import Quicksearch from '../../../vue/components/Quicksearch.vue';
+
+STUDIP.domReady(() => {
+ if (jQuery(".oer_search").length) {
+ STUDIP.OER.initSearch();
+ }
+ jQuery(".serversettings .index_server a").on("click", function () {
+ var host_id = jQuery(this).closest("tr").data("host_id");
+ var active = jQuery(this).is(".checked") ? 0 : 1;
+ var a = this;
+ jQuery.ajax({
+ "url": STUDIP.ABSOLUTE_URI_STUDIP + "dispatch.php/oer/admin/toggle_index_server",
+ "data": {
+ 'host_id': host_id,
+ 'active': active
+ },
+ "type": "post",
+ "success": function (html) {
+ jQuery(a).html(html);
+ if (active) {
+ jQuery(a).addClass("checked").removeClass("unchecked");
+ } else {
+ jQuery(a).addClass("unchecked").removeClass("checked");
+ }
+ }
+ });
+ return false;
+ });
+ jQuery(".serversettings .active a").on("click", function () {
+ var host_id = jQuery(this).closest("tr").data("host_id");
+ var active = jQuery(this).is(".checked") ? 0 : 1;
+ var a = this;
+ jQuery.ajax({
+ "url": STUDIP.ABSOLUTE_URI_STUDIP + "dispatch.php/oer/admin/toggle_server_active",
+ "data": {
+ 'host_id': host_id,
+ 'active': active
+ },
+ "type": "post",
+ "success": function (html) {
+ jQuery(a).html(html);
+ if (active) {
+ jQuery(a).addClass("checked").removeClass("unchecked");
+ } else {
+ jQuery(a).addClass("unchecked").removeClass("checked");
+ }
+ }
+ });
+ return false;
+ });
+
+});
+
+STUDIP.dialogReady(() => {
+ if ($('.oercampus_editmaterial').length) {
+
+ STUDIP.Vue.load().then(({createApp}) => {
+ STUDIP.OER.EditApp = createApp({
+ el: '.oercampus_editmaterial',
+ data: {
+ name: $('.oercampus_editmaterial input.oername').val(),
+ logo_url: $('.oercampus_editmaterial .logo_file').data("oldurl"),
+ customlogo: $('.oercampus_editmaterial .logo_file').data("customlogo"),
+ filename: $('.oercampus_editmaterial .file.drag-and-drop').data("filename"),
+ filesize: $('.oercampus_editmaterial .file.drag-and-drop').data("filesize"),
+ tags: $('.oercampus_editmaterial .oer_tags').data("defaulttags"),
+ minimumTags: 5
+ },
+ mounted: function () {
+ jQuery("#difficulty_slider_edit").slider({
+ range: true,
+ min: 1,
+ max: 12,
+ values: [jQuery("#difficulty_start").val(), jQuery("#difficulty_end").val()],
+ change: function (event, ui) {
+ jQuery("#difficulty_start").val(ui.values[0]);
+ jQuery("#difficulty_end").val(ui.values[1]);
+ }
+ });
+ },
+ methods: {
+ editName: function () {
+ this.name = $('.oername').val();
+ },
+ editImage: function (event) {
+ let reader = new FileReader();
+ let vue = this;
+ reader.addEventListener("load", function () {
+ vue.logo_url = reader.result;
+ vue.customlogo = true;
+ }, false);
+ reader.readAsDataURL(
+ event.target.files.length > 0
+ ? event.target.files[0]
+ : event.dataTransfer.files[0]
+ );
+ },
+ dropImage: function (event) {
+ window.document.getElementById("oer_logo_uploader").files = event.dataTransfer.files;
+ this.editImage(event);
+ },
+ editFile: function (event) {
+ this.filename = event.target.files[0].name;
+ this.filesize = event.target.files[0].size;
+ if (!this.name) {
+ this.name = this.filename;
+ $('.oername').val(this.name);
+ }
+ },
+ dropFile: function (event) {
+ window.document.getElementById("oer_file").files = event.dataTransfer.files;
+ this.editFile(event);
+ },
+ addTag: function () {
+ if (this.minimumTags < this.tags.length) {
+ this.minimumTags = this.tags.length + 1;
+ } else {
+ this.minimumTags++;
+ }
+ },
+ removeTag: function (i) {
+ this.$delete(this.tags, i);
+ if ((this.minimumTags > this.tags.length) && (this.minimumTags > 5)) {
+ this.minimumTags--;
+ }
+ }
+ },
+ computed: {
+ displayTags () {
+ const result = this.tags.concat([]);
+ while (result.length < this.minimumTags) {
+ result.push('');
+ }
+ return result;
+ }
+ },
+ components: { Quicksearch }
+ });
+ });
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/opengraph.js b/resources/assets/javascripts/bootstrap/opengraph.js
new file mode 100644
index 0000000..9b4ce3c
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/opengraph.js
@@ -0,0 +1,55 @@
+function handleOpenGraphSections() {
+ $('.opengraph-area:not(.handled)').each(function() {
+ var items = $('.opengraph', this),
+ switcher;
+ if (items.length > 1) {
+ items.filter(':gt(0)').addClass('hidden');
+
+ switcher = $('<ul class="switcher">');
+ $('<li><button class="switch-left" disabled>&lt;</button></li>').appendTo(switcher);
+ $('<li><button class="switch-right">&gt;</button></li>').appendTo(switcher);
+ switcher.prependTo(this);
+ }
+
+ $(this).addClass('handled');
+ });
+}
+
+STUDIP.ready(handleOpenGraphSections);
+$(document).on('ajaxComplete', handleOpenGraphSections);
+
+$(document).on('click', '.opengraph-area .switcher button', function (event) {
+ var direction = $(this).is('.switch-left') ? 'left' : 'right',
+ current = $(this)
+ .closest('.opengraph-area')
+ .find('.opengraph:visible'),
+ switcher = $(this).closest('.switcher'),
+ buttons = {
+ left: $('.switch-left', switcher),
+ right: $('.switch-right', switcher)
+ };
+
+ if (direction === 'left') {
+ current = current
+ .addClass('hidden')
+ .prev()
+ .removeClass('hidden');
+ buttons.left.attr('disabled', current.prev('.opengraph').length === 0);
+ buttons.right.attr('disabled', false);
+ } else {
+ current = current
+ .addClass('hidden')
+ .next()
+ .removeClass('hidden');
+ buttons.left.attr('disabled', false);
+ buttons.right.attr('disabled', current.next('.opengraph').length === 0);
+ }
+
+ event.preventDefault();
+}).on('click', '.opengraph a.flash-embedder', function (event) {
+ let url = $(this).attr('href');
+ let template = _.template('<iframe width="100%" height="200px" frameborder="0" src="<%= url %>" allowfullscreen></iframe>');
+ $(this).replaceWith(template({ url: url }));
+
+ event.preventDefault();
+});
diff --git a/resources/assets/javascripts/bootstrap/personal_notifications.js b/resources/assets/javascripts/bootstrap/personal_notifications.js
new file mode 100644
index 0000000..c154292
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/personal_notifications.js
@@ -0,0 +1,10 @@
+$(document).on('click', '#notification_list .mark_as_read', STUDIP.PersonalNotifications.markAsRead);
+
+STUDIP.domReady(() => {
+ STUDIP.PersonalNotifications.initialize();
+
+ $('#notification_container .mark-all-as-read')
+ .click(STUDIP.PersonalNotifications.markAllAsRead);
+ $('#notification_list')
+ .mouseenter(STUDIP.PersonalNotifications.setSeen);
+});
diff --git a/resources/assets/javascripts/bootstrap/qr_code.js b/resources/assets/javascripts/bootstrap/qr_code.js
new file mode 100644
index 0000000..4fad876
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/qr_code.js
@@ -0,0 +1,41 @@
+jQuery(document).on('click', 'a[data-qr-code]', STUDIP.QRCode.show);
+
+STUDIP.ready((event) => {
+ $('code.qr', event.target).each(function () {
+ let content = $(this).text().trim();
+ let code = $('<div class="qrcode">').hide();
+ STUDIP.QRCode.generate(code[0], content, {
+ width: 1024,
+ height: 1024
+ });
+ $(this).replaceWith(code);
+ setTimeout(() => code.show(), 0);
+ });
+ jQuery(document).on(
+ 'click',
+ '#qr_code .PrintAction',
+ function() {
+ //We must hide the other page elements for the print view functionality.
+ //Furthermore we must set the width and height of the qr-code.
+ jQuery('#layout_wrapper').css(
+ {
+ display: 'none'
+ }
+ );
+ jQuery('#qr_code').addClass('print-view');
+
+ //Now we can print:
+ window.print();
+ }
+ );
+
+ jQuery(document).on(
+ 'fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange',
+ function(event) {
+ //After the print action is called
+ //we must reset the style changes made above:
+ jQuery('#layout_wrapper').removeAttr('style');
+ }
+ );
+});
+
diff --git a/resources/assets/javascripts/bootstrap/questionnaire.js b/resources/assets/javascripts/bootstrap/questionnaire.js
new file mode 100644
index 0000000..33ead0b
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/questionnaire.js
@@ -0,0 +1,75 @@
+jQuery(document).on('paste', '.questionnaire_edit .options > li input', function(ui) {
+ var event = ui.originalEvent;
+ var text = event.clipboardData.getData('text');
+ text = text.split(/[\n\t]/);
+ if (text.length > 1) {
+ if (text[0]) {
+ this.value += text.shift().trim();
+ }
+ var current = jQuery(this).closest('li');
+ for (var i in text) {
+ if (text[i].trim()) {
+ var li = jQuery(
+ jQuery(this)
+ .closest('.options')
+ .data('optiontemplate')
+ );
+ li.find('input:text').val(text[i].trim());
+ li.insertAfter(current);
+ current = li;
+ }
+ }
+ STUDIP.Questionnaire.Test.updateCheckboxValues();
+ event.preventDefault();
+ }
+});
+jQuery(document).on('blur', '.questionnaire_edit .options > li:last-child input:text', function() {
+ if (this.value) {
+ jQuery(this)
+ .closest('.options')
+ .append(
+ jQuery(this)
+ .closest('.options')
+ .data('optiontemplate')
+ );
+ jQuery(this)
+ .closest('.options')
+ .find('li:last-child input')
+ .focus();
+ }
+ STUDIP.Questionnaire.Test.updateCheckboxValues();
+});
+jQuery(document).on('click', '.questionnaire_edit .options .delete', function() {
+ var icon = this;
+ STUDIP.Dialog.confirm(
+ jQuery(this)
+ .closest('.questionnaire_edit')
+ .find('.delete_question')
+ .text(),
+ function() {
+ jQuery(icon)
+ .closest('li')
+ .fadeOut(function() {
+ jQuery(this).remove();
+ STUDIP.Questionnaire.Test.updateCheckboxValues();
+ });
+ }
+ );
+});
+jQuery(document).on('click', '.questionnaire_edit .options .add', function() {
+ jQuery(this)
+ .closest('.options')
+ .append(
+ jQuery(this)
+ .closest('.options')
+ .data('optiontemplate')
+ );
+ jQuery(this)
+ .closest('.options')
+ .find('li:last-child input:text')
+ .focus();
+ STUDIP.Questionnaire.Test.updateCheckboxValues();
+});
+jQuery(document).on('change', '.show_validation_hints .questionnaire_answer [data-question_type=Vote] input', function() {
+ STUDIP.Questionnaire.Vote.validator.call($(this).closest("article")[0]);
+});
diff --git a/resources/assets/javascripts/bootstrap/quick_search.js b/resources/assets/javascripts/bootstrap/quick_search.js
new file mode 100644
index 0000000..e1c7275
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/quick_search.js
@@ -0,0 +1,21 @@
+//must be overridden to display html in autocomplete like avatars:
+$.widget('studip.quicksearch', $.ui.autocomplete, {
+ _renderItem (ul, item) {
+ let li = $('<li>');
+ li.data('item.autocomplete', item);
+ if (item.disabled) {
+ li.addClass('ui-state-disabled');
+ }
+ $('<a>').html(item.label).appendTo(li);
+ li.appendTo(ul);
+
+ return li;
+ },
+
+ _renderMenu (ul, items) {
+ $(ul).addClass('studip-quicksearch');
+ items.forEach((item) => {
+ this._renderItemData(ul, item);
+ });
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/raumzeit.js b/resources/assets/javascripts/bootstrap/raumzeit.js
new file mode 100644
index 0000000..5f7d534
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/raumzeit.js
@@ -0,0 +1,163 @@
+import { $gettext } from '../lib/gettext.js';
+
+STUDIP.Dialog.handlers.header['X-Raumzeit-Update-Times'] = function(json) {
+ var info = $.parseJSON(json);
+ $('.course-admin #course-' + info.course_id + ' .raumzeit').html(info.html);
+};
+
+STUDIP.ready(function () {
+ $('#block_appointments_days input').click(function() {
+ var clicked_id = parseInt(this.id.split('_').pop(), 10);
+ if (clicked_id === 0 || clicked_id === 1) {
+ $('#block_appointments_days input:checkbox').prop('checked', function(i) {
+ return i === clicked_id;
+ });
+ } else {
+ $('#block_appointments_days_0').prop('checked', false);
+ $('#block_appointments_days_1').prop('checked', false);
+ }
+ });
+});
+
+$(document).on('change', 'select[name=room_sd]', function() {
+ $('input[type=radio][name=room][value=room]').prop('checked', true);
+});
+
+$(document).on('focus', 'input[name=freeRoomText_sd]', function() {
+ $('input[type=radio][name=room][value=freetext]').prop('checked', true);
+});
+
+$(document).on('click', '.bookable_rooms_action', function(event) {
+ var select = $(this).prev('select')[0],
+ me = $(this);
+ if (select !== null && select !== undefined) {
+ if (me.data('state') === 'enabled') {
+ STUDIP.Raumzeit.disableBookableRooms(me);
+ } else {
+ if (me.data('options') === undefined) {
+ me.data(
+ 'options',
+ $(select)
+ .children('option')
+ .clone(true)
+ );
+ } else {
+ $(select)
+ .empty()
+ .append(me.data('options').clone(true));
+ }
+
+ if (
+ $(this)
+ .parents('form')
+ .attr('action')
+ .split('saveDate/').length > 1
+ ) {
+ var singleDate = $(this)
+ .parents('form')
+ .attr('action')
+ .split('saveDate/')[1]
+ .split('?')[0];
+ } else {
+ var singleDate = undefined;
+ }
+ if ($("input[name='checked_dates']").length > 0) {
+ var checked_dates = $("input[name='checked_dates']")
+ .val()
+ .split(',');
+ var ndate = [];
+ } else {
+ var checked_dates = [singleDate];
+ var startDate = $("input[name='date']").val();
+ var start_time = $("input[name='start_time']")
+ .val()
+ .split(':');
+ var end_time = $("input[name='end_time']")
+ .val()
+ .split(':');
+ var date_obj = [
+ { name: 'startDate', value: startDate },
+ { name: 'start_stunde', value: start_time[0] },
+ { name: 'start_minute', value: start_time[1] },
+ { name: 'end_stunde', value: end_time[0] },
+ { name: 'end_minute', value: end_time[1] }
+ ];
+ }
+
+ $.ajax({
+ type: 'POST',
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/resources/helpers/bookable_rooms',
+ data: {
+ rooms: _.map(select.options, 'value'),
+ selected_dates: checked_dates,
+ singleDateID: singleDate,
+ new_date: date_obj
+ },
+ success: function(result) {
+ if ($.isArray(result)) {
+ if (result.length) {
+ var not_bookable_rooms = _.map(result, function(v) {
+ return $(select)
+ .children('option[value=' + v + ']')
+ .text()
+ .trim();
+ });
+ select.title =
+ $gettext('Nicht buchbare Räume:') + ' ' + not_bookable_rooms.join(', ');
+ } else {
+ select.title = '';
+ }
+ _.each(result, function(v) {
+ $(select)
+ .children('option[value=' + v + ']')
+ .prop('disabled', true);
+ });
+ } else {
+ select.title = '';
+ }
+ me.attr('title', $gettext('Alle Räume anzeigen'));
+ me.data('state', 'enabled');
+ }
+ });
+ }
+ }
+ event.preventDefault();
+});
+
+$(document).on('change', 'input[name="singledate[]"]', function() {
+ STUDIP.Raumzeit.disableBookableRooms($('.bookable_rooms_action'));
+});
+
+STUDIP.ready((event) => {
+ $('.bookable_rooms_action', event.target).show();
+});
+
+$(document).on('change', '.datesBulkActions', function() {
+ var $button = $(this).next('button');
+ if ($(this).val() === 'delete') {
+ $button.attr('data-confirm', $gettext('Wollen Sie die gewünschten Termine wirklich löschen?'));
+ } else {
+ if ($button.attr('data-confirm')) {
+ $button.removeAttr('data-confirm');
+ }
+ }
+});
+
+$(document).on('change', '#edit-cycle', function() {
+ var start = $('input[name=start_time]', this)[0],
+ end = $('input[name=end_time]', this)[0],
+ changed =
+ start.defaultValue &&
+ end.defaultValue &&
+ (start.value !== start.defaultValue || end.value !== end.defaultValue);
+ // check if new time exceeds the current one and add security question if necessary
+ if (changed && (start.value < start.defaultValue || end.value > end.defaultValue)) {
+ $(this).attr(
+ 'data-confirm',
+ $gettext('Wenn Sie die regelmäßige Zeit ändern, verlieren Sie die Raumbuchungen für alle in der Zukunft liegenden Termine! Sind Sie sicher, dass Sie die regelmäßige Zeit ändern möchten?')
+ );
+ } else {
+ // remove security question - not necessary (any more)
+ $(this).attr('data-confirm', null);
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/resource-tree-widget.js b/resources/assets/javascripts/bootstrap/resource-tree-widget.js
new file mode 100644
index 0000000..bf5b2d9
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/resource-tree-widget.js
@@ -0,0 +1,48 @@
+jQuery(document).ready(
+ function () {
+ jQuery(document).on(
+ 'click',
+ '.resource-tree .expand-action',
+ function (event) {
+ var li_element = jQuery(event.target).parent();
+ if (!li_element) {
+ return;
+ }
+ jQuery(event.target).css('transform', 'rotate(90deg)');
+
+ jQuery(li_element).siblings().css('display', 'none');
+ //Show the layer of resources that lies
+ //below the clicked resource:
+ var ul_elements = jQuery(li_element).children('ul');
+ jQuery(ul_elements).css('display', 'block');
+ jQuery(ul_elements).children('li').css('display', 'list-item');
+
+ jQuery(event.target).removeClass('expand-action');
+ jQuery(event.target).addClass('collapse-action');
+ }
+ );
+
+
+ jQuery(document).on(
+ 'click',
+ '.resource-tree .collapse-action',
+ function (event) {
+ var li_element = jQuery(event.target).parent();
+ if (!li_element) {
+ return;
+ }
+ jQuery(event.target).css('transform', '');
+
+ jQuery(li_element).siblings().css('display', '');
+ //Show the layer of resources that lies
+ //below the clicked resource:
+ var ul_elements = jQuery(li_element).children('ul');
+ jQuery(ul_elements).css('display', 'none');
+ jQuery(ul_elements).children('li').css('display', 'none');
+
+ jQuery(event.target).removeClass('collapse-action');
+ jQuery(event.target).addClass('expand-action');
+ }
+ );
+ }
+);
diff --git a/resources/assets/javascripts/bootstrap/resources.js b/resources/assets/javascripts/bootstrap/resources.js
new file mode 100644
index 0000000..9ceb8fa
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/resources.js
@@ -0,0 +1,999 @@
+import {$gettext} from '../lib/gettext.js';
+
+STUDIP.ready(function () {
+ //Event definitions:
+ jQuery(document).on(
+ 'change',
+ '.room-search-form .criteria-selector',
+ function (event) {
+ STUDIP.Resources.addSearchCriteriaToRoomSearchWidget(
+ event.target
+ );
+ }
+ );
+
+ jQuery(document).on(
+ 'click',
+ '.room-search-form .criteria-list .remove-icon',
+ function (event) {
+ STUDIP.Resources.removeSearchCriteriaFromRoomSearchWidget(
+ event.target
+ );
+ }
+ );
+
+ //permission list:
+
+ jQuery(document).on(
+ 'click',
+ '.resource-permission-list-action.apply-to-all-action',
+ function (event) {
+ var table = jQuery('#RoomGroupCommonPermissionTable')[0];
+ var tr = jQuery(event.target).parents('tr')[0];
+ if (!tr) {
+ return;
+ }
+ var user_id = jQuery(tr).data('user_id');
+ STUDIP.Resources.addUserToPermissionList(user_id, table);
+ //Delete the row:
+ jQuery(tr).remove();
+ }
+ );
+
+ //Temporary permission list: Set the hidden checkbox to the state of the
+ //proxy checkbox:
+ jQuery(document).on('change', '#resource-temporary-permissions input.bulk-proxy', function () {
+ var bulk_checked = jQuery(this).prop('checked');
+ var bulk_indeterminate = jQuery(this).prop('indeterminate');
+ if (bulk_checked || bulk_indeterminate) {
+ jQuery('#resource-temporary-permissions input.bulk-datetime-enable').prop('checked', true);
+ } else {
+ jQuery('#resource-temporary-permissions input.bulk-datetime-enable').prop('checked', false);
+ }
+ });
+
+ //Dialog for adding/editing bookings:
+
+ if (jQuery('form.create-booking-form').length) {
+ STUDIP.Resources.moveTimeOptions(jQuery('input[name="booking_style"]:checked').val());
+ }
+
+ //Set the date selector in the sidebar to the date from the session,
+ //if that is set and no date is set in the URL.
+ var date_set = false;
+ var url_param_string = window.location.search;
+ if (url_param_string) {
+ var url_params = new URLSearchParams(url_param_string);
+ if (url_params.get('defaultDate')) {
+ date_set = true;
+ }
+ }
+ if (!date_set) {
+ var date_input = jQuery('#booking-plan-jmpdate')[0];
+ if (date_input) {
+ var session_date_string = sessionStorage.getItem('booking_plan_date');
+ if (session_date_string) {
+ //The date string is in the format YYYY-MM-DD and has to be
+ //converted to the format DD.MM.YYYY.
+ var date_parts = session_date_string.split('-');
+ jQuery(date_input).val(date_parts[2] + '.' + date_parts[1] + '.' + date_parts[0]);
+ }
+ }
+ }
+
+ //other:
+
+ jQuery(document).on(
+ 'click',
+ '.booking-list-interval .takes-place-status-toggle',
+ STUDIP.Resources.toggleBookingIntervalStatus
+ );
+
+ jQuery(document).on(
+ 'click',
+ '.resource-category-properties-table .add-action',
+ STUDIP.Resources.addResourcePropertyToTable
+ );
+
+ jQuery(document).on(
+ 'click',
+ '.resource-request-properties-table .add-action',
+ STUDIP.Resources.addPropertyToRequest
+ );
+
+ jQuery(document).on(
+ 'click',
+ '.resource-category-properties-table .delete-action, .resource-request-properties-table .delete-action',
+ function (event) {
+ var row = jQuery(event.target).parents('tr')[0];
+ if (!row) {
+ return;
+ }
+
+ var property_id = jQuery(row).data('property_id');
+
+ //Enable the property in the "add property" select element:
+ var table = jQuery(row).parents('table')[0];
+ if (!table) {
+ return;
+ }
+
+ var select = jQuery(table).find('select.available-property-select')[0];
+ if (!select) {
+ select = jQuery(table).find('select.requestable-properties-select')[0];
+ if (!select) {
+ return;
+ }
+ }
+
+ var option = jQuery(select).find('option[value="' + property_id + '"]')[0];
+ if (!option) {
+ return;
+ }
+ jQuery(option).removeAttr('disabled');
+
+ //As a final step: delete the row:
+ jQuery(row).remove();
+ }
+ );
+
+ //Event handlers for the individual booking plan print view:
+ jQuery('.sidebar .colour-selector').draggable(
+ {
+ cursorAt: {
+ left: 28, top: 15
+ },
+ appendTo: 'body',
+ helper: function () {
+ var dragged_item = jQuery(
+ '<div class="dragged-colour"></div>'
+ );
+ jQuery(dragged_item).css(
+ {
+ backgroundColor: jQuery(this).css('background-color'),
+ width: jQuery(this).css('width'),
+ height: jQuery(this).css('height'),
+ zIndex: 1000
+ }
+ );
+ return dragged_item;
+ },
+ revert: true
+ }
+ );
+
+ jQuery(document).on(
+ 'click',
+ '.colour-selector',
+ function (event) {
+ jQuery(event.target).children().click();
+ }
+ );
+
+ jQuery(document).on(
+ 'change',
+ '.colour-selector input[type="color"]',
+ function (event) {
+ jQuery(event.target).parent().css(
+ 'background-color',
+ jQuery(event.target).val()
+ );
+ }
+ );
+
+ jQuery(document).on(
+ 'dragenter',
+ '.individual-booking-plan .appointment-booking-plan .schedule_entry',
+ function (event) {
+ jQuery(event.target).css('opacity', '0.7');
+ }
+ );
+
+ jQuery(document).on(
+ 'dragleave',
+ '.individual-booking-plan .appointment-booking-plan .schedule_entry',
+ function (event) {
+ jQuery(event.target).css('opacity', '1.0');
+ }
+ );
+
+ jQuery(document).on(
+ 'dragend',
+ '.dragged-colour',
+ function (event) {
+ jQuery(event.target).css(
+ {
+ 'top': '0px',
+ 'left': '0px'
+ }
+ );
+ }
+ );
+
+ jQuery('.schedule_entry').droppable(
+ {
+ drop: function (event, ui_element) {
+ event.preventDefault();
+
+ var booking_plan_entry = event.target;
+ var new_background_colour = jQuery(
+ ui_element.helper
+ ).css('background-color');
+
+ jQuery(booking_plan_entry).css(
+ 'background-color',
+ new_background_colour
+ );
+
+ jQuery(booking_plan_entry).find('dl').css(
+ {
+ backgroundColor: new_background_colour,
+ borderColor: new_background_colour
+ }
+ );
+ jQuery(booking_plan_entry).find('dt').css(
+ 'background-color',
+ new_background_colour
+ );
+ }
+ }
+ );
+
+ //For the message functionality of the resource system:
+
+ jQuery(document).on(
+ 'click',
+ '.resources_messages-form .selection-area .remove-icon',
+ function (event) {
+ jQuery(event.target).parent().remove();
+ }
+ );
+
+ //Handle the selection of room "sources":
+
+ jQuery(document).on(
+ 'click',
+ '.resources_messages-form input[name="room_selection"]',
+ function (event) {
+ //Hide the select field or the search field depending
+ //on the room selection radio button:
+ var room_selection = jQuery(event.target).val();
+ if (room_selection == 'search') {
+ jQuery(
+ '.resources_messages-form select[name="clipboard_id"]'
+ ).attr('disabled', 'disabled');
+ jQuery(
+ '.resources_messages-form input[name="room_name_parameter"]'
+ ).removeAttr('disabled');
+ } else {
+ jQuery(
+ '.resources_messages-form input[name="room_name_parameter"]'
+ ).attr('disabled', 'disabled');
+ jQuery(
+ '.resources_messages-form select[name="clipboard_id"]'
+ ).removeAttr('disabled');
+ }
+ }
+ );
+
+ //Handle the selection of recipient "sources":
+
+ jQuery(document).on(
+ 'click',
+ '.resources_messages-form input[name="recipient_selection"]',
+ function (event) {
+ var recipient_selection = jQuery(event.target).val();
+ if (recipient_selection == 'permission') {
+ jQuery('#RecipientMode_Booking').css('display', 'none');
+ jQuery('#RecipientMode_Permission').css('display', 'block');
+ } else {
+ jQuery('#RecipientMode_Permission').css('display', 'none');
+ jQuery('#RecipientMode_Booking').css('display', 'block');
+ }
+ }
+ )
+
+ //For the view resources/resource/assign:
+
+ jQuery(document).on(
+ 'change',
+ '.create-booking-form .booking-type-selection select',
+ function (event) {
+ if (jQuery(event.target).prop('tagName') != 'SELECT') {
+ return;
+ }
+ var booking_type = jQuery(event.target).val();
+ var form = jQuery(event.target).parents('form')[0];
+ if (!form) {
+ return;
+ }
+
+ //Activate the correct text for the separable rooms option:
+ var separable_room_booking_spans = jQuery(form).find(
+ 'label.separable-room-booking span'
+ );
+ for (var span of separable_room_booking_spans) {
+ var span_booking_type = jQuery(span).data('booking_type');
+ if (span_booking_type == booking_type) {
+ jQuery(span).css('display', 'inline');
+ } else {
+ jQuery(span).css('display', 'none');
+ }
+ }
+
+ //Activate the correct legend for the comment fieldset:
+ var comment_fieldset_legends = jQuery(form).find(
+ 'fieldset.comment-fieldset legend'
+ );
+ for (var legend of comment_fieldset_legends) {
+ var legend_booking_type = jQuery(legend).data('booking_type');
+ if (legend_booking_type == booking_type) {
+ jQuery(legend).css('display', 'block');
+ } else {
+ jQuery(legend).css('display', 'none');
+ }
+ }
+
+ //Activate the correct booking_type 2 elements:
+ jQuery("*[data-booking_type='2']").each(function () {
+ if (booking_type == '2') {
+ jQuery(this).show();
+ } else {
+ jQuery(this).hide();
+ }
+ });
+ }
+ );
+
+ jQuery(document).on(
+ 'change',
+ 'input[name="booking_style"]',
+ function () {
+ STUDIP.Resources.moveTimeOptions($(this).val());
+ }
+ );
+
+ jQuery(document).on(
+ 'change',
+ '.semester-time-option',
+ function () {
+ if (~$(this).attr('name').indexOf("begin")) {
+ $("#BookingStartDateInput").prop("disabled", true);
+ } else {
+ $("#RepetitionEndInput").prop("disabled", true);
+ $("#RepetitionEndInput").val($("input[name='semester_course_end_date']").val());
+ $("#HiddenRepetitionEndInput").prop("disabled", false);
+ $("#HiddenRepetitionEndInput").val($("input[name='semester_course_end_date']").val());
+ }
+ $(".semester-selector").parent().show();
+ }
+ );
+
+ jQuery(document).on(
+ 'change',
+ '.manual-time-option',
+ function () {
+ if (~$(this).attr('name').indexOf("begin")) {
+ $("#BookingStartDateInput").prop("disabled", false);
+ } else {
+ $("#RepetitionEndInput").prop("disabled", false);
+ $("#HiddenRepetitionEndInput").prop("disabled", true);
+ }
+ if (!$("#BookingStartDateInput").prop("disabled")
+ && !$("#RepetitionEndInput").prop("disabled")) {
+ $(".semester-selector").parent().hide();
+ }
+ }
+ );
+
+ jQuery(document).on(
+ 'change',
+ '.manual-time-fields input[type="text"]',
+ function () {
+ var ds = $(this).val().split('.');
+ var d = new Date(ds[1] + '/' + ds[0] + '/' + ds[2]);
+ var day_numer = (d.getDay() || 7);
+
+ if ($(this).attr('id') == 'BookingStartDateInput') {
+ $("#begin_date-weekdays span").addClass('invisible');
+ $("#begin_date-weekdays #" + day_numer).removeClass('invisible');
+ var start_date_parts = jQuery(this).val().split('.');
+ var repetition_end_date_parts = jQuery("#RepetitionEndInput").val().split('.');
+ var start_date = new Date(
+ start_date_parts[2] + '-' + start_date_parts[1] + '-' + start_date_parts[0]
+ + 'T00:00:00'
+ );
+ var repetition_end_date = new Date(
+ repetition_end_date_parts[2] + '-' + repetition_end_date_parts[1] + '-'
+ + repetition_end_date_parts[0] + 'T00:00:00'
+ );
+
+ if (start_date > repetition_end_date
+ && $("input[name='selected_end']:checked").val() != 'semester_course_end') {
+ $("#RepetitionEndInput").prop('defaultValue', $(this).val());
+ $("#RepetitionEndInput").val($(this).val()).trigger('change');
+ }
+
+ if (!$('#multiday').prop('checked')
+ || $("#BookingEndDateInput").prop('defaultValue') ==
+ $("#BookingEndDateInput").val()) {
+ $("#BookingEndDateInput").prop('defaultValue', $(this).val());
+ $("#BookingEndDateInput").val($(this).val()).trigger('change');
+ }
+ updateRepeatEndSemesterByTimestamp(Math.floor(d / 1000));
+ } else if ($(this).attr('id') == 'BookingEndDateInput') {
+ $("#end_date-weekdays span").addClass('invisible');
+ $("#end_date-weekdays #" + day_numer).removeClass('invisible');
+ }
+ }
+ );
+
+ jQuery(document).on(
+ 'change',
+ 'input[name="begin_date"]',
+ function () {
+ if (!$('#multiday').prop('checked')) {
+ $('#end_date_section input').val($(this).val());
+ }
+ }
+ );
+
+ $(".new-clipboard-form #add-clipboard-button").removeAttr("disabled");
+ var selected_clipboard_id = $('.clipboard-selector').val();
+ $(".clipboard-area[data-id='" + selected_clipboard_id + "']").removeClass('invisible');
+ if ($(".clipboard-area[data-id='" + selected_clipboard_id + "']").find(".empty-clipboard-message").hasClass("invisible")) {
+ $("#clipboard-group-container").find('.widget-links').removeClass('invisible');
+ }
+
+ $('.special-item-switch').each(function () {
+ if ($(this).prop('checked') == false) {
+ $(this).next('label').children(':not(span)').hide();
+ }
+ });
+
+ jQuery(document).on(
+ 'click',
+ '.special-item-switch',
+ function () {
+ $(this).next('label').children(':not(span)').toggle();
+ }
+ );
+
+ jQuery(document).on(
+ 'click',
+ '#booking-plan-jmpdate-submit',
+ function () {
+ var picked = $('#booking-plan-jmpdate').val();
+ var iso_date_string = '';
+ if (picked.includes('.')) {
+ var good_format = picked.split('.');
+ var day = good_format[0];
+ var month = good_format[1];
+ var year = good_format[2];
+ iso_date_string = year.padStart(4, "20") + '-' + month.padStart(2, "0") + '-' + day.padStart(2, "0");
+ } else if (picked.includes('/')) {
+ var bad_format = picked.split('/');
+ var day = bad_format[1];
+ var month = bad_format[0];
+ var year = bad_format[2];
+ iso_date_string = year.padStart(4, "20") + '-' + month.padStart(2, "0") + '-' + day.padStart(2, "0");
+ } else if (picked.includes('-')) {
+ iso_date_string = picked;
+ }
+ if (iso_date_string) {
+ $('*[data-resources-fullcalendar="1"]').each(function () {
+ $(this)[0].calendar.gotoDate(iso_date_string);
+ });
+ updateDateURL();
+ }
+ }
+ );
+
+ jQuery(document).on(
+ 'change',
+ 'select[name="special__time_range_semester_id"]',
+ function () {
+ var selected_option = $(this).find('option:selected');
+ if (selected_option) {
+ var begin = new Date(parseInt(selected_option.attr('data-begin') + '000'));
+ var end = new Date(parseInt(selected_option.attr('data-end') + '000'));
+ $('input[name="special__time_range_begin_date"]').val(
+ $.datepicker.formatDate('dd.mm.yy', begin)
+ );
+ $('input[name="special__time_range_end_date"]').val(
+ $.datepicker.formatDate('dd.mm.yy', end)
+ );
+ }
+ }
+ );
+
+ jQuery(document).on(
+ 'click',
+ '.fc-button',
+ function () {
+ if ($(this).hasClass('fc-dayGridMonth-button')) {
+ updateViewURL('dayGridMonth')
+ } else if ($(this).hasClass('fc-timeGridWeek-button')) {
+ updateViewURL('timeGridWeek')
+ } else if ($(this).hasClass('fc-timeGridDay-button')) {
+ updateViewURL('timeGridDay')
+ } else if ($(this).hasClass('fc-today-button')
+ || $(this).hasClass('fc-prev-button')
+ || $(this).hasClass('fc-next-button')) {
+ updateDateURL();
+ }
+ }
+ );
+
+ jQuery(document).on(
+ 'blur',
+ '.hasDatepicker',
+ function () {
+ var new_val = $(this).val();
+ switch ($(this).attr('name')) {
+ case 'permissions[begin_date][]':
+ case 'permissions[end_date][]':
+ case 'bulk_begin_date':
+ case 'bulk_end_date':
+ var now = new Date();
+ if (new_val.split('.').length === 1) {
+ $(this).val(new_val + '.' + ((now.getMonth() + 1) < 10 ? '0' + (now.getMonth() + 1) : (now.getMonth() + 1)) + '.' + now.getFullYear());
+ } else if (new_val.split('.').length === 2) {
+ $(this).val(new_val + '.' + now.getFullYear());
+ }
+ break;
+ case 'permissions[begin_time][]':
+ case 'permissions[end_time][]':
+ case 'bulk_begin_time':
+ case 'bulk_end_time':
+ if (new_val.split(':').length === 1) {
+ $(this).val(new_val + ':00');
+ }
+ break;
+ }
+ }
+ );
+
+ jQuery(document).on(
+ 'change blur',
+ '#resource-temporary-permission-bulk-datetime input',
+ function () {
+ var targets = '';
+ switch ($(this).attr('name')) {
+ case 'bulk_begin_date':
+ targets = 'permissions[begin_date][]';
+ break;
+ case 'bulk_begin_time':
+ targets = 'permissions[begin_time][]';
+ break;
+ case 'bulk_end_date':
+ targets = 'permissions[end_date][]';
+ break;
+ case 'bulk_end_time':
+ targets = 'permissions[end_time][]';
+ break;
+ }
+ var new_val = $(this).val();
+ $('.resource-temporary-permission-row input[name="' + targets + '"]').each(function () {
+ if ($(this).parents('tr').find('input[name="selected_permission_ids[]"]').prop('checked')) {
+ $(this).val(new_val);
+ }
+ });
+ }
+ );
+
+ function updateRepeatEndSemesterByTimestamp(timestamp, api_url = 'api.php/semesters') {
+ var semester = null;
+ jQuery.ajax(
+ STUDIP.URLHelper.getURL(api_url),
+ {
+ method: 'get',
+ dataType: 'json',
+ success: function (data) {
+ if (data) {
+ Object.values(data.collection).forEach(item => {
+ if (timestamp >= item.begin && timestamp < item.end) {
+ semester = item;
+ }
+ });
+ if (semester) {
+ $("#semester_course_name").text(semester.title);
+ $(".semester-time-option").prop('disabled', false);
+ } else {
+ if (data.pagination && data.pagination.links.next != api_url) {
+ semester = updateRepeatEndSemesterByTimestamp(timestamp, data.pagination.links.next);
+ } else {
+ $("#semester_course_name").text('außerhalb definierter Zeiten');
+ $(".semester-time-option").prop('checked', false);
+ $(".semester-time-option").prop('disabled', true);
+ $(".manual-time-option").prop('checked', true);
+ $(".manual-time-option").trigger('change');
+ }
+ }
+ }
+ }
+ }
+ );
+ }
+
+ function updateViewURL(defaultView) {
+ var sURLVariables = window.location.href.split(/[?&]/);
+ var changed = false;
+ for (var i = 0; i < sURLVariables.length; i++) {
+ var sParameterName = sURLVariables[i].split('=');
+ if (sParameterName[0] == "defaultView") {
+ sParameterName[1] = defaultView;
+ sURLVariables[i] = sParameterName.join('=');
+ changed = true;
+ }
+ }
+ if (!changed) {
+ sURLVariables.push('defaultView=' + defaultView);
+ }
+ if (sURLVariables.length > 2) {
+ var newurl = sURLVariables[0] + '?' + sURLVariables[1] + '&';
+ sURLVariables.shift();
+ sURLVariables.shift();
+ newurl += sURLVariables.join('&');
+ } else {
+ var newurl = sURLVariables.join('?');
+ }
+ history.pushState({}, null, newurl);
+ var std_day = newurl.replace(/&?allday=\d+/, '');
+ $('.booking-plan-std_view').attr('href', std_day);
+ $('.booking-plan-allday_view').attr('href', std_day + '&allday=1');
+ };
+
+ function updateDateURL() {
+ var changedmoment;
+ $('*[data-resources-fullcalendar="1"]').each(function () {
+ changedmoment = $(this)[0].calendar.getDate();
+ });
+ if (changedmoment) {
+ //Get the timestamp:
+ var timestamp = changedmoment.getTime() / 1000;
+
+ //Set the URL parameter for the "export bookings" action
+ //in the sidebar:
+ var export_action = jQuery('#export-resource-bookings-action');
+ if (export_action.length > 0) {
+ var export_url = jQuery(export_action).attr('href');
+ if (export_url.search(/[?&]timestamp=/) >= 0) {
+ export_url = export_url.replace(
+ /timestamp=[0-9]{0,10}/,
+ 'timestamp=' + timestamp
+ );
+ } else {
+ if (export_url.search(/\?/) >= 0) {
+ export_url += '&timestamp=' + timestamp;
+ } else {
+ export_url += '?timestamp=' + timestamp;
+ }
+ }
+ jQuery(export_action).attr('href', export_url);
+ }
+
+ //Now change the URL of the window.
+ var changeddate = STUDIP.Fullcalendar.toRFC3339String(changedmoment).split('T')[0];
+ var sURLVariables = window.location.href.split(/[?&]/);
+ var changed = false;
+ for (var i = 0; i < sURLVariables.length; i++) {
+ var sParameterName = sURLVariables[i].split('=');
+ if (sParameterName[0] == "defaultDate") {
+ sParameterName[1] = changeddate;
+ sURLVariables[i] = sParameterName.join('=');
+ changed = true;
+ }
+ }
+ if (!changed) {
+ sURLVariables.push('defaultDate=' + changeddate);
+ }
+ if (sURLVariables.length > 2) {
+ var newurl = sURLVariables[0] + '?' + sURLVariables[1] + '&';
+ sURLVariables.shift();
+ sURLVariables.shift();
+ newurl += sURLVariables.join('&');
+ } else {
+ var newurl = sURLVariables.join('?');
+ }
+ history.pushState({}, null, newurl);
+ var std_day = newurl.replace(/&?allday=\d+/, '');
+ $('.booking-plan-std_view').attr('href', std_day);
+ $('.booking-plan-allday_view').attr('href', std_day + '&allday=1');
+ $('#booking-plan-jmpdate').val(changedmoment.toLocaleDateString('de-DE'));
+ //Store the date in the sessionStorage:
+ sessionStorage.setItem('booking_plan_date', changeddate)
+ }
+ };
+
+ jQuery('#booking-plan-jmpdate').datepicker(
+ {
+ dateFormat: 'dd.mm.yy'
+ }
+ );
+ jQuery('.resource-booking-time-fields input[type="date"]').datepicker(
+ {
+ dateFormat: 'yy-mm-dd'
+ }
+ );
+
+ var nodes = jQuery('*.resource-plan[data-resources-fullcalendar="1"]');
+ jQuery.each(nodes, function (index, node) {
+ STUDIP.loadChunk('fullcalendar').then(() => {
+ //Get the default date from the sessionStorage, if it is set
+ //and no date is specified in the url.
+ var use_session_date = true;
+ var url_param_string = window.location.search;
+ if (url_param_string) {
+ var url_params = new URLSearchParams(url_param_string);
+ if (url_params.get('defaultDate')) {
+ use_session_date = false;
+ }
+ }
+ if (node.calendar == undefined) {
+ if (jQuery(node).hasClass('semester-plan')) {
+ STUDIP.Fullcalendar.createSemesterCalendarFromNode(
+ node,
+ {
+ loading: function (isLoading) {
+ if (!isLoading) {
+ var h = jQuery('section.studip-fullcalendar-header');
+ if (h) {
+ jQuery(h).removeClass('invisible');
+ jQuery(h).insertAfter('.fc .fc-toolbar');
+ }
+ }
+ }
+ }
+ );
+ } else {
+ var config = {
+ studip_functions: {
+ drop_event:
+ STUDIP.Resources.dropEventInRoomGroupBookingPlan,
+ resize_event:
+ STUDIP.Resources.resizeEventInRoomGroupBookingPlan
+ },
+ loading: function (isLoading) {
+ if (!isLoading) {
+ var h = jQuery('section.studip-fullcalendar-header');
+ if (h) {
+ jQuery(h).removeClass('invisible');
+ jQuery(h).insertAfter('.fc .fc-toolbar');
+ }
+ }
+ }
+ };
+ if (use_session_date) {
+ var session_date_string = sessionStorage.getItem('booking_plan_date');
+ if (session_date_string) {
+ config.defaultDate = session_date_string;
+ }
+ }
+ STUDIP.Fullcalendar.createFromNode(node, config);
+ }
+ }
+ });
+ });
+
+ //Check if an individual booking plan is to be displayed:
+ var nodes = jQuery('.individual-booking-plan[data-resources-fullcalendar="1"]');
+ jQuery.each(nodes, function (index, node) {
+ STUDIP.loadChunk('fullcalendar').then(() => {
+ STUDIP.Fullcalendar.createFromNode(
+ node,
+ {
+ eventPositioned: function (info, calendar_event, dom_element, view) {
+ var calendar_event = info.event;
+ var dom_element = info.el;
+ var view = info.view;
+ jQuery(dom_element).droppable({
+ drop: function (event, ui_element) {
+ event.preventDefault();
+
+ var booking_plan_entry = event.target;
+ var new_background_colour = jQuery(
+ ui_element.helper
+ ).css('background-color');
+
+ jQuery(booking_plan_entry).css(
+ 'background-color',
+ new_background_colour
+ );
+ jQuery(booking_plan_entry).css(
+ 'border-color',
+ new_background_colour
+ );
+
+ jQuery(booking_plan_entry).find('dl').css({
+ backgroundColor: new_background_colour,
+ borderColor: new_background_colour
+ });
+ jQuery(booking_plan_entry).find('dt').css(
+ 'background-color',
+ new_background_colour
+ );
+ }
+ });
+ var h = jQuery('section.studip-fullcalendar-header').clone();
+ if (h) {
+ jQuery(h).removeClass('invisible');
+ jQuery(h).insertAfter('.fc .fc-toolbar');
+ }
+ }
+ }
+ );
+ });
+ });
+
+ jQuery(document).on(
+ 'click',
+ '.create-booking-form .delete-assigned-user-icon',
+ function (event) {
+ var quicksearch = jQuery(event.target).parent().find('input');
+ if (!quicksearch) {
+ return;
+ }
+ jQuery(quicksearch).val('');
+ }
+ );
+
+ jQuery(document).on(
+ 'click',
+ '.request-list .request-marking-icon',
+ function (event) {
+ event.preventDefault();
+ STUDIP.Resources.toggleRequestMarked(event.target);
+ }
+ );
+
+ $(document).on(
+ 'click',
+ "button[name='bulk-book-requests']",
+ function (event) {
+ STUDIP.Dialog.confirm(
+ $gettext('Wollen Sie die im Plan gezeigten Anfragen wirklich buchen?')
+ ).done(function () {
+ STUDIP.Resources.bookAllCalendarRequests();
+ });
+ }
+ );
+
+
+ $(document).on('click', '.fc-request-event',
+ function () {
+ var parent_table_row = $(this).closest('tr');
+
+ if($(parent_table_row).length) {
+ $(parent_table_row).toggleClass('resource-planning-selected-request')
+ }
+ var objectData = $(this).data();
+ var eventData = {
+ id: objectData.eventId,
+ title: objectData.eventTitle,
+ start: objectData.eventBegin,
+ end: objectData.eventEnd,
+ studip_weekday_begin: objectData.eventStudip_weekday_begin,
+ studip_weekday_end: objectData.eventStudip_weekday_end,
+ request_id: objectData.eventRequest,
+ tooltip: objectData.eventTooltip,
+ studip_api_urls: {},
+ studip_view_urls: {edit: objectData.eventView_urls_edit},
+ editable: false,
+ color: objectData.eventColor,
+ textColor: '#000'
+ };
+
+ var calendarSektion = $('*[data-resources-fullcalendar="1"]')[0];
+ if (calendarSektion) {
+ var calendar = calendarSektion.calendar;
+ if (calendar && eventData) {
+ var existingRequestEvent = calendar.getEventById(eventData.id);
+ if (existingRequestEvent) {
+ existingRequestEvent.remove();
+
+ var remainingRequestEvents = 0;
+ $('.fc-request-event').each(function () {
+ if (calendar.getEventById($(this).data().eventId)) {
+ remainingRequestEvents++;
+ }
+ });
+ if (remainingRequestEvents < 1) {
+ $("button[name='bulk-book-requests']").prop('disabled', true);
+ }
+ } else {
+ STUDIP.Fullcalendar.convertSemesterEvents(eventData, calendar.getDate().toString());
+ var overlap = false;
+ var checkStart = new Date(eventData.start);
+ var checkEnd = new Date(eventData.end);
+ $(calendar.getEvents()).each(function () {
+ // start-time in between any of the events
+ if ((checkStart >= this.start && checkStart < this.end)
+ //end-time in between any of the events
+ || (checkEnd > this.start && checkEnd <= this.end)
+ //any of the events in between/on the start-time and end-time
+ || (checkStart <= this.start && checkEnd >= this.end)) {
+ overlap = true
+ }
+ });
+ if (overlap) {
+ eventData.icon = 'exclaim-circle-full';
+ }
+ calendar.addEvent(eventData);
+ $("button[name='bulk-book-requests']").prop('disabled', false);
+ }
+
+ }
+ }
+ }
+ );
+});
+
+
+STUDIP.domReady(function() {
+ jQuery(document).on(
+ 'click',
+ '.room-clipboard-group-action',
+ function (event) {
+ //Get the IDs of the rooms of the clipboard:
+ var active_clipboard = jQuery(event.target).parents("#clipboard-group-container").find(
+ '.clipboard-area:not(.invisible)'
+ )[0];
+ if (!active_clipboard) {
+ //Something is wrong with the HTML.
+ return;
+ }
+
+ var clipboard_id = jQuery(active_clipboard).data('id');
+ var action_needs_items = jQuery(event.target).data('needs_items');
+ var show_in_dialog = jQuery(event.target).data('show_in_dialog');
+ var ids = [];
+ if (action_needs_items) {
+ var items = jQuery(active_clipboard).find(
+ 'tr.clipboard-item:not(.clipboard-item-template)'
+ );
+
+ for (var item of items) {
+ var input = jQuery(item).find("input[name='selected_clipboard_items[]']:checked")[0];
+ if (input) {
+ var id = jQuery(item).data('range_id');
+ //Check if id is an md5 sum:
+ if (id.match(/[0-9a-f]{32}/)) {
+ ids.push(id);
+ }
+ }
+ }
+ if (ids.length == items.length) {
+ //All items are selected. No need to use the Range-IDs, we
+ //can use the clipboard-ID instead.
+ action_needs_items = false;
+ }
+ }
+
+ var url_path = jQuery(event.target).data('url_path');
+ url_path = url_path.replace(/CLIPBOARD_ID/, clipboard_id);
+
+ var complete_url = STUDIP.URLHelper.getURL(
+ url_path,
+ (
+ action_needs_items ? {'resource_ids': ids} : null
+ )
+ );
+
+ if (show_in_dialog) {
+ //If we have collected at least one ID we can create a dialog
+ //displaying the comments of all the selected rooms:
+ STUDIP.Dialog.fromURL(
+ complete_url,
+ {
+ size: 'normal'
+ }
+ );
+ } else {
+ //Show the action in a new tab:
+ window.open(complete_url, '_blank');
+ }
+ return false;
+ }
+ );
+});
diff --git a/resources/assets/javascripts/bootstrap/responsive.js b/resources/assets/javascripts/bootstrap/responsive.js
new file mode 100644
index 0000000..5dffb4e
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/responsive.js
@@ -0,0 +1,39 @@
+/*jslint esversion: 6*/
+
+// Build responsive menu on domready or resize
+STUDIP.domReady(() => {
+ const cache = STUDIP.Cache.getInstance('responsive.');
+ if (STUDIP.Navigation.navigation !== undefined) {
+ cache.set('navigation', STUDIP.Navigation.navigation);
+ STUDIP.Cookie.set('responsive-navigation-hash', STUDIP.Navigation.hash);
+ } else {
+ STUDIP.Navigation.navigation = cache.get('navigation');
+ }
+
+ STUDIP.Responsive.engage();
+}, true);
+
+// Trigger search in responsive display
+$(document).on('click', '#quicksearch .quicksearchbutton', function() {
+ if ($('html').is(':not(.responsive-display)') || $('#quicksearch').is('.open')) {
+ return;
+ }
+
+ $('#quicksearch').addClass('open');
+ $('.quicksearchbox').focus();
+
+ return false;
+}).on('blur', '#quicksearch.open .quicksearchbox', function() {
+ if (!this.value.trim().length) {
+ $('#quicksearch').removeClass('open');
+ }
+}).on('autocompleteopen', event => {
+ if ($(event.target).closest('#quicksearch').length === 0) {
+ return;
+ }
+ $('body > .ui-autocomplete').css({
+ left: 0,
+ right: 0,
+ boxSizing: 'border-box'
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/scroll_to_top.js b/resources/assets/javascripts/bootstrap/scroll_to_top.js
new file mode 100644
index 0000000..2f6ac57
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/scroll_to_top.js
@@ -0,0 +1,7 @@
+STUDIP.domReady(() => {
+ // Test if the header is actually present
+ if ($('#scroll-to-top').length > 0) {
+ STUDIP.ScrollToTop.enable();
+ STUDIP.ScrollToTop.moveBack();
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/search.js b/resources/assets/javascripts/bootstrap/search.js
new file mode 100644
index 0000000..6b94a02
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/search.js
@@ -0,0 +1,146 @@
+STUDIP.domReady(() => {
+ var cache = STUDIP.Search.getCache();
+ // initially hide all filters except for the semester filter
+ $('#reset-search').hide();
+ STUDIP.Search.hideAllFilters();
+ $('div#semester_filter').show();
+ STUDIP.Search.setActiveCategory('show_all_categories');
+ STUDIP.Search.showActiveFilters(STUDIP.Search.getFilter());
+
+ // searchterm and category can be passed by URL parameters (e.g. through the quicksearch)
+ var searchterm = $('#search-results').data('searchterm');
+ var category = $('#search-results').data('category') || location.hash.slice(1);
+ if(searchterm) {
+ cache.set('searchterm', searchterm);
+ if (category) {
+ STUDIP.Search.setActiveCategory(category);
+ }
+ }
+
+ // Clear search term
+ $('#reset-search').on('click', function () {
+ STUDIP.Search.resetSearch();
+ return false;
+ });
+
+ // Start search on Enter
+ $('#search-input').on('keypress', function (e) {
+ if (e.which === 13) {
+ STUDIP.Search.doSearch(STUDIP.Search.getFilter());
+ return false;
+ }
+ });
+
+ // Delegate events on sidebar categories so we don't have to bind them
+ // one by one (probably needs some work...) TODO refactor
+ $('a[id^="search_category"]').on('click', function () {
+ var category = this.id.substr(this.id.lastIndexOf('_') + 1, this.id.length);
+ var old_category = cache.get('search_category');
+ STUDIP.Search.showAllCategories(old_category);
+ STUDIP.Search.toggleLinkText(old_category);
+ cache.set('search_category', category);
+ STUDIP.Search.showAllCategories(category);
+ STUDIP.Search.expandCategory(category);
+ STUDIP.Search.toggleLinkText(category);
+ STUDIP.Search.setActiveCategory(category);
+ STUDIP.Search.showActiveFilters(STUDIP.Search.getFilter());
+ return false;
+ });
+
+ // click on 'Alle Ergebnisse'
+ $('a#show_all_categories').on('click', function() {
+ var category = cache.get('search_category');
+ STUDIP.Search.toggleLinkText(category);
+ STUDIP.Search.showAllCategories(category);
+ if (!STUDIP.Search.resultsInCategory) {
+ STUDIP.Search.resetFilters();
+ }
+ });
+
+ // perform a new search when another filter is selected by the user
+ $('#globalsearch-page select[id$="_select"]').on('change', function () {
+ STUDIP.Search.showActiveFilters(STUDIP.Search.getFilter());
+ STUDIP.Search.doSearch(STUDIP.Search.getFilter());
+ return false;
+ }).closest('form').on('submit', function(e) {
+ e.preventDefault();
+ });
+
+ // set main search bar if a searchterm was typed in before
+ $('#search-input').val(function() {
+ if (cache.get('searchterm')) {
+ STUDIP.Search.doSearch(STUDIP.Search.getFilter());
+ if (cache.get('search_category')) {
+ STUDIP.Search.setActiveCategory(cache.get('search_category'));
+ }
+ }
+ return cache.get('searchterm');
+ });
+
+ // Delegate events on result container so we don't have to bind them
+ // one by one
+ $('#search-results').on('click', '.search-category a', function () {
+ var category = $(this).closest('.search-category').data('category');
+ STUDIP.Search.toggleLinkText(category);
+ STUDIP.Search.expandCategory(category);
+ STUDIP.Search.setActiveCategory(category);
+ return false;
+ });
+
+ // Start searching 500 ms after user stopped typing.
+ $('#search-input').keyup(_.debounce(function () {
+ if ($('#search-input').val().trim().length >= STUDIP.Search.searchTermLength) {
+ STUDIP.Search.doSearch(STUDIP.Search.getFilter());
+ }
+ }, 500));
+
+ // Click on search button
+ $('#search-btn').click(function () {
+ STUDIP.Search.doSearch(STUDIP.Search.getFilter());
+ return false;
+ });
+
+ // Event driven history changes
+ var history_timeout;
+ $(document).on('searched.studip search-category-change.studip', function (event, info) {
+ let url = location.href.split('#')[0];
+
+ if (info.category && info.category !== 'show_all_categories') {
+ url += `#${info.category}`;
+ }
+ url = STUDIP.URLHelper.getURL(url, {
+ q: info.needle || cache.get('searchterm')
+ });
+
+ // We need to put the history change on a timeout since category changes
+ // occur more than once in a short period of time
+ clearTimeout(history_timeout);
+ history_timeout = setTimeout(() => {
+ if (location.href !== url) {
+ history.pushState({
+ needle: info.needle || STUDIP.Search.getCache().get('searchterm'),
+ category: info.category
+ }, '', url)
+ }
+ }, 50);
+ });
+ $(window).on('popstate', function (event) {
+ if (!event.originalEvent.state) {
+ return;
+ }
+
+ let state = event.originalEvent.state;
+
+ if (state.category) {
+ if (state.category === 'show_all_categories') {
+ $('a#show_all_categories').click();
+ } else {
+ $(`a#search_category_${state.category}`).click();
+ }
+ }
+ if (state.needle && state.needle !== STUDIP.Search.getCache().get('searchterm')) {
+ $('#search-input').val(state.needle);
+ STUDIP.Search.doSearch(STUDIP.Search.getFilter());
+ }
+ })
+});
diff --git a/resources/assets/javascripts/bootstrap/selection.js b/resources/assets/javascripts/bootstrap/selection.js
new file mode 100644
index 0000000..07369cd
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/selection.js
@@ -0,0 +1,34 @@
+function findList(selector, context) {
+ var list = $(context)
+ .closest('.studip-selection')
+ .find(selector);
+ if (list.is('ul')) {
+ return list;
+ }
+ return list.find('ul:first');
+}
+
+$(document).on('click', '.studip-selection:not(.disabled) li:not(.empty-placeholder)', function() {
+ var remove = $(this).is('.studip-selection-selected li'),
+ item_id = $(this).data().selectionId,
+ attr_name =
+ $(this)
+ .closest('.studip-selection')
+ .data().attributeName || 'selected',
+ list;
+ if (remove) {
+ list = findList('.studip-selection-selectable', this);
+ $('input[type=hidden]', this).remove();
+ } else {
+ list = findList('.studip-selection-selected', this);
+ $('<input type="hidden" name="' + attr_name + '[]">')
+ .val(item_id)
+ .prependTo(this);
+ }
+
+ $(this)
+ .remove()
+ .appendTo(list);
+
+ return false;
+});
diff --git a/resources/assets/javascripts/bootstrap/settings.js b/resources/assets/javascripts/bootstrap/settings.js
new file mode 100644
index 0000000..a8c7ccd
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/settings.js
@@ -0,0 +1,76 @@
+// Copy elements value to another element on change
+// Used for title choosers
+$(document).on('change', '[data-target]', function() {
+ var target = $(this).data().target;
+ $(target).val(this.value);
+});
+
+STUDIP.domReady(() => {
+ $('#edit_userdata').on('change', 'input[name^=email]', function() {
+ var changed = false;
+ $('#edit_userdata input[name^=email]').each(function() {
+ changed = changed || this.value !== this.defaultValue;
+ });
+ $('#edit_userdata .email-change-confirm').toggle(changed);
+ });
+
+ $('#edit_userdata .email-change-confirm').hide();
+});
+
+//
+$(document).on('change', '#settings-notifications :checkbox', function() {
+ var name = $(this).attr('name');
+
+ if (name === 'all[all]') {
+ $(this)
+ .closest('table')
+ .find(':checkbox')
+ .prop('checked', this.checked);
+ return;
+ }
+
+ if (/all\[columns\]/.test(name)) {
+ var index =
+ $(this)
+ .closest('td')
+ .index() + 2;
+ $(this)
+ .closest('table')
+ .find('tbody td:nth-child(' + index + ') :checkbox')
+ .prop('checked', this.checked);
+ } else if (/all\[rows\]/.test(name)) {
+ $(this)
+ .closest('td')
+ .siblings()
+ .find(':checkbox')
+ .prop('checked', this.checked);
+ }
+
+ $('.notification.settings tbody :checkbox[name^=all]').each(function() {
+ var other = $(this)
+ .closest('td')
+ .siblings()
+ .find(':checkbox');
+ this.checked = other.filter(':not(:checked)').length === 0;
+ });
+
+ $('.notification.settings thead :checkbox').each(function() {
+ var index =
+ $(this)
+ .closest('td')
+ .index() + 2,
+ other = $(this)
+ .closest('table')
+ .find('tbody td:nth-child(' + index + ') :checkbox');
+ this.checked = other.filter(':not(:checked)').length === 0;
+ });
+});
+
+$(document).on('input', '#new_password', function() {
+ var message = $(this).data().message;
+ if (this.validity.patternMismatch) {
+ this.setCustomValidity(message);
+ } else {
+ this.setCustomValidity('');
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/sidebar.js b/resources/assets/javascripts/bootstrap/sidebar.js
new file mode 100644
index 0000000..bc7329c
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/sidebar.js
@@ -0,0 +1,64 @@
+// (De|Re)activate when help tours start|stop
+$(document).on('tourstart.studip tourend.studip', function(event) {
+ STUDIP.Sidebar.setSticky(event.type === 'tourend.studip');
+});
+
+// Handle dynamic content
+if (window.MutationObserver !== undefined) {
+ // Attach mutation observer to #layout_content and trigger it on
+ // changes to class and style attributes (which affect the height
+ // of the content). Trigger a recalculation of the sticky kit when
+ // a mutation occurs so the sidebar will
+ $(document).ready(function() {
+ if ($('#layout_content').length === 0) {
+ return;
+ }
+ var target = $('#layout_content').get(0),
+ stickyObserver = new window.MutationObserver(function() {
+ window.requestAnimationFrame(function() {
+ $(document.body).trigger('sticky_kit:recalc');
+ });
+ });
+ stickyObserver.observe(target, {
+ attributes: true,
+ attributeFilter: ['style', 'class'],
+ characterData: true,
+ childList: true,
+ subtree: true
+ });
+ });
+} else {
+ // Stores document height (we will need this to check for changes)
+ var doc_height;
+
+ function heightChangeHandler() {
+ var curr_height = $(document).height();
+ if (doc_height !== curr_height) {
+ doc_height = curr_height;
+ $(document.body).trigger('sticky_kit:recalc');
+ }
+ }
+
+ STUDIP.domReady(() => {
+ doc_height = $(document).height();
+ });
+
+ // Recalculcate positions on ajax and img load events.
+ // Inside the handlers the current document height is compared
+ // to the previous height before the event occured so recalculation
+ // only happens on actual changes
+ $(document).on('ajaxComplete', heightChangeHandler);
+ $(document).on('load', '#layout_content img', heightChangeHandler);
+
+ // Specialized handler to trigger recalculation when wysiwyg
+ // instances are created.
+ $(document).on('load.wysiwyg', 'textarea', function() {
+ $(document.body).trigger('sticky_kit:recalc');
+ });
+}
+
+// Engage
+STUDIP.domReady(() => {
+ STUDIP.Sidebar.setSticky();
+ STUDIP.Sidebar.checkActiveLineHeight();
+});
diff --git a/resources/assets/javascripts/bootstrap/skip_links.js b/resources/assets/javascripts/bootstrap/skip_links.js
new file mode 100644
index 0000000..cdd178f
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/skip_links.js
@@ -0,0 +1,8 @@
+STUDIP.domReady(STUDIP.SkipLinks.initialize);
+
+jQuery(document).on('keyup', STUDIP.SkipLinks.showSkipLinkNavigation);
+jQuery(document).on('click', function(event) {
+ if (!jQuery(event.target).is('#skip_link_navigation a')) {
+ STUDIP.SkipLinks.moveSkipLinkNavigationOut();
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/smiley.js b/resources/assets/javascripts/bootstrap/smiley.js
new file mode 100644
index 0000000..4315dd9
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/smiley.js
@@ -0,0 +1,19 @@
+$(document).on('click', '.smiley-toggle', function(event) {
+ var element = $(this);
+
+ element.prop('disabled', true).addClass('ajax');
+
+ $.getJSON(element.attr('href')).then(function(json) {
+ var container = $(element)
+ .closest('.ui-dialog-content,#layout_content')
+ .first();
+ $('.messagebox', container).remove();
+ container.prepend(json.message);
+
+ element
+ .toggleClass('favorite', json.state)
+ .removeClass('ajax')
+ .prop('disabled', false);
+ });
+ event.preventDefault();
+});
diff --git a/resources/assets/javascripts/bootstrap/smiley_picker.js b/resources/assets/javascripts/bootstrap/smiley_picker.js
new file mode 100644
index 0000000..0c5ea35
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/smiley_picker.js
@@ -0,0 +1,7 @@
+// Navigation: Load any url in this very same dialog
+$(document).on('click', '.smiley-picker .navigation a', STUDIP.SmileyPicker.handleNavigationClick);
+
+// Smiley:
+// Execute select handler with selected smiley's code
+// (typically adds the code to a certain textarea)
+$(document).on('click', '.smiley-picker .smiley', STUDIP.SmileyPicker.handleSmileyClick);
diff --git a/resources/assets/javascripts/bootstrap/startpage.js b/resources/assets/javascripts/bootstrap/startpage.js
new file mode 100644
index 0000000..2af7955
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/startpage.js
@@ -0,0 +1,34 @@
+STUDIP.domReady(() => {
+ if ($('html').is(':not(.responsive-display)')) {
+ STUDIP.startpage.init();
+ }
+});
+
+// Add handler for "read all" on news widget
+$(document).on('click', '#start-index a[href*="newswidget/read_all"]', function(event) {
+ var icon = $(this),
+ url = icon.attr('href'),
+ widget = icon.closest('.studip-widget');
+
+ icon.prop('disabled', true).addClass('ajaxing');
+
+ $.getJSON(url).then(function(response) {
+ if (response) {
+ $('article.new', widget).removeClass('new');
+ $('.news-comments-unread', widget)
+ .removeClass('news-comments-unread')
+ .removeAttr('title');
+
+ // It is approriate to use attr() to modify data here since
+ // the attribute's value is displayed via css, thus it needs
+ // to be actually in the DOM.
+ $('#nav_start [data-badge]')
+ .attr('data-badge', 0)
+ .trigger('badgechange');
+
+ icon.remove();
+ }
+ });
+
+ event.preventDefault();
+});
diff --git a/resources/assets/javascripts/bootstrap/statusgroups.js b/resources/assets/javascripts/bootstrap/statusgroups.js
new file mode 100644
index 0000000..e6b6ed4
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/statusgroups.js
@@ -0,0 +1,73 @@
+/*jslint esversion: 6*/
+STUDIP.ready(function() {
+ STUDIP.Statusgroups.ajax_endpoint = $('meta[name="statusgroups-ajax-movable-endpoint"]').attr('content');
+ STUDIP.Statusgroups.apply();
+
+ $('a.get-group-members').on('click', function() {
+ var dataEl = $('article#group-members-' + $(this).data('group-id')),
+ url;
+ if ($.trim(dataEl.html()).length === 0) {
+ url = $(this).data('get-members-url');
+
+ dataEl.html(
+ $('<img>').attr({
+ width: 32,
+ height: 32,
+ src: STUDIP.ASSETS_URL + 'images/ajax-indicator-black.svg'
+ })
+ );
+
+ $.get(url).done(function(html) {
+ dataEl.html(html);
+ $(document).trigger('refresh-handlers');
+ });
+ }
+ });
+
+ var handle = '.sg-sortable-handle';
+ // Check for touch device
+ handle = '.sg-sortable-handle';
+ if (window.matchMedia('(hover: none)').matches) {
+ $('.course-statusgroups[data-sortable]').addClass('by-touch');
+ }
+
+ var index_before = null;
+ $('.course-statusgroups[data-sortable]')
+ .sortable({
+ axis: 'y',
+ containment: 'parent',
+ forcePlaceholderSize: true,
+ handle: handle,
+ items: '> .draggable',
+ placeholder: 'sortable-placeholder',
+ start: function(event, ui) {
+ index_before = ui.item.index();
+ },
+ stop: function(event, ui) {
+ if (index_before === ui.item.index()) {
+ return;
+ }
+
+ var url = $(this).data('sortable');
+ $.post(url, {
+ id: ui.item.attr('id'),
+ index: ui.item.index() - 1
+ });
+ }
+ });
+});
+
+STUDIP.ready(function() {
+ $('.nestable').each(function() {
+ $(this).nestable({
+ rootClass: 'nestable',
+ maxDepth: $(this).data('max-depth') || 5
+ });
+ });
+});
+
+$(document).on('submit', '#order_form', function() {
+ let structure = $('.nestable').nestable('serialize');
+ let json_data = JSON.stringify(structure);
+ $('#ordering').val(json_data);
+});
diff --git a/resources/assets/javascripts/bootstrap/studip_helper_attributes.js b/resources/assets/javascripts/bootstrap/studip_helper_attributes.js
new file mode 100644
index 0000000..a433bb2
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/studip_helper_attributes.js
@@ -0,0 +1,229 @@
+import { $gettext } from '../lib/gettext.js';
+
+/**
+ * This file provides a set of global handlers.
+ */
+
+var proxy_elements_selector = ':checkbox[data-proxyfor], :radio[data-proxyfor]';
+var proxied_elements_selector = ':checkbox[data-proxiedby], :radio[data-proxiedby]';
+
+function connectProxyAndProxied(event) {
+ $(proxy_elements_selector).each(function () {
+ const proxy = $(this);
+ const proxyId = proxy.uniqueId().attr('id');
+ const proxied = proxy.data('proxyfor');
+ // The following seems like a hack but works perfectly fine.
+ $(proxied).each(function () {
+ const proxiedBy = ($(this).attr('data-proxiedby') || '').split(',').filter(a => a.length > 0);
+ proxiedBy.push(`#${proxyId}`);
+
+ $(this)
+ .attr('data-proxiedby', proxiedBy.join(','))
+ .data('proxiedby', `#${proxyId}`);
+ });
+ }).trigger('update.proxy');
+}
+
+// Use a checkbox as a proxy for a set of other checkboxes. Define
+// proxied elements by a css selector in attribute "data-proxyfor".
+$(document).on('change', proxy_elements_selector, function (event, force) {
+ // Detect if event was triggered natively (triggered events have no
+ // originalEvent)
+ if (event.originalEvent !== undefined || !!force) {
+ const proxied = $(this).data('proxyfor');
+ $(proxied)
+ .filter(':not(:disabled)')
+ .prop('checked', this.checked)
+ .prop('indeterminate', false)
+ .each((index, element) => {
+ const proxiedBy = $(element).attr('data-proxiedby');
+ $(proxiedBy)
+ .filter((idx, item) => item !== this)
+ .trigger('update.proxy');
+ })
+ .filter('[data-proxyfor]')
+ .trigger('change', [true]);
+ }
+}).on('update.proxy', proxy_elements_selector, function () {
+ const proxied = $(this).data('proxyfor');
+ const $proxied = $(proxied).filter(':not(:disabled)');
+ const $checked = $proxied.filter(':checked');
+ const $indeterminate = $proxied.filter(function () {
+ return $(this).prop('indeterminate');
+ });
+ $(this).prop('checked', $proxied.length > 0 && $proxied.length === $checked.length);
+ $(this).prop(
+ 'indeterminate',
+ ($checked.length > 0 && $checked.length < $proxied.length) || $indeterminate.length > 0
+ );
+ $(this).trigger('change');
+}).on('change', proxied_elements_selector, function () {
+ //In case of radio buttons in a group that are deselected,
+ //we must trigger the update.proxy event for each radio
+ //button in the group, if the proxy is another element
+ //than the proxy for "this" element.
+ if ($(this).is(':radio')) {
+ var proxy = $(this).data('proxiedby');
+ var name = $(this).attr('name');
+ var radio_button_group = $(`:radio[name="${name}"]`);
+ $(radio_button_group).each(function () {
+ var button_proxy = $(this).data('proxiedby');
+ if (button_proxy != proxy) {
+ $(button_proxy).trigger('update.proxy');
+ }
+ });
+ } else {
+ const proxy = $(this).attr('data-proxiedby');
+ $(proxy).trigger('update.proxy');
+ }
+});
+
+STUDIP.ready(connectProxyAndProxied);
+$(document).on('refresh-handlers', connectProxyAndProxied);
+
+// Use a checkbox or radiobox as a toggle switch for the disabled attribute of
+// another set of elements. Define set of elements to disable/enable if item is
+// neither :checked nor :indeterminate by a css selector in attribute
+// "data-activates" / "deactivates".
+$(document).on('change', '[data-activates],[data-deactivates]', function() {
+ if (!$(this).is(':checkbox,:radio')) {
+ return;
+ }
+
+ ['activates', 'deactivates'].forEach((type) => {
+ var selector = $(this).data(type);
+ if (selector === undefined || $(this).prop('disabled')) {
+ return;
+ }
+
+ var state = $(this).prop('checked') || $(this).prop('indeterminate') || false;
+ $(selector).each(function() {
+ var condition = $(this).data(`${type}Condition`),
+ toggle = state && (!condition || $(condition).length > 0);
+ $(this)
+ .attr('disabled', type === 'activates' ? !toggle : toggle)
+ .trigger('update.proxy');
+ });
+ });
+});
+
+STUDIP.ready((event) => {
+ $('[data-activates],[data-deactivates]', event.target).trigger('change');
+});
+
+// Use a select as a toggle switch for the disabled attribute of another
+// element. Define element to disable if select has a value different from
+// an empty string by a css selector in attribute "data-activates".
+$(document).on('change update.proxy', 'select[data-activates]', function() {
+ var activates = $(this).data('activates'),
+ disabled = $(this).is(':disabled') || $(this).val().length === 0;
+ $(activates).attr('disabled', disabled);
+});
+
+STUDIP.ready((event) => {
+ $('select[data-activates]', event.target).trigger('change');
+});
+
+// Enable the user to set the checked state on a subset of related
+// checkboxes by clicking the first checkbox of the subset and then
+// clicking the last checkbox of the subset while holding down the shift
+// key, thus toggling all the checkboxes in between.
+// This only works if the first and last checkbox of the subset are set
+// to the same state.
+var last_element = null;
+$(document).on('click', '[data-shiftcheck] :checkbox', function(event) {
+ if (!event.originalEvent || last_element === event.target) {
+ return;
+ }
+
+ if (last_element !== null && event.shiftKey) {
+ var $this = $(event.target),
+ $form = $this.closest('form'),
+ name = $this.attr('name'),
+ state = $this.prop('checked'),
+ $last = $(last_element),
+ children,
+ idx0,
+ idx1,
+ tmp;
+
+ if ($form.is($last.closest('form')) && name === $last.attr('name') && state === $last.prop('checked')) {
+ children = $form.find(':checkbox[name="' + name + '"]:not(:disabled)');
+ idx0 = children.index(event.target);
+ idx1 = children.index(last_element);
+ if (idx0 > idx1) {
+ tmp = idx0;
+ idx0 = idx1;
+ idx1 = tmp;
+ }
+ children.slice(idx0, idx1).prop('checked', state);
+ }
+ }
+
+ last_element = event.target;
+});
+
+// Lets the user confirm a specific action (submit or click event).
+function confirmation_handler(event) {
+ if (!event.isDefaultPrevented()) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ var element = $(event.currentTarget).closest('[data-confirm]'),
+ question =
+ element.data().confirm ||
+ element.attr('title') ||
+ element.find('[title]:first').attr('title') ||
+ $gettext('Wollen Sie die Aktion wirklich ausführen?');
+
+ STUDIP.Dialog.confirm(question).done(function() {
+ var content = element.data().confirm;
+
+ // We need to trigger the native event because for
+ // some reason, jQuery's .trigger() won't always
+ // work. Thus the data-confirm attribute will be removed
+ // so that the original event can be executed
+ element
+ .removeAttr('data-confirm')
+ .get(0)
+ [event.type]();
+
+ // Reapply the data-confirm attribute
+ window.setTimeout(function() {
+ element.attr('data-confirm', content);
+ }, 0);
+ });
+ }
+}
+$(document).on(
+ 'click',
+ 'a[data-confirm],input[data-confirm],button[data-confirm],img[data-confirm]',
+ confirmation_handler
+);
+$(document).on('submit', 'form[data-confirm]', confirmation_handler);
+
+// Ensures an element has the same value as another element.
+$(document).on('change', 'input[data-must-equal]', function() {
+ var value = $(this).val(),
+ rel = $(this).data().mustEqual,
+ other = $(rel).val(),
+ labels = $.map([this, rel], function(element) {
+ var label = $(element)
+ .closest('label')
+ .text();
+ label = label || $('label[for="' + $(element).attr('id') + '"]').text();
+ return $.trim(label.split(':')[0]);
+ }),
+ error_message = $gettext('Die beiden Werte "$1" und "$2" stimmen nicht überein. '),
+ matches = error_message.match(/\$\d/g);
+
+ $.each(matches, function(i) {
+ error_message = error_message.replace(this, labels[i]);
+ });
+
+ if (value !== other) {
+ this.setCustomValidity(error_message);
+ } else {
+ this.setCustomValidity('');
+ }
+});
diff --git a/resources/assets/javascripts/bootstrap/subcourses.js b/resources/assets/javascripts/bootstrap/subcourses.js
new file mode 100644
index 0000000..cf777da
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/subcourses.js
@@ -0,0 +1,73 @@
+// Open action menu on click on the icon
+$(document).on('click', '.toggle-subcourses', function(event) {
+ var row = $(this).closest('tr');
+
+ if ($(this).hasClass('open')) {
+ $(this).removeClass('open');
+ $(this)
+ .children('.icon-shape-remove')
+ .addClass('hidden-js');
+ $(this)
+ .children('.icon-shape-add')
+ .removeClass('hidden-js');
+ $(
+ 'tr.subcourse-' +
+ $(this)
+ .closest('tr')
+ .data('course-id')
+ ).addClass('hidden-js');
+ row.removeClass('has-subcourses');
+ } else if ($(this).hasClass('loaded')) {
+ $(this).addClass('open');
+ $(this)
+ .children('.icon-shape-add')
+ .addClass('hidden-js');
+ $(this)
+ .children('.icon-shape-remove')
+ .removeClass('hidden-js');
+ $(
+ 'tr.subcourse-' +
+ $(this)
+ .closest('tr')
+ .data('course-id')
+ ).removeClass('hidden-js');
+ row.addClass('has-subcourses');
+ } else {
+ $.ajax($(this).data('get-subcourses-url'), {
+ beforeSend: function(xhr, settings) {
+ $('<div class="loading" style="padding: 10px">')
+ .html(
+ $('<img>')
+ .attr('src', STUDIP.ASSETS_URL + 'images/ajax-indicator-black.svg')
+ .css('width', '24')
+ .css('height', '24')
+ )
+ .insertAfter(row);
+ },
+ success: function(data, status, xhr) {
+ $(row)
+ .siblings('div.loading')
+ .remove();
+ $(data).insertAfter(row);
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ alert('Status: ' + textStatus + '\nError: ' + errorThrown);
+ }
+ });
+ $(this)
+ .addClass('loaded')
+ .addClass('open');
+ $(this)
+ .children('.icon-shape-add')
+ .addClass('hidden-js');
+ $(this)
+ .children('.icon-shape-remove')
+ .removeClass('hidden-js');
+ row.addClass('has-subcourses');
+ }
+
+ // Stop event so the following close event will not be fired
+ event.stopPropagation();
+
+ return false;
+});
diff --git a/resources/assets/javascripts/bootstrap/tabbable_widget.js b/resources/assets/javascripts/bootstrap/tabbable_widget.js
new file mode 100644
index 0000000..c89eb16
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/tabbable_widget.js
@@ -0,0 +1,31 @@
+$(document).on('click', '.tabbable-widget > nav > a', function(event) {
+ var selector = $(this).attr('href');
+ $(this)
+ .addClass('active')
+ .siblings()
+ .removeClass('active');
+ $(selector)
+ .addClass('active')
+ .siblings('section')
+ .removeClass('active')
+ .end();
+
+ // Delay resetting of scroll top until browser is no longer busy
+ // (otherwise the scrolled element will not reset - at least in FF)
+ setTimeout(function() {
+ $(selector).scrollTop(0);
+ }, 0);
+
+ if (history.pushState) {
+ history.pushState(null, null, selector);
+ }
+
+ event.preventDefault();
+});
+STUDIP.domReady(() => {
+ if (!location.hash) {
+ return;
+ }
+
+ $('.tabbable-widget > nav > a[href="' + location.hash + '"]').click();
+});
diff --git a/resources/assets/javascripts/bootstrap/tables.js b/resources/assets/javascripts/bootstrap/tables.js
new file mode 100644
index 0000000..f4afc52
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/tables.js
@@ -0,0 +1,42 @@
+/*jslint esversion: 6*/
+
+STUDIP.domReady(function() {
+ if (window.MutationObserver !== undefined) {
+ var observer = new window.MutationObserver(function(mutations) {
+ mutations.forEach(function(mutation) {
+ if (mutation.attributeName === 'class') {
+ if (
+ $(mutation.target)
+ .attr('class')
+ .indexOf('open') !== -1
+ ) {
+ $(mutation.target)
+ .next()
+ .find('td')
+ .slideDown()
+ .find('.detailscontainer')
+ .hide()
+ .slideDown();
+ } else {
+ $(mutation.target)
+ .next()
+ .show()
+ .find('td')
+ .slideUp()
+ .find('.detailscontainer')
+ .slideUp();
+ }
+ }
+ });
+ });
+ $('table.withdetails > tbody > tr:not(.details)').each(function(index, element) {
+ observer.observe(element, { attributes: true });
+ });
+ }
+});
+
+STUDIP.ready(function (event) {
+ $('table.sortable-table:not(.tablesorter)', event.target).each((index, element) => {
+ STUDIP.Table.enhanceSortableTable(element);
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/tfa.js b/resources/assets/javascripts/bootstrap/tfa.js
new file mode 100644
index 0000000..4880825
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/tfa.js
@@ -0,0 +1,42 @@
+$(document).on('keyup', '.tfa-code-input input', function (event) {
+ this.value = this.value.replace(/^D/g, '');
+ if (event.keyCode === 8) {
+ $(this).prev('input').focus();
+ if (this.value.length === 0) {
+ $(this).prev('input').val('');
+ }
+ } else if (event.keyCode === 46) {
+ $(this).nextAll('input:not(:hidden)').each(function () {
+ $(this).prev().val(this.value);
+ this.value = '';
+ });
+ } else if (event.keyCode === 37) {
+ $(this).prev('input').focus();
+ } else if (event.keyCode === 39) {
+ $(this).next('input').focus();
+ } else if (event.key >= '0' && event.key <= '9') {
+ this.value = event.key;
+ $(this).next('input').focus();
+ } else if (event.keyCode === 36) {
+ $(this).parent().find('input:not(:hidden):first').focus();
+ }
+}).on('keydown', '.tfa-code-input', function (event) {
+ if (event.key >= '0' && event.key <= '9') {
+ this.value = '';
+ event.preventDefault();
+ }
+}).on('paste', '.tfa-code-input input', function (event) {
+ this.value = '';
+ $(this).one('input', function () {
+ const pastedValue = this.value.trim();
+ if (!pastedValue.match(/^\d{6}$/)) {
+ return;
+ }
+
+ const container = $(this).closest('.tfa-code-input');
+ for (let i = 0; i < 6; i += 1) {
+ $(`input:eq(${i})`, container).val(pastedValue.substr(i, 1))
+ }
+ $('input:last', container).focus();
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/tooltip.js b/resources/assets/javascripts/bootstrap/tooltip.js
new file mode 100644
index 0000000..8f97d6c
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/tooltip.js
@@ -0,0 +1,68 @@
+/*jslint esversion: 6*/
+
+// Attach global hover handler for tooltips.
+// Applies to all elements having a "data-tooltip" attribute.
+// Tooltip may be provided in the data-attribute itself or by
+// defining a title attribute. The latter is prefered due to
+// the obvious accessibility issues.
+
+var timeout = null;
+
+STUDIP.Tooltip.threshold = 6;
+
+$(document).on('mouseenter mouseleave', '[data-tooltip],.tooltip:has(.tooltip-content)', function(event) {
+ let data = $(this).data();
+
+ const visible = event.type === 'mouseenter';
+ const offset = $(this).offset();
+ const x = offset.left + $(this).outerWidth(true) / 2;
+ const y = offset.top;
+ const delay = data.hasOwnProperty('tooltipDelay') ? data.tooltipDelay : 300;
+
+ let content;
+ let tooltip;
+
+ if (!data.tooltipObject) {
+ // If tooltip has not yet been created (first hover), obtain it's
+ // contents and create the actual tooltip object.
+ if (!data.tooltip || !$.isPlainObject(data.tooltip)) {
+ content = $('<div/>').text(data.tooltip || $(this).attr('title')).html();
+ } else if (data.tooltip.hasOwnProperty('html')) {
+ content = data.tooltip.html;
+ } else if (data.tooltip.hasOwnProperty('text')) {
+ content = data.tooltip.text;
+ } else {
+ throw "Invalid content for tooltip via data";
+ }
+ if (!content) {
+ content = $(this).find('.tooltip-content').remove().html();
+ }
+ $(this).attr('title', '');
+ $(this).attr('data-tooltip', content);
+
+ tooltip = new STUDIP.Tooltip(x, y, content);
+
+ data.tooltipObject = tooltip;
+
+ $(this).on('remove', function() {
+ tooltip.remove();
+ });
+ } else if (visible) {
+ // If tooltip has already been created, update it's position.
+ // This is neccessary if the surrounding content is scrollable AND has
+ // been scrolled. Otherwise the tooltip would appear at it's previous
+ // and now wrong location.
+ data.tooltipObject.position(x, y);
+ }
+
+ if (visible) {
+ $('.studip-tooltip').not(data.tooltipObject).hide();
+ data.tooltipObject.show();
+ } else {
+ timeout = setTimeout(() => data.tooltipObject.hide(), delay);
+ }
+}).on('mouseenter', '.studip-tooltip', () => {
+ clearTimeout(timeout);
+}).on('mouseleave', '.studip-tooltip', function() {
+ $(this).hide();
+});
diff --git a/resources/assets/javascripts/bootstrap/tour.js b/resources/assets/javascripts/bootstrap/tour.js
new file mode 100644
index 0000000..32be5e4
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/tour.js
@@ -0,0 +1,52 @@
+/* ------------------------------------------------------------------------
+ * 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
+ *
+ */
+
+STUDIP.domReady(() => {
+ //STUDIP.Tour.started = false;
+ STUDIP.Tour.pending_ajax_request = false;
+
+ jQuery(document).keyup(function(event) {
+ if (STUDIP.Tour.started && event.keyCode === 37 && jQuery('#tour_prev').is(':visible')) {
+ STUDIP.Tour.prev();
+ } else if (STUDIP.Tour.started && event.keyCode === 39 && jQuery('#tour_next').is(':visible')) {
+ STUDIP.Tour.next();
+ } else if (STUDIP.Tour.started && event.keyCode === 27 && jQuery('#tour_end').is(':visible')) {
+ STUDIP.Tour.destroy();
+ }
+ });
+
+ jQuery(document).on('keyright', function(event) {
+ STUDIP.Tour.prev();
+ });
+ jQuery(document).on('click', '.tour_link', function(event) {
+ event.preventDefault();
+ STUDIP.Tour.init(jQuery(this).attr('id'), 1);
+ });
+
+ jQuery(document).on('click', '#tour_next', function() {
+ STUDIP.Tour.next();
+ });
+
+ jQuery(document).on('click', '#tour_prev', function() {
+ STUDIP.Tour.prev();
+ });
+
+ jQuery(document).on('click', '#tour_end', function() {
+ STUDIP.Tour.destroy();
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/vue.js b/resources/assets/javascripts/bootstrap/vue.js
new file mode 100644
index 0000000..550f097
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/vue.js
@@ -0,0 +1,59 @@
+/**
+ * The following block of code is used to automatically register your
+ * Vue components. It will recursively scan this directory for the Vue
+ * components and automatically register them with their "basename".
+ *
+ * Eg. ./components/ExampleComponent.vue -> <example-component></example-component>
+ */
+STUDIP.ready(() => {
+ $('[data-vue-app]').each(function () {
+ if ($(this).is('[data-vue-app-created]')) {
+ return;
+ }
+
+ const config = Object.assign({}, {
+ id: false,
+ components: [],
+ store: false
+ }, $(this).data().vueApp);
+
+ let data = {};
+ if (config.id && window.STUDIP.AppData && window.STUDIP.AppData.hasOwnProperty(config.id)) {
+ data = window.STUDIP.AppData[config.id];
+ }
+
+ let components = {};
+ config.components.forEach(component => {
+ components[component] = () => import(`../../../vue/components/${component}.vue`);
+ });
+
+ STUDIP.Vue.load().then(async ({createApp, store}) => {
+ let vm;
+ if (config.store) {
+ const storeConfig = await import(`../../../vue/store/${config.store}.js`);
+ console.log('store', storeConfig.default);
+
+ store.registerModule(config.id, storeConfig.default, {root: true});
+
+ Object.keys(data).forEach(command => {
+ store.commit(`${config.id}/${command}`, data[command]);
+ });
+ vm = createApp({
+ components,
+ ...mapGetters()
+ });
+ } else {
+ vm = createApp({data, components});
+ }
+ // import myCoursesStore from '../stores/MyCoursesStore.js';
+ //
+ // myCoursesStore.namespaced = true;
+ //
+ // store.registerModule('my-courses', myCoursesStore);
+
+ vm.$mount(this);
+ });
+
+ $(this).attr('data-vue-app-created', '');
+ });
+});
diff --git a/resources/assets/javascripts/bootstrap/wiki.js b/resources/assets/javascripts/bootstrap/wiki.js
new file mode 100644
index 0000000..6590a49
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/wiki.js
@@ -0,0 +1,146 @@
+/**
+ * This file contains all wiki related javascript.
+ *
+ * For now this is the "submit and edit" functionality via ajax.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @copyright Stud.IP Core Group
+ * @license GPL2 or any later version
+ * @since Stud.IP 3.3
+ */
+
+$(document).on('click', '#wiki button[name="submit-and-edit"]', function(event) {
+ var form = $(this).closest('form'),
+ data = {},
+ form_data,
+ i,
+ id,
+ wysiwyg_editor = false;
+
+ if (STUDIP.editor_enabled) {
+ id = $('textarea[name="body"]', form).attr('id');
+ wysiwyg_editor = CKEDITOR.instances[id];
+ wysiwyg_editor.setData(STUDIP.wysiwyg.markAsHtml(wysiwyg_editor.getData()));
+ wysiwyg_editor.updateElement();
+ }
+
+ form_data = form.serializeArray();
+
+ // Show ajax overlay to indicate activity (and prevent buttons to be
+ // clicked again)
+ STUDIP.Overlay.show(true, form.css('position', 'relative'));
+
+ // Include this button into form's data
+ form_data.push({
+ name: $(this).attr('name'),
+ value: true
+ });
+
+ // Transform data into an easier accessible format
+ for (i = 0; i < form_data.length; i += 1) {
+ data[form_data[i].name] = form_data[i].value;
+ }
+
+ // Check version
+ $.getJSON(
+ STUDIP.URLHelper.getURL('dispatch.php/wiki/version_check/' + data.version, {
+ keyword: data.wiki
+ })
+ )
+ .then(function(response, status, jqxhr) {
+ var error = jqxhr.getResponseHeader('X-Studip-Error'),
+ to_confirm = jqxhr.getResponseHeader('X-Studip-Confirm'),
+ confirmed = false;
+ // Unrecoverable error
+ if (response === false) {
+ window.alert(error);
+ return;
+ }
+ // Saving needs confirmation (newer version available?)
+ if (response === null) {
+ confirmed = window.confirm(error + '\n\n' + to_confirm);
+ } else {
+ confirmed = true;
+ }
+ // Ready to save
+ if (confirmed) {
+ $.ajax({
+ type: (form.attr('method') || 'GET').toUpperCase(),
+ url: STUDIP.URLHelper.getURL('dispatch.php/wiki/store/' + data.version),
+ data: {
+ keyword: data.wiki,
+ body: data.body
+ },
+ dataType: 'json'
+ }).then(function(response) {
+ var textarea = $('textarea[name=body]', form);
+
+ // Update header info containing version and author
+ $(form)
+ .closest('table')
+ .prev('table')
+ .find('td:last-child')
+ .html(response.zusatz);
+
+ // Update version field
+ $('input[type=hidden][name=version]', form).val(response.version);
+
+ if (wysiwyg_editor) {
+ wysiwyg_editor.setData(response.body);
+ } else {
+ // Store current selection/caret position
+ textarea.storeSelection();
+
+ // Update textarea, restore selection/caret position
+ textarea.val(response.body);
+ textarea.prop('defaultValue', textarea.val());
+ textarea.restoreSelection();
+ textarea.change();
+ textarea.focus();
+ }
+
+ // Remove messages (and display new messages, if any)
+ $('#layout_content .messagebox').remove();
+ if (response.messages !== false) {
+ $(response.messages).prependTo('#layout_content');
+ }
+ });
+ }
+ })
+ .always(function() {
+ // Always hide overlay when ajax request is complete
+ STUDIP.Overlay.hide();
+ });
+
+ event.preventDefault();
+});
+
+$(document).on('keyup change', '#wiki textarea[name=body]', function() {
+ // Disable "save and edit" button if text was not changed
+ $('#wiki button[name="submit-and-edit"]').prop('disabled', this.value === this.defaultValue);
+});
+
+STUDIP.domReady(() => {
+ if (!STUDIP.editor_enabled) {
+ // Trigger above disable mechanism only when not using wysiwyg
+ $('#wiki textarea[name=body]').change();
+ } else {
+ $(document).off('keyup change', '#wiki textarea[name=body]');
+ }
+});
+
+$(document).on('change', '#wiki-config .global-permissions :checkbox', function () {
+ if ($(this).is(':checked')) {
+ return;
+ }
+
+ $('#wiki-config .read-permissions [data-activates],[data-deactivates]').filter(':checked').change();
+}).on('change', '#wiki-config .read-permissions :radio', function () {
+ $('#wiki-config .edit-permissions:has(:radio[disabled]:checked) :radio:not([disabled]):first').prop('checked', true);
+});
+
+$(document).on('click', '.wiki-index-more', function (ev) {
+ ev.preventDefault();
+ $(this).parent().toggle();
+ $(this).parent().nextAll('li').toggle();
+});
diff --git a/resources/assets/javascripts/bootstrap/wysiwyg.js b/resources/assets/javascripts/bootstrap/wysiwyg.js
new file mode 100644
index 0000000..cb7fbf1
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/wysiwyg.js
@@ -0,0 +1,61 @@
+STUDIP.domReady(() => {
+ if (STUDIP.editor_enabled) {
+ // replace areas visible on page load
+ replaceVisibleTextareas();
+
+ // replace areas that are created or shown after page load
+ // remove editors that become hidden after page load
+ // show, hide and create do not raise an event, use interval timer
+ setInterval(replaceVisibleTextareas, 300);
+ }
+
+ // when attaching to hidden textareas, or textareas who's parents are
+ // hidden, the editor does not function properly; therefore attach to
+ // visible textareas only
+ function replaceVisibleTextareas() {
+ $('textarea.wysiwyg').each(function() {
+ var editor = CKEDITOR.dom.element.get(this).getEditor();
+ if (!editor && $(this).is(':visible')) {
+ STUDIP.wysiwyg.replace(this);
+ } else if (editor && editor.container && $(editor.container.$).is(':hidden')) {
+ editor.destroy(true);
+ }
+ });
+ }
+
+ // customize existing dialog windows
+ CKEDITOR.on('dialogDefinition', function(ev) {
+ var dialogName = ev.data.name,
+ dialogDefinition = ev.data.definition;
+
+ if (dialogName == 'table') {
+ var infoTab = dialogDefinition.getContents('info');
+ infoTab.get('txtBorder')['default'] = '';
+ infoTab.get('txtWidth')['default'] = '';
+ infoTab.get('txtCellSpace')['default'] = '';
+ infoTab.get('txtCellPad')['default'] = '';
+
+ var advancedTab = dialogDefinition.getContents('advanced');
+ advancedTab.get('advCSSClasses')['default'] = 'content';
+ }
+ });
+});
+
+// Hotfix for Dialogs
+
+$.widget( "ui.dialog", $.ui.dialog, {
+
+ // jQuery UI v1.11+ fix to accommodate CKEditor (and other iframed content) inside a dialog
+ // @see http://bugs.jqueryui.com/ticket/9087
+ // @see http://dev.ckeditor.com/ticket/10269
+
+ _allowInteraction: function( event ) {
+ return this._super( event ) ||
+
+ // addresses general interaction issues with iframes inside a dialog
+ event.target.ownerDocument !== this.document[ 0 ] ||
+
+ // addresses interaction issues with CKEditor's dialog windows and iframe-based dropdowns in IE
+ !!$( event.target ).closest( ".cke_dialog, .cke_dialog_background_cover, .cke" ).length;
+ }
+});
diff --git a/resources/assets/javascripts/chunk-loader.js b/resources/assets/javascripts/chunk-loader.js
new file mode 100644
index 0000000..bdecb5d
--- /dev/null
+++ b/resources/assets/javascripts/chunk-loader.js
@@ -0,0 +1,88 @@
+/*jslint esversion: 6*/
+STUDIP.loadScript = function (script_name) {
+ return new Promise(function (resolve, reject) {
+ let script = document.createElement('script');
+ script.src = `${STUDIP.ASSETS_URL}${script_name}`;
+ script.onload = resolve;
+ script.onerror = reject;
+ document.head.appendChild(script);
+ });
+};
+
+STUDIP.loadChunk = (function () {
+ var mathjax_promise = null;
+
+ return function (chunk) {
+ var promise = null;
+ switch (chunk) {
+
+ case 'code-highlight':
+ promise = import(
+ /* webpackChunkName: "code-highlight" */
+ './chunks/code-highlight'
+ ).then(({default: hljs}) => {
+ return hljs;
+ });
+ break;
+
+ case 'chartist':
+ promise = import(
+ /* webpackChunkName: "chartist" */
+ './chunks/chartist'
+ ).then(({ default: Chartist }) => Chartist);
+ break;
+
+ case 'fullcalendar':
+ promise = import(
+ /* webpackChunkName: "fullcalendar" */
+ './chunks/fullcalendar'
+ );
+ break;
+
+ case 'tablesorter':
+ promise = import(
+ /* webpackChunkName: "tablesorter" */
+ './chunks/tablesorter'
+ );
+ break;
+
+ case 'mathjax':
+ if (mathjax_promise === null) {
+ mathjax_promise = STUDIP.loadScript(
+ 'javascripts/mathjax/MathJax.js?config=TeX-AMS_HTML,default'
+ ).then(() => {
+ (function (origPrint) {
+ window.print = function () {
+ MathJax.Hub.Queue(
+ ['Delay', MathJax.Callback, 700],
+ origPrint
+ );
+ };
+ })(window.print);
+
+ mathjax_loaded = true;
+
+ return MathJax;
+ }).catch(() => {
+ mathjax_loaded = false;
+ });
+ }
+ promise = mathjax_promise;
+ break;
+
+ case 'vue':
+ promise = import(
+ /* webpackChunkName: "vue.js" */
+ './chunks/vue'
+ );
+ break;
+
+ default:
+ promise = Promise.reject('Unknown chunk');
+ }
+
+ return promise.catch((error) => {
+ console.error(`Could not load chunk ${chunk}`, error);
+ });
+ };
+}());
diff --git a/resources/assets/javascripts/chunks/chartist.js b/resources/assets/javascripts/chunks/chartist.js
new file mode 100644
index 0000000..05a01bf
--- /dev/null
+++ b/resources/assets/javascripts/chunks/chartist.js
@@ -0,0 +1,4 @@
+import Chartist from "chartist"
+import "chartist/dist/chartist.css"
+
+export default Chartist
diff --git a/resources/assets/javascripts/chunks/code-highlight.js b/resources/assets/javascripts/chunks/code-highlight.js
new file mode 100644
index 0000000..9428e8e
--- /dev/null
+++ b/resources/assets/javascripts/chunks/code-highlight.js
@@ -0,0 +1,18 @@
+import "highlight.js/styles/tomorrow.css"
+
+import hljs from "highlight.js/lib/core.js"
+
+hljs.registerLanguage('cpp', require('highlight.js/lib/languages/cpp'))
+hljs.registerLanguage('css', require('highlight.js/lib/languages/css'))
+hljs.registerLanguage('diff', require('highlight.js/lib/languages/diff'))
+hljs.registerLanguage('java', require('highlight.js/lib/languages/java'))
+hljs.registerLanguage('javascript', require('highlight.js/lib/languages/javascript'))
+hljs.registerLanguage('json', require('highlight.js/lib/languages/json'))
+hljs.registerLanguage('php', require('highlight.js/lib/languages/php'))
+hljs.registerLanguage('python', require('highlight.js/lib/languages/python'))
+hljs.registerLanguage('ruby', require('highlight.js/lib/languages/ruby'))
+hljs.registerLanguage('scss', require('highlight.js/lib/languages/scss'))
+hljs.registerLanguage('sql', require('highlight.js/lib/languages/sql'))
+hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml'))
+
+export default hljs
diff --git a/resources/assets/javascripts/chunks/fullcalendar.js b/resources/assets/javascripts/chunks/fullcalendar.js
new file mode 100644
index 0000000..ee28099
--- /dev/null
+++ b/resources/assets/javascripts/chunks/fullcalendar.js
@@ -0,0 +1,11 @@
+import '@fullcalendar/core/main.css';
+import '@fullcalendar/daygrid/main.css';
+import '@fullcalendar/timegrid/main.css';
+import '@fullcalendar/timeline/main.css';
+import '@fullcalendar/resource-timeline/main.css';
+
+import "../../stylesheets/fullcalendar.scss";
+
+import Fullcalendar from '../lib/fullcalendar.js';
+
+STUDIP.Fullcalendar = Fullcalendar;
diff --git a/resources/assets/javascripts/chunks/tablesorter.js b/resources/assets/javascripts/chunks/tablesorter.js
new file mode 100644
index 0000000..2a86f6d
--- /dev/null
+++ b/resources/assets/javascripts/chunks/tablesorter.js
@@ -0,0 +1,18 @@
+import "tablesorter/dist/js/jquery.tablesorter"
+import "tablesorter/dist/js/extras/jquery.tablesorter.pager.min.js"
+import "tablesorter/dist/js/jquery.tablesorter.widgets.js"
+
+jQuery.tablesorter.addParser({
+ id: 'htmldata',
+ is: function (s, table, cell, $cell) {
+ var c = table.config,
+ p = c.parserMetadataName || 'sortValue';
+ return $(cell).data(p) !== undefined;
+ },
+ format: function (s, table, cell) {
+ var c = table.config,
+ p = c.parserMetadataName || 'sortValue';
+ return $(cell).data(p);
+ },
+ type: 'text'
+});
diff --git a/resources/assets/javascripts/chunks/vue.js b/resources/assets/javascripts/chunks/vue.js
new file mode 100644
index 0000000..bfcb393
--- /dev/null
+++ b/resources/assets/javascripts/chunks/vue.js
@@ -0,0 +1,54 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import Router from "vue-router";
+import eventBus from '../lib/event-bus.js';
+import GetTextPlugin from 'vue-gettext';
+import { getLocale, getVueConfig } from '../lib/gettext.js';
+import PortalVue from 'portal-vue';
+import BaseComponents from '../../../vue/base-components.js';
+import BaseDirectives from "../../../vue/base-directives.js";
+
+Vue.mixin({
+ methods: {
+ globalEmit(...args) {
+ eventBus.emit(...args);
+ },
+ globalOn(...args) {
+ eventBus.on(...args);
+ },
+ },
+});
+
+Vue.use(GetTextPlugin, getVueConfig());
+eventBus.on('studip:set-locale', (locale) => {
+ Vue.config.language = locale;
+})
+
+registerGlobalComponents();
+registerGlobalDirectives();
+
+Vue.use(Vuex);
+const store = new Vuex.Store({});
+
+Vue.use(Router);
+
+Vue.use(PortalVue);
+
+function createApp(options, ...args) {
+ Vue.config.language = getLocale();
+ return new Vue({ store, ...options }, ...args);
+}
+
+function registerGlobalComponents() {
+ for (const [name, component] of Object.entries(BaseComponents)) {
+ Vue.component(name, component);
+ }
+}
+
+function registerGlobalDirectives() {
+ for (const [name, directive] of Object.entries(BaseDirectives)) {
+ Vue.directive(name, directive);
+ }
+}
+
+export { Vue, createApp, eventBus, store };
diff --git a/resources/assets/javascripts/entry-admission.js b/resources/assets/javascripts/entry-admission.js
new file mode 100644
index 0000000..08f597b
--- /dev/null
+++ b/resources/assets/javascripts/entry-admission.js
@@ -0,0 +1,2 @@
+import "./jquery/jstree/jquery.jstree.js"
+import "./bootstrap/admission.js"
diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js
new file mode 100644
index 0000000..7024738
--- /dev/null
+++ b/resources/assets/javascripts/entry-base.js
@@ -0,0 +1,95 @@
+import './public-path.js'
+
+// promise polyfill needed for IE11 to load tablesorter
+import 'es6-promise/auto'
+
+import "../stylesheets/studip-jquery-ui.less"
+import "../stylesheets/studip.less"
+// Basic scss support
+import "../stylesheets/studip.scss"
+
+import lodash from "lodash"
+window._ = lodash
+
+import QRCode from "./vendor/qrcode-04f46c6.js"
+window.QRCode = QRCode
+
+import "./jquery-bundle.js"
+
+import "./init.js"
+import "./bootstrap/responsive.js"
+import "./chunk-loader.js"
+import "./bootstrap/vue.js"
+
+import "./bootstrap/my-courses.js";
+
+import "./studip-ui.js"
+import "./bootstrap/fullscreen.js"
+import "./bootstrap/tfa.js"
+import "./bootstrap/tables.js"
+import "./bootstrap/studip_helper_attributes.js"
+import "./bootstrap/header_magic.js"
+import "./bootstrap/header_navigation.js"
+import "./bootstrap/personal_notifications.js"
+import "./bootstrap/sidebar.js"
+import "./bootstrap/smiley_picker.js"
+import "./bootstrap/dialog.js"
+import "./bootstrap/jsupdater.js"
+import "./bootstrap/files.js"
+import "./bootstrap/news.js"
+import "./bootstrap/messages.js"
+import "./bootstrap/quick_search.js"
+import "./bootstrap/multi_select.js"
+import "./bootstrap/multi_person_search.js"
+import "./bootstrap/skip_links.js"
+import "./bootstrap/i18n_input.js"
+import "./bootstrap/forms.js"
+import "./bootstrap/calendar_dialog.js"
+import "./bootstrap/drag_and_drop_upload.js"
+import "./bootstrap/admin_sem_classes.js"
+import "./bootstrap/cronjobs.js"
+import "./bootstrap/contentbox.js"
+import "./bootstrap/dates.js"
+import "./bootstrap/tour.js"
+import "./bootstrap/questionnaire.js"
+import "./bootstrap/qr_code.js"
+import "./bootstrap/startpage.js"
+import "./bootstrap/wiki.js"
+import "./bootstrap/course_wizard.js"
+import "./bootstrap/smiley.js"
+import "./bootstrap/big_image_handler.js"
+import "./bootstrap/opengraph.js"
+import "./bootstrap/actionmenu.js"
+import "./bootstrap/article.js"
+import "./bootstrap/copyable_links.js"
+import "./bootstrap/selection.js"
+import "./bootstrap/data_secure.js"
+import "./bootstrap/tooltip.js"
+import "./bootstrap/lightbox.js"
+import "./bootstrap/application.js"
+import "./bootstrap/global_search.js"
+import "./bootstrap/search.js"
+import "./bootstrap/mvv_difflog.js"
+import "./bootstrap/members.js"
+import "./bootstrap/avatar.js"
+import "./bootstrap/raumzeit.js"
+import "./bootstrap/settings.js"
+import "./bootstrap/subcourses.js"
+import "./bootstrap/tabbable_widget.js"
+import "./bootstrap/clipboard.js"
+import "./bootstrap/resources.js"
+import "./bootstrap/resource-tree-widget.js"
+import "./bootstrap/fullcalendar.js"
+import "./bootstrap/inline-editing.js"
+import "./bootstrap/gradebook.js"
+import "./bootstrap/blubber.js"
+import "./bootstrap/consultations.js"
+import "./bootstrap/scroll_to_top.js"
+import "./bootstrap/admin-courses.js"
+import "./bootstrap/cache-admin.js"
+import "./bootstrap/oer.js"
+import "./bootstrap/courseware.js"
+
+import "./mvv_course_wizard.js"
+import "./mvv.js"
+import "./feedback.js"
diff --git a/resources/assets/javascripts/entry-installer.js b/resources/assets/javascripts/entry-installer.js
new file mode 100644
index 0000000..92a8812
--- /dev/null
+++ b/resources/assets/javascripts/entry-installer.js
@@ -0,0 +1,2 @@
+import "../stylesheets/scss/installer.scss"
+import "./bootstrap/installer.js"
diff --git a/resources/assets/javascripts/entry-statusgroups.js b/resources/assets/javascripts/entry-statusgroups.js
new file mode 100644
index 0000000..a6d0531
--- /dev/null
+++ b/resources/assets/javascripts/entry-statusgroups.js
@@ -0,0 +1,4 @@
+import "../stylesheets/statusgroups.less"
+
+import "jquery-nestable"
+import "./bootstrap/statusgroups.js"
diff --git a/resources/assets/javascripts/entry-wysiwyg.js b/resources/assets/javascripts/entry-wysiwyg.js
new file mode 100644
index 0000000..c1edaaa
--- /dev/null
+++ b/resources/assets/javascripts/entry-wysiwyg.js
@@ -0,0 +1,4 @@
+import "../stylesheets/wysiwyg.less"
+
+import "../../../public/assets/javascripts/ckeditor/ckeditor.js"
+import "./bootstrap/wysiwyg.js"
diff --git a/resources/assets/javascripts/feedback.js b/resources/assets/javascripts/feedback.js
new file mode 100644
index 0000000..849980b
--- /dev/null
+++ b/resources/assets/javascripts/feedback.js
@@ -0,0 +1,169 @@
+STUDIP.Feedback = {
+
+ initiate: function(feedback)
+ {
+ var range_id = $(feedback).attr('for');
+ var range_type = $(feedback).attr('type');
+ var course_id = $(feedback).attr('context');
+
+ $(feedback).load(STUDIP.URLHelper.getURL('dispatch.php/course/feedback/index_for/' + range_id + '/' + range_type + '?cid=' + course_id), function()
+ {
+ if ($('.feedback-delete').length) {
+ $('.feedback-delete').prop("onclick", null).off("click");
+ $('.feedback-delete').click(function (event) {
+ event.preventDefault();
+ var id = $(this).attr('data-id');
+ STUDIP.Dialog.confirm($(this).attr('data-confirm')).done(function() {
+ STUDIP.Feedback.delete(id,feedback);
+ });
+ });
+ }
+ STUDIP.Feedback.initiateView();
+ });
+ },
+
+ initiateView: function() {
+ $('.feedback-entry-add').prop("onclick", null).off("click");
+ $('.feedback-entry-add').find('.accept').click(function (event) {
+ event.preventDefault();
+ var id = $(this).closest('article').attr('data-id');
+ var feedback_id = $(this).closest('form').serialize();
+ STUDIP.Feedback.addEntry(id,feedback_id);
+ });
+ $('.feedback-entry-edit').prop("onclick", null).off("click");
+ $('.feedback-entry-edit').click(function (event) {
+ event.preventDefault();
+ var entry_id = $(this).closest('article').attr('data-id');
+ var feedback_id = $(this).closest('.feedback-stream').attr('data-id');
+ STUDIP.Feedback.editEntryForm(entry_id,feedback_id) ;
+ });
+ $('.feedback-entry-delete').prop("onclick", null).off("click");
+ $('.feedback-entry-delete').click(function (event) {
+ event.preventDefault();
+ var entry_id = $(this).closest('article').attr('data-id');
+ var feedback_id = $(this).closest('.feedback-stream').attr('data-id');
+ STUDIP.Dialog.confirm($(this).attr('data-confirm')).done(function() {
+ STUDIP.Feedback.deleteEntry(entry_id,feedback_id);
+ });
+ });
+ STUDIP.Feedback.initiateFeedbackEntryForm();
+ if ($('table.sortable-table').length) {
+ $('table.sortable-table').each(function(index, element) {
+ STUDIP.Table.enhanceSortableTable(element);
+ });
+ }
+ },
+ delete: function(id,feedback)
+ {
+ var url = STUDIP.URLHelper.getURL('dispatch.php/course/feedback/delete/' + id);
+ request = $.ajax({
+ url: url,
+ type: 'post'
+ });
+ request.done(function()
+ {
+ STUDIP.Feedback.initiate(feedback);
+ });
+ },
+ addEntry: function(feedback_id,data)
+ {
+ var url = STUDIP.URLHelper.getURL('dispatch.php/course/feedback/entry_add/' + feedback_id);
+ request = $.ajax({
+ url: url,
+ type: 'post',
+ data: data
+ });
+ request.done(function()
+ {
+ STUDIP.Feedback.reloadView(feedback_id);
+ });
+
+ },
+ editEntryForm: function(entry_id,feedback_id)
+ {
+ url = STUDIP.URLHelper.getURL('dispatch.php/course/feedback/entry_edit_form/' + entry_id);
+ $('#feedback-stream-' + feedback_id).find('.feedback-view').load(url, function() {
+ STUDIP.Feedback.initiateFeedbackEntryForm();
+ $('#feedback-stream-' + feedback_id).find('.accept ').prop("onclick", null).off("click");
+ $('#feedback-stream-' + feedback_id).find('.accept ').click(function (event) {
+ event.preventDefault();
+ var data = $(this).closest('form').serialize();
+ STUDIP.Feedback.editEntry(entry_id,feedback_id,data);
+ });
+ $('#feedback-stream-' + feedback_id).find('.cancel').prop("onclick", null).off("click");
+ $('#feedback-stream-' + feedback_id).find('.cancel').click(function (event) {
+ event.preventDefault();
+ STUDIP.Feedback.reloadView(feedback_id);
+ });
+ });
+ },
+ editEntry: function(entry_id,feedback_id,data)
+ {
+ var url = STUDIP.URLHelper.getURL('dispatch.php/course/feedback/entry_edit/' + entry_id);
+ request = $.ajax({
+ url: url,
+ type: 'post',
+ data: data
+ });
+ request.done(function()
+ {
+ STUDIP.Feedback.reloadView(feedback_id);
+ });
+ },
+ deleteEntry: function(entry_id,feedback_id)
+ {
+ var url = STUDIP.URLHelper.getURL('dispatch.php/course/feedback/entry_delete/' + entry_id);
+ request = $.ajax({
+ url: url,
+ type: 'post',
+ });
+ request.done(function()
+ {
+ STUDIP.Feedback.reloadView(feedback_id);
+ });
+ },
+ reloadView: function(feedback_id) {
+ url = STUDIP.URLHelper.getURL('dispatch.php/course/feedback/view/' + feedback_id);
+ $('#feedback-stream-' + feedback_id).find('.feedback-view').load(url, function() {
+ STUDIP.Feedback.initiateView();
+ });
+ },
+ initiateFeedbackEntryForm: function() {
+ if ($('.star-rating').length) {
+ $('.star-rating').hover(
+ function() {
+ $(this).addClass('hover');
+ $(this).prevAll('.star-rating').addClass('hover');
+ $(this).nextAll('.star-rating').addClass('out');
+ }, function() {
+ $(this).removeClass('hover');
+ $(this).siblings('.star-rating').removeClass('hover out');
+ }
+ );
+ $('.star-rating-input').change(
+ function() {
+ $(this).parent().addClass('checked');
+ $(this).parent().prevAll('.star-rating').addClass('checked');
+ $(this).parent().nextAll('.star-rating').removeClass('checked');
+ }
+ );
+ }
+ if ($('.feedback-entry-cancel').length) {
+ $('.feedback-entry-cancel').prop("onclick", null).off("click");
+ $('.feedback-entry-cancel').click(function (event) {
+ event.preventDefault();
+ $(this).closest('form')[0].reset();
+ $(this).closest('form').find('.star-rating').removeClass('checked');
+ });
+ }
+ }
+}
+
+STUDIP.ready(function (event) {
+ STUDIP.Feedback.initiateFeedbackEntryForm();
+ if ($('div.feedback-elements').length) {
+ $('div.feedback-elements', event.target).each((index, element) => {
+ STUDIP.Feedback.initiate(element);
+ });
+ }
+});
diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js
new file mode 100644
index 0000000..caf944e
--- /dev/null
+++ b/resources/assets/javascripts/init.js
@@ -0,0 +1,175 @@
+import Vue from './lib/studip-vue.js';
+
+import ActionMenu from './lib/actionmenu.js';
+import admin_sem_class from './lib/admin_sem_class.js';
+import Admission from './lib/admission.js';
+import Arbeitsgruppen from './lib/arbeitsgruppen.js';
+import Archive from './lib/archive.js';
+import Audio from './lib/audio.js';
+import Avatar from './lib/avatar.js';
+import BigImageHandler from './lib/big_image_handler.js';
+import Blubber from './lib/blubber.js';
+import Browse from './lib/browse.js';
+import Cache from './lib/cache.js';
+import Calendar from './lib/calendar.js';
+import CalendarDialog from './lib/calendar_dialog.js';
+import Clipboard from './lib/clipboard.js';
+import Cookie from './lib/cookie.js';
+import CourseWizard from './lib/course_wizard.js';
+import createURLHelper from './lib/url_helper.js';
+import CSS from './lib/css.js';
+import Dates from './lib/dates.js';
+import Dialog from './lib/dialog.js';
+import Dialogs from './lib/dialogs.js';
+import DragAndDropUpload from './lib/drag_and_drop_upload.js';
+import enrollment from './lib/enrollment.js';
+import eventBus from './lib/event-bus.js';
+import extractCallback from './lib/extract_callback.js';
+import Files from './lib/files.js';
+import FilesDashboard from './lib/files_dashboard.js';
+import Folders from './lib/folders.js';
+import Forms from './lib/forms.js';
+import Fullscreen from './lib/fullscreen.js';
+import GlobalSearch from './lib/global_search.js';
+import HeaderMagic from './lib/header_magic.js';
+import i18n from './lib/i18n.js';
+import Instschedule from './lib/instschedule.js';
+import InlineEditing from './lib/inline-editing.js';
+import JSONAPI, { jsonapi } from './lib/jsonapi.js';
+import JSUpdater from './lib/jsupdater.js';
+import Lightbox from './lib/lightbox.js';
+import Markup from './lib/markup.js';
+import Members from './lib/members.js';
+import Messages from './lib/messages.js';
+import MultiPersonSearch from './lib/multi_person_search.js';
+import MultiSelect from './lib/multi_select.js';
+import NavigationShrinker from './lib/navigation_shrinker.js';
+import News from './lib/news.js';
+import OER from './lib/oer.js';
+import OldUpload from './lib/old_upload.js';
+import Overlapping from './lib/overlapping.js';
+import Overlay from './lib/overlay.js';
+import PageLayout from './lib/page_layout.js';
+import parseOptions from './lib/parse_options.js';
+import PersonalNotifications from './lib/personal_notifications.js';
+import Plus from './lib/plus.js';
+import QRCode from './lib/qr_code.js';
+import Questionnaire from './lib/questionnaire.js';
+import QuickSearch from './lib/quick_search.js';
+import Raumzeit from './lib/raumzeit.js';
+import {ready, domReady, dialogReady} from './lib/ready.js';
+import register from './lib/register.js';
+import Report from './lib/report.js';
+import Resources from './lib/resources.js';
+import Responsive from './lib/responsive.js';
+import RESTAPI, { api } from './lib/restapi.js';
+import Schedule from './lib/schedule.js';
+import Scroll from './lib/scroll.js';
+import Search from './lib/search.js';
+import Sidebar from './lib/sidebar.js';
+import SkipLinks from './lib/skip_links.js';
+import SmileyPicker from './lib/smiley_picker.js';
+import startpage from './lib/startpage.js';
+import Statusgroups from './lib/statusgroups.js';
+import study_area_selection from './lib/study_area_selection.js';
+import Table from './lib/table.js';
+import TableOfContents from './lib/table-of-contents.js';
+import Toolbar from './lib/toolbar.js';
+import Tooltip from './lib/tooltip.js';
+import Tour from './lib/tour.js';
+import * as Gettext from './lib/gettext.js';
+import UserFilter from './lib/user_filter.js';
+import wysiwyg from './lib/wysiwyg.js';
+import ScrollToTop from './lib/scroll_to_top.js';
+
+const configURLHelper = _.get(window, 'STUDIP.URLHelper', {});
+const URLHelper = createURLHelper(configURLHelper);
+
+window.STUDIP = _.assign(window.STUDIP || {}, {
+ ActionMenu,
+ admin_sem_class,
+ Admission,
+ api,
+ Arbeitsgruppen,
+ Archive,
+ Audio,
+ Avatar,
+ BigImageHandler,
+ Blubber,
+ Browse,
+ Cache,
+ Calendar,
+ CalendarDialog,
+ Cookie,
+ CourseWizard,
+ CSS,
+ Dates,
+ Dialog,
+ Dialogs,
+ DragAndDropUpload,
+ enrollment,
+ eventBus,
+ extractCallback,
+ Files,
+ FilesDashboard,
+ Folders,
+ Forms,
+ Fullscreen,
+ Gettext,
+ GlobalSearch,
+ HeaderMagic,
+ i18n,
+ Instschedule,
+ InlineEditing,
+ jsonapi,
+ JSONAPI,
+ JSUpdater,
+ Lightbox,
+ Markup,
+ Members,
+ Messages,
+ MultiPersonSearch,
+ MultiSelect,
+ NavigationShrinker,
+ News,
+ OER,
+ OldUpload,
+ Overlapping,
+ Overlay,
+ PageLayout,
+ parseOptions,
+ PersonalNotifications,
+ Plus,
+ QRCode,
+ Questionnaire,
+ QuickSearch,
+ Raumzeit,
+ register,
+ Report,
+ Responsive,
+ RESTAPI,
+ Schedule,
+ Scroll,
+ Search,
+ Sidebar,
+ SkipLinks,
+ SmileyPicker,
+ startpage,
+ Statusgroups,
+ study_area_selection,
+ Table,
+ TableOfContents,
+ Toolbar,
+ Tooltip,
+ Tour,
+ URLHelper,
+ UserFilter,
+ wysiwyg,
+ Resources,
+ Clipboard,
+ ready,
+ domReady,
+ dialogReady,
+ ScrollToTop,
+ Vue
+});
diff --git a/resources/assets/javascripts/jquery-bundle.js b/resources/assets/javascripts/jquery-bundle.js
new file mode 100644
index 0000000..1f121c7
--- /dev/null
+++ b/resources/assets/javascripts/jquery-bundle.js
@@ -0,0 +1,182 @@
+import 'expose-loader?exposes[]=$&exposes[]=jQuery!jquery';
+
+import { setLocale } from './lib/gettext.js';
+
+import 'jquery-ui/ui/widget.js';
+import 'jquery-ui/ui/position.js';
+import 'jquery-ui/ui/data.js';
+import 'jquery-ui/ui/disable-selection.js';
+import 'jquery-ui/ui/escape-selector.js';
+import 'jquery-ui/ui/focusable.js';
+import 'jquery-ui/ui/form.js';
+import 'jquery-ui/ui/form-reset-mixin.js';
+import 'jquery-ui/ui/ie.js';
+import 'jquery-ui/ui/jquery-1-7.js';
+import 'jquery-ui/ui/keycode.js';
+import 'jquery-ui/ui/labels.js';
+import 'jquery-ui/ui/plugin.js';
+import 'jquery-ui/ui/safe-active-element.js';
+import 'jquery-ui/ui/safe-blur.js';
+import 'jquery-ui/ui/scroll-parent.js';
+import 'jquery-ui/ui/tabbable.js';
+import 'jquery-ui/ui/unique-id.js';
+import 'jquery-ui/ui/version.js';
+import 'jquery-ui/ui/widgets/draggable.js';
+import 'jquery-ui/ui/widgets/droppable.js';
+import 'jquery-ui/ui/widgets/resizable.js';
+import 'jquery-ui/ui/widgets/selectable.js';
+import 'jquery-ui/ui/widgets/sortable.js';
+import 'jquery-ui/ui/widgets/accordion.js';
+import 'jquery-ui/ui/widgets/autocomplete.js';
+import 'jquery-ui/ui/widgets/button.js';
+import 'jquery-ui/ui/widgets/checkboxradio.js';
+import 'jquery-ui/ui/widgets/controlgroup.js';
+import 'jquery-ui/ui/widgets/datepicker.js';
+import 'jquery-ui/ui/widgets/dialog.js';
+import 'jquery-ui/ui/widgets/menu.js';
+import 'jquery-ui/ui/widgets/mouse.js';
+import 'jquery-ui/ui/widgets/progressbar.js';
+import 'jquery-ui/ui/widgets/selectmenu.js';
+import 'jquery-ui/ui/widgets/slider.js';
+import 'jquery-ui/ui/widgets/spinner.js';
+import 'jquery-ui/ui/widgets/tabs.js';
+import 'jquery-ui/ui/widgets/tooltip.js';
+import 'jquery-ui/ui/effect.js';
+import 'jquery-ui/ui/effects/effect-blind.js';
+import 'jquery-ui/ui/effects/effect-bounce.js';
+import 'jquery-ui/ui/effects/effect-clip.js';
+import 'jquery-ui/ui/effects/effect-drop.js';
+import 'jquery-ui/ui/effects/effect-explode.js';
+import 'jquery-ui/ui/effects/effect-fade.js';
+import 'jquery-ui/ui/effects/effect-fold.js';
+import 'jquery-ui/ui/effects/effect-highlight.js';
+import 'jquery-ui/ui/effects/effect-puff.js';
+import 'jquery-ui/ui/effects/effect-pulsate.js';
+import 'jquery-ui/ui/effects/effect-scale.js';
+import 'jquery-ui/ui/effects/effect-shake.js';
+import 'jquery-ui/ui/effects/effect-size.js';
+import 'jquery-ui/ui/effects/effect-slide.js';
+import 'jquery-ui/ui/effects/effect-transfer.js';
+
+import 'jquery-ui-timepicker-addon';
+
+import 'multiselect';
+
+import 'jquery.scrollto';
+
+import 'jquery-ui-touch-punch';
+
+import './studip-jquery-tweaks.js';
+import './studip-jquery.multi-select.tweaks.js';
+import './studip-jquery-selection-helper.js';
+
+import select2 from 'select2/dist/js/select2.full.js';
+
+import 'sticky-kit/dist/sticky-kit.js';
+
+import 'blueimp-file-upload';
+import 'blueimp-file-upload/js/jquery.iframe-transport.js';
+
+import './jquery/jquery.filtertable-1.5.7.js';
+import './jquery/autoresize.jquery.min.js';
+
+import { $gettext } from './lib/gettext.js';
+import Toolbar from './lib/toolbar.js';
+
+$.fn.extend({
+ // Adds the toolbar to an element
+ addToolbar: function(button_set) {
+ return this.each(function() {
+ Toolbar.initialize(this, button_set);
+ });
+ }
+});
+
+// Create jQuery "plugin" that just reverses the elements' order. This is
+// neccessary since the navigation is built and afterwards, we need to
+// check the navigation's open status in reverse order (from bottom to top)
+jQuery.fn.reverse = [].reverse;
+
+$.fn.extend({
+ showAjaxNotification: function(position) {
+ position = position || 'left';
+ return this.each(function() {
+ if ($(this).data('ajax_notification')) {
+ return;
+ }
+
+ $(this).wrap('<span class="ajax_notification" />');
+ var that = this,
+ notification = $('<span class="notification" />')
+ .hide()
+ .insertBefore(this),
+ changes = {
+ marginLeft: 0,
+ marginRight: 0
+ };
+
+ changes[position === 'right' ? 'marginRight' : 'marginLeft'] = notification.outerWidth(true);
+
+ $(this)
+ .data({
+ ajax_notification: notification
+ })
+ .parent()
+ .animate(changes, 'fast', function() {
+ var offset = $(that).position(),
+ styles = {
+ left: offset.left - notification.outerWidth(true),
+ top:
+ offset.top +
+ Math.max(0, Math.floor(($(that).height() - notification.outerHeight(true)) / 2))
+ };
+ if (position === 'right') {
+ styles.left += $(this).outerWidth(true);
+ }
+ notification.css(styles).fadeIn('fast');
+ });
+ });
+ },
+ hideAjaxNotification: function() {
+ return this.each(function() {
+ var $this = $(this).stop(),
+ notification = $this.data('ajax_notification');
+ if (!notification) {
+ return;
+ }
+
+ notification.stop().fadeOut('fast', function() {
+ $this.animate({ marginLeft: 0, marginRight: 0 }, 'fast', function() {
+ $this.unwrap();
+ });
+ $(this).remove();
+ });
+ $(this).removeData('ajax_notification');
+ });
+ }
+});
+
+$.extend($.expr[':'], {
+ invalid: function(elem, index, match) {
+ var invalids = document.querySelectorAll(':invalid'),
+ result = false,
+ len = invalids.length || 0,
+ i;
+
+ for (i = 0; i < len; i += 1) {
+ if (elem === invalids[i]) {
+ result = true;
+ break;
+ }
+ }
+
+ return result;
+ }
+});
+
+$(document).ready(async () => {
+ await setLocale();
+ STUDIP.ready.trigger('dom');
+}).on('dialog-update', (event, data) => {
+ STUDIP.ready.trigger('dialog', data.dialog);
+});
diff --git a/resources/assets/javascripts/jquery/autoresize.jquery.min.js b/resources/assets/javascripts/jquery/autoresize.jquery.min.js
new file mode 100644
index 0000000..4b8d315
--- /dev/null
+++ b/resources/assets/javascripts/jquery/autoresize.jquery.min.js
@@ -0,0 +1,7 @@
+/*
+ * jQuery autoResize (textarea auto-resizer)
+ * @copyright James Padolsey http://james.padolsey.com
+ * @version 1.04
+ */
+
+(function(a){a.fn.autoResize=function(j){var b=a.extend({onResize:function(){},animate:true,animateDuration:150,animateCallback:function(){},extraSpace:20,limit:1000},j);this.filter('textarea').each(function(){var c=a(this).css({resize:'none','overflow-y':'hidden'}),k=c.height(),f=(function(){var l=['height','width','lineHeight','textDecoration','letterSpacing'],h={};a.each(l,function(d,e){h[e]=c.css(e)});return c.clone().removeAttr('id').removeAttr('name').css({position:'absolute',top:0,left:-9999}).css(h).attr('tabIndex','-1').insertBefore(c)})(),i=null,g=function(){f.height(0).val(a(this).val()).scrollTop(10000);var d=Math.max(f.scrollTop(),k)+b.extraSpace,e=a(this).add(f);if(i===d){return}i=d;if(d>=b.limit){a(this).css('overflow-y','');return}b.onResize.call(this);b.animate&&c.css('display')==='block'?e.stop().animate({height:d},b.animateDuration,b.animateCallback):e.height(d)};c.unbind('.dynSiz').bind('keyup.dynSiz',g).bind('keydown.dynSiz',g).bind('change.dynSiz',g)});return this}})(jQuery); \ No newline at end of file
diff --git a/resources/assets/javascripts/jquery/jquery.filtertable-1.5.7.js b/resources/assets/javascripts/jquery/jquery.filtertable-1.5.7.js
new file mode 100644
index 0000000..e65af7f
--- /dev/null
+++ b/resources/assets/javascripts/jquery/jquery.filtertable-1.5.7.js
@@ -0,0 +1,402 @@
+/**
+ * jquery.filterTable
+ *
+ * This plugin will add a search filter to tables. When typing in the filter,
+ * any rows that do not contain the filter will be hidden.
+ *
+ * Utilizes bindWithDelay() if available. https://github.com/bgrins/bindWithDelay
+ *
+ * @version v1.5.7
+ * @author Sunny Walker, swalker@hawaii.edu
+ * @license MIT
+ */
+(function ($) {
+ var jversion = $.fn.jquery.split('.'),
+ jmajor = parseFloat(jversion[0]),
+ jminor = parseFloat(jversion[1]);
+ // build the pseudo selector for jQuery < 1.8
+ if (jmajor < 2 && jminor < 8) {
+ // build the case insensitive filtering functionality as a pseudo-selector expression
+ $.expr[':'].filterTableFind = function (a, i, m) {
+ return $(a).text().toUpperCase().indexOf(m[3].toUpperCase().replace(/"""/g, '"').replace(/"\\"/g, "\\")) >= 0;
+ };
+ // build the case insensitive all-words filtering functionality as a pseudo-selector expression
+ $.expr[':'].filterTableFindAny = function (a, i, m) {
+ // build an array of each non-falsey value passed
+ var raw_args = m[3].split(/[\s,]/),
+ args = [];
+ $.each(raw_args, function (j, v) {
+ var t = v.replace(/^\s+|\s$/g, '');
+ if (t) {
+ args.push(t);
+ }
+ });
+ // if there aren't any non-falsey values to search for, abort
+ if (!args.length) {
+ return false;
+ }
+ return function (a) {
+ var found = false;
+ $.each(args, function (j, v) {
+ if ($(a).text().toUpperCase().indexOf(v.toUpperCase().replace(/"""/g, '"').replace(/"\\"/g, "\\")) >= 0) {
+ found = true;
+ return false;
+ }
+ });
+ return found;
+ };
+ };
+ // build the case insensitive all-words filtering functionality as a pseudo-selector expression
+ $.expr[':'].filterTableFindAll = function (a, i, m) {
+ // build an array of each non-falsey value passed
+ var raw_args = m[3].split(/[\s,]/),
+ args = [];
+ $.each(raw_args, function (j, v) {
+ var t = v.replace(/^\s+|\s$/g, '');
+ if (t) {
+ args.push(t);
+ }
+ });
+ // if there aren't any non-falsey values to search for, abort
+ if (!args.length) {
+ return false;
+ }
+ return function (a) {
+ // how many terms were found?
+ var found = 0;
+ $.each(args, function (j, v) {
+ if ($(a).text().toUpperCase().indexOf(v.toUpperCase().replace(/"""/g, '"').replace(/"\\"/g, "\\")) >= 0) {
+ // found another term
+ found++;
+ }
+ });
+ return found === args.length; // did we find all of them in this cell?
+ };
+ };
+ } else {
+ // build the pseudo selector for jQuery >= 1.8
+ $.expr[':'].filterTableFind = jQuery.expr.createPseudo(function (arg) {
+ return function (el) {
+ return $(el).text().toUpperCase().indexOf(arg.toUpperCase().replace(/"""/g, '"').replace(/"\\"/g, "\\")) >= 0;
+ };
+ });
+ $.expr[':'].filterTableFindAny = jQuery.expr.createPseudo(function (arg) {
+ // build an array of each non-falsey value passed
+ var raw_args = arg.split(/[\s,]/),
+ args = [];
+ $.each(raw_args, function (i, v) {
+ // trim the string
+ var t = v.replace(/^\s+|\s$/g, '');
+ if (t) {
+ args.push(t);
+ }
+ });
+ // if there aren't any non-falsey values to search for, abort
+ if (!args.length) {
+ return false;
+ }
+ return function (el) {
+ var found = false;
+ $.each(args, function (i, v) {
+ if ($(el).text().toUpperCase().indexOf(v.toUpperCase().replace(/"""/g, '"').replace(/"\\"/g, "\\")) >= 0) {
+ found = true;
+ // short-circuit the searching since this cell has one of the terms
+ return false;
+ }
+ });
+ return found;
+ };
+ });
+ $.expr[':'].filterTableFindAll = jQuery.expr.createPseudo(function (arg) {
+ // build an array of each non-falsey value passed
+ var raw_args = arg.split(/[\s,]/),
+ args = [];
+ $.each(raw_args, function (i, v) {
+ // trim the string
+ var t = v.replace(/^\s+|\s$/g, '');
+ if (t) {
+ args.push(t);
+ }
+ });
+ // if there aren't any non-falsey values to search for, abort
+ if (!args.length) {
+ return false;
+ }
+ return function (el) {
+ // how many terms were found?
+ var found = 0;
+ $.each(args, function (i, v) {
+ if ($(el).text().toUpperCase().indexOf(v.toUpperCase().replace(/"""/g, '"').replace(/"\\"/g, "\\")) >= 0) {
+ // found another term
+ found++;
+ }
+ });
+ // did we find all of them in this cell?
+ return found === args.length;
+ };
+ });
+ }
+ // define the filterTable plugin
+ $.fn.filterTable = function (options) {
+ // start off with some default settings
+ var defaults = {
+ // make the filter input field autofocused (not recommended for accessibility)
+ autofocus: false,
+
+ // callback function: function (term, table){}
+ callback: null,
+
+ // class to apply to the container
+ containerClass: 'filter-table',
+
+ // tag name of the container
+ containerTag: 'p',
+
+ // jQuery expression method to use for filtering
+ filterExpression: 'filterTableFind',
+
+ // if true, the table's tfoot(s) will be hidden when the table is filtered
+ hideTFootOnFilter: false,
+
+ // class applied to cells containing the filter term
+ highlightClass: 'alt',
+
+ // don't filter the contents of cells with this class
+ ignoreClass: '',
+
+ // don't filter the contents of these columns
+ ignoreColumns: [],
+
+ // use the element with this selector for the filter input field instead of creating one
+ inputSelector: null,
+
+ // name of filter input field
+ inputName: '',
+
+ // tag name of the filter input tag
+ inputType: 'search',
+
+ // text to precede the filter input tag
+ label: 'Filter:',
+
+ // filter only when at least this number of characters are in the filter input field
+ minChars: 1,
+
+ // don't show the filter on tables with at least this number of rows
+ minRows: 8,
+
+ // HTML5 placeholder text for the filter field
+ placeholder: 'search this table',
+
+ // prevent the return key in the filter input field from trigger form submits
+ preventReturnKey: true,
+
+ // list of phrases to quick fill the search
+ quickList: [],
+
+ // class of each quick list item
+ quickListClass: 'quick',
+
+ // quick list item label to clear the filter (e.g., '&times; Clear filter')
+ quickListClear: '',
+
+ // tag surrounding quick list items (e.g., ul)
+ quickListGroupTag: '',
+
+ // tag type of each quick list item (e.g., a or li)
+ quickListTag: 'a',
+
+ // class applied to visible rows
+ visibleClass: 'visible'
+ },
+ // mimic PHP's htmlspecialchars() function
+ hsc = function (text) {
+ return text.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+ },
+ // merge the user's settings into the defaults
+ settings = $.extend({}, defaults, options);
+
+ // handle the actual table filtering
+ var doFiltering = function (table, q) {
+ // cache the tbody element
+ var tbody = table.find('tbody');
+ // if the filtering query is blank or the number of chars is less than the minChars option
+ if (q === '' || q.length < settings.minChars) {
+ // show all rows
+ tbody.find('tr').show().addClass(settings.visibleClass);
+ // remove the row highlight from all cells
+ tbody.find('td').removeClass(settings.highlightClass);
+ // show footer if the setting was specified
+ if (settings.hideTFootOnFilter) {
+ table.find('tfoot').show();
+ }
+ } else {
+ // if the filter query is not blank
+ var all_tds = tbody.find('td');
+ // hide all rows, assuming none were found
+ tbody.find('tr').hide().removeClass(settings.visibleClass);
+ // remove previous highlights
+ all_tds.removeClass(settings.highlightClass);
+ // hide footer if the setting was specified
+ if (settings.hideTFootOnFilter) {
+ table.find('tfoot').hide();
+ }
+ if (settings.ignoreColumns.length) {
+ var tds = [];
+ if (settings.ignoreClass) {
+ all_tds = all_tds.not('.' + settings.ignoreClass);
+ }
+ tds = all_tds.filter(':' + settings.filterExpression + '("' + q + '")');
+ tds.each(function () {
+ var t = $(this),
+ col = t.parent().children().index(t);
+ if ($.inArray(col, settings.ignoreColumns) === -1) {
+ t.addClass(settings.highlightClass).closest('tr').show().addClass(settings.visibleClass);
+ }
+ });
+ } else {
+ if (settings.ignoreClass) {
+ all_tds = all_tds.not('.' + settings.ignoreClass);
+ }
+ // highlight (class=alt) only the cells that match the query and show their rows
+ all_tds.filter(':' + settings.filterExpression + '("' + q + '")').addClass(settings.highlightClass).closest('tr').show().addClass(settings.visibleClass);
+ }
+ }
+ // call the callback function
+ if (settings.callback) {
+ settings.callback(q, table);
+ }
+ }; // doFiltering()
+
+ return this.each(function () {
+ // cache the table
+ var t = $(this),
+ // cache the tbody
+ tbody = t.find('tbody'),
+ // placeholder for the filter field container DOM node
+ container = null,
+ // placeholder for the quick list items
+ quicks = null,
+ // placeholder for the field field DOM node
+ filter = null,
+ // was the filter created or chosen from an existing element?
+ created_filter = true;
+
+ // only if object is a table and there's a tbody and at least minRows trs and hasn't already had a filter added
+ if (t[0].nodeName === 'TABLE' && tbody.length > 0 && (settings.minRows === 0 || (settings.minRows > 0 && tbody.find('tr').length >= settings.minRows)) && !t.prev().hasClass(settings.containerClass)) {
+ // use a single existing field as the filter input field
+ if (settings.inputSelector && $(settings.inputSelector).length === 1) {
+ filter = $(settings.inputSelector);
+ // container to hold the quick list options
+ container = filter.parent();
+ created_filter = false;
+ } else {
+ // create the filter input field (and container)
+ // build the container tag for the filter field
+ container = $('<' + settings.containerTag + ' />');
+ // add any classes that need to be added
+ if (settings.containerClass !== '') {
+ container.addClass(settings.containerClass);
+ }
+ // add the label for the filter field
+ container.prepend(settings.label + ' ');
+ // build the filter field
+ filter = $('<input type="' + settings.inputType + '" placeholder="' + settings.placeholder + '" name="' + settings.inputName + '" />');
+ // prevent return in the filter field from submitting any forms
+ if (settings.preventReturnKey) {
+ filter.on('keydown', function (ev) {
+ if ((ev.keyCode || ev.which) === 13) {
+ ev.preventDefault();
+ return false;
+ }
+ });
+ }
+ }
+
+ // add the autofocus attribute if requested
+ if (settings.autofocus) {
+ filter.attr('autofocus', true);
+ }
+
+ // does bindWithDelay() exist?
+ if ($.fn.bindWithDelay) {
+ // bind doFiltering() to keyup (delayed)
+ filter.bindWithDelay('keyup', function () {
+ doFiltering(t, $(this).val());
+ }, 200);
+ } else {
+ // just bind to onKeyUp
+ // bind doFiltering() to keyup
+ filter.bind('keyup', function () {
+ doFiltering(t, $(this).val());
+ });
+ }
+
+ // bind doFiltering() to additional events
+ filter.bind('click search input paste blur', function () {
+ doFiltering(t, $(this).val());
+ });
+
+ // add the filter field to the container if it was created by the plugin
+ if (created_filter) {
+ container.append(filter);
+ }
+
+ // are there any quick list items to add?
+ if (settings.quickList.length > 0 || settings.quickListClear) {
+ quicks = settings.quickListGroupTag ? $('<' + settings.quickListGroupTag + ' />') : container;
+ // for each quick list item...
+ $.each(settings.quickList, function (index, value) {
+ // build the quick list item link
+ var q = $('<' + settings.quickListTag + ' class="' + settings.quickListClass + '" />');
+ // add the item's text
+ q.text(hsc(value));
+ if (q[0].nodeName === 'A') {
+ // add a (worthless) href to the item if it's an anchor tag so that it gets the browser's link treatment
+ q.attr('href', '#');
+ }
+ // bind the click event to it
+ q.bind('click', function (e) {
+ // stop the normal anchor tag behavior from happening
+ e.preventDefault();
+ // send the quick list value over to the filter field and trigger the event
+ filter.val(value).focus().trigger('click');
+ });
+ // add the quick list link to the quick list groups container
+ quicks.append(q);
+ });
+
+ // add the quick list clear item if a label has been specified
+ if (settings.quickListClear) {
+ // build the clear item
+ var q = $('<' + settings.quickListTag + ' class="' + settings.quickListClass + '" />');
+ // add the label text
+ q.html(settings.quickListClear);
+ if (q[0].nodeName === 'A') {
+ // add a (worthless) href to the item if it's an anchor tag so that it gets the browser's link treatment
+ q.attr('href', '#');
+ }
+ // bind the click event to it
+ q.bind('click', function (e) {
+ e.preventDefault();
+ // clear the quick list value and trigger the event
+ filter.val('').focus().trigger('click');
+ });
+ // add the clear item to the quick list groups container
+ quicks.append(q);
+ }
+
+ // add the quick list groups container to the DOM if it isn't already there
+ if (quicks !== container) {
+ container.append(quicks);
+ }
+ }
+
+ // add the filter field and quick list container to just before the table if it was created by the plugin
+ if (created_filter) {
+ t.before(container);
+ }
+ }
+ }); // return this.each
+ }; // $.fn.filterTable
+})(jQuery);
diff --git a/resources/assets/javascripts/jquery/jstree/jquery.jstree.js b/resources/assets/javascripts/jquery/jstree/jquery.jstree.js
new file mode 100644
index 0000000..c92c2ac
--- /dev/null
+++ b/resources/assets/javascripts/jquery/jstree/jquery.jstree.js
@@ -0,0 +1,4564 @@
+/*
+ * jsTree 1.0-rc3
+ * http://jstree.com/
+ *
+ * Copyright (c) 2010 Ivan Bozhanov (vakata.com)
+ *
+ * Licensed same as jquery - under the terms of either the MIT License or the GPL Version 2 License
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * $Date: 2011-02-09 01:17:14 +0200 (ср, 09 февр 2011) $
+ * $Revision: 236 $
+ */
+
+/*jslint browser: true, onevar: true, undef: true, bitwise: true, strict: true */
+/*global window : false, clearInterval: false, clearTimeout: false, document: false, setInterval: false, setTimeout: false, jQuery: false, navigator: false, XSLTProcessor: false, DOMParser: false, XMLSerializer: false, ActiveXObject: false */
+
+"use strict";
+
+// top wrapper to prevent multiple inclusion (is this OK?)
+(function () { if(jQuery && jQuery.jstree) { return; }
+ var is_ie6 = false, is_ie7 = false, is_ff2 = false;
+
+/*
+ * jsTree core
+ */
+(function ($) {
+ // Common functions not related to jsTree
+ // decided to move them to a `vakata` "namespace"
+ $.vakata = {};
+ // CSS related functions
+ $.vakata.css = {
+ get_css : function(rule_name, delete_flag, sheet) {
+ rule_name = rule_name.toLowerCase();
+ var css_rules = sheet.cssRules || sheet.rules,
+ j = 0;
+ do {
+ if(css_rules.length && j > css_rules.length + 5) { return false; }
+ if(css_rules[j].selectorText && css_rules[j].selectorText.toLowerCase() == rule_name) {
+ if(delete_flag === true) {
+ if(sheet.removeRule) { sheet.removeRule(j); }
+ if(sheet.deleteRule) { sheet.deleteRule(j); }
+ return true;
+ }
+ else { return css_rules[j]; }
+ }
+ }
+ while (css_rules[++j]);
+ return false;
+ },
+ add_css : function(rule_name, sheet) {
+ if($.jstree.css.get_css(rule_name, false, sheet)) { return false; }
+ if(sheet.insertRule) { sheet.insertRule(rule_name + ' { }', 0); } else { sheet.addRule(rule_name, null, 0); }
+ return $.vakata.css.get_css(rule_name);
+ },
+ remove_css : function(rule_name, sheet) {
+ return $.vakata.css.get_css(rule_name, true, sheet);
+ },
+ add_sheet : function(opts) {
+ var tmp = false, is_new = true;
+ if(opts.str) {
+ if(opts.title) { tmp = $("style[id='" + opts.title + "-stylesheet']")[0]; }
+ if(tmp) { is_new = false; }
+ else {
+ tmp = document.createElement("style");
+ tmp.setAttribute('type',"text/css");
+ if(opts.title) { tmp.setAttribute("id", opts.title + "-stylesheet"); }
+ }
+ if(tmp.styleSheet) {
+ if(is_new) {
+ document.getElementsByTagName("head")[0].appendChild(tmp);
+ tmp.styleSheet.cssText = opts.str;
+ }
+ else {
+ tmp.styleSheet.cssText = tmp.styleSheet.cssText + " " + opts.str;
+ }
+ }
+ else {
+ tmp.appendChild(document.createTextNode(opts.str));
+ document.getElementsByTagName("head")[0].appendChild(tmp);
+ }
+ return tmp.sheet || tmp.styleSheet;
+ }
+ if(opts.url) {
+ if(document.createStyleSheet) {
+ try { tmp = document.createStyleSheet(opts.url); } catch (e) { }
+ }
+ else {
+ tmp = document.createElement('link');
+ tmp.rel = 'stylesheet';
+ tmp.type = 'text/css';
+ tmp.media = "all";
+ tmp.href = opts.url;
+ document.getElementsByTagName("head")[0].appendChild(tmp);
+ return tmp.styleSheet;
+ }
+ }
+ }
+ };
+
+ // private variables
+ var instances = [], // instance array (used by $.jstree.reference/create/focused)
+ focused_instance = -1, // the index in the instance array of the currently focused instance
+ plugins = {}, // list of included plugins
+ prepared_move = {}; // for the move_node function
+
+ // jQuery plugin wrapper (thanks to jquery UI widget function)
+ $.fn.jstree = function (settings) {
+ var isMethodCall = (typeof settings == 'string'), // is this a method call like $().jstree("open_node")
+ args = Array.prototype.slice.call(arguments, 1),
+ returnValue = this;
+
+ // if a method call execute the method on all selected instances
+ if(isMethodCall) {
+ if(settings.substring(0, 1) == '_') { return returnValue; }
+ this.each(function() {
+ var instance = instances[$.data(this, "jstree_instance_id")],
+ methodValue = (instance && $.isFunction(instance[settings])) ? instance[settings].apply(instance, args) : instance;
+ if(typeof methodValue !== "undefined" && (settings.indexOf("is_") === 0 || (methodValue !== true && methodValue !== false))) { returnValue = methodValue; return false; }
+ });
+ }
+ else {
+ this.each(function() {
+ // extend settings and allow for multiple hashes and $.data
+ var instance_id = $.data(this, "jstree_instance_id"),
+ a = [],
+ b = settings ? $.extend({}, true, settings) : {},
+ c = $(this),
+ s = false,
+ t = [];
+ a = a.concat(args);
+ if(c.data("jstree")) { a.push(c.data("jstree")); }
+ b = a.length ? $.extend.apply(null, [true, b].concat(a)) : b;
+
+ // if an instance already exists, destroy it first
+ if(typeof instance_id !== "undefined" && instances[instance_id]) { instances[instance_id].destroy(); }
+ // push a new empty object to the instances array
+ instance_id = parseInt(instances.push({}),10) - 1;
+ // store the jstree instance id to the container element
+ $.data(this, "jstree_instance_id", instance_id);
+ // clean up all plugins
+ b.plugins = $.isArray(b.plugins) ? b.plugins : $.jstree.defaults.plugins.slice();
+ b.plugins.unshift("core");
+ // only unique plugins
+ b.plugins = b.plugins.sort().join(",,").replace(/(,|^)([^,]+)(,,\2)+(,|$)/g,"$1$2$4").replace(/,,+/g,",").replace(/,$/,"").split(",");
+
+ // extend defaults with passed data
+ s = $.extend(true, {}, $.jstree.defaults, b);
+ s.plugins = b.plugins;
+ $.each(plugins, function (i, val) {
+ if($.inArray(i, s.plugins) === -1) { s[i] = null; delete s[i]; }
+ else { t.push(i); }
+ });
+ s.plugins = t;
+
+ // push the new object to the instances array (at the same time set the default classes to the container) and init
+ instances[instance_id] = new $.jstree._instance(instance_id, $(this).addClass("jstree jstree-" + instance_id), s);
+ // init all activated plugins for this instance
+ $.each(instances[instance_id]._get_settings().plugins, function (i, val) { instances[instance_id].data[val] = {}; });
+ $.each(instances[instance_id]._get_settings().plugins, function (i, val) { if(plugins[val]) { plugins[val].__init.apply(instances[instance_id]); } });
+ // initialize the instance
+ setTimeout(function() { if(instances[instance_id]) { instances[instance_id].init(); } }, 0);
+ });
+ }
+ // return the jquery selection (or if it was a method call that returned a value - the returned value)
+ return returnValue;
+ };
+ // object to store exposed functions and objects
+ $.jstree = {
+ defaults : {
+ plugins : []
+ },
+ _focused : function () { return instances[focused_instance] || null; },
+ _reference : function (needle) {
+ // get by instance id
+ if(instances[needle]) { return instances[needle]; }
+ // get by DOM (if still no luck - return null
+ var o = $(needle);
+ if(!o.length && typeof needle === "string") { o = $("#" + needle); }
+ if(!o.length) { return null; }
+ return instances[o.closest(".jstree").data("jstree_instance_id")] || null;
+ },
+ _instance : function (index, container, settings) {
+ // for plugins to store data in
+ this.data = { core : {} };
+ this.get_settings = function () { return $.extend(true, {}, settings); };
+ this._get_settings = function () { return settings; };
+ this.get_index = function () { return index; };
+ this.get_container = function () { return container; };
+ this.get_container_ul = function () { return container.children("ul:eq(0)"); };
+ this._set_settings = function (s) {
+ settings = $.extend(true, {}, settings, s);
+ };
+ },
+ _fn : { },
+ plugin : function (pname, pdata) {
+ pdata = $.extend({}, {
+ __init : $.noop,
+ __destroy : $.noop,
+ _fn : {},
+ defaults : false
+ }, pdata);
+ plugins[pname] = pdata;
+
+ $.jstree.defaults[pname] = pdata.defaults;
+ $.each(pdata._fn, function (i, val) {
+ val.plugin = pname;
+ val.old = $.jstree._fn[i];
+ $.jstree._fn[i] = function () {
+ var rslt,
+ func = val,
+ args = Array.prototype.slice.call(arguments),
+ evnt = new $.Event("before.jstree"),
+ rlbk = false;
+
+ if(this.data.core.locked === true && i !== "unlock" && i !== "is_locked") { return; }
+
+ // Check if function belongs to the included plugins of this instance
+ do {
+ if(func && func.plugin && $.inArray(func.plugin, this._get_settings().plugins) !== -1) { break; }
+ func = func.old;
+ } while(func);
+ if(!func) { return; }
+
+ // context and function to trigger events, then finally call the function
+ if(i.indexOf("_") === 0) {
+ rslt = func.apply(this, args);
+ }
+ else {
+ rslt = this.get_container().triggerHandler(evnt, { "func" : i, "inst" : this, "args" : args, "plugin" : func.plugin });
+ if(rslt === false) { return; }
+ if(typeof rslt !== "undefined") { args = rslt; }
+
+ rslt = func.apply(
+ $.extend({}, this, {
+ __callback : function (data) {
+ this.get_container().triggerHandler( i + '.jstree', { "inst" : this, "args" : args, "rslt" : data, "rlbk" : rlbk });
+ },
+ __rollback : function () {
+ rlbk = this.get_rollback();
+ return rlbk;
+ },
+ __call_old : function (replace_arguments) {
+ return func.old.apply(this, (replace_arguments ? Array.prototype.slice.call(arguments, 1) : args ) );
+ }
+ }), args);
+ }
+
+ // return the result
+ return rslt;
+ };
+ $.jstree._fn[i].old = val.old;
+ $.jstree._fn[i].plugin = pname;
+ });
+ },
+ rollback : function (rb) {
+ if(rb) {
+ if(!$.isArray(rb)) { rb = [ rb ]; }
+ $.each(rb, function (i, val) {
+ instances[val.i].set_rollback(val.h, val.d);
+ });
+ }
+ }
+ };
+ // set the prototype for all instances
+ $.jstree._fn = $.jstree._instance.prototype = {};
+
+ // load the css when DOM is ready
+ $(function() {
+ // code is copied from jQuery ($.browser is deprecated + there is a bug in IE)
+ var u = navigator.userAgent.toLowerCase(),
+ v = (u.match( /.+?(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [0,'0'])[1],
+ css_string = '' +
+ '.jstree ul, .jstree li { display:block; margin:0 0 0 0; padding:0 0 0 0; list-style-type:none; } ' +
+ '.jstree li { display:block; min-height:18px; line-height:18px; white-space:nowrap; margin-left:18px; min-width:18px; } ' +
+ '.jstree-rtl li { margin-left:0; margin-right:18px; } ' +
+ '.jstree > ul > li { margin-left:0px; } ' +
+ '.jstree-rtl > ul > li { margin-right:0px; } ' +
+ '.jstree ins { display:inline-block; text-decoration:none; width:18px; height:18px; margin:0 0 0 0; padding:0; } ' +
+ '.jstree a { display:inline-block; line-height:16px; height:16px; color:black; white-space:nowrap; text-decoration:none; padding:1px 2px; margin:0; } ' +
+ '.jstree a:focus { outline: none; } ' +
+ '.jstree a > ins { height:16px; width:16px; } ' +
+ '.jstree a > .jstree-icon { margin-right:3px; } ' +
+ '.jstree-rtl a > .jstree-icon { margin-left:3px; margin-right:0; } ' +
+ 'li.jstree-open > ul { display:block; } ' +
+ 'li.jstree-closed > ul { display:none; } ';
+ // Correct IE 6 (does not support the > CSS selector)
+ if(/msie/.test(u) && parseInt(v, 10) == 6) {
+ is_ie6 = true;
+
+ // fix image flicker and lack of caching
+ try {
+ document.execCommand("BackgroundImageCache", false, true);
+ } catch (err) { }
+
+ css_string += '' +
+ '.jstree li { height:18px; margin-left:0; margin-right:0; } ' +
+ '.jstree li li { margin-left:18px; } ' +
+ '.jstree-rtl li li { margin-left:0px; margin-right:18px; } ' +
+ 'li.jstree-open ul { display:block; } ' +
+ 'li.jstree-closed ul { display:none !important; } ' +
+ '.jstree li a { display:inline; border-width:0 !important; padding:0px 2px !important; } ' +
+ '.jstree li a ins { height:16px; width:16px; margin-right:3px; } ' +
+ '.jstree-rtl li a ins { margin-right:0px; margin-left:3px; } ';
+ }
+ // Correct IE 7 (shifts anchor nodes onhover)
+ if(/msie/.test(u) && parseInt(v, 10) == 7) {
+ is_ie7 = true;
+ css_string += '.jstree li a { border-width:0 !important; padding:0px 2px !important; } ';
+ }
+ // correct ff2 lack of display:inline-block
+ if(!/compatible/.test(u) && /mozilla/.test(u) && parseFloat(v, 10) < 1.9) {
+ is_ff2 = true;
+ css_string += '' +
+ '.jstree ins { display:-moz-inline-box; } ' +
+ '.jstree li { line-height:12px; } ' + // WHY??
+ '.jstree a { display:-moz-inline-box; } ' +
+ '.jstree .jstree-no-icons .jstree-checkbox { display:-moz-inline-stack !important; } ';
+ /* this shouldn't be here as it is theme specific */
+ }
+ // the default stylesheet
+ $.vakata.css.add_sheet({ str : css_string, title : "jstree" });
+ });
+
+ // core functions (open, close, create, update, delete)
+ $.jstree.plugin("core", {
+ __init : function () {
+ this.data.core.locked = false;
+ this.data.core.to_open = this.get_settings().core.initially_open;
+ this.data.core.to_load = this.get_settings().core.initially_load;
+ },
+ defaults : {
+ html_titles : false,
+ animation : 500,
+ initially_open : [],
+ initially_load : [],
+ open_parents : true,
+ notify_plugins : true,
+ rtl : false,
+ load_open : false,
+ strings : {
+ loading : "Loading ...",
+ new_node : "New node",
+ multiple_selection : "Multiple selection"
+ }
+ },
+ _fn : {
+ init : function () {
+ this.set_focus();
+ if(this._get_settings().core.rtl) {
+ this.get_container().addClass("jstree-rtl").css("direction", "rtl");
+ }
+ this.get_container().html("<ul><li class='jstree-last jstree-leaf'><ins>&#160;</ins><a class='jstree-loading' href='#'><ins class='jstree-icon'>&#160;</ins>" + this._get_string("loading") + "</a></li></ul>");
+ this.data.core.li_height = this.get_container_ul().find("li.jstree-closed, li.jstree-leaf").eq(0).height() || 18;
+
+ this.get_container()
+ .delegate("li > ins", "click.jstree", $.proxy(function (event) {
+ var trgt = $(event.target);
+ // if(trgt.is("ins") && event.pageY - trgt.offset().top < this.data.core.li_height) { this.toggle_node(trgt); }
+ this.toggle_node(trgt);
+ }, this))
+ .bind("mousedown.jstree", $.proxy(function () {
+ this.set_focus(); // This used to be setTimeout(set_focus,0) - why?
+ }, this))
+ .bind("dblclick.jstree", function (event) {
+ var sel;
+ if(document.selection && document.selection.empty) { document.selection.empty(); }
+ else {
+ if(window.getSelection) {
+ sel = window.getSelection();
+ try {
+ sel.removeAllRanges();
+ sel.collapse();
+ } catch (err) { }
+ }
+ }
+ });
+ if(this._get_settings().core.notify_plugins) {
+ this.get_container()
+ .bind("load_node.jstree", $.proxy(function (e, data) {
+ var o = this._get_node(data.rslt.obj),
+ t = this;
+ if(o === -1) { o = this.get_container_ul(); }
+ if(!o.length) { return; }
+ o.find("li").each(function () {
+ var th = $(this);
+ if(th.data("jstree")) {
+ $.each(th.data("jstree"), function (plugin, values) {
+ if(t.data[plugin] && $.isFunction(t["_" + plugin + "_notify"])) {
+ t["_" + plugin + "_notify"].call(t, th, values);
+ }
+ });
+ }
+ });
+ }, this));
+ }
+ if(this._get_settings().core.load_open) {
+ this.get_container()
+ .bind("load_node.jstree", $.proxy(function (e, data) {
+ var o = this._get_node(data.rslt.obj),
+ t = this;
+ if(o === -1) { o = this.get_container_ul(); }
+ if(!o.length) { return; }
+ o.find("li.jstree-open:not(:has(ul))").each(function () {
+ t.load_node(this, $.noop, $.noop);
+ });
+ }, this));
+ }
+ this.__callback();
+ this.load_node(-1, function () { this.loaded(); this.reload_nodes(); });
+ },
+ destroy : function () {
+ var i,
+ n = this.get_index(),
+ s = this._get_settings(),
+ _this = this;
+
+ $.each(s.plugins, function (i, val) {
+ try { plugins[val].__destroy.apply(_this); } catch(err) { }
+ });
+ this.__callback();
+ // set focus to another instance if this one is focused
+ if(this.is_focused()) {
+ for(i in instances) {
+ if(instances.hasOwnProperty(i) && i != n) {
+ instances[i].set_focus();
+ break;
+ }
+ }
+ }
+ // if no other instance found
+ if(n === focused_instance) { focused_instance = -1; }
+ // remove all traces of jstree in the DOM (only the ones set using jstree*) and cleans all events
+ this.get_container()
+ .unbind(".jstree")
+ .undelegate(".jstree")
+ .removeData("jstree_instance_id")
+ .find("[class^='jstree']")
+ .addBack()
+ .attr("class", function () { return this.className.replace(/jstree[^ ]*|$/ig,''); });
+ $(document)
+ .unbind(".jstree-" + n)
+ .undelegate(".jstree-" + n);
+ // remove the actual data
+ instances[n] = null;
+ delete instances[n];
+ },
+
+ _core_notify : function (n, data) {
+ if(data.opened) {
+ this.open_node(n, false, true);
+ }
+ },
+
+ lock : function () {
+ this.data.core.locked = true;
+ this.get_container().children("ul").addClass("jstree-locked").css("opacity","0.7");
+ this.__callback({});
+ },
+ unlock : function () {
+ this.data.core.locked = false;
+ this.get_container().children("ul").removeClass("jstree-locked").css("opacity","1");
+ this.__callback({});
+ },
+ is_locked : function () { return this.data.core.locked; },
+ save_opened : function () {
+ var _this = this;
+ this.data.core.to_open = [];
+ this.get_container_ul().find("li.jstree-open").each(function () {
+ if(this.id) { _this.data.core.to_open.push("#" + this.id.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:")); }
+ });
+ this.__callback(_this.data.core.to_open);
+ },
+ save_loaded : function () { },
+ reload_nodes : function (is_callback) {
+ var _this = this,
+ done = true,
+ current = [],
+ remaining = [];
+ if(!is_callback) {
+ this.data.core.reopen = false;
+ this.data.core.refreshing = true;
+ this.data.core.to_open = $.map($.makeArray(this.data.core.to_open), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); });
+ this.data.core.to_load = $.map($.makeArray(this.data.core.to_load), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); });
+ if(this.data.core.to_open.length) {
+ this.data.core.to_load = this.data.core.to_load.concat(this.data.core.to_open);
+ }
+ }
+ if(this.data.core.to_load.length) {
+ $.each(this.data.core.to_load, function (i, val) {
+ if(val == "#") { return true; }
+ if($(val).length) { current.push(val); }
+ else { remaining.push(val); }
+ });
+ if(current.length) {
+ this.data.core.to_load = remaining;
+ $.each(current, function (i, val) {
+ if(!_this._is_loaded(val)) {
+ _this.load_node(val, function () { _this.reload_nodes(true); }, function () { _this.reload_nodes(true); });
+ done = false;
+ }
+ });
+ }
+ }
+ if(this.data.core.to_open.length) {
+ $.each(this.data.core.to_open, function (i, val) {
+ _this.open_node(val, false, true);
+ });
+ }
+ if(done) {
+ // TODO: find a more elegant approach to syncronizing returning requests
+ if(this.data.core.reopen) { clearTimeout(this.data.core.reopen); }
+ this.data.core.reopen = setTimeout(function () { _this.__callback({}, _this); }, 50);
+ this.data.core.refreshing = false;
+ this.reopen();
+ }
+ },
+ reopen : function () {
+ var _this = this;
+ if(this.data.core.to_open.length) {
+ $.each(this.data.core.to_open, function (i, val) {
+ _this.open_node(val, false, true);
+ });
+ }
+ this.__callback({});
+ },
+ refresh : function (obj) {
+ var _this = this;
+ this.save_opened();
+ if(!obj) { obj = -1; }
+ obj = this._get_node(obj);
+ if(!obj) { obj = -1; }
+ if(obj !== -1) { obj.children("UL").remove(); }
+ else { this.get_container_ul().empty(); }
+ this.load_node(obj, function () { _this.__callback({ "obj" : obj}); _this.reload_nodes(); });
+ },
+ // Dummy function to fire after the first load (so that there is a jstree.loaded event)
+ loaded : function () {
+ this.__callback();
+ },
+ // deal with focus
+ set_focus : function () {
+ if(this.is_focused()) { return; }
+ var f = $.jstree._focused();
+ if(f) { f.unset_focus(); }
+
+ this.get_container().addClass("jstree-focused");
+ focused_instance = this.get_index();
+ this.__callback();
+ },
+ is_focused : function () {
+ return focused_instance == this.get_index();
+ },
+ unset_focus : function () {
+ if(this.is_focused()) {
+ this.get_container().removeClass("jstree-focused");
+ focused_instance = -1;
+ }
+ this.__callback();
+ },
+
+ // traverse
+ _get_node : function (obj) {
+ var $obj = $(obj, this.get_container());
+ if($obj.is(".jstree") || obj == -1) { return -1; }
+ $obj = $obj.closest("li", this.get_container());
+ return $obj.length ? $obj : false;
+ },
+ _get_next : function (obj, strict) {
+ obj = this._get_node(obj);
+ if(obj === -1) { return this.get_container().find("> ul > li:first-child"); }
+ if(!obj.length) { return false; }
+ if(strict) { return (obj.nextAll("li").size() > 0) ? obj.nextAll("li:eq(0)") : false; }
+
+ if(obj.hasClass("jstree-open")) { return obj.find("li:eq(0)"); }
+ else if(obj.nextAll("li").size() > 0) { return obj.nextAll("li:eq(0)"); }
+ else { return obj.parentsUntil(".jstree","li").next("li").eq(0); }
+ },
+ _get_prev : function (obj, strict) {
+ obj = this._get_node(obj);
+ if(obj === -1) { return this.get_container().find("> ul > li:last-child"); }
+ if(!obj.length) { return false; }
+ if(strict) { return (obj.prevAll("li").length > 0) ? obj.prevAll("li:eq(0)") : false; }
+
+ if(obj.prev("li").length) {
+ obj = obj.prev("li").eq(0);
+ while(obj.hasClass("jstree-open")) { obj = obj.children("ul:eq(0)").children("li:last"); }
+ return obj;
+ }
+ else { var o = obj.parentsUntil(".jstree","li:eq(0)"); return o.length ? o : false; }
+ },
+ _get_parent : function (obj) {
+ obj = this._get_node(obj);
+ if(obj == -1 || !obj.length) { return false; }
+ var o = obj.parentsUntil(".jstree", "li:eq(0)");
+ return o.length ? o : -1;
+ },
+ _get_children : function (obj) {
+ obj = this._get_node(obj);
+ if(obj === -1) { return this.get_container().children("ul:eq(0)").children("li"); }
+ if(!obj.length) { return false; }
+ return obj.children("ul:eq(0)").children("li");
+ },
+ get_path : function (obj, id_mode) {
+ var p = [],
+ _this = this;
+ obj = this._get_node(obj);
+ if(obj === -1 || !obj || !obj.length) { return false; }
+ obj.parentsUntil(".jstree", "li").each(function () {
+ p.push( id_mode ? this.id : _this.get_text(this) );
+ });
+ p.reverse();
+ p.push( id_mode ? obj.attr("id") : this.get_text(obj) );
+ return p;
+ },
+
+ // string functions
+ _get_string : function (key) {
+ return this._get_settings().core.strings[key] || key;
+ },
+
+ is_open : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-open"); },
+ is_closed : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-closed"); },
+ is_leaf : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-leaf"); },
+ correct_state : function (obj) {
+ obj = this._get_node(obj);
+ if(!obj || obj === -1) { return false; }
+ obj.removeClass("jstree-closed jstree-open").addClass("jstree-leaf").children("ul").remove();
+ this.__callback({ "obj" : obj });
+ },
+ // open/close
+ open_node : function (obj, callback, skip_animation) {
+ obj = this._get_node(obj);
+ if(!obj.length) { return false; }
+ if(!obj.hasClass("jstree-closed")) { if(callback) { callback.call(); } return false; }
+ var s = skip_animation || is_ie6 ? 0 : this._get_settings().core.animation,
+ t = this;
+ if(!this._is_loaded(obj)) {
+ obj.children("a").addClass("jstree-loading");
+ this.load_node(obj, function () { t.open_node(obj, callback, skip_animation); }, callback);
+ }
+ else {
+ if(this._get_settings().core.open_parents) {
+ obj.parentsUntil(".jstree",".jstree-closed").each(function () {
+ t.open_node(this, false, true);
+ });
+ }
+ if(s) { obj.children("ul").css("display","none"); }
+ obj.removeClass("jstree-closed").addClass("jstree-open").children("a").removeClass("jstree-loading");
+ if(s) { obj.children("ul").stop(true, true).slideDown(s, function () { this.style.display = ""; t.after_open(obj); }); }
+ else { t.after_open(obj); }
+ this.__callback({ "obj" : obj });
+ if(callback) { callback.call(); }
+ }
+ },
+ after_open : function (obj) { this.__callback({ "obj" : obj }); },
+ close_node : function (obj, skip_animation) {
+ obj = this._get_node(obj);
+ var s = skip_animation || is_ie6 ? 0 : this._get_settings().core.animation,
+ t = this;
+ if(!obj.length || !obj.hasClass("jstree-open")) { return false; }
+ if(s) { obj.children("ul").attr("style","display:block !important"); }
+ obj.removeClass("jstree-open").addClass("jstree-closed");
+ if(s) { obj.children("ul").stop(true, true).slideUp(s, function () { this.style.display = ""; t.after_close(obj); }); }
+ else { t.after_close(obj); }
+ this.__callback({ "obj" : obj });
+ },
+ after_close : function (obj) { this.__callback({ "obj" : obj }); },
+ toggle_node : function (obj) {
+ obj = this._get_node(obj);
+ if(obj.hasClass("jstree-closed")) { return this.open_node(obj); }
+ if(obj.hasClass("jstree-open")) { return this.close_node(obj); }
+ },
+ open_all : function (obj, do_animation, original_obj) {
+ obj = obj ? this._get_node(obj) : -1;
+ if(!obj || obj === -1) { obj = this.get_container_ul(); }
+ if(original_obj) {
+ obj = obj.find("li.jstree-closed");
+ }
+ else {
+ original_obj = obj;
+ if(obj.is(".jstree-closed")) { obj = obj.find("li.jstree-closed").addBack(); }
+ else { obj = obj.find("li.jstree-closed"); }
+ }
+ var _this = this;
+ obj.each(function () {
+ var __this = this;
+ if(!_this._is_loaded(this)) { _this.open_node(this, function() { _this.open_all(__this, do_animation, original_obj); }, !do_animation); }
+ else { _this.open_node(this, false, !do_animation); }
+ });
+ // so that callback is fired AFTER all nodes are open
+ if(original_obj.find('li.jstree-closed').length === 0) { this.__callback({ "obj" : original_obj }); }
+ },
+ close_all : function (obj, do_animation) {
+ var _this = this;
+ obj = obj ? this._get_node(obj) : this.get_container();
+ if(!obj || obj === -1) { obj = this.get_container_ul(); }
+ obj.find("li.jstree-open").addBack().each(function () { _this.close_node(this, !do_animation); });
+ this.__callback({ "obj" : obj });
+ },
+ clean_node : function (obj) {
+ obj = obj && obj != -1 ? $(obj) : this.get_container_ul();
+ obj = obj.is("li") ? obj.find("li").addBack() : obj.find("li");
+ obj.removeClass("jstree-last")
+ .filter("li:last-child").addClass("jstree-last").end()
+ .filter(":has(li)")
+ .not(".jstree-open").removeClass("jstree-leaf").addClass("jstree-closed");
+ obj.not(".jstree-open, .jstree-closed").addClass("jstree-leaf").children("ul").remove();
+ this.__callback({ "obj" : obj });
+ },
+ // rollback
+ get_rollback : function () {
+ this.__callback();
+ return { i : this.get_index(), h : this.get_container().children("ul").clone(true), d : this.data };
+ },
+ set_rollback : function (html, data) {
+ this.get_container().empty().append(html);
+ this.data = data;
+ this.__callback();
+ },
+ // Dummy functions to be overwritten by any datastore plugin included
+ load_node : function (obj, s_call, e_call) { this.__callback({ "obj" : obj }); },
+ _is_loaded : function (obj) { return true; },
+
+ // Basic operations: create
+ create_node : function (obj, position, js, callback, is_loaded) {
+ obj = this._get_node(obj);
+ position = typeof position === "undefined" ? "last" : position;
+ var d = $("<li />"),
+ s = this._get_settings().core,
+ tmp;
+
+ if(obj !== -1 && !obj.length) { return false; }
+ if(!is_loaded && !this._is_loaded(obj)) { this.load_node(obj, function () { this.create_node(obj, position, js, callback, true); }); return false; }
+
+ this.__rollback();
+
+ if(typeof js === "string") { js = { "data" : js }; }
+ if(!js) { js = {}; }
+ if(js.attr) { d.attr(js.attr); }
+ if(js.metadata) { d.data(js.metadata); }
+ if(js.state) { d.addClass("jstree-" + js.state); }
+ if(!js.data) { js.data = this._get_string("new_node"); }
+ if(!$.isArray(js.data)) { tmp = js.data; js.data = []; js.data.push(tmp); }
+ $.each(js.data, function (i, m) {
+ tmp = $("<a />");
+ if($.isFunction(m)) { m = m.call(this, js); }
+ if(typeof m == "string") { tmp.attr('href','#')[ s.html_titles ? "html" : "text" ](m); }
+ else {
+ if(!m.attr) { m.attr = {}; }
+ if(!m.attr.href) { m.attr.href = '#'; }
+ tmp.attr(m.attr)[ s.html_titles ? "html" : "text" ](m.title);
+ if(m.language) { tmp.addClass(m.language); }
+ }
+ tmp.prepend("<ins class='jstree-icon'>&#160;</ins>");
+ if(!m.icon && js.icon) { m.icon = js.icon; }
+ if(m.icon) {
+ if(m.icon.indexOf("/") === -1) { tmp.children("ins").addClass(m.icon); }
+ else { tmp.children("ins").css("background","url('" + m.icon + "') center center no-repeat"); }
+ }
+ d.append(tmp);
+ });
+ d.prepend("<ins class='jstree-icon'>&#160;</ins>");
+ if(obj === -1) {
+ obj = this.get_container();
+ if(position === "before") { position = "first"; }
+ if(position === "after") { position = "last"; }
+ }
+ switch(position) {
+ case "before": obj.before(d); tmp = this._get_parent(obj); break;
+ case "after" : obj.after(d); tmp = this._get_parent(obj); break;
+ case "inside":
+ case "first" :
+ if(!obj.children("ul").length) { obj.append("<ul />"); }
+ obj.children("ul").prepend(d);
+ tmp = obj;
+ break;
+ case "last":
+ if(!obj.children("ul").length) { obj.append("<ul />"); }
+ obj.children("ul").append(d);
+ tmp = obj;
+ break;
+ default:
+ if(!obj.children("ul").length) { obj.append("<ul />"); }
+ if(!position) { position = 0; }
+ tmp = obj.children("ul").children("li").eq(position);
+ if(tmp.length) { tmp.before(d); }
+ else { obj.children("ul").append(d); }
+ tmp = obj;
+ break;
+ }
+ if(tmp === -1 || tmp.get(0) === this.get_container().get(0)) { tmp = -1; }
+ this.clean_node(tmp);
+ this.__callback({ "obj" : d, "parent" : tmp });
+ if(callback) { callback.call(this, d); }
+ return d;
+ },
+ // Basic operations: rename (deal with text)
+ get_text : function (obj) {
+ obj = this._get_node(obj);
+ if(!obj.length) { return false; }
+ var s = this._get_settings().core.html_titles;
+ obj = obj.children("a:eq(0)");
+ if(s) {
+ obj = obj.clone();
+ obj.children("INS").remove();
+ return obj.html();
+ }
+ else {
+ obj = obj.contents().filter(function() { return this.nodeType == 3; })[0];
+ return obj.nodeValue;
+ }
+ },
+ set_text : function (obj, val) {
+ obj = this._get_node(obj);
+ if(!obj.length) { return false; }
+ obj = obj.children("a:eq(0)");
+ if(this._get_settings().core.html_titles) {
+ var tmp = obj.children("INS").clone();
+ obj.html(val).prepend(tmp);
+ this.__callback({ "obj" : obj, "name" : val });
+ return true;
+ }
+ else {
+ obj = obj.contents().filter(function() { return this.nodeType == 3; })[0];
+ this.__callback({ "obj" : obj, "name" : val });
+ return (obj.nodeValue = val);
+ }
+ },
+ rename_node : function (obj, val) {
+ obj = this._get_node(obj);
+ this.__rollback();
+ if(obj && obj.length && this.set_text.apply(this, Array.prototype.slice.call(arguments))) { this.__callback({ "obj" : obj, "name" : val }); }
+ },
+ // Basic operations: deleting nodes
+ delete_node : function (obj) {
+ obj = this._get_node(obj);
+ if(!obj.length) { return false; }
+ this.__rollback();
+ var p = this._get_parent(obj), prev = $([]), t = this;
+ obj.each(function () {
+ prev = prev.add(t._get_prev(this));
+ });
+ obj = obj.detach();
+ if(p !== -1 && p.find("> ul > li").length === 0) {
+ p.removeClass("jstree-open jstree-closed").addClass("jstree-leaf");
+ }
+ this.clean_node(p);
+ this.__callback({ "obj" : obj, "prev" : prev, "parent" : p });
+ return obj;
+ },
+ prepare_move : function (o, r, pos, cb, is_cb) {
+ var p = {};
+
+ p.ot = $.jstree._reference(o) || this;
+ p.o = p.ot._get_node(o);
+ p.r = r === - 1 ? -1 : this._get_node(r);
+ p.p = (typeof pos === "undefined" || pos === false) ? "last" : pos; // TODO: move to a setting
+ if(!is_cb && prepared_move.o && prepared_move.o[0] === p.o[0] && prepared_move.r[0] === p.r[0] && prepared_move.p === p.p) {
+ this.__callback(prepared_move);
+ if(cb) { cb.call(this, prepared_move); }
+ return;
+ }
+ p.ot = $.jstree._reference(p.o) || this;
+ p.rt = $.jstree._reference(p.r) || this; // r === -1 ? p.ot : $.jstree._reference(p.r) || this
+ if(p.r === -1 || !p.r) {
+ p.cr = -1;
+ switch(p.p) {
+ case "first":
+ case "before":
+ case "inside":
+ p.cp = 0;
+ break;
+ case "after":
+ case "last":
+ p.cp = p.rt.get_container().find(" > ul > li").length;
+ break;
+ default:
+ p.cp = p.p;
+ break;
+ }
+ }
+ else {
+ if(!/^(before|after)$/.test(p.p) && !this._is_loaded(p.r)) {
+ return this.load_node(p.r, function () { this.prepare_move(o, r, pos, cb, true); });
+ }
+ switch(p.p) {
+ case "before":
+ p.cp = p.r.index();
+ p.cr = p.rt._get_parent(p.r);
+ break;
+ case "after":
+ p.cp = p.r.index() + 1;
+ p.cr = p.rt._get_parent(p.r);
+ break;
+ case "inside":
+ case "first":
+ p.cp = 0;
+ p.cr = p.r;
+ break;
+ case "last":
+ p.cp = p.r.find(" > ul > li").length;
+ p.cr = p.r;
+ break;
+ default:
+ p.cp = p.p;
+ p.cr = p.r;
+ break;
+ }
+ }
+ p.np = p.cr == -1 ? p.rt.get_container() : p.cr;
+ p.op = p.ot._get_parent(p.o);
+ p.cop = p.o.index();
+ if(p.op === -1) { p.op = p.ot ? p.ot.get_container() : this.get_container(); }
+ if(!/^(before|after)$/.test(p.p) && p.op && p.np && p.op[0] === p.np[0] && p.o.index() < p.cp) { p.cp++; }
+ //if(p.p === "before" && p.op && p.np && p.op[0] === p.np[0] && p.o.index() < p.cp) { p.cp--; }
+ p.or = p.np.find(" > ul > li:nth-child(" + (p.cp + 1) + ")");
+ prepared_move = p;
+ this.__callback(prepared_move);
+ if(cb) { cb.call(this, prepared_move); }
+ },
+ check_move : function () {
+ var obj = prepared_move, ret = true, r = obj.r === -1 ? this.get_container() : obj.r;
+ if(!obj || !obj.o || obj.or[0] === obj.o[0]) { return false; }
+ if(!obj.cy) {
+ if(obj.op && obj.np && obj.op[0] === obj.np[0] && obj.cp - 1 === obj.o.index()) { return false; }
+ obj.o.each(function () {
+ if(r.parentsUntil(".jstree", "li").addBack().index(this) !== -1) { ret = false; return false; }
+ });
+ }
+ return ret;
+ },
+ move_node : function (obj, ref, position, is_copy, is_prepared, skip_check) {
+ if(!is_prepared) {
+ return this.prepare_move(obj, ref, position, function (p) {
+ this.move_node(p, false, false, is_copy, true, skip_check);
+ });
+ }
+ if(is_copy) {
+ prepared_move.cy = true;
+ }
+ if(!skip_check && !this.check_move()) { return false; }
+
+ this.__rollback();
+ var o = false;
+ if(is_copy) {
+ o = obj.o.clone(true);
+ o.find("*[id]").addBack().each(function () {
+ if(this.id) { this.id = "copy_" + this.id; }
+ });
+ }
+ else { o = obj.o; }
+
+ if(obj.or.length) { obj.or.before(o); }
+ else {
+ if(!obj.np.children("ul").length) { $("<ul />").appendTo(obj.np); }
+ obj.np.children("ul:eq(0)").append(o);
+ }
+
+ try {
+ obj.ot.clean_node(obj.op);
+ obj.rt.clean_node(obj.np);
+ if(!obj.op.find("> ul > li").length) {
+ obj.op.removeClass("jstree-open jstree-closed").addClass("jstree-leaf").children("ul").remove();
+ }
+ } catch (e) { }
+
+ if(is_copy) {
+ prepared_move.cy = true;
+ prepared_move.oc = o;
+ }
+ this.__callback(prepared_move);
+ return prepared_move;
+ },
+ _get_move : function () { return prepared_move; }
+ }
+ });
+})(jQuery);
+//*/
+
+/*
+ * jsTree ui plugin
+ * This plugins handles selecting/deselecting/hovering/dehovering nodes
+ */
+(function ($) {
+ var scrollbar_width, e1, e2;
+ $(function() {
+ if (/msie/.test(navigator.userAgent.toLowerCase())) {
+ e1 = $('<textarea cols="10" rows="2"></textarea>').css({ position: 'absolute', top: -1000, left: 0 }).appendTo('body');
+ e2 = $('<textarea cols="10" rows="2" style="overflow: hidden;"></textarea>').css({ position: 'absolute', top: -1000, left: 0 }).appendTo('body');
+ scrollbar_width = e1.width() - e2.width();
+ e1.add(e2).remove();
+ }
+ else {
+ e1 = $('<div />').css({ width: 100, height: 100, overflow: 'auto', position: 'absolute', top: -1000, left: 0 })
+ .prependTo('body').append('<div />').find('div').css({ width: '100%', height: 200 });
+ scrollbar_width = 100 - e1.width();
+ e1.parent().remove();
+ }
+ });
+ $.jstree.plugin("ui", {
+ __init : function () {
+ this.data.ui.selected = $();
+ this.data.ui.last_selected = false;
+ this.data.ui.hovered = null;
+ this.data.ui.to_select = this.get_settings().ui.initially_select;
+
+ this.get_container()
+ .delegate("a", "click.jstree", $.proxy(function (event) {
+ event.preventDefault();
+ event.currentTarget.blur();
+ if(!$(event.currentTarget).hasClass("jstree-loading")) {
+ this.select_node(event.currentTarget, true, event);
+ }
+ }, this))
+ .delegate("a", "mouseenter.jstree", $.proxy(function (event) {
+ if(!$(event.currentTarget).hasClass("jstree-loading")) {
+ this.hover_node(event.target);
+ }
+ }, this))
+ .delegate("a", "mouseleave.jstree", $.proxy(function (event) {
+ if(!$(event.currentTarget).hasClass("jstree-loading")) {
+ this.dehover_node(event.target);
+ }
+ }, this))
+ .bind("reopen.jstree", $.proxy(function () {
+ this.reselect();
+ }, this))
+ .bind("get_rollback.jstree", $.proxy(function () {
+ this.dehover_node();
+ this.save_selected();
+ }, this))
+ .bind("set_rollback.jstree", $.proxy(function () {
+ this.reselect();
+ }, this))
+ .bind("close_node.jstree", $.proxy(function (event, data) {
+ var s = this._get_settings().ui,
+ obj = this._get_node(data.rslt.obj),
+ clk = (obj && obj.length) ? obj.children("ul").find("a.jstree-clicked") : $(),
+ _this = this;
+ if(s.selected_parent_close === false || !clk.length) { return; }
+ clk.each(function () {
+ _this.deselect_node(this);
+ if(s.selected_parent_close === "select_parent") { _this.select_node(obj); }
+ });
+ }, this))
+ .bind("delete_node.jstree", $.proxy(function (event, data) {
+ var s = this._get_settings().ui.select_prev_on_delete,
+ obj = this._get_node(data.rslt.obj),
+ clk = (obj && obj.length) ? obj.find("a.jstree-clicked") : [],
+ _this = this;
+ clk.each(function () { _this.deselect_node(this); });
+ if(s && clk.length) {
+ data.rslt.prev.each(function () {
+ if(this.parentNode) { _this.select_node(this); return false; /* if return false is removed all prev nodes will be selected */}
+ });
+ }
+ }, this))
+ .bind("move_node.jstree", $.proxy(function (event, data) {
+ if(data.rslt.cy) {
+ data.rslt.oc.find("a.jstree-clicked").removeClass("jstree-clicked");
+ }
+ }, this));
+ },
+ defaults : {
+ select_limit : -1, // 0, 1, 2 ... or -1 for unlimited
+ select_multiple_modifier : "ctrl", // on, or ctrl, shift, alt
+ select_range_modifier : "shift",
+ selected_parent_close : "select_parent", // false, "deselect", "select_parent"
+ selected_parent_open : true,
+ select_prev_on_delete : true,
+ disable_selecting_children : false,
+ initially_select : []
+ },
+ _fn : {
+ _get_node : function (obj, allow_multiple) {
+ if(typeof obj === "undefined" || obj === null) { return allow_multiple ? this.data.ui.selected : this.data.ui.last_selected; }
+ var $obj = $(obj, this.get_container());
+ if($obj.is(".jstree") || obj == -1) { return -1; }
+ $obj = $obj.closest("li", this.get_container());
+ return $obj.length ? $obj : false;
+ },
+ _ui_notify : function (n, data) {
+ if(data.selected) {
+ this.select_node(n, false);
+ }
+ },
+ save_selected : function () {
+ var _this = this;
+ this.data.ui.to_select = [];
+ this.data.ui.selected.each(function () { if(this.id) { _this.data.ui.to_select.push("#" + this.id.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:")); } });
+ this.__callback(this.data.ui.to_select);
+ },
+ reselect : function () {
+ var _this = this,
+ s = this.data.ui.to_select;
+ s = $.map($.makeArray(s), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); });
+ // this.deselect_all(); WHY deselect, breaks plugin state notifier?
+ $.each(s, function (i, val) { if(val && val !== "#") { _this.select_node(val); } });
+ this.data.ui.selected = this.data.ui.selected.filter(function () { return this.parentNode; });
+ this.__callback();
+ },
+ refresh : function (obj) {
+ this.save_selected();
+ return this.__call_old();
+ },
+ hover_node : function (obj) {
+ obj = this._get_node(obj);
+ if(!obj.length) { return false; }
+ //if(this.data.ui.hovered && obj.get(0) === this.data.ui.hovered.get(0)) { return; }
+ if(!obj.hasClass("jstree-hovered")) { this.dehover_node(); }
+ this.data.ui.hovered = obj.children("a").addClass("jstree-hovered").parent();
+ this._fix_scroll(obj);
+ this.__callback({ "obj" : obj });
+ },
+ dehover_node : function () {
+ var obj = this.data.ui.hovered, p;
+ if(!obj || !obj.length) { return false; }
+ p = obj.children("a").removeClass("jstree-hovered").parent();
+ if(this.data.ui.hovered[0] === p[0]) { this.data.ui.hovered = null; }
+ this.__callback({ "obj" : obj });
+ },
+ select_node : function (obj, check, e) {
+ obj = this._get_node(obj);
+ if(obj == -1 || !obj || !obj.length) { return false; }
+ var s = this._get_settings().ui,
+ is_multiple = (s.select_multiple_modifier == "on" || (s.select_multiple_modifier !== false && e && e[s.select_multiple_modifier + "Key"])),
+ is_range = (s.select_range_modifier !== false && e && e[s.select_range_modifier + "Key"] && this.data.ui.last_selected && this.data.ui.last_selected[0] !== obj[0] && this.data.ui.last_selected.parent()[0] === obj.parent()[0]),
+ is_selected = this.is_selected(obj),
+ proceed = true,
+ t = this;
+ if(check) {
+ if(s.disable_selecting_children && is_multiple &&
+ (
+ (obj.parentsUntil(".jstree","li").children("a.jstree-clicked").length) ||
+ (obj.children("ul").find("a.jstree-clicked:eq(0)").length)
+ )
+ ) {
+ return false;
+ }
+ proceed = false;
+ switch(!0) {
+ case (is_range):
+ this.data.ui.last_selected.addClass("jstree-last-selected");
+ obj = obj[ obj.index() < this.data.ui.last_selected.index() ? "nextUntil" : "prevUntil" ](".jstree-last-selected").addBack();
+ if(s.select_limit == -1 || obj.length < s.select_limit) {
+ this.data.ui.last_selected.removeClass("jstree-last-selected");
+ this.data.ui.selected.each(function () {
+ if(this !== t.data.ui.last_selected[0]) { t.deselect_node(this); }
+ });
+ is_selected = false;
+ proceed = true;
+ }
+ else {
+ proceed = false;
+ }
+ break;
+ case (is_selected && !is_multiple):
+ this.deselect_all();
+ is_selected = false;
+ proceed = true;
+ break;
+ case (!is_selected && !is_multiple):
+ if(s.select_limit == -1 || s.select_limit > 0) {
+ this.deselect_all();
+ proceed = true;
+ }
+ break;
+ case (is_selected && is_multiple):
+ this.deselect_node(obj);
+ break;
+ case (!is_selected && is_multiple):
+ if(s.select_limit == -1 || this.data.ui.selected.length + 1 <= s.select_limit) {
+ proceed = true;
+ }
+ break;
+ }
+ }
+ if(proceed && !is_selected) {
+ if(!is_range) { this.data.ui.last_selected = obj; }
+ obj.children("a").addClass("jstree-clicked");
+ if(s.selected_parent_open) {
+ obj.parents(".jstree-closed").each(function () { t.open_node(this, false, true); });
+ }
+ this.data.ui.selected = this.data.ui.selected.add(obj);
+ this._fix_scroll(obj.eq(0));
+ this.__callback({ "obj" : obj, "e" : e });
+ }
+ },
+ _fix_scroll : function (obj) {
+ var c = this.get_container()[0], t;
+ if(c.scrollHeight > c.offsetHeight) {
+ obj = this._get_node(obj);
+ if(!obj || obj === -1 || !obj.length || !obj.is(":visible")) { return; }
+ t = obj.offset().top - this.get_container().offset().top;
+ if(t < 0) {
+ c.scrollTop = c.scrollTop + t - 1;
+ }
+ if(t + this.data.core.li_height + (c.scrollWidth > c.offsetWidth ? scrollbar_width : 0) > c.offsetHeight) {
+ c.scrollTop = c.scrollTop + (t - c.offsetHeight + this.data.core.li_height + 1 + (c.scrollWidth > c.offsetWidth ? scrollbar_width : 0));
+ }
+ }
+ },
+ deselect_node : function (obj) {
+ obj = this._get_node(obj);
+ if(!obj.length) { return false; }
+ if(this.is_selected(obj)) {
+ obj.children("a").removeClass("jstree-clicked");
+ this.data.ui.selected = this.data.ui.selected.not(obj);
+ if(this.data.ui.last_selected.get(0) === obj.get(0)) { this.data.ui.last_selected = this.data.ui.selected.eq(0); }
+ this.__callback({ "obj" : obj });
+ }
+ },
+ toggle_select : function (obj) {
+ obj = this._get_node(obj);
+ if(!obj.length) { return false; }
+ if(this.is_selected(obj)) { this.deselect_node(obj); }
+ else { this.select_node(obj); }
+ },
+ is_selected : function (obj) { return this.data.ui.selected.index(this._get_node(obj)) >= 0; },
+ get_selected : function (context) {
+ return context ? $(context).find("a.jstree-clicked").parent() : this.data.ui.selected;
+ },
+ deselect_all : function (context) {
+ var ret = context ? $(context).find("a.jstree-clicked").parent() : this.get_container().find("a.jstree-clicked").parent();
+ ret.children("a.jstree-clicked").removeClass("jstree-clicked");
+ this.data.ui.selected = $([]);
+ this.data.ui.last_selected = false;
+ this.__callback({ "obj" : ret });
+ }
+ }
+ });
+ // include the selection plugin by default
+ $.jstree.defaults.plugins.push("ui");
+})(jQuery);
+//*/
+
+/*
+ * jsTree CRRM plugin
+ * Handles creating/renaming/removing/moving nodes by user interaction.
+ */
+(function ($) {
+ $.jstree.plugin("crrm", {
+ __init : function () {
+ this.get_container()
+ .bind("move_node.jstree", $.proxy(function (e, data) {
+ if(this._get_settings().crrm.move.open_onmove) {
+ var t = this;
+ data.rslt.np.parentsUntil(".jstree").addBack().filter(".jstree-closed").each(function () {
+ t.open_node(this, false, true);
+ });
+ }
+ }, this));
+ },
+ defaults : {
+ input_width_limit : 200,
+ move : {
+ always_copy : false, // false, true or "multitree"
+ open_onmove : true,
+ default_position : "last",
+ check_move : function (m) { return true; }
+ }
+ },
+ _fn : {
+ _show_input : function (obj, callback) {
+ obj = this._get_node(obj);
+ var rtl = this._get_settings().core.rtl,
+ w = this._get_settings().crrm.input_width_limit,
+ w1 = obj.children("ins").width(),
+ w2 = obj.find("> a:visible > ins").width() * obj.find("> a:visible > ins").length,
+ t = this.get_text(obj),
+ h1 = $("<div />", { css : { "position" : "absolute", "top" : "-200px", "left" : (rtl ? "0px" : "-1000px"), "visibility" : "hidden" } }).appendTo("body"),
+ h2 = obj.css("position","relative").append(
+ $("<input />", {
+ "value" : t,
+ "class" : "jstree-rename-input",
+ // "size" : t.length,
+ "css" : {
+ "padding" : "0",
+ "border" : "1px solid silver",
+ "position" : "absolute",
+ "left" : (rtl ? "auto" : (w1 + w2 + 4) + "px"),
+ "right" : (rtl ? (w1 + w2 + 4) + "px" : "auto"),
+ "top" : "0px",
+ "height" : (this.data.core.li_height - 2) + "px",
+ "lineHeight" : (this.data.core.li_height - 2) + "px",
+ "width" : "150px" // will be set a bit further down
+ },
+ "blur" : $.proxy(function () {
+ var i = obj.children(".jstree-rename-input"),
+ v = i.val();
+ if(v === "") { v = t; }
+ h1.remove();
+ i.remove(); // rollback purposes
+ this.set_text(obj,t); // rollback purposes
+ this.rename_node(obj, v);
+ callback.call(this, obj, v, t);
+ obj.css("position","");
+ }, this),
+ "keyup" : function (event) {
+ var key = event.keyCode || event.which;
+ if(key == 27) { this.value = t; this.blur(); return; }
+ else if(key == 13) { this.blur(); return; }
+ else {
+ h2.width(Math.min(h1.text("pW" + this.value).width(),w));
+ }
+ },
+ "keypress" : function(event) {
+ var key = event.keyCode || event.which;
+ if(key == 13) { return false; }
+ }
+ })
+ ).children(".jstree-rename-input");
+ this.set_text(obj, "");
+ h1.css({
+ fontFamily : h2.css('fontFamily') || '',
+ fontSize : h2.css('fontSize') || '',
+ fontWeight : h2.css('fontWeight') || '',
+ fontStyle : h2.css('fontStyle') || '',
+ fontStretch : h2.css('fontStretch') || '',
+ fontVariant : h2.css('fontVariant') || '',
+ letterSpacing : h2.css('letterSpacing') || '',
+ wordSpacing : h2.css('wordSpacing') || ''
+ });
+ h2.width(Math.min(h1.text("pW" + h2[0].value).width(),w))[0].select();
+ },
+ rename : function (obj) {
+ obj = this._get_node(obj);
+ this.__rollback();
+ var f = this.__callback;
+ this._show_input(obj, function (obj, new_name, old_name) {
+ f.call(this, { "obj" : obj, "new_name" : new_name, "old_name" : old_name });
+ });
+ },
+ create : function (obj, position, js, callback, skip_rename) {
+ var t, _this = this;
+ obj = this._get_node(obj);
+ if(!obj) { obj = -1; }
+ this.__rollback();
+ t = this.create_node(obj, position, js, function (t) {
+ var p = this._get_parent(t),
+ pos = $(t).index();
+ if(callback) { callback.call(this, t); }
+ if(p.length && p.hasClass("jstree-closed")) { this.open_node(p, false, true); }
+ if(!skip_rename) {
+ this._show_input(t, function (obj, new_name, old_name) {
+ _this.__callback({ "obj" : obj, "name" : new_name, "parent" : p, "position" : pos });
+ });
+ }
+ else { _this.__callback({ "obj" : t, "name" : this.get_text(t), "parent" : p, "position" : pos }); }
+ });
+ return t;
+ },
+ remove : function (obj) {
+ obj = this._get_node(obj, true);
+ var p = this._get_parent(obj), prev = this._get_prev(obj);
+ this.__rollback();
+ obj = this.delete_node(obj);
+ if(obj !== false) { this.__callback({ "obj" : obj, "prev" : prev, "parent" : p }); }
+ },
+ check_move : function () {
+ if(!this.__call_old()) { return false; }
+ var s = this._get_settings().crrm.move;
+ if(!s.check_move.call(this, this._get_move())) { return false; }
+ return true;
+ },
+ move_node : function (obj, ref, position, is_copy, is_prepared, skip_check) {
+ var s = this._get_settings().crrm.move;
+ if(!is_prepared) {
+ if(typeof position === "undefined") { position = s.default_position; }
+ if(position === "inside" && !s.default_position.match(/^(before|after)$/)) { position = s.default_position; }
+ return this.__call_old(true, obj, ref, position, is_copy, false, skip_check);
+ }
+ // if the move is already prepared
+ if(s.always_copy === true || (s.always_copy === "multitree" && obj.rt.get_index() !== obj.ot.get_index() )) {
+ is_copy = true;
+ }
+ this.__call_old(true, obj, ref, position, is_copy, true, skip_check);
+ },
+
+ cut : function (obj) {
+ obj = this._get_node(obj, true);
+ if(!obj || !obj.length) { return false; }
+ this.data.crrm.cp_nodes = false;
+ this.data.crrm.ct_nodes = obj;
+ this.__callback({ "obj" : obj });
+ },
+ copy : function (obj) {
+ obj = this._get_node(obj, true);
+ if(!obj || !obj.length) { return false; }
+ this.data.crrm.ct_nodes = false;
+ this.data.crrm.cp_nodes = obj;
+ this.__callback({ "obj" : obj });
+ },
+ paste : function (obj) {
+ obj = this._get_node(obj);
+ if(!obj || !obj.length) { return false; }
+ var nodes = this.data.crrm.ct_nodes ? this.data.crrm.ct_nodes : this.data.crrm.cp_nodes;
+ if(!this.data.crrm.ct_nodes && !this.data.crrm.cp_nodes) { return false; }
+ if(this.data.crrm.ct_nodes) { this.move_node(this.data.crrm.ct_nodes, obj); this.data.crrm.ct_nodes = false; }
+ if(this.data.crrm.cp_nodes) { this.move_node(this.data.crrm.cp_nodes, obj, false, true); }
+ this.__callback({ "obj" : obj, "nodes" : nodes });
+ }
+ }
+ });
+ // include the crr plugin by default
+ // $.jstree.defaults.plugins.push("crrm");
+})(jQuery);
+//*/
+
+/*
+ * jsTree themes plugin
+ * Handles loading and setting themes, as well as detecting path to themes, etc.
+ */
+(function ($) {
+ var themes_loaded = [];
+ // this variable stores the path to the themes folder - if left as false - it will be autodetected
+ $.jstree._themes = false;
+ $.jstree.plugin("themes", {
+ __init : function () {
+ this.get_container()
+ .bind("init.jstree", $.proxy(function () {
+ var s = this._get_settings().themes;
+ this.data.themes.dots = s.dots;
+ this.data.themes.icons = s.icons;
+ this.set_theme(s.theme, s.url);
+ }, this))
+ .bind("loaded.jstree", $.proxy(function () {
+ // bound here too, as simple HTML tree's won't honor dots & icons otherwise
+ if(!this.data.themes.dots) { this.hide_dots(); }
+ else { this.show_dots(); }
+ if(!this.data.themes.icons) { this.hide_icons(); }
+ else { this.show_icons(); }
+ }, this));
+ },
+ defaults : {
+ theme : "default",
+ url : false,
+ dots : true,
+ icons : true
+ },
+ _fn : {
+ set_theme : function (theme_name, theme_url) {
+ if(!theme_name) { return false; }
+ if(!theme_url) { theme_url = $.jstree._themes + theme_name + '/style.css'; }
+ if($.inArray(theme_url, themes_loaded) == -1) {
+ $.vakata.css.add_sheet({ "url" : theme_url });
+ themes_loaded.push(theme_url);
+ }
+ if(this.data.themes.theme != theme_name) {
+ this.get_container().removeClass('jstree-' + this.data.themes.theme);
+ this.data.themes.theme = theme_name;
+ }
+ this.get_container().addClass('jstree-' + theme_name);
+ if(!this.data.themes.dots) { this.hide_dots(); }
+ else { this.show_dots(); }
+ if(!this.data.themes.icons) { this.hide_icons(); }
+ else { this.show_icons(); }
+ this.__callback();
+ },
+ get_theme : function () { return this.data.themes.theme; },
+
+ show_dots : function () { this.data.themes.dots = true; this.get_container().children("ul").removeClass("jstree-no-dots"); },
+ hide_dots : function () { this.data.themes.dots = false; this.get_container().children("ul").addClass("jstree-no-dots"); },
+ toggle_dots : function () { if(this.data.themes.dots) { this.hide_dots(); } else { this.show_dots(); } },
+
+ show_icons : function () { this.data.themes.icons = true; this.get_container().children("ul").removeClass("jstree-no-icons"); },
+ hide_icons : function () { this.data.themes.icons = false; this.get_container().children("ul").addClass("jstree-no-icons"); },
+ toggle_icons: function () { if(this.data.themes.icons) { this.hide_icons(); } else { this.show_icons(); } }
+ }
+ });
+ // autodetect themes path
+ $(function () {
+ if($.jstree._themes === false) {
+ $("script").each(function () {
+ if(this.src.toString().match(/jquery\.jstree[^\/]*?\.js(\?.*)?$/)) {
+ $.jstree._themes = this.src.toString().replace(/jquery\.jstree[^\/]*?\.js(\?.*)?$/, "") + 'themes/';
+ return false;
+ }
+ });
+ }
+ if($.jstree._themes === false) { $.jstree._themes = "themes/"; }
+ });
+ // include the themes plugin by default
+ $.jstree.defaults.plugins.push("themes");
+})(jQuery);
+//*/
+
+/*
+ * jsTree hotkeys plugin
+ * Enables keyboard navigation for all tree instances
+ * Depends on the jstree ui & jquery hotkeys plugins
+ */
+(function ($) {
+ var bound = [];
+ function exec(i, event) {
+ var f = $.jstree._focused(), tmp;
+ if(f && f.data && f.data.hotkeys && f.data.hotkeys.enabled) {
+ tmp = f._get_settings().hotkeys[i];
+ if(tmp) { return tmp.call(f, event); }
+ }
+ }
+ $.jstree.plugin("hotkeys", {
+ __init : function () {
+ if(typeof $.hotkeys === "undefined") { throw "jsTree hotkeys: jQuery hotkeys plugin not included."; }
+ if(!this.data.ui) { throw "jsTree hotkeys: jsTree UI plugin not included."; }
+ $.each(this._get_settings().hotkeys, function (i, v) {
+ if(v !== false && $.inArray(i, bound) == -1) {
+ $(document).bind("keydown", i, function (event) { return exec(i, event); });
+ bound.push(i);
+ }
+ });
+ this.get_container()
+ .bind("lock.jstree", $.proxy(function () {
+ if(this.data.hotkeys.enabled) { this.data.hotkeys.enabled = false; this.data.hotkeys.revert = true; }
+ }, this))
+ .bind("unlock.jstree", $.proxy(function () {
+ if(this.data.hotkeys.revert) { this.data.hotkeys.enabled = true; }
+ }, this));
+ this.enable_hotkeys();
+ },
+ defaults : {
+ "up" : function () {
+ var o = this.data.ui.hovered || this.data.ui.last_selected || -1;
+ this.hover_node(this._get_prev(o));
+ return false;
+ },
+ "ctrl+up" : function () {
+ var o = this.data.ui.hovered || this.data.ui.last_selected || -1;
+ this.hover_node(this._get_prev(o));
+ return false;
+ },
+ "shift+up" : function () {
+ var o = this.data.ui.hovered || this.data.ui.last_selected || -1;
+ this.hover_node(this._get_prev(o));
+ return false;
+ },
+ "down" : function () {
+ var o = this.data.ui.hovered || this.data.ui.last_selected || -1;
+ this.hover_node(this._get_next(o));
+ return false;
+ },
+ "ctrl+down" : function () {
+ var o = this.data.ui.hovered || this.data.ui.last_selected || -1;
+ this.hover_node(this._get_next(o));
+ return false;
+ },
+ "shift+down" : function () {
+ var o = this.data.ui.hovered || this.data.ui.last_selected || -1;
+ this.hover_node(this._get_next(o));
+ return false;
+ },
+ "left" : function () {
+ var o = this.data.ui.hovered || this.data.ui.last_selected;
+ if(o) {
+ if(o.hasClass("jstree-open")) { this.close_node(o); }
+ else { this.hover_node(this._get_prev(o)); }
+ }
+ return false;
+ },
+ "ctrl+left" : function () {
+ var o = this.data.ui.hovered || this.data.ui.last_selected;
+ if(o) {
+ if(o.hasClass("jstree-open")) { this.close_node(o); }
+ else { this.hover_node(this._get_prev(o)); }
+ }
+ return false;
+ },
+ "shift+left" : function () {
+ var o = this.data.ui.hovered || this.data.ui.last_selected;
+ if(o) {
+ if(o.hasClass("jstree-open")) { this.close_node(o); }
+ else { this.hover_node(this._get_prev(o)); }
+ }
+ return false;
+ },
+ "right" : function () {
+ var o = this.data.ui.hovered || this.data.ui.last_selected;
+ if(o && o.length) {
+ if(o.hasClass("jstree-closed")) { this.open_node(o); }
+ else { this.hover_node(this._get_next(o)); }
+ }
+ return false;
+ },
+ "ctrl+right" : function () {
+ var o = this.data.ui.hovered || this.data.ui.last_selected;
+ if(o && o.length) {
+ if(o.hasClass("jstree-closed")) { this.open_node(o); }
+ else { this.hover_node(this._get_next(o)); }
+ }
+ return false;
+ },
+ "shift+right" : function () {
+ var o = this.data.ui.hovered || this.data.ui.last_selected;
+ if(o && o.length) {
+ if(o.hasClass("jstree-closed")) { this.open_node(o); }
+ else { this.hover_node(this._get_next(o)); }
+ }
+ return false;
+ },
+ "space" : function () {
+ if(this.data.ui.hovered) { this.data.ui.hovered.children("a:eq(0)").click(); }
+ return false;
+ },
+ "ctrl+space" : function (event) {
+ event.type = "click";
+ if(this.data.ui.hovered) { this.data.ui.hovered.children("a:eq(0)").trigger(event); }
+ return false;
+ },
+ "shift+space" : function (event) {
+ event.type = "click";
+ if(this.data.ui.hovered) { this.data.ui.hovered.children("a:eq(0)").trigger(event); }
+ return false;
+ },
+ "f2" : function () { this.rename(this.data.ui.hovered || this.data.ui.last_selected); },
+ "del" : function () { this.remove(this.data.ui.hovered || this._get_node(null)); }
+ },
+ _fn : {
+ enable_hotkeys : function () {
+ this.data.hotkeys.enabled = true;
+ },
+ disable_hotkeys : function () {
+ this.data.hotkeys.enabled = false;
+ }
+ }
+ });
+})(jQuery);
+//*/
+
+/*
+ * jsTree JSON plugin
+ * The JSON data store. Datastores are build by overriding the `load_node` and `_is_loaded` functions.
+ */
+(function ($) {
+ $.jstree.plugin("json_data", {
+ __init : function() {
+ var s = this._get_settings().json_data;
+ if(s.progressive_unload) {
+ this.get_container().bind("after_close.jstree", function (e, data) {
+ data.rslt.obj.children("ul").remove();
+ });
+ }
+ },
+ defaults : {
+ // `data` can be a function:
+ // * accepts two arguments - node being loaded and a callback to pass the result to
+ // * will be executed in the current tree's scope & ajax won't be supported
+ data : false,
+ ajax : false,
+ correct_state : true,
+ progressive_render : false,
+ progressive_unload : false
+ },
+ _fn : {
+ load_node : function (obj, s_call, e_call) { var _this = this; this.load_node_json(obj, function () { _this.__callback({ "obj" : _this._get_node(obj) }); s_call.call(this); }, e_call); },
+ _is_loaded : function (obj) {
+ var s = this._get_settings().json_data;
+ obj = this._get_node(obj);
+ return obj == -1 || !obj || (!s.ajax && !s.progressive_render && !$.isFunction(s.data)) || obj.is(".jstree-open, .jstree-leaf") || obj.children("ul").children("li").length > 0;
+ },
+ refresh : function (obj) {
+ obj = this._get_node(obj);
+ var s = this._get_settings().json_data;
+ if(obj && obj !== -1 && s.progressive_unload && ($.isFunction(s.data) || !!s.ajax)) {
+ obj.removeData("jstree_children");
+ }
+ return this.__call_old();
+ },
+ load_node_json : function (obj, s_call, e_call) {
+ var s = this.get_settings().json_data, d,
+ error_func = function () {},
+ success_func = function () {};
+ obj = this._get_node(obj);
+
+ if(obj && obj !== -1 && (s.progressive_render || s.progressive_unload) && !obj.is(".jstree-open, .jstree-leaf") && obj.children("ul").children("li").length === 0 && obj.data("jstree_children")) {
+ d = this._parse_json(obj.data("jstree_children"), obj);
+ if(d) {
+ obj.append(d);
+ if(!s.progressive_unload) { obj.removeData("jstree_children"); }
+ }
+ this.clean_node(obj);
+ if(s_call) { s_call.call(this); }
+ return;
+ }
+
+ if(obj && obj !== -1) {
+ if(obj.data("jstree_is_loading")) { return; }
+ else { obj.data("jstree_is_loading",true); }
+ }
+ switch(!0) {
+ case (!s.data && !s.ajax): throw "Neither data nor ajax settings supplied.";
+ // function option added here for easier model integration (also supporting async - see callback)
+ case ($.isFunction(s.data)):
+ s.data.call(this, obj, $.proxy(function (d) {
+ d = this._parse_json(d, obj);
+ if(!d) {
+ if(obj === -1 || !obj) {
+ if(s.correct_state) { this.get_container().children("ul").empty(); }
+ }
+ else {
+ obj.children("a.jstree-loading").removeClass("jstree-loading");
+ obj.removeData("jstree_is_loading");
+ if(s.correct_state) { this.correct_state(obj); }
+ }
+ if(e_call) { e_call.call(this); }
+ }
+ else {
+ if(obj === -1 || !obj) { this.get_container().children("ul").empty().append(d.children()); }
+ else { obj.append(d).children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); }
+ this.clean_node(obj);
+ if(s_call) { s_call.call(this); }
+ }
+ }, this));
+ break;
+ case (!!s.data && !s.ajax) || (!!s.data && !!s.ajax && (!obj || obj === -1)):
+ if(!obj || obj == -1) {
+ d = this._parse_json(s.data, obj);
+ if(d) {
+ this.get_container().children("ul").empty().append(d.children());
+ this.clean_node();
+ }
+ else {
+ if(s.correct_state) { this.get_container().children("ul").empty(); }
+ }
+ }
+ if(s_call) { s_call.call(this); }
+ break;
+ case (!s.data && !!s.ajax) || (!!s.data && !!s.ajax && obj && obj !== -1):
+ error_func = function (x, t, e) {
+ var ef = this.get_settings().json_data.ajax.error;
+ if(ef) { ef.call(this, x, t, e); }
+ if(obj != -1 && obj.length) {
+ obj.children("a.jstree-loading").removeClass("jstree-loading");
+ obj.removeData("jstree_is_loading");
+ if(t === "success" && s.correct_state) { this.correct_state(obj); }
+ }
+ else {
+ if(t === "success" && s.correct_state) { this.get_container().children("ul").empty(); }
+ }
+ if(e_call) { e_call.call(this); }
+ };
+ success_func = function (d, t, x) {
+ var sf = this.get_settings().json_data.ajax.success;
+ if(sf) { d = sf.call(this,d,t,x) || d; }
+ if(d === "" || (d && d.toString && d.toString().replace(/^[\s\n]+$/,"") === "") || (!$.isArray(d) && !$.isPlainObject(d))) {
+ return error_func.call(this, x, t, "");
+ }
+ d = this._parse_json(d, obj);
+ if(d) {
+ if(obj === -1 || !obj) { this.get_container().children("ul").empty().append(d.children()); }
+ else { obj.append(d).children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); }
+ this.clean_node(obj);
+ if(s_call) { s_call.call(this); }
+ }
+ else {
+ if(obj === -1 || !obj) {
+ if(s.correct_state) {
+ this.get_container().children("ul").empty();
+ if(s_call) { s_call.call(this); }
+ }
+ }
+ else {
+ obj.children("a.jstree-loading").removeClass("jstree-loading");
+ obj.removeData("jstree_is_loading");
+ if(s.correct_state) {
+ this.correct_state(obj);
+ if(s_call) { s_call.call(this); }
+ }
+ }
+ }
+ };
+ s.ajax.context = this;
+ s.ajax.error = error_func;
+ s.ajax.success = success_func;
+ if(!s.ajax.dataType) { s.ajax.dataType = "json"; }
+ if($.isFunction(s.ajax.url)) { s.ajax.url = s.ajax.url.call(this, obj); }
+ if($.isFunction(s.ajax.data)) { s.ajax.data = s.ajax.data.call(this, obj); }
+ $.ajax(s.ajax);
+ break;
+ }
+ },
+ _parse_json : function (js, obj, is_callback) {
+ var d = false,
+ p = this._get_settings(),
+ s = p.json_data,
+ t = p.core.html_titles,
+ tmp, i, j, ul1, ul2;
+
+ if(!js) { return d; }
+ if(s.progressive_unload && obj && obj !== -1) {
+ obj.data("jstree_children", d);
+ }
+ if($.isArray(js)) {
+ d = $('<ul>');
+ if(!js.length) { return false; }
+ for(i = 0, j = js.length; i < j; i++) {
+ tmp = this._parse_json(js[i], obj, true);
+ if(tmp.length) {
+ d = d.append(tmp);
+ }
+ }
+ d = d.children();
+ }
+ else {
+ if(typeof js == "string") { js = { data : js }; }
+ if(!js.data && js.data !== "") { return d; }
+ d = $("<li />");
+ if(js.attr) { d.attr(js.attr); }
+ if(js.metadata) { d.data(js.metadata); }
+ if(js.state) { d.addClass("jstree-" + js.state); }
+ if(!$.isArray(js.data)) { tmp = js.data; js.data = []; js.data.push(tmp); }
+ $.each(js.data, function (i, m) {
+ tmp = $("<a />");
+ if($.isFunction(m)) { m = m.call(this, js); }
+ if(typeof m == "string") { tmp.attr('href','#')[ t ? "html" : "text" ](m); }
+ else {
+ if(!m.attr) { m.attr = {}; }
+ if(!m.attr.href) { m.attr.href = '#'; }
+ tmp.attr(m.attr)[ t ? "html" : "text" ](m.title);
+ if(m.language) { tmp.addClass(m.language); }
+ }
+ tmp.prepend("<ins class='jstree-icon'>&#160;</ins>");
+ if(!m.icon && js.icon) { m.icon = js.icon; }
+ if(m.icon) {
+ if(m.icon.indexOf("/") === -1) { tmp.children("ins").addClass(m.icon); }
+ else { tmp.children("ins").css("background","url('" + m.icon + "') center center no-repeat"); }
+ }
+ d.append(tmp);
+ });
+ d.prepend("<ins class='jstree-icon'>&#160;</ins>");
+ if(js.children) {
+ if(s.progressive_render && js.state !== "open") {
+ d.addClass("jstree-closed").data("jstree_children", js.children);
+ }
+ else {
+ if(s.progressive_unload) { d.data("jstree_children", js.children); }
+ if($.isArray(js.children) && js.children.length) {
+ tmp = this._parse_json(js.children, obj, true);
+ if(tmp.length) {
+ ul2 = $("<ul />");
+ ul2.append(tmp);
+ d.append(ul2);
+ }
+ }
+ }
+ }
+ }
+ if(!is_callback) {
+ ul1 = $("<ul />");
+ ul1.append(d);
+ d = ul1;
+ }
+ return d;
+ },
+ get_json : function (obj, li_attr, a_attr, is_callback) {
+ var result = [],
+ s = this._get_settings(),
+ _this = this,
+ tmp1, tmp2, li, a, t, lang;
+ obj = this._get_node(obj);
+ if(!obj || obj === -1) { obj = this.get_container().find("> ul > li"); }
+ li_attr = $.isArray(li_attr) ? li_attr : [ "id", "class" ];
+ if(!is_callback && this.data.types) { li_attr.push(s.types.type_attr); }
+ a_attr = $.isArray(a_attr) ? a_attr : [ ];
+
+ obj.each(function () {
+ li = $(this);
+ tmp1 = { data : [] };
+ if(li_attr.length) { tmp1.attr = { }; }
+ $.each(li_attr, function (i, v) {
+ tmp2 = li.attr(v);
+ if(tmp2 && tmp2.length && tmp2.replace(/jstree[^ ]*/ig,'').length) {
+ tmp1.attr[v] = (" " + tmp2).replace(/ jstree[^ ]*/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,"");
+ }
+ });
+ if(li.hasClass("jstree-open")) { tmp1.state = "open"; }
+ if(li.hasClass("jstree-closed")) { tmp1.state = "closed"; }
+ if(li.data()) { tmp1.metadata = li.data(); }
+ a = li.children("a");
+ a.each(function () {
+ t = $(this);
+ if(
+ a_attr.length ||
+ $.inArray("languages", s.plugins) !== -1 ||
+ t.children("ins").get(0).style.backgroundImage.length ||
+ (t.children("ins").get(0).className && t.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').length)
+ ) {
+ lang = false;
+ if($.inArray("languages", s.plugins) !== -1 && $.isArray(s.languages) && s.languages.length) {
+ $.each(s.languages, function (l, lv) {
+ if(t.hasClass(lv)) {
+ lang = lv;
+ return false;
+ }
+ });
+ }
+ tmp2 = { attr : { }, title : _this.get_text(t, lang) };
+ $.each(a_attr, function (k, z) {
+ tmp2.attr[z] = (" " + (t.attr(z) || "")).replace(/ jstree[^ ]*/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,"");
+ });
+ if($.inArray("languages", s.plugins) !== -1 && $.isArray(s.languages) && s.languages.length) {
+ $.each(s.languages, function (k, z) {
+ if(t.hasClass(z)) { tmp2.language = z; return true; }
+ });
+ }
+ if(t.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').replace(/^\s+$/ig,"").length) {
+ tmp2.icon = t.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,"");
+ }
+ if(t.children("ins").get(0).style.backgroundImage.length) {
+ tmp2.icon = t.children("ins").get(0).style.backgroundImage.replace("url(","").replace(")","");
+ }
+ }
+ else {
+ tmp2 = _this.get_text(t);
+ }
+ if(a.length > 1) { tmp1.data.push(tmp2); }
+ else { tmp1.data = tmp2; }
+ });
+ li = li.find("> ul > li");
+ if(li.length) { tmp1.children = _this.get_json(li, li_attr, a_attr, true); }
+ result.push(tmp1);
+ });
+ return result;
+ }
+ }
+ });
+})(jQuery);
+//*/
+
+/*
+ * jsTree languages plugin
+ * Adds support for multiple language versions in one tree
+ * This basically allows for many titles coexisting in one node, but only one of them being visible at any given time
+ * This is useful for maintaining the same structure in many languages (hence the name of the plugin)
+ */
+(function ($) {
+ var sh = false;
+ $.jstree.plugin("languages", {
+ __init : function () { this._load_css(); },
+ defaults : [],
+ _fn : {
+ set_lang : function (i) {
+ var langs = this._get_settings().languages,
+ st = false,
+ selector = ".jstree-" + this.get_index() + ' a';
+ if(!$.isArray(langs) || langs.length === 0) { return false; }
+ if($.inArray(i,langs) == -1) {
+ if(!!langs[i]) { i = langs[i]; }
+ else { return false; }
+ }
+ if(i == this.data.languages.current_language) { return true; }
+ st = $.vakata.css.get_css(selector + "." + this.data.languages.current_language, false, sh);
+ if(st !== false) { st.style.display = "none"; }
+ st = $.vakata.css.get_css(selector + "." + i, false, sh);
+ if(st !== false) { st.style.display = ""; }
+ this.data.languages.current_language = i;
+ this.__callback(i);
+ return true;
+ },
+ get_lang : function () {
+ return this.data.languages.current_language;
+ },
+ _get_string : function (key, lang) {
+ var langs = this._get_settings().languages,
+ s = this._get_settings().core.strings;
+ if($.isArray(langs) && langs.length) {
+ lang = (lang && $.inArray(lang,langs) != -1) ? lang : this.data.languages.current_language;
+ }
+ if(s[lang] && s[lang][key]) { return s[lang][key]; }
+ if(s[key]) { return s[key]; }
+ return key;
+ },
+ get_text : function (obj, lang) {
+ obj = this._get_node(obj) || this.data.ui.last_selected;
+ if(!obj.size()) { return false; }
+ var langs = this._get_settings().languages,
+ s = this._get_settings().core.html_titles;
+ if($.isArray(langs) && langs.length) {
+ lang = (lang && $.inArray(lang,langs) != -1) ? lang : this.data.languages.current_language;
+ obj = obj.children("a." + lang);
+ }
+ else { obj = obj.children("a:eq(0)"); }
+ if(s) {
+ obj = obj.clone();
+ obj.children("INS").remove();
+ return obj.html();
+ }
+ else {
+ obj = obj.contents().filter(function() { return this.nodeType == 3; })[0];
+ return obj.nodeValue;
+ }
+ },
+ set_text : function (obj, val, lang) {
+ obj = this._get_node(obj) || this.data.ui.last_selected;
+ if(!obj.size()) { return false; }
+ var langs = this._get_settings().languages,
+ s = this._get_settings().core.html_titles,
+ tmp;
+ if($.isArray(langs) && langs.length) {
+ lang = (lang && $.inArray(lang,langs) != -1) ? lang : this.data.languages.current_language;
+ obj = obj.children("a." + lang);
+ }
+ else { obj = obj.children("a:eq(0)"); }
+ if(s) {
+ tmp = obj.children("INS").clone();
+ obj.html(val).prepend(tmp);
+ this.__callback({ "obj" : obj, "name" : val, "lang" : lang });
+ return true;
+ }
+ else {
+ obj = obj.contents().filter(function() { return this.nodeType == 3; })[0];
+ this.__callback({ "obj" : obj, "name" : val, "lang" : lang });
+ return (obj.nodeValue = val);
+ }
+ },
+ _load_css : function () {
+ var langs = this._get_settings().languages,
+ str = "/* languages css */",
+ selector = ".jstree-" + this.get_index() + ' a',
+ ln;
+ if($.isArray(langs) && langs.length) {
+ this.data.languages.current_language = langs[0];
+ for(ln = 0; ln < langs.length; ln++) {
+ str += selector + "." + langs[ln] + " {";
+ if(langs[ln] != this.data.languages.current_language) { str += " display:none; "; }
+ str += " } ";
+ }
+ sh = $.vakata.css.add_sheet({ 'str' : str, 'title' : "jstree-languages" });
+ }
+ },
+ create_node : function (obj, position, js, callback) {
+ var t = this.__call_old(true, obj, position, js, function (t) {
+ var langs = this._get_settings().languages,
+ a = t.children("a"),
+ ln;
+ if($.isArray(langs) && langs.length) {
+ for(ln = 0; ln < langs.length; ln++) {
+ if(!a.is("." + langs[ln])) {
+ t.append(a.eq(0).clone().removeClass(langs.join(" ")).addClass(langs[ln]));
+ }
+ }
+ a.not("." + langs.join(", .")).remove();
+ }
+ if(callback) { callback.call(this, t); }
+ });
+ return t;
+ }
+ }
+ });
+})(jQuery);
+//*/
+
+/*
+ * jsTree cookies plugin
+ * Stores the currently opened/selected nodes in a cookie and then restores them
+ * Depends on the jquery.cookie plugin
+ */
+(function ($) {
+ $.jstree.plugin("cookies", {
+ __init : function () {
+ if(typeof $.cookie === "undefined") { throw "jsTree cookie: jQuery cookie plugin not included."; }
+
+ var s = this._get_settings().cookies,
+ tmp;
+ if(!!s.save_loaded) {
+ tmp = $.cookie(s.save_loaded);
+ if(tmp && tmp.length) { this.data.core.to_load = tmp.split(","); }
+ }
+ if(!!s.save_opened) {
+ tmp = $.cookie(s.save_opened);
+ if(tmp && tmp.length) { this.data.core.to_open = tmp.split(","); }
+ }
+ if(!!s.save_selected) {
+ tmp = $.cookie(s.save_selected);
+ if(tmp && tmp.length && this.data.ui) { this.data.ui.to_select = tmp.split(","); }
+ }
+ this.get_container()
+ .one( ( this.data.ui ? "reselect" : "reopen" ) + ".jstree", $.proxy(function () {
+ this.get_container()
+ .bind("open_node.jstree close_node.jstree select_node.jstree deselect_node.jstree", $.proxy(function (e) {
+ if(this._get_settings().cookies.auto_save) { this.save_cookie((e.handleObj.namespace + e.handleObj.type).replace("jstree","")); }
+ }, this));
+ }, this));
+ },
+ defaults : {
+ save_loaded : "jstree_load",
+ save_opened : "jstree_open",
+ save_selected : "jstree_select",
+ auto_save : true,
+ cookie_options : {}
+ },
+ _fn : {
+ save_cookie : function (c) {
+ if(this.data.core.refreshing) { return; }
+ var s = this._get_settings().cookies;
+ if(!c) { // if called manually and not by event
+ if(s.save_loaded) {
+ this.save_loaded();
+ $.cookie(s.save_loaded, this.data.core.to_load.join(","), s.cookie_options);
+ }
+ if(s.save_opened) {
+ this.save_opened();
+ $.cookie(s.save_opened, this.data.core.to_open.join(","), s.cookie_options);
+ }
+ if(s.save_selected && this.data.ui) {
+ this.save_selected();
+ $.cookie(s.save_selected, this.data.ui.to_select.join(","), s.cookie_options);
+ }
+ return;
+ }
+ switch(c) {
+ case "open_node":
+ case "close_node":
+ if(!!s.save_opened) {
+ this.save_opened();
+ $.cookie(s.save_opened, this.data.core.to_open.join(","), s.cookie_options);
+ }
+ if(!!s.save_loaded) {
+ this.save_loaded();
+ $.cookie(s.save_loaded, this.data.core.to_load.join(","), s.cookie_options);
+ }
+ break;
+ case "select_node":
+ case "deselect_node":
+ if(!!s.save_selected && this.data.ui) {
+ this.save_selected();
+ $.cookie(s.save_selected, this.data.ui.to_select.join(","), s.cookie_options);
+ }
+ break;
+ }
+ }
+ }
+ });
+ // include cookies by default
+ // $.jstree.defaults.plugins.push("cookies");
+})(jQuery);
+//*/
+
+/*
+ * jsTree sort plugin
+ * Sorts items alphabetically (or using any other function)
+ */
+(function ($) {
+ $.jstree.plugin("sort", {
+ __init : function () {
+ this.get_container()
+ .bind("load_node.jstree", $.proxy(function (e, data) {
+ var obj = this._get_node(data.rslt.obj);
+ obj = obj === -1 ? this.get_container().children("ul") : obj.children("ul");
+ this.sort(obj);
+ }, this))
+ .bind("rename_node.jstree create_node.jstree create.jstree", $.proxy(function (e, data) {
+ this.sort(data.rslt.obj.parent());
+ }, this))
+ .bind("move_node.jstree", $.proxy(function (e, data) {
+ var m = data.rslt.np == -1 ? this.get_container() : data.rslt.np;
+ this.sort(m.children("ul"));
+ }, this));
+ },
+ defaults : function (a, b) { return this.get_text(a) > this.get_text(b) ? 1 : -1; },
+ _fn : {
+ sort : function (obj) {
+ var s = this._get_settings().sort,
+ t = this;
+ obj.append($.makeArray(obj.children("li")).sort($.proxy(s, t)));
+ obj.find("> li > ul").each(function() { t.sort($(this)); });
+ this.clean_node(obj);
+ }
+ }
+ });
+})(jQuery);
+//*/
+
+/*
+ * jsTree DND plugin
+ * Drag and drop plugin for moving/copying nodes
+ */
+(function ($) {
+ var o = false,
+ r = false,
+ m = false,
+ ml = false,
+ sli = false,
+ sti = false,
+ dir1 = false,
+ dir2 = false,
+ last_pos = false;
+ $.vakata.dnd = {
+ is_down : false,
+ is_drag : false,
+ helper : false,
+ scroll_spd : 10,
+ init_x : 0,
+ init_y : 0,
+ threshold : 5,
+ helper_left : 5,
+ helper_top : 10,
+ user_data : {},
+
+ drag_start : function (e, data, html) {
+ if($.vakata.dnd.is_drag) { $.vakata.drag_stop({}); }
+ try {
+ e.currentTarget.unselectable = "on";
+ e.currentTarget.onselectstart = function() { return false; };
+ if(e.currentTarget.style) { e.currentTarget.style.MozUserSelect = "none"; }
+ } catch(err) { }
+ $.vakata.dnd.init_x = e.pageX;
+ $.vakata.dnd.init_y = e.pageY;
+ $.vakata.dnd.user_data = data;
+ $.vakata.dnd.is_down = true;
+ $.vakata.dnd.helper = $("<div id='vakata-dragged' />").html(html); //.fadeTo(10,0.25);
+ $(document).bind("mousemove", $.vakata.dnd.drag);
+ $(document).bind("mouseup", $.vakata.dnd.drag_stop);
+ return false;
+ },
+ drag : function (e) {
+ if(!$.vakata.dnd.is_down) { return; }
+ if(!$.vakata.dnd.is_drag) {
+ if(Math.abs(e.pageX - $.vakata.dnd.init_x) > 5 || Math.abs(e.pageY - $.vakata.dnd.init_y) > 5) {
+ $.vakata.dnd.helper.appendTo("body");
+ $.vakata.dnd.is_drag = true;
+ $(document).triggerHandler("drag_start.vakata", { "event" : e, "data" : $.vakata.dnd.user_data });
+ }
+ else { return; }
+ }
+
+ // maybe use a scrolling parent element instead of document?
+ if(e.type === "mousemove") { // thought of adding scroll in order to move the helper, but mouse poisition is n/a
+ var d = $(document), t = d.scrollTop(), l = d.scrollLeft();
+ if(e.pageY - t < 20) {
+ if(sti && dir1 === "down") { clearInterval(sti); sti = false; }
+ if(!sti) { dir1 = "up"; sti = setInterval(function () { $(document).scrollTop($(document).scrollTop() - $.vakata.dnd.scroll_spd); }, 150); }
+ }
+ else {
+ if(sti && dir1 === "up") { clearInterval(sti); sti = false; }
+ }
+ if($(window).height() - (e.pageY - t) < 20) {
+ if(sti && dir1 === "up") { clearInterval(sti); sti = false; }
+ if(!sti) { dir1 = "down"; sti = setInterval(function () { $(document).scrollTop($(document).scrollTop() + $.vakata.dnd.scroll_spd); }, 150); }
+ }
+ else {
+ if(sti && dir1 === "down") { clearInterval(sti); sti = false; }
+ }
+
+ if(e.pageX - l < 20) {
+ if(sli && dir2 === "right") { clearInterval(sli); sli = false; }
+ if(!sli) { dir2 = "left"; sli = setInterval(function () { $(document).scrollLeft($(document).scrollLeft() - $.vakata.dnd.scroll_spd); }, 150); }
+ }
+ else {
+ if(sli && dir2 === "left") { clearInterval(sli); sli = false; }
+ }
+ if($(window).width() - (e.pageX - l) < 20) {
+ if(sli && dir2 === "left") { clearInterval(sli); sli = false; }
+ if(!sli) { dir2 = "right"; sli = setInterval(function () { $(document).scrollLeft($(document).scrollLeft() + $.vakata.dnd.scroll_spd); }, 150); }
+ }
+ else {
+ if(sli && dir2 === "right") { clearInterval(sli); sli = false; }
+ }
+ }
+
+ $.vakata.dnd.helper.css({ left : (e.pageX + $.vakata.dnd.helper_left) + "px", top : (e.pageY + $.vakata.dnd.helper_top) + "px" });
+ $(document).triggerHandler("drag.vakata", { "event" : e, "data" : $.vakata.dnd.user_data });
+ },
+ drag_stop : function (e) {
+ if(sli) { clearInterval(sli); }
+ if(sti) { clearInterval(sti); }
+ $(document).unbind("mousemove", $.vakata.dnd.drag);
+ $(document).unbind("mouseup", $.vakata.dnd.drag_stop);
+ $(document).triggerHandler("drag_stop.vakata", { "event" : e, "data" : $.vakata.dnd.user_data });
+ $.vakata.dnd.helper.remove();
+ $.vakata.dnd.init_x = 0;
+ $.vakata.dnd.init_y = 0;
+ $.vakata.dnd.user_data = {};
+ $.vakata.dnd.is_down = false;
+ $.vakata.dnd.is_drag = false;
+ }
+ };
+ $(function() {
+ var css_string = '#vakata-dragged { display:block; margin:0 0 0 0; padding:4px 4px 4px 24px; position:absolute; top:-2000px; line-height:16px; z-index:10000; } ';
+ $.vakata.css.add_sheet({ str : css_string, title : "vakata" });
+ });
+
+ $.jstree.plugin("dnd", {
+ __init : function () {
+ this.data.dnd = {
+ active : false,
+ after : false,
+ inside : false,
+ before : false,
+ off : false,
+ prepared : false,
+ w : 0,
+ to1 : false,
+ to2 : false,
+ cof : false,
+ cw : false,
+ ch : false,
+ i1 : false,
+ i2 : false,
+ mto : false
+ };
+ this.get_container()
+ .bind("mouseenter.jstree", $.proxy(function (e) {
+ if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) {
+ if(this.data.themes) {
+ m.attr("class", "jstree-" + this.data.themes.theme);
+ if(ml) { ml.attr("class", "jstree-" + this.data.themes.theme); }
+ $.vakata.dnd.helper.attr("class", "jstree-dnd-helper jstree-" + this.data.themes.theme);
+ }
+ //if($(e.currentTarget).find("> ul > li").length === 0) {
+ if(e.currentTarget === e.target && $.vakata.dnd.user_data.obj && $($.vakata.dnd.user_data.obj).length && $($.vakata.dnd.user_data.obj).parents(".jstree:eq(0)")[0] !== e.target) { // node should not be from the same tree
+ var tr = $.jstree._reference(e.target), dc;
+ if(tr.data.dnd.foreign) {
+ dc = tr._get_settings().dnd.drag_check.call(this, { "o" : o, "r" : tr.get_container(), is_root : true });
+ if(dc === true || dc.inside === true || dc.before === true || dc.after === true) {
+ $.vakata.dnd.helper.children("ins").attr("class","jstree-ok");
+ }
+ }
+ else {
+ tr.prepare_move(o, tr.get_container(), "last");
+ if(tr.check_move()) {
+ $.vakata.dnd.helper.children("ins").attr("class","jstree-ok");
+ }
+ }
+ }
+ }
+ }, this))
+ .bind("mouseup.jstree", $.proxy(function (e) {
+ //if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree && $(e.currentTarget).find("> ul > li").length === 0) {
+ if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree && e.currentTarget === e.target && $.vakata.dnd.user_data.obj && $($.vakata.dnd.user_data.obj).length && $($.vakata.dnd.user_data.obj).parents(".jstree:eq(0)")[0] !== e.target) { // node should not be from the same tree
+ var tr = $.jstree._reference(e.currentTarget), dc;
+ if(tr.data.dnd.foreign) {
+ dc = tr._get_settings().dnd.drag_check.call(this, { "o" : o, "r" : tr.get_container(), is_root : true });
+ if(dc === true || dc.inside === true || dc.before === true || dc.after === true) {
+ tr._get_settings().dnd.drag_finish.call(this, { "o" : o, "r" : tr.get_container(), is_root : true });
+ }
+ }
+ else {
+ tr.move_node(o, tr.get_container(), "last", e[tr._get_settings().dnd.copy_modifier + "Key"]);
+ }
+ }
+ }, this))
+ .bind("mouseleave.jstree", $.proxy(function (e) {
+ if(e.relatedTarget && e.relatedTarget.id && e.relatedTarget.id === "jstree-marker-line") {
+ return false;
+ }
+ if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) {
+ if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); }
+ if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); }
+ if(this.data.dnd.to1) { clearTimeout(this.data.dnd.to1); }
+ if(this.data.dnd.to2) { clearTimeout(this.data.dnd.to2); }
+ if($.vakata.dnd.helper.children("ins").hasClass("jstree-ok")) {
+ $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid");
+ }
+ }
+ }, this))
+ .bind("mousemove.jstree", $.proxy(function (e) {
+ if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) {
+ var cnt = this.get_container()[0];
+
+ // Horizontal scroll
+ if(e.pageX + 24 > this.data.dnd.cof.left + this.data.dnd.cw) {
+ if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); }
+ this.data.dnd.i1 = setInterval($.proxy(function () { this.scrollLeft += $.vakata.dnd.scroll_spd; }, cnt), 100);
+ }
+ else if(e.pageX - 24 < this.data.dnd.cof.left) {
+ if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); }
+ this.data.dnd.i1 = setInterval($.proxy(function () { this.scrollLeft -= $.vakata.dnd.scroll_spd; }, cnt), 100);
+ }
+ else {
+ if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); }
+ }
+
+ // Vertical scroll
+ if(e.pageY + 24 > this.data.dnd.cof.top + this.data.dnd.ch) {
+ if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); }
+ this.data.dnd.i2 = setInterval($.proxy(function () { this.scrollTop += $.vakata.dnd.scroll_spd; }, cnt), 100);
+ }
+ else if(e.pageY - 24 < this.data.dnd.cof.top) {
+ if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); }
+ this.data.dnd.i2 = setInterval($.proxy(function () { this.scrollTop -= $.vakata.dnd.scroll_spd; }, cnt), 100);
+ }
+ else {
+ if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); }
+ }
+
+ }
+ }, this))
+ .bind("scroll.jstree", $.proxy(function (e) {
+ if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree && m && ml) {
+ m.hide();
+ ml.hide();
+ }
+ }, this))
+ .delegate("a", "mousedown.jstree", $.proxy(function (e) {
+ if(e.which === 1) {
+ this.start_drag(e.currentTarget, e);
+ return false;
+ }
+ }, this))
+ .delegate("a", "mouseenter.jstree", $.proxy(function (e) {
+ if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) {
+ this.dnd_enter(e.currentTarget);
+ }
+ }, this))
+ .delegate("a", "mousemove.jstree", $.proxy(function (e) {
+ if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) {
+ if(!r || !r.length || r.children("a")[0] !== e.currentTarget) {
+ this.dnd_enter(e.currentTarget);
+ }
+ if(typeof this.data.dnd.off.top === "undefined") { this.data.dnd.off = $(e.target).offset(); }
+ this.data.dnd.w = (e.pageY - (this.data.dnd.off.top || 0)) % this.data.core.li_height;
+ if(this.data.dnd.w < 0) { this.data.dnd.w += this.data.core.li_height; }
+ this.dnd_show();
+ }
+ }, this))
+ .delegate("a", "mouseleave.jstree", $.proxy(function (e) {
+ if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) {
+ if(e.relatedTarget && e.relatedTarget.id && e.relatedTarget.id === "jstree-marker-line") {
+ return false;
+ }
+ if(m) { m.hide(); }
+ if(ml) { ml.hide(); }
+ /*
+ var ec = $(e.currentTarget).closest("li"),
+ er = $(e.relatedTarget).closest("li");
+ if(er[0] !== ec.prev()[0] && er[0] !== ec.next()[0]) {
+ if(m) { m.hide(); }
+ if(ml) { ml.hide(); }
+ }
+ */
+ this.data.dnd.mto = setTimeout(
+ (function (t) { return function () { t.dnd_leave(e); }; })(this),
+ 0);
+ }
+ }, this))
+ .delegate("a", "mouseup.jstree", $.proxy(function (e) {
+ if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) {
+ this.dnd_finish(e);
+ }
+ }, this));
+
+ $(document)
+ .bind("drag_stop.vakata", $.proxy(function () {
+ if(this.data.dnd.to1) { clearTimeout(this.data.dnd.to1); }
+ if(this.data.dnd.to2) { clearTimeout(this.data.dnd.to2); }
+ if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); }
+ if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); }
+ this.data.dnd.after = false;
+ this.data.dnd.before = false;
+ this.data.dnd.inside = false;
+ this.data.dnd.off = false;
+ this.data.dnd.prepared = false;
+ this.data.dnd.w = false;
+ this.data.dnd.to1 = false;
+ this.data.dnd.to2 = false;
+ this.data.dnd.i1 = false;
+ this.data.dnd.i2 = false;
+ this.data.dnd.active = false;
+ this.data.dnd.foreign = false;
+ if(m) { m.css({ "top" : "-2000px" }); }
+ if(ml) { ml.css({ "top" : "-2000px" }); }
+ }, this))
+ .bind("drag_start.vakata", $.proxy(function (e, data) {
+ if(data.data.jstree) {
+ var et = $(data.event.target);
+ if(et.closest(".jstree").hasClass("jstree-" + this.get_index())) {
+ this.dnd_enter(et);
+ }
+ }
+ }, this));
+ /*
+ .bind("keydown.jstree-" + this.get_index() + " keyup.jstree-" + this.get_index(), $.proxy(function(e) {
+ if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree && !this.data.dnd.foreign) {
+ var h = $.vakata.dnd.helper.children("ins");
+ if(e[this._get_settings().dnd.copy_modifier + "Key"] && h.hasClass("jstree-ok")) {
+ h.parent().html(h.parent().html().replace(/ \(Copy\)$/, "") + " (Copy)");
+ }
+ else {
+ h.parent().html(h.parent().html().replace(/ \(Copy\)$/, ""));
+ }
+ }
+ }, this)); */
+
+
+
+ var s = this._get_settings().dnd;
+ if(s.drag_target) {
+ $(document)
+ .delegate(s.drag_target, "mousedown.jstree-" + this.get_index(), $.proxy(function (e) {
+ o = e.target;
+ $.vakata.dnd.drag_start(e, { jstree : true, obj : e.target }, "<ins class='jstree-icon'></ins>" + $(e.target).text() );
+ if(this.data.themes) {
+ if(m) { m.attr("class", "jstree-" + this.data.themes.theme); }
+ if(ml) { ml.attr("class", "jstree-" + this.data.themes.theme); }
+ $.vakata.dnd.helper.attr("class", "jstree-dnd-helper jstree-" + this.data.themes.theme);
+ }
+ $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid");
+ var cnt = this.get_container();
+ this.data.dnd.cof = cnt.offset();
+ this.data.dnd.cw = parseInt(cnt.width(),10);
+ this.data.dnd.ch = parseInt(cnt.height(),10);
+ this.data.dnd.foreign = true;
+ e.preventDefault();
+ }, this));
+ }
+ if(s.drop_target) {
+ $(document)
+ .delegate(s.drop_target, "mouseenter.jstree-" + this.get_index(), $.proxy(function (e) {
+ if(this.data.dnd.active && this._get_settings().dnd.drop_check.call(this, { "o" : o, "r" : $(e.target), "e" : e })) {
+ $.vakata.dnd.helper.children("ins").attr("class","jstree-ok");
+ }
+ }, this))
+ .delegate(s.drop_target, "mouseleave.jstree-" + this.get_index(), $.proxy(function (e) {
+ if(this.data.dnd.active) {
+ $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid");
+ }
+ }, this))
+ .delegate(s.drop_target, "mouseup.jstree-" + this.get_index(), $.proxy(function (e) {
+ if(this.data.dnd.active && $.vakata.dnd.helper.children("ins").hasClass("jstree-ok")) {
+ this._get_settings().dnd.drop_finish.call(this, { "o" : o, "r" : $(e.target), "e" : e });
+ }
+ }, this));
+ }
+ },
+ defaults : {
+ copy_modifier : "ctrl",
+ check_timeout : 100,
+ open_timeout : 500,
+ drop_target : ".jstree-drop",
+ drop_check : function (data) { return true; },
+ drop_finish : $.noop,
+ drag_target : ".jstree-draggable",
+ drag_finish : $.noop,
+ drag_check : function (data) { return { after : false, before : false, inside : true }; }
+ },
+ _fn : {
+ dnd_prepare : function () {
+ if(!r || !r.length) { return; }
+ this.data.dnd.off = r.offset();
+ if(this._get_settings().core.rtl) {
+ this.data.dnd.off.right = this.data.dnd.off.left + r.width();
+ }
+ if(this.data.dnd.foreign) {
+ var a = this._get_settings().dnd.drag_check.call(this, { "o" : o, "r" : r });
+ this.data.dnd.after = a.after;
+ this.data.dnd.before = a.before;
+ this.data.dnd.inside = a.inside;
+ this.data.dnd.prepared = true;
+ return this.dnd_show();
+ }
+ this.prepare_move(o, r, "before");
+ this.data.dnd.before = this.check_move();
+ this.prepare_move(o, r, "after");
+ this.data.dnd.after = this.check_move();
+ if(this._is_loaded(r)) {
+ this.prepare_move(o, r, "inside");
+ this.data.dnd.inside = this.check_move();
+ }
+ else {
+ this.data.dnd.inside = false;
+ }
+ this.data.dnd.prepared = true;
+ return this.dnd_show();
+ },
+ dnd_show : function () {
+ if(!this.data.dnd.prepared) { return; }
+ var o = ["before","inside","after"],
+ r = false,
+ rtl = this._get_settings().core.rtl,
+ pos;
+ if(this.data.dnd.w < this.data.core.li_height/3) { o = ["before","inside","after"]; }
+ else if(this.data.dnd.w <= this.data.core.li_height*2/3) {
+ o = this.data.dnd.w < this.data.core.li_height/2 ? ["inside","before","after"] : ["inside","after","before"];
+ }
+ else { o = ["after","inside","before"]; }
+ $.each(o, $.proxy(function (i, val) {
+ if(this.data.dnd[val]) {
+ $.vakata.dnd.helper.children("ins").attr("class","jstree-ok");
+ r = val;
+ return false;
+ }
+ }, this));
+ if(r === false) { $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid"); }
+
+ pos = rtl ? (this.data.dnd.off.right - 18) : (this.data.dnd.off.left + 10);
+ switch(r) {
+ case "before":
+ m.css({ "left" : pos + "px", "top" : (this.data.dnd.off.top - 6) + "px" }).show();
+ if(ml) { ml.css({ "left" : (pos + 8) + "px", "top" : (this.data.dnd.off.top - 1) + "px" }).show(); }
+ break;
+ case "after":
+ m.css({ "left" : pos + "px", "top" : (this.data.dnd.off.top + this.data.core.li_height - 6) + "px" }).show();
+ if(ml) { ml.css({ "left" : (pos + 8) + "px", "top" : (this.data.dnd.off.top + this.data.core.li_height - 1) + "px" }).show(); }
+ break;
+ case "inside":
+ m.css({ "left" : pos + ( rtl ? -4 : 4) + "px", "top" : (this.data.dnd.off.top + this.data.core.li_height/2 - 5) + "px" }).show();
+ if(ml) { ml.hide(); }
+ break;
+ default:
+ m.hide();
+ if(ml) { ml.hide(); }
+ break;
+ }
+ last_pos = r;
+ return r;
+ },
+ dnd_open : function () {
+ this.data.dnd.to2 = false;
+ this.open_node(r, $.proxy(this.dnd_prepare,this), true);
+ },
+ dnd_finish : function (e) {
+ if(this.data.dnd.foreign) {
+ if(this.data.dnd.after || this.data.dnd.before || this.data.dnd.inside) {
+ this._get_settings().dnd.drag_finish.call(this, { "o" : o, "r" : r, "p" : last_pos });
+ }
+ }
+ else {
+ this.dnd_prepare();
+ this.move_node(o, r, last_pos, e[this._get_settings().dnd.copy_modifier + "Key"]);
+ }
+ o = false;
+ r = false;
+ m.hide();
+ if(ml) { ml.hide(); }
+ },
+ dnd_enter : function (obj) {
+ if(this.data.dnd.mto) {
+ clearTimeout(this.data.dnd.mto);
+ this.data.dnd.mto = false;
+ }
+ var s = this._get_settings().dnd;
+ this.data.dnd.prepared = false;
+ r = this._get_node(obj);
+ if(s.check_timeout) {
+ // do the calculations after a minimal timeout (users tend to drag quickly to the desired location)
+ if(this.data.dnd.to1) { clearTimeout(this.data.dnd.to1); }
+ this.data.dnd.to1 = setTimeout($.proxy(this.dnd_prepare, this), s.check_timeout);
+ }
+ else {
+ this.dnd_prepare();
+ }
+ if(s.open_timeout) {
+ if(this.data.dnd.to2) { clearTimeout(this.data.dnd.to2); }
+ if(r && r.length && r.hasClass("jstree-closed")) {
+ // if the node is closed - open it, then recalculate
+ this.data.dnd.to2 = setTimeout($.proxy(this.dnd_open, this), s.open_timeout);
+ }
+ }
+ else {
+ if(r && r.length && r.hasClass("jstree-closed")) {
+ this.dnd_open();
+ }
+ }
+ },
+ dnd_leave : function (e) {
+ this.data.dnd.after = false;
+ this.data.dnd.before = false;
+ this.data.dnd.inside = false;
+ $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid");
+ m.hide();
+ if(ml) { ml.hide(); }
+ if(r && r[0] === e.target.parentNode) {
+ if(this.data.dnd.to1) {
+ clearTimeout(this.data.dnd.to1);
+ this.data.dnd.to1 = false;
+ }
+ if(this.data.dnd.to2) {
+ clearTimeout(this.data.dnd.to2);
+ this.data.dnd.to2 = false;
+ }
+ }
+ },
+ start_drag : function (obj, e) {
+ o = this._get_node(obj);
+ if(this.data.ui && this.is_selected(o)) { o = this._get_node(null, true); }
+ var dt = o.length > 1 ? this._get_string("multiple_selection") : this.get_text(o),
+ cnt = this.get_container();
+ if(!this._get_settings().core.html_titles) { dt = dt.replace(/</ig,"&lt;").replace(/>/ig,"&gt;"); }
+ $.vakata.dnd.drag_start(e, { jstree : true, obj : o }, "<ins class='jstree-icon'></ins>" + dt );
+ if(this.data.themes) {
+ if(m) { m.attr("class", "jstree-" + this.data.themes.theme); }
+ if(ml) { ml.attr("class", "jstree-" + this.data.themes.theme); }
+ $.vakata.dnd.helper.attr("class", "jstree-dnd-helper jstree-" + this.data.themes.theme);
+ }
+ this.data.dnd.cof = cnt.offset();
+ this.data.dnd.cw = parseInt(cnt.width(),10);
+ this.data.dnd.ch = parseInt(cnt.height(),10);
+ this.data.dnd.active = true;
+ }
+ }
+ });
+ $(function() {
+ var css_string = '' +
+ '#vakata-dragged ins { display:block; text-decoration:none; width:16px; height:16px; margin:0 0 0 0; padding:0; position:absolute; top:4px; left:4px; ' +
+ ' -moz-border-radius:4px; border-radius:4px; -webkit-border-radius:4px; ' +
+ '} ' +
+ '#vakata-dragged .jstree-ok { background:green; } ' +
+ '#vakata-dragged .jstree-invalid { background:red; } ' +
+ '#jstree-marker { padding:0; margin:0; font-size:12px; overflow:hidden; height:12px; width:8px; position:absolute; top:-30px; z-index:10001; background-repeat:no-repeat; display:none; background-color:transparent; text-shadow:1px 1px 1px white; color:black; line-height:10px; } ' +
+ '#jstree-marker-line { padding:0; margin:0; line-height:0%; font-size:1px; overflow:hidden; height:1px; width:100px; position:absolute; top:-30px; z-index:10000; background-repeat:no-repeat; display:none; background-color:#456c43; ' +
+ ' cursor:pointer; border:1px solid #eeeeee; border-left:0; -moz-box-shadow: 0px 0px 2px #666; -webkit-box-shadow: 0px 0px 2px #666; box-shadow: 0px 0px 2px #666; ' +
+ ' -moz-border-radius:1px; border-radius:1px; -webkit-border-radius:1px; ' +
+ '}' +
+ '';
+ $.vakata.css.add_sheet({ str : css_string, title : "jstree" });
+ m = $("<div />").attr({ id : "jstree-marker" }).hide().html("&raquo;")
+ .bind("mouseleave mouseenter", function (e) {
+ m.hide();
+ ml.hide();
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ return false;
+ })
+ .appendTo("body");
+ ml = $("<div />").attr({ id : "jstree-marker-line" }).hide()
+ .bind("mouseup", function (e) {
+ if(r && r.length) {
+ r.children("a").trigger(e);
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ return false;
+ }
+ })
+ .bind("mouseleave", function (e) {
+ var rt = $(e.relatedTarget);
+ if(rt.is(".jstree") || rt.closest(".jstree").length === 0) {
+ if(r && r.length) {
+ r.children("a").trigger(e);
+ m.hide();
+ ml.hide();
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ return false;
+ }
+ }
+ })
+ .appendTo("body");
+ $(document).bind("drag_start.vakata", function (e, data) {
+ if(data.data.jstree) { m.show(); if(ml) { ml.show(); } }
+ });
+ $(document).bind("drag_stop.vakata", function (e, data) {
+ if(data.data.jstree) { m.hide(); if(ml) { ml.hide(); } }
+ });
+ });
+})(jQuery);
+//*/
+
+/*
+ * jsTree checkbox plugin
+ * Inserts checkboxes in front of every node
+ * Depends on the ui plugin
+ * DOES NOT WORK NICELY WITH MULTITREE DRAG'N'DROP
+ */
+(function ($) {
+ $.jstree.plugin("checkbox", {
+ __init : function () {
+ this.data.checkbox.noui = this._get_settings().checkbox.override_ui;
+ if(this.data.ui && this.data.checkbox.noui) {
+ this.select_node = this.deselect_node = this.deselect_all = $.noop;
+ this.get_selected = this.get_checked;
+ }
+
+ this.get_container()
+ .bind("open_node.jstree create_node.jstree clean_node.jstree refresh.jstree", $.proxy(function (e, data) {
+ this._prepare_checkboxes(data.rslt.obj);
+ }, this))
+ .bind("loaded.jstree", $.proxy(function (e) {
+ this._prepare_checkboxes();
+ }, this))
+ .delegate( (this.data.ui && this.data.checkbox.noui ? "a" : "ins.jstree-checkbox") , "click.jstree", $.proxy(function (e) {
+ e.preventDefault();
+ if(this._get_node(e.target).hasClass("jstree-checked")) { this.uncheck_node(e.target); }
+ else { this.check_node(e.target); }
+ if(this.data.ui && this.data.checkbox.noui) {
+ this.save_selected();
+ if(this.data.cookies) { this.save_cookie("select_node"); }
+ }
+ else {
+ e.stopImmediatePropagation();
+ return false;
+ }
+ }, this));
+ },
+ defaults : {
+ override_ui : false,
+ two_state : false,
+ real_checkboxes : false,
+ checked_parent_open : true,
+ real_checkboxes_names : function (n) { return [ ("check_" + (n[0].id || Math.ceil(Math.random() * 10000))) , 1]; }
+ },
+ __destroy : function () {
+ this.get_container()
+ .find("input.jstree-real-checkbox").removeClass("jstree-real-checkbox").end()
+ .find("ins.jstree-checkbox").remove();
+ },
+ _fn : {
+ _checkbox_notify : function (n, data) {
+ if(data.checked) {
+ this.check_node(n, false);
+ }
+ },
+ _prepare_checkboxes : function (obj) {
+ obj = !obj || obj == -1 ? this.get_container().find("> ul > li") : this._get_node(obj);
+ if(obj === false) { return; } // added for removing root nodes
+ var c, _this = this, t, ts = this._get_settings().checkbox.two_state, rc = this._get_settings().checkbox.real_checkboxes, rcn = this._get_settings().checkbox.real_checkboxes_names;
+ obj.each(function () {
+ t = $(this);
+ c = t.is("li") && (t.hasClass("jstree-checked") || (rc && t.children(":checked").length)) ? "jstree-checked" : "jstree-unchecked";
+ t.find("li").addBack().each(function () {
+ var $t = $(this), nm;
+ $t.children("a" + (_this.data.languages ? "" : ":eq(0)") ).not(":has(.jstree-checkbox)").prepend("<ins class='jstree-checkbox'>&#160;</ins>").parent().not(".jstree-checked, .jstree-unchecked").addClass( ts ? "jstree-unchecked" : c );
+ if(rc) {
+ if(!$t.children(":checkbox").length) {
+ nm = rcn.call(_this, $t);
+ $t.prepend("<input type='checkbox' class='jstree-real-checkbox' id='" + nm[0] + "' name='" + nm[0] + "' value='" + nm[1] + "' />");
+ }
+ else {
+ $t.children(":checkbox").addClass("jstree-real-checkbox");
+ }
+ }
+ if(!ts) {
+ if(c === "jstree-checked" || $t.hasClass("jstree-checked") || $t.children(':checked').length) {
+ $t.find("li").addBack().addClass("jstree-checked").children(":checkbox").prop("checked", true);
+ }
+ }
+ else {
+ if($t.hasClass("jstree-checked") || $t.children(':checked').length) {
+ $t.addClass("jstree-checked").children(":checkbox").prop("checked", true);
+ }
+ }
+ });
+ });
+ if(!ts) {
+ obj.find(".jstree-checked").parent().parent().each(function () { _this._repair_state(this); });
+ }
+ },
+ change_state : function (obj, state) {
+ obj = this._get_node(obj);
+ var coll = false, rc = this._get_settings().checkbox.real_checkboxes;
+ if(!obj || obj === -1) { return false; }
+ state = (state === false || state === true) ? state : obj.hasClass("jstree-checked");
+ if(this._get_settings().checkbox.two_state) {
+ if(state) {
+ obj.removeClass("jstree-checked").addClass("jstree-unchecked");
+ if(rc) { obj.children(":checkbox").prop("checked", false); }
+ }
+ else {
+ obj.removeClass("jstree-unchecked").addClass("jstree-checked");
+ if(rc) { obj.children(":checkbox").prop("checked", true); }
+ }
+ }
+ else {
+ if(state) {
+ coll = obj.find("li").addBack();
+ if(!coll.filter(".jstree-checked, .jstree-undetermined").length) { return false; }
+ coll.removeClass("jstree-checked jstree-undetermined").addClass("jstree-unchecked");
+ if(rc) { coll.children(":checkbox").prop("checked", false); }
+ }
+ else {
+ coll = obj.find("li").addBack();
+ if(!coll.filter(".jstree-unchecked, .jstree-undetermined").length) { return false; }
+ coll.removeClass("jstree-unchecked jstree-undetermined").addClass("jstree-checked");
+ if(rc) { coll.children(":checkbox").prop("checked", true); }
+ if(this.data.ui) { this.data.ui.last_selected = obj; }
+ this.data.checkbox.last_selected = obj;
+ }
+ obj.parentsUntil(".jstree", "li").each(function () {
+ var $this = $(this);
+ if(state) {
+ if($this.children("ul").children("li.jstree-checked, li.jstree-undetermined").length) {
+ $this.parentsUntil(".jstree", "li").addBack().removeClass("jstree-checked jstree-unchecked").addClass("jstree-undetermined");
+ if(rc) { $this.parentsUntil(".jstree", "li").addBack().children(":checkbox").prop("checked", false); }
+ return false;
+ }
+ else {
+ $this.removeClass("jstree-checked jstree-undetermined").addClass("jstree-unchecked");
+ if(rc) { $this.children(":checkbox").prop("checked", false); }
+ }
+ }
+ else {
+ if($this.children("ul").children("li.jstree-unchecked, li.jstree-undetermined").length) {
+ $this.parentsUntil(".jstree", "li").addBack().removeClass("jstree-checked jstree-unchecked").addClass("jstree-undetermined");
+ if(rc) { $this.parentsUntil(".jstree", "li").addBack().children(":checkbox").prop("checked", false); }
+ return false;
+ }
+ else {
+ $this.removeClass("jstree-unchecked jstree-undetermined").addClass("jstree-checked");
+ if(rc) { $this.children(":checkbox").prop("checked", true); }
+ }
+ }
+ });
+ }
+ if(this.data.ui && this.data.checkbox.noui) { this.data.ui.selected = this.get_checked(); }
+ this.__callback(obj);
+ return true;
+ },
+ check_node : function (obj) {
+ if(this.change_state(obj, false)) {
+ obj = this._get_node(obj);
+ if(this._get_settings().checkbox.checked_parent_open) {
+ var t = this;
+ obj.parents(".jstree-closed").each(function () { t.open_node(this, false, true); });
+ }
+ this.__callback({ "obj" : obj });
+ }
+ },
+ uncheck_node : function (obj) {
+ if(this.change_state(obj, true)) { this.__callback({ "obj" : this._get_node(obj) }); }
+ },
+ check_all : function () {
+ var _this = this,
+ coll = this._get_settings().checkbox.two_state ? this.get_container_ul().find("li") : this.get_container_ul().children("li");
+ coll.each(function () {
+ _this.change_state(this, false);
+ });
+ this.__callback();
+ },
+ uncheck_all : function () {
+ var _this = this,
+ coll = this._get_settings().checkbox.two_state ? this.get_container_ul().find("li") : this.get_container_ul().children("li");
+ coll.each(function () {
+ _this.change_state(this, true);
+ });
+ this.__callback();
+ },
+
+ is_checked : function(obj) {
+ obj = this._get_node(obj);
+ return obj.length ? obj.is(".jstree-checked") : false;
+ },
+ get_checked : function (obj, get_all) {
+ obj = !obj || obj === -1 ? this.get_container() : this._get_node(obj);
+ return get_all || this._get_settings().checkbox.two_state ? obj.find(".jstree-checked") : obj.find("> ul > .jstree-checked, .jstree-undetermined > ul > .jstree-checked");
+ },
+ get_unchecked : function (obj, get_all) {
+ obj = !obj || obj === -1 ? this.get_container() : this._get_node(obj);
+ return get_all || this._get_settings().checkbox.two_state ? obj.find(".jstree-unchecked") : obj.find("> ul > .jstree-unchecked, .jstree-undetermined > ul > .jstree-unchecked");
+ },
+
+ show_checkboxes : function () { this.get_container().children("ul").removeClass("jstree-no-checkboxes"); },
+ hide_checkboxes : function () { this.get_container().children("ul").addClass("jstree-no-checkboxes"); },
+
+ _repair_state : function (obj) {
+ obj = this._get_node(obj);
+ if(!obj.length) { return; }
+ if(this._get_settings().checkbox.two_state) {
+ obj.find('li').addBack().not('.jstree-checked').removeClass('jstree-undetermined').addClass('jstree-unchecked').children(':checkbox').prop('checked', true);
+ return;
+ }
+ var rc = this._get_settings().checkbox.real_checkboxes,
+ a = obj.find("> ul > .jstree-checked").length,
+ b = obj.find("> ul > .jstree-undetermined").length,
+ c = obj.find("> ul > li").length;
+ if(c === 0) { if(obj.hasClass("jstree-undetermined")) { this.change_state(obj, false); } }
+ else if(a === 0 && b === 0) { this.change_state(obj, true); }
+ else if(a === c) { this.change_state(obj, false); }
+ else {
+ obj.parentsUntil(".jstree","li").addBack().removeClass("jstree-checked jstree-unchecked").addClass("jstree-undetermined");
+ if(rc) { obj.parentsUntil(".jstree", "li").addBack().children(":checkbox").prop("checked", false); }
+ }
+ },
+ reselect : function () {
+ if(this.data.ui && this.data.checkbox.noui) {
+ var _this = this,
+ s = this.data.ui.to_select;
+ s = $.map($.makeArray(s), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); });
+ this.deselect_all();
+ $.each(s, function (i, val) { _this.check_node(val); });
+ this.__callback();
+ }
+ else {
+ this.__call_old();
+ }
+ },
+ save_loaded : function () {
+ var _this = this;
+ this.data.core.to_load = [];
+ this.get_container_ul().find("li.jstree-closed.jstree-undetermined").each(function () {
+ if(this.id) { _this.data.core.to_load.push("#" + this.id); }
+ });
+ }
+ }
+ });
+ $(function() {
+ var css_string = '.jstree .jstree-real-checkbox { display:none; } ';
+ $.vakata.css.add_sheet({ str : css_string, title : "jstree" });
+ });
+})(jQuery);
+//*/
+
+/*
+ * jsTree XML plugin
+ * The XML data store. Datastores are build by overriding the `load_node` and `_is_loaded` functions.
+ */
+(function ($) {
+ $.vakata.xslt = function (xml, xsl, callback) {
+ var r = false, p, q, s;
+ // IE9
+ if(r === false && window.ActiveXObject) {
+ try {
+ r = new ActiveXObject("Msxml2.XSLTemplate");
+ q = new ActiveXObject("Msxml2.DOMDocument");
+ q.loadXML(xml);
+ s = new ActiveXObject("Msxml2.FreeThreadedDOMDocument");
+ s.loadXML(xsl);
+ r.stylesheet = s;
+ p = r.createProcessor();
+ p.input = q;
+ p.transform();
+ r = p.output;
+ }
+ catch (e) { }
+ }
+ xml = $.parseXML(xml);
+ xsl = $.parseXML(xsl);
+ // FF, Chrome
+ if(r === false && typeof (XSLTProcessor) !== "undefined") {
+ p = new XSLTProcessor();
+ p.importStylesheet(xsl);
+ r = p.transformToFragment(xml, document);
+ r = $('<div />').append(r).html();
+ }
+ // OLD IE
+ if(r === false && typeof (xml.transformNode) !== "undefined") {
+ r = xml.transformNode(xsl);
+ }
+ callback.call(null, r);
+ };
+ var xsl = {
+ 'nest' : '<' + '?xml version="1.0" encoding="utf-8" ?>' +
+ '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >' +
+ '<xsl:output method="html" encoding="utf-8" omit-xml-declaration="yes" standalone="no" indent="no" media-type="text/html" />' +
+ '<xsl:template match="/">' +
+ ' <xsl:call-template name="nodes">' +
+ ' <xsl:with-param name="node" select="/root" />' +
+ ' </xsl:call-template>' +
+ '</xsl:template>' +
+ '<xsl:template name="nodes">' +
+ ' <xsl:param name="node" />' +
+ ' <ul>' +
+ ' <xsl:for-each select="$node/item">' +
+ ' <xsl:variable name="children" select="count(./item) &gt; 0" />' +
+ ' <li>' +
+ ' <xsl:attribute name="class">' +
+ ' <xsl:if test="position() = last()">jstree-last </xsl:if>' +
+ ' <xsl:choose>' +
+ ' <xsl:when test="@state = \'open\'">jstree-open </xsl:when>' +
+ ' <xsl:when test="$children or @hasChildren or @state = \'closed\'">jstree-closed </xsl:when>' +
+ ' <xsl:otherwise>jstree-leaf </xsl:otherwise>' +
+ ' </xsl:choose>' +
+ ' <xsl:value-of select="@class" />' +
+ ' </xsl:attribute>' +
+ ' <xsl:for-each select="@*">' +
+ ' <xsl:if test="name() != \'class\' and name() != \'state\' and name() != \'hasChildren\'">' +
+ ' <xsl:attribute name="{name()}"><xsl:value-of select="." /></xsl:attribute>' +
+ ' </xsl:if>' +
+ ' </xsl:for-each>' +
+ ' <ins class="jstree-icon"><xsl:text>&#xa0;</xsl:text></ins>' +
+ ' <xsl:for-each select="content/name">' +
+ ' <a>' +
+ ' <xsl:attribute name="href">' +
+ ' <xsl:choose>' +
+ ' <xsl:when test="@href"><xsl:value-of select="@href" /></xsl:when>' +
+ ' <xsl:otherwise>#</xsl:otherwise>' +
+ ' </xsl:choose>' +
+ ' </xsl:attribute>' +
+ ' <xsl:attribute name="class"><xsl:value-of select="@lang" /> <xsl:value-of select="@class" /></xsl:attribute>' +
+ ' <xsl:attribute name="style"><xsl:value-of select="@style" /></xsl:attribute>' +
+ ' <xsl:for-each select="@*">' +
+ ' <xsl:if test="name() != \'style\' and name() != \'class\' and name() != \'href\'">' +
+ ' <xsl:attribute name="{name()}"><xsl:value-of select="." /></xsl:attribute>' +
+ ' </xsl:if>' +
+ ' </xsl:for-each>' +
+ ' <ins>' +
+ ' <xsl:attribute name="class">jstree-icon ' +
+ ' <xsl:if test="string-length(attribute::icon) > 0 and not(contains(@icon,\'/\'))"><xsl:value-of select="@icon" /></xsl:if>' +
+ ' </xsl:attribute>' +
+ ' <xsl:if test="string-length(attribute::icon) > 0 and contains(@icon,\'/\')"><xsl:attribute name="style">background:url(<xsl:value-of select="@icon" />) center center no-repeat;</xsl:attribute></xsl:if>' +
+ ' <xsl:text>&#xa0;</xsl:text>' +
+ ' </ins>' +
+ ' <xsl:copy-of select="./child::node()" />' +
+ ' </a>' +
+ ' </xsl:for-each>' +
+ ' <xsl:if test="$children or @hasChildren"><xsl:call-template name="nodes"><xsl:with-param name="node" select="current()" /></xsl:call-template></xsl:if>' +
+ ' </li>' +
+ ' </xsl:for-each>' +
+ ' </ul>' +
+ '</xsl:template>' +
+ '</xsl:stylesheet>',
+
+ 'flat' : '<' + '?xml version="1.0" encoding="utf-8" ?>' +
+ '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >' +
+ '<xsl:output method="html" encoding="utf-8" omit-xml-declaration="yes" standalone="no" indent="no" media-type="text/xml" />' +
+ '<xsl:template match="/">' +
+ ' <ul>' +
+ ' <xsl:for-each select="//item[not(@parent_id) or @parent_id=0 or not(@parent_id = //item/@id)]">' + /* the last `or` may be removed */
+ ' <xsl:call-template name="nodes">' +
+ ' <xsl:with-param name="node" select="." />' +
+ ' <xsl:with-param name="is_last" select="number(position() = last())" />' +
+ ' </xsl:call-template>' +
+ ' </xsl:for-each>' +
+ ' </ul>' +
+ '</xsl:template>' +
+ '<xsl:template name="nodes">' +
+ ' <xsl:param name="node" />' +
+ ' <xsl:param name="is_last" />' +
+ ' <xsl:variable name="children" select="count(//item[@parent_id=$node/attribute::id]) &gt; 0" />' +
+ ' <li>' +
+ ' <xsl:attribute name="class">' +
+ ' <xsl:if test="$is_last = true()">jstree-last </xsl:if>' +
+ ' <xsl:choose>' +
+ ' <xsl:when test="@state = \'open\'">jstree-open </xsl:when>' +
+ ' <xsl:when test="$children or @hasChildren or @state = \'closed\'">jstree-closed </xsl:when>' +
+ ' <xsl:otherwise>jstree-leaf </xsl:otherwise>' +
+ ' </xsl:choose>' +
+ ' <xsl:value-of select="@class" />' +
+ ' </xsl:attribute>' +
+ ' <xsl:for-each select="@*">' +
+ ' <xsl:if test="name() != \'parent_id\' and name() != \'hasChildren\' and name() != \'class\' and name() != \'state\'">' +
+ ' <xsl:attribute name="{name()}"><xsl:value-of select="." /></xsl:attribute>' +
+ ' </xsl:if>' +
+ ' </xsl:for-each>' +
+ ' <ins class="jstree-icon"><xsl:text>&#xa0;</xsl:text></ins>' +
+ ' <xsl:for-each select="content/name">' +
+ ' <a>' +
+ ' <xsl:attribute name="href">' +
+ ' <xsl:choose>' +
+ ' <xsl:when test="@href"><xsl:value-of select="@href" /></xsl:when>' +
+ ' <xsl:otherwise>#</xsl:otherwise>' +
+ ' </xsl:choose>' +
+ ' </xsl:attribute>' +
+ ' <xsl:attribute name="class"><xsl:value-of select="@lang" /> <xsl:value-of select="@class" /></xsl:attribute>' +
+ ' <xsl:attribute name="style"><xsl:value-of select="@style" /></xsl:attribute>' +
+ ' <xsl:for-each select="@*">' +
+ ' <xsl:if test="name() != \'style\' and name() != \'class\' and name() != \'href\'">' +
+ ' <xsl:attribute name="{name()}"><xsl:value-of select="." /></xsl:attribute>' +
+ ' </xsl:if>' +
+ ' </xsl:for-each>' +
+ ' <ins>' +
+ ' <xsl:attribute name="class">jstree-icon ' +
+ ' <xsl:if test="string-length(attribute::icon) > 0 and not(contains(@icon,\'/\'))"><xsl:value-of select="@icon" /></xsl:if>' +
+ ' </xsl:attribute>' +
+ ' <xsl:if test="string-length(attribute::icon) > 0 and contains(@icon,\'/\')"><xsl:attribute name="style">background:url(<xsl:value-of select="@icon" />) center center no-repeat;</xsl:attribute></xsl:if>' +
+ ' <xsl:text>&#xa0;</xsl:text>' +
+ ' </ins>' +
+ ' <xsl:copy-of select="./child::node()" />' +
+ ' </a>' +
+ ' </xsl:for-each>' +
+ ' <xsl:if test="$children">' +
+ ' <ul>' +
+ ' <xsl:for-each select="//item[@parent_id=$node/attribute::id]">' +
+ ' <xsl:call-template name="nodes">' +
+ ' <xsl:with-param name="node" select="." />' +
+ ' <xsl:with-param name="is_last" select="number(position() = last())" />' +
+ ' </xsl:call-template>' +
+ ' </xsl:for-each>' +
+ ' </ul>' +
+ ' </xsl:if>' +
+ ' </li>' +
+ '</xsl:template>' +
+ '</xsl:stylesheet>'
+ },
+ escape_xml = function(string) {
+ return string
+ .toString()
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&apos;');
+ };
+ $.jstree.plugin("xml_data", {
+ defaults : {
+ data : false,
+ ajax : false,
+ xsl : "flat",
+ clean_node : false,
+ correct_state : true,
+ get_skip_empty : false,
+ get_include_preamble : true
+ },
+ _fn : {
+ load_node : function (obj, s_call, e_call) { var _this = this; this.load_node_xml(obj, function () { _this.__callback({ "obj" : _this._get_node(obj) }); s_call.call(this); }, e_call); },
+ _is_loaded : function (obj) {
+ var s = this._get_settings().xml_data;
+ obj = this._get_node(obj);
+ return obj == -1 || !obj || (!s.ajax && !$.isFunction(s.data)) || obj.is(".jstree-open, .jstree-leaf") || obj.children("ul").children("li").size() > 0;
+ },
+ load_node_xml : function (obj, s_call, e_call) {
+ var s = this.get_settings().xml_data,
+ error_func = function () {},
+ success_func = function () {};
+
+ obj = this._get_node(obj);
+ if(obj && obj !== -1) {
+ if(obj.data("jstree_is_loading")) { return; }
+ else { obj.data("jstree_is_loading",true); }
+ }
+ switch(!0) {
+ case (!s.data && !s.ajax): throw "Neither data nor ajax settings supplied.";
+ case ($.isFunction(s.data)):
+ s.data.call(this, obj, $.proxy(function (d) {
+ this.parse_xml(d, $.proxy(function (d) {
+ if(d) {
+ d = d.replace(/ ?xmlns="[^"]*"/ig, "");
+ if(d.length > 10) {
+ d = $(d);
+ if(obj === -1 || !obj) { this.get_container().children("ul").empty().append(d.children()); }
+ else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.append(d); obj.removeData("jstree_is_loading"); }
+ if(s.clean_node) { this.clean_node(obj); }
+ if(s_call) { s_call.call(this); }
+ }
+ else {
+ if(obj && obj !== -1) {
+ obj.children("a.jstree-loading").removeClass("jstree-loading");
+ obj.removeData("jstree_is_loading");
+ if(s.correct_state) {
+ this.correct_state(obj);
+ if(s_call) { s_call.call(this); }
+ }
+ }
+ else {
+ if(s.correct_state) {
+ this.get_container().children("ul").empty();
+ if(s_call) { s_call.call(this); }
+ }
+ }
+ }
+ }
+ }, this));
+ }, this));
+ break;
+ case (!!s.data && !s.ajax) || (!!s.data && !!s.ajax && (!obj || obj === -1)):
+ if(!obj || obj == -1) {
+ this.parse_xml(s.data, $.proxy(function (d) {
+ if(d) {
+ d = d.replace(/ ?xmlns="[^"]*"/ig, "");
+ if(d.length > 10) {
+ d = $(d);
+ this.get_container().children("ul").empty().append(d.children());
+ if(s.clean_node) { this.clean_node(obj); }
+ if(s_call) { s_call.call(this); }
+ }
+ }
+ else {
+ if(s.correct_state) {
+ this.get_container().children("ul").empty();
+ if(s_call) { s_call.call(this); }
+ }
+ }
+ }, this));
+ }
+ break;
+ case (!s.data && !!s.ajax) || (!!s.data && !!s.ajax && obj && obj !== -1):
+ error_func = function (x, t, e) {
+ var ef = this.get_settings().xml_data.ajax.error;
+ if(ef) { ef.call(this, x, t, e); }
+ if(obj !== -1 && obj.length) {
+ obj.children("a.jstree-loading").removeClass("jstree-loading");
+ obj.removeData("jstree_is_loading");
+ if(t === "success" && s.correct_state) { this.correct_state(obj); }
+ }
+ else {
+ if(t === "success" && s.correct_state) { this.get_container().children("ul").empty(); }
+ }
+ if(e_call) { e_call.call(this); }
+ };
+ success_func = function (d, t, x) {
+ d = x.responseText;
+ var sf = this.get_settings().xml_data.ajax.success;
+ if(sf) { d = sf.call(this,d,t,x) || d; }
+ if(d === "" || (d && d.toString && d.toString().replace(/^[\s\n]+$/,"") === "")) {
+ return error_func.call(this, x, t, "");
+ }
+ this.parse_xml(d, $.proxy(function (d) {
+ if(d) {
+ d = d.replace(/ ?xmlns="[^"]*"/ig, "");
+ if(d.length > 10) {
+ d = $(d);
+ if(obj === -1 || !obj) { this.get_container().children("ul").empty().append(d.children()); }
+ else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.append(d); obj.removeData("jstree_is_loading"); }
+ if(s.clean_node) { this.clean_node(obj); }
+ if(s_call) { s_call.call(this); }
+ }
+ else {
+ if(obj && obj !== -1) {
+ obj.children("a.jstree-loading").removeClass("jstree-loading");
+ obj.removeData("jstree_is_loading");
+ if(s.correct_state) {
+ this.correct_state(obj);
+ if(s_call) { s_call.call(this); }
+ }
+ }
+ else {
+ if(s.correct_state) {
+ this.get_container().children("ul").empty();
+ if(s_call) { s_call.call(this); }
+ }
+ }
+ }
+ }
+ }, this));
+ };
+ s.ajax.context = this;
+ s.ajax.error = error_func;
+ s.ajax.success = success_func;
+ if(!s.ajax.dataType) { s.ajax.dataType = "xml"; }
+ if($.isFunction(s.ajax.url)) { s.ajax.url = s.ajax.url.call(this, obj); }
+ if($.isFunction(s.ajax.data)) { s.ajax.data = s.ajax.data.call(this, obj); }
+ $.ajax(s.ajax);
+ break;
+ }
+ },
+ parse_xml : function (xml, callback) {
+ var s = this._get_settings().xml_data;
+ $.vakata.xslt(xml, xsl[s.xsl], callback);
+ },
+ get_xml : function (tp, obj, li_attr, a_attr, is_callback) {
+ var result = "",
+ s = this._get_settings(),
+ _this = this,
+ tmp1, tmp2, li, a, lang;
+ if(!tp) { tp = "flat"; }
+ if(!is_callback) { is_callback = 0; }
+ obj = this._get_node(obj);
+ if(!obj || obj === -1) { obj = this.get_container().find("> ul > li"); }
+ li_attr = $.isArray(li_attr) ? li_attr : [ "id", "class" ];
+ if(!is_callback && this.data.types && $.inArray(s.types.type_attr, li_attr) === -1) { li_attr.push(s.types.type_attr); }
+
+ a_attr = $.isArray(a_attr) ? a_attr : [ ];
+
+ if(!is_callback) {
+ if(s.xml_data.get_include_preamble) {
+ result += '<' + '?xml version="1.0" encoding="UTF-8"?' + '>';
+ }
+ result += "<root>";
+ }
+ obj.each(function () {
+ result += "<item";
+ li = $(this);
+ $.each(li_attr, function (i, v) {
+ var t = li.attr(v);
+ if(!s.xml_data.get_skip_empty || typeof t !== "undefined") {
+ result += " " + v + "=\"" + escape_xml((" " + (t || "")).replace(/ jstree[^ ]*/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,"")) + "\"";
+ }
+ });
+ if(li.hasClass("jstree-open")) { result += " state=\"open\""; }
+ if(li.hasClass("jstree-closed")) { result += " state=\"closed\""; }
+ if(tp === "flat") { result += " parent_id=\"" + escape_xml(is_callback) + "\""; }
+ result += ">";
+ result += "<content>";
+ a = li.children("a");
+ a.each(function () {
+ tmp1 = $(this);
+ lang = false;
+ result += "<name";
+ if($.inArray("languages", s.plugins) !== -1) {
+ $.each(s.languages, function (k, z) {
+ if(tmp1.hasClass(z)) { result += " lang=\"" + escape_xml(z) + "\""; lang = z; return false; }
+ });
+ }
+ if(a_attr.length) {
+ $.each(a_attr, function (k, z) {
+ var t = tmp1.attr(z);
+ if(!s.xml_data.get_skip_empty || typeof t !== "undefined") {
+ result += " " + z + "=\"" + escape_xml((" " + t || "").replace(/ jstree[^ ]*/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,"")) + "\"";
+ }
+ });
+ }
+ if(tmp1.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').replace(/^\s+$/ig,"").length) {
+ result += ' icon="' + escape_xml(tmp1.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,"")) + '"';
+ }
+ if(tmp1.children("ins").get(0).style.backgroundImage.length) {
+ result += ' icon="' + escape_xml(tmp1.children("ins").get(0).style.backgroundImage.replace("url(","").replace(")","").replace(/'/ig,"").replace(/"/ig,"")) + '"';
+ }
+ result += ">";
+ result += "<![CDATA[" + _this.get_text(tmp1, lang) + "]]>";
+ result += "</name>";
+ });
+ result += "</content>";
+ tmp2 = li[0].id || true;
+ li = li.find("> ul > li");
+ if(li.length) { tmp2 = _this.get_xml(tp, li, li_attr, a_attr, tmp2); }
+ else { tmp2 = ""; }
+ if(tp == "nest") { result += tmp2; }
+ result += "</item>";
+ if(tp == "flat") { result += tmp2; }
+ });
+ if(!is_callback) { result += "</root>"; }
+ return result;
+ }
+ }
+ });
+})(jQuery);
+//*/
+
+/*
+ * jsTree search plugin
+ * Enables both sync and async search on the tree
+ * DOES NOT WORK WITH JSON PROGRESSIVE RENDER
+ */
+(function ($) {
+ if($().jquery.split('.')[1] >= 8) {
+ $.expr[':'].jstree_contains = $.expr.createPseudo(function(search) {
+ return function(a) {
+ return (a.textContent || a.innerText || "").toLowerCase().indexOf(search.toLowerCase())>=0;
+ };
+ });
+ $.expr[':'].jstree_title_contains = $.expr.createPseudo(function(search) {
+ return function(a) {
+ return (a.getAttribute("title") || "").toLowerCase().indexOf(search.toLowerCase())>=0;
+ };
+ });
+ }
+ else {
+ $.expr[':'].jstree_contains = function(a,i,m){
+ return (a.textContent || a.innerText || "").toLowerCase().indexOf(m[3].toLowerCase())>=0;
+ };
+ $.expr[':'].jstree_title_contains = function(a,i,m) {
+ return (a.getAttribute("title") || "").toLowerCase().indexOf(m[3].toLowerCase())>=0;
+ };
+ }
+ $.jstree.plugin("search", {
+ __init : function () {
+ this.data.search.str = "";
+ this.data.search.result = $();
+ if(this._get_settings().search.show_only_matches) {
+ this.get_container()
+ .bind("search.jstree", function (e, data) {
+ $(this).children("ul").find("li").hide().removeClass("jstree-last");
+ data.rslt.nodes.parentsUntil(".jstree").addBack().show()
+ .filter("ul").each(function () { $(this).children("li:visible").eq(-1).addClass("jstree-last"); });
+ })
+ .bind("clear_search.jstree", function () {
+ $(this).children("ul").find("li").css("display","").end().end().jstree("clean_node", -1);
+ });
+ }
+ },
+ defaults : {
+ ajax : false,
+ search_method : "jstree_contains", // for case insensitive - jstree_contains
+ show_only_matches : false
+ },
+ _fn : {
+ search : function (str, skip_async) {
+ if($.trim(str) === "") { this.clear_search(); return; }
+ var s = this.get_settings().search,
+ t = this,
+ error_func = function () { },
+ success_func = function () { };
+ this.data.search.str = str;
+
+ if(!skip_async && s.ajax !== false && this.get_container_ul().find("li.jstree-closed:not(:has(ul)):eq(0)").length > 0) {
+ this.search.supress_callback = true;
+ error_func = function () { };
+ success_func = function (d, t, x) {
+ var sf = this.get_settings().search.ajax.success;
+ if(sf) { d = sf.call(this,d,t,x) || d; }
+ this.data.search.to_open = d;
+ this._search_open();
+ };
+ s.ajax.context = this;
+ s.ajax.error = error_func;
+ s.ajax.success = success_func;
+ if($.isFunction(s.ajax.url)) { s.ajax.url = s.ajax.url.call(this, str); }
+ if($.isFunction(s.ajax.data)) { s.ajax.data = s.ajax.data.call(this, str); }
+ if(!s.ajax.data) { s.ajax.data = { "search_string" : str }; }
+ if(!s.ajax.dataType || /^json/.exec(s.ajax.dataType)) { s.ajax.dataType = "json"; }
+ $.ajax(s.ajax);
+ return;
+ }
+ if(this.data.search.result.length) { this.clear_search(); }
+ this.data.search.result = this.get_container().find("a" + (this.data.languages ? "." + this.get_lang() : "" ) + ":" + (s.search_method) + "(" + this.data.search.str + ")");
+ this.data.search.result.addClass("jstree-search").parent().parents(".jstree-closed").each(function () {
+ t.open_node(this, false, true);
+ });
+ this.__callback({ nodes : this.data.search.result, str : str });
+ },
+ clear_search : function (str) {
+ this.data.search.result.removeClass("jstree-search");
+ this.__callback(this.data.search.result);
+ this.data.search.result = $();
+ },
+ _search_open : function (is_callback) {
+ var _this = this,
+ done = true,
+ current = [],
+ remaining = [];
+ if(this.data.search.to_open.length) {
+ $.each(this.data.search.to_open, function (i, val) {
+ if(val == "#") { return true; }
+ if($(val).length && $(val).is(".jstree-closed")) { current.push(val); }
+ else { remaining.push(val); }
+ });
+ if(current.length) {
+ this.data.search.to_open = remaining;
+ $.each(current, function (i, val) {
+ _this.open_node(val, function () { _this._search_open(true); });
+ });
+ done = false;
+ }
+ }
+ if(done) { this.search(this.data.search.str, true); }
+ }
+ }
+ });
+})(jQuery);
+//*/
+
+/*
+ * jsTree contextmenu plugin
+ */
+(function ($) {
+ $.vakata.context = {
+ hide_on_mouseleave : false,
+
+ cnt : $("<div id='vakata-contextmenu' />"),
+ vis : false,
+ tgt : false,
+ par : false,
+ func : false,
+ data : false,
+ rtl : false,
+ show : function (s, t, x, y, d, p, rtl) {
+ $.vakata.context.rtl = !!rtl;
+ var html = $.vakata.context.parse(s), h, w;
+ if(!html) { return; }
+ $.vakata.context.vis = true;
+ $.vakata.context.tgt = t;
+ $.vakata.context.par = p || t || null;
+ $.vakata.context.data = d || null;
+ $.vakata.context.cnt
+ .html(html)
+ .css({ "visibility" : "hidden", "display" : "block", "left" : 0, "top" : 0 });
+
+ if($.vakata.context.hide_on_mouseleave) {
+ $.vakata.context.cnt
+ .one("mouseleave", function(e) { $.vakata.context.hide(); });
+ }
+
+ h = $.vakata.context.cnt.height();
+ w = $.vakata.context.cnt.width();
+ if(x + w > $(document).width()) {
+ x = $(document).width() - (w + 5);
+ $.vakata.context.cnt.find("li > ul").addClass("right");
+ }
+ if(y + h > $(document).height()) {
+ y = y - (h + t[0].offsetHeight);
+ $.vakata.context.cnt.find("li > ul").addClass("bottom");
+ }
+
+ $.vakata.context.cnt
+ .css({ "left" : x, "top" : y })
+ .find("li:has(ul)")
+ .bind("mouseenter", function (e) {
+ var w = $(document).width(),
+ h = $(document).height(),
+ ul = $(this).children("ul").show();
+ if(w !== $(document).width()) { ul.toggleClass("right"); }
+ if(h !== $(document).height()) { ul.toggleClass("bottom"); }
+ })
+ .bind("mouseleave", function (e) {
+ $(this).children("ul").hide();
+ })
+ .end()
+ .css({ "visibility" : "visible" })
+ .show();
+ $(document).triggerHandler("context_show.vakata");
+ },
+ hide : function () {
+ $.vakata.context.vis = false;
+ $.vakata.context.cnt.attr("class","").css({ "visibility" : "hidden" });
+ $(document).triggerHandler("context_hide.vakata");
+ },
+ parse : function (s, is_callback) {
+ if(!s) { return false; }
+ var str = "",
+ tmp = false,
+ was_sep = true;
+ if(!is_callback) { $.vakata.context.func = {}; }
+ str += "<ul>";
+ $.each(s, function (i, val) {
+ if(!val) { return true; }
+ $.vakata.context.func[i] = val.action;
+ if(!was_sep && val.separator_before) {
+ str += "<li class='vakata-separator vakata-separator-before'></li>";
+ }
+ was_sep = false;
+ str += "<li class='" + (val._class || "") + (val._disabled ? " jstree-contextmenu-disabled " : "") + "'><ins ";
+ if(val.icon && val.icon.indexOf("/") === -1) { str += " class='" + val.icon + "' "; }
+ if(val.icon && val.icon.indexOf("/") !== -1) { str += " style='background:url(" + val.icon + ") center center no-repeat;' "; }
+ str += ">&#160;</ins><a href='#' rel='" + i + "'>";
+ if(val.submenu) {
+ str += "<span style='float:" + ($.vakata.context.rtl ? "left" : "right") + ";'>&raquo;</span>";
+ }
+ str += val.label + "</a>";
+ if(val.submenu) {
+ tmp = $.vakata.context.parse(val.submenu, true);
+ if(tmp) { str += tmp; }
+ }
+ str += "</li>";
+ if(val.separator_after) {
+ str += "<li class='vakata-separator vakata-separator-after'></li>";
+ was_sep = true;
+ }
+ });
+ str = str.replace(/<li class\='vakata-separator vakata-separator-after'\><\/li\>$/,"");
+ str += "</ul>";
+ $(document).triggerHandler("context_parse.vakata");
+ return str.length > 10 ? str : false;
+ },
+ exec : function (i) {
+ if($.isFunction($.vakata.context.func[i])) {
+ // if is string - eval and call it!
+ $.vakata.context.func[i].call($.vakata.context.data, $.vakata.context.par);
+ return true;
+ }
+ else { return false; }
+ }
+ };
+ $(function () {
+ var css_string = '' +
+ '#vakata-contextmenu { display:block; visibility:hidden; left:0; top:-200px; position:absolute; margin:0; padding:0; min-width:180px; background:#ebebeb; border:1px solid silver; z-index:10000; *width:180px; } ' +
+ '#vakata-contextmenu ul { min-width:180px; *width:180px; } ' +
+ '#vakata-contextmenu ul, #vakata-contextmenu li { margin:0; padding:0; list-style-type:none; display:block; } ' +
+ '#vakata-contextmenu li { line-height:20px; min-height:20px; position:relative; padding:0px; } ' +
+ '#vakata-contextmenu li a { padding:1px 6px; line-height:17px; display:block; text-decoration:none; margin:1px 1px 0 1px; } ' +
+ '#vakata-contextmenu li ins { float:left; width:16px; height:16px; text-decoration:none; margin-right:2px; } ' +
+ '#vakata-contextmenu li a:hover, #vakata-contextmenu li.vakata-hover > a { background:gray; color:white; } ' +
+ '#vakata-contextmenu li ul { display:none; position:absolute; top:-2px; left:100%; background:#ebebeb; border:1px solid gray; } ' +
+ '#vakata-contextmenu .right { right:100%; left:auto; } ' +
+ '#vakata-contextmenu .bottom { bottom:-1px; top:auto; } ' +
+ '#vakata-contextmenu li.vakata-separator { min-height:0; height:1px; line-height:1px; font-size:1px; overflow:hidden; margin:0 2px; background:silver; /* border-top:1px solid #fefefe; */ padding:0; } ';
+ $.vakata.css.add_sheet({ str : css_string, title : "vakata" });
+ $.vakata.context.cnt
+ .delegate("a","click", function (e) { e.preventDefault(); })
+ .delegate("a","mouseup", function (e) {
+ if(!$(this).parent().hasClass("jstree-contextmenu-disabled") && $.vakata.context.exec($(this).attr("rel"))) {
+ $.vakata.context.hide();
+ }
+ else { $(this).blur(); }
+ })
+ .delegate("a","mouseover", function () {
+ $.vakata.context.cnt.find(".vakata-hover").removeClass("vakata-hover");
+ })
+ .appendTo("body");
+ $(document).bind("mousedown", function (e) { if($.vakata.context.vis && !$.contains($.vakata.context.cnt[0], e.target)) { $.vakata.context.hide(); } });
+ if(typeof $.hotkeys !== "undefined") {
+ $(document)
+ .bind("keydown", "up", function (e) {
+ if($.vakata.context.vis) {
+ var o = $.vakata.context.cnt.find("ul:visible").last().children(".vakata-hover").removeClass("vakata-hover").prevAll("li:not(.vakata-separator)").first();
+ if(!o.length) { o = $.vakata.context.cnt.find("ul:visible").last().children("li:not(.vakata-separator)").last(); }
+ o.addClass("vakata-hover");
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ }
+ })
+ .bind("keydown", "down", function (e) {
+ if($.vakata.context.vis) {
+ var o = $.vakata.context.cnt.find("ul:visible").last().children(".vakata-hover").removeClass("vakata-hover").nextAll("li:not(.vakata-separator)").first();
+ if(!o.length) { o = $.vakata.context.cnt.find("ul:visible").last().children("li:not(.vakata-separator)").first(); }
+ o.addClass("vakata-hover");
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ }
+ })
+ .bind("keydown", "right", function (e) {
+ if($.vakata.context.vis) {
+ $.vakata.context.cnt.find(".vakata-hover").children("ul").show().children("li:not(.vakata-separator)").removeClass("vakata-hover").first().addClass("vakata-hover");
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ }
+ })
+ .bind("keydown", "left", function (e) {
+ if($.vakata.context.vis) {
+ $.vakata.context.cnt.find(".vakata-hover").children("ul").hide().children(".vakata-separator").removeClass("vakata-hover");
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ }
+ })
+ .bind("keydown", "esc", function (e) {
+ $.vakata.context.hide();
+ e.preventDefault();
+ })
+ .bind("keydown", "space", function (e) {
+ $.vakata.context.cnt.find(".vakata-hover").last().children("a").click();
+ e.preventDefault();
+ });
+ }
+ });
+
+ $.jstree.plugin("contextmenu", {
+ __init : function () {
+ this.get_container()
+ .delegate("a", "contextmenu.jstree", $.proxy(function (e) {
+ e.preventDefault();
+ if(!$(e.currentTarget).hasClass("jstree-loading")) {
+ this.show_contextmenu(e.currentTarget, e.pageX, e.pageY);
+ }
+ }, this))
+ .delegate("a", "click.jstree", $.proxy(function (e) {
+ if(this.data.contextmenu) {
+ $.vakata.context.hide();
+ }
+ }, this))
+ .bind("destroy.jstree", $.proxy(function () {
+ // TODO: move this to descruct method
+ if(this.data.contextmenu) {
+ $.vakata.context.hide();
+ }
+ }, this));
+ $(document).bind("context_hide.vakata", $.proxy(function () { this.data.contextmenu = false; }, this));
+ },
+ defaults : {
+ select_node : false, // requires UI plugin
+ show_at_node : true,
+ items : { // Could be a function that should return an object like this one
+ "create" : {
+ "separator_before" : false,
+ "separator_after" : true,
+ "label" : "Create",
+ "action" : function (obj) { this.create(obj); }
+ },
+ "rename" : {
+ "separator_before" : false,
+ "separator_after" : false,
+ "label" : "Rename",
+ "action" : function (obj) { this.rename(obj); }
+ },
+ "remove" : {
+ "separator_before" : false,
+ "icon" : false,
+ "separator_after" : false,
+ "label" : "Delete",
+ "action" : function (obj) { if(this.is_selected(obj)) { this.remove(); } else { this.remove(obj); } }
+ },
+ "ccp" : {
+ "separator_before" : true,
+ "icon" : false,
+ "separator_after" : false,
+ "label" : "Edit",
+ "action" : false,
+ "submenu" : {
+ "cut" : {
+ "separator_before" : false,
+ "separator_after" : false,
+ "label" : "Cut",
+ "action" : function (obj) { this.cut(obj); }
+ },
+ "copy" : {
+ "separator_before" : false,
+ "icon" : false,
+ "separator_after" : false,
+ "label" : "Copy",
+ "action" : function (obj) { this.copy(obj); }
+ },
+ "paste" : {
+ "separator_before" : false,
+ "icon" : false,
+ "separator_after" : false,
+ "label" : "Paste",
+ "action" : function (obj) { this.paste(obj); }
+ }
+ }
+ }
+ }
+ },
+ _fn : {
+ show_contextmenu : function (obj, x, y) {
+ obj = this._get_node(obj);
+ var s = this.get_settings().contextmenu,
+ a = obj.children("a:visible:eq(0)"),
+ o = false,
+ i = false;
+ if(s.select_node && this.data.ui && !this.is_selected(obj)) {
+ this.deselect_all();
+ this.select_node(obj, true);
+ }
+ if(s.show_at_node || typeof x === "undefined" || typeof y === "undefined") {
+ o = a.offset();
+ x = o.left;
+ y = o.top + this.data.core.li_height;
+ }
+ i = obj.data("jstree") && obj.data("jstree").contextmenu ? obj.data("jstree").contextmenu : s.items;
+ if($.isFunction(i)) { i = i.call(this, obj); }
+ this.data.contextmenu = true;
+ $.vakata.context.show(i, a, x, y, this, obj, this._get_settings().core.rtl);
+ if(this.data.themes) { $.vakata.context.cnt.attr("class", "jstree-" + this.data.themes.theme + "-context"); }
+ }
+ }
+ });
+})(jQuery);
+//*/
+
+/*
+ * jsTree types plugin
+ * Adds support types of nodes
+ * You can set an attribute on each li node, that represents its type.
+ * According to the type setting the node may get custom icon/validation rules
+ */
+(function ($) {
+ $.jstree.plugin("types", {
+ __init : function () {
+ var s = this._get_settings().types;
+ this.data.types.attach_to = [];
+ this.get_container()
+ .bind("init.jstree", $.proxy(function () {
+ var types = s.types,
+ attr = s.type_attr,
+ icons_css = "",
+ _this = this;
+
+ $.each(types, function (i, tp) {
+ $.each(tp, function (k, v) {
+ if(!/^(max_depth|max_children|icon|valid_children)$/.test(k)) { _this.data.types.attach_to.push(k); }
+ });
+ if(!tp.icon) { return true; }
+ if( tp.icon.image || tp.icon.position) {
+ if(i == "default") { icons_css += '.jstree-' + _this.get_index() + ' a > .jstree-icon { '; }
+ else { icons_css += '.jstree-' + _this.get_index() + ' li[' + attr + '="' + i + '"] > a > .jstree-icon { '; }
+ if(tp.icon.image) { icons_css += ' background-image:url(' + tp.icon.image + '); '; }
+ if(tp.icon.position){ icons_css += ' background-position:' + tp.icon.position + '; '; }
+ else { icons_css += ' background-position:0 0; '; }
+ icons_css += '} ';
+ }
+ });
+ if(icons_css !== "") { $.vakata.css.add_sheet({ 'str' : icons_css, title : "jstree-types" }); }
+ }, this))
+ .bind("before.jstree", $.proxy(function (e, data) {
+ var s, t,
+ o = this._get_settings().types.use_data ? this._get_node(data.args[0]) : false,
+ d = o && o !== -1 && o.length ? o.data("jstree") : false;
+ if(d && d.types && d.types[data.func] === false) { e.stopImmediatePropagation(); return false; }
+ if($.inArray(data.func, this.data.types.attach_to) !== -1) {
+ if(!data.args[0] || (!data.args[0].tagName && !data.args[0].jquery)) { return; }
+ s = this._get_settings().types.types;
+ t = this._get_type(data.args[0]);
+ if(
+ (
+ (s[t] && typeof s[t][data.func] !== "undefined") ||
+ (s["default"] && typeof s["default"][data.func] !== "undefined")
+ ) && this._check(data.func, data.args[0]) === false
+ ) {
+ e.stopImmediatePropagation();
+ return false;
+ }
+ }
+ }, this));
+ if(is_ie6) {
+ this.get_container()
+ .bind("load_node.jstree set_type.jstree", $.proxy(function (e, data) {
+ var r = data && data.rslt && data.rslt.obj && data.rslt.obj !== -1 ? this._get_node(data.rslt.obj).parent() : this.get_container_ul(),
+ c = false,
+ s = this._get_settings().types;
+ $.each(s.types, function (i, tp) {
+ if(tp.icon && (tp.icon.image || tp.icon.position)) {
+ c = i === "default" ? r.find("li > a > .jstree-icon") : r.find("li[" + s.type_attr + "='" + i + "'] > a > .jstree-icon");
+ if(tp.icon.image) { c.css("backgroundImage","url(" + tp.icon.image + ")"); }
+ c.css("backgroundPosition", tp.icon.position || "0 0");
+ }
+ });
+ }, this));
+ }
+ },
+ defaults : {
+ // defines maximum number of root nodes (-1 means unlimited, -2 means disable max_children checking)
+ max_children : -1,
+ // defines the maximum depth of the tree (-1 means unlimited, -2 means disable max_depth checking)
+ max_depth : -1,
+ // defines valid node types for the root nodes
+ valid_children : "all",
+
+ // whether to use $.data
+ use_data : false,
+ // where is the type stores (the rel attribute of the LI element)
+ type_attr : "rel",
+ // a list of types
+ types : {
+ // the default type
+ "default" : {
+ "max_children" : -1,
+ "max_depth" : -1,
+ "valid_children": "all"
+
+ // Bound functions - you can bind any other function here (using boolean or function)
+ //"select_node" : true
+ }
+ }
+ },
+ _fn : {
+ _types_notify : function (n, data) {
+ if(data.type && this._get_settings().types.use_data) {
+ this.set_type(data.type, n);
+ }
+ },
+ _get_type : function (obj) {
+ obj = this._get_node(obj);
+ return (!obj || !obj.length) ? false : obj.attr(this._get_settings().types.type_attr) || "default";
+ },
+ set_type : function (str, obj) {
+ obj = this._get_node(obj);
+ var ret = (!obj.length || !str) ? false : obj.attr(this._get_settings().types.type_attr, str);
+ if(ret) { this.__callback({ obj : obj, type : str}); }
+ return ret;
+ },
+ _check : function (rule, obj, opts) {
+ obj = this._get_node(obj);
+ var v = false, t = this._get_type(obj), d = 0, _this = this, s = this._get_settings().types, data = false;
+ if(obj === -1) {
+ if(!!s[rule]) { v = s[rule]; }
+ else { return; }
+ }
+ else {
+ if(t === false) { return; }
+ data = s.use_data ? obj.data("jstree") : false;
+ if(data && data.types && typeof data.types[rule] !== "undefined") { v = data.types[rule]; }
+ else if(!!s.types[t] && typeof s.types[t][rule] !== "undefined") { v = s.types[t][rule]; }
+ else if(!!s.types["default"] && typeof s.types["default"][rule] !== "undefined") { v = s.types["default"][rule]; }
+ }
+ if($.isFunction(v)) { v = v.call(this, obj); }
+ if(rule === "max_depth" && obj !== -1 && opts !== false && s.max_depth !== -2 && v !== 0) {
+ // also include the node itself - otherwise if root node it is not checked
+ obj.children("a:eq(0)").parentsUntil(".jstree","li").each(function (i) {
+ // check if current depth already exceeds global tree depth
+ if(s.max_depth !== -1 && s.max_depth - (i + 1) <= 0) { v = 0; return false; }
+ d = (i === 0) ? v : _this._check(rule, this, false);
+ // check if current node max depth is already matched or exceeded
+ if(d !== -1 && d - (i + 1) <= 0) { v = 0; return false; }
+ // otherwise - set the max depth to the current value minus current depth
+ if(d >= 0 && (d - (i + 1) < v || v < 0) ) { v = d - (i + 1); }
+ // if the global tree depth exists and it minus the nodes calculated so far is less than `v` or `v` is unlimited
+ if(s.max_depth >= 0 && (s.max_depth - (i + 1) < v || v < 0) ) { v = s.max_depth - (i + 1); }
+ });
+ }
+ return v;
+ },
+ check_move : function () {
+ if(!this.__call_old()) { return false; }
+ var m = this._get_move(),
+ s = m.rt._get_settings().types,
+ mc = m.rt._check("max_children", m.cr),
+ md = m.rt._check("max_depth", m.cr),
+ vc = m.rt._check("valid_children", m.cr),
+ ch = 0, d = 1, t;
+
+ if(vc === "none") { return false; }
+ if($.isArray(vc) && m.ot && m.ot._get_type) {
+ m.o.each(function () {
+ if($.inArray(m.ot._get_type(this), vc) === -1) { d = false; return false; }
+ });
+ if(d === false) { return false; }
+ }
+ if(s.max_children !== -2 && mc !== -1) {
+ ch = m.cr === -1 ? this.get_container().find("> ul > li").not(m.o).length : m.cr.find("> ul > li").not(m.o).length;
+ if(ch + m.o.length > mc) { return false; }
+ }
+ if(s.max_depth !== -2 && md !== -1) {
+ d = 0;
+ if(md === 0) { return false; }
+ if(typeof m.o.d === "undefined") {
+ // TODO: deal with progressive rendering and async when checking max_depth (how to know the depth of the moved node)
+ t = m.o;
+ while(t.length > 0) {
+ t = t.find("> ul > li");
+ d ++;
+ }
+ m.o.d = d;
+ }
+ if(md - m.o.d < 0) { return false; }
+ }
+ return true;
+ },
+ create_node : function (obj, position, js, callback, is_loaded, skip_check) {
+ if(!skip_check && (is_loaded || this._is_loaded(obj))) {
+ var p = (typeof position == "string" && position.match(/^before|after$/i) && obj !== -1) ? this._get_parent(obj) : this._get_node(obj),
+ s = this._get_settings().types,
+ mc = this._check("max_children", p),
+ md = this._check("max_depth", p),
+ vc = this._check("valid_children", p),
+ ch;
+ if(typeof js === "string") { js = { data : js }; }
+ if(!js) { js = {}; }
+ if(vc === "none") { return false; }
+ if($.isArray(vc)) {
+ if(!js.attr || !js.attr[s.type_attr]) {
+ if(!js.attr) { js.attr = {}; }
+ js.attr[s.type_attr] = vc[0];
+ }
+ else {
+ if($.inArray(js.attr[s.type_attr], vc) === -1) { return false; }
+ }
+ }
+ if(s.max_children !== -2 && mc !== -1) {
+ ch = p === -1 ? this.get_container().find("> ul > li").length : p.find("> ul > li").length;
+ if(ch + 1 > mc) { return false; }
+ }
+ if(s.max_depth !== -2 && md !== -1 && (md - 1) < 0) { return false; }
+ }
+ return this.__call_old(true, obj, position, js, callback, is_loaded, skip_check);
+ }
+ }
+ });
+})(jQuery);
+//*/
+
+/*
+ * jsTree HTML plugin
+ * The HTML data store. Datastores are build by replacing the `load_node` and `_is_loaded` functions.
+ */
+(function ($) {
+ $.jstree.plugin("html_data", {
+ __init : function () {
+ // this used to use html() and clean the whitespace, but this way any attached data was lost
+ this.data.html_data.original_container_html = this.get_container().find(" > ul > li").clone(true);
+ // remove white space from LI node - otherwise nodes appear a bit to the right
+ this.data.html_data.original_container_html.find("li").addBack().contents().filter(function() { return this.nodeType == 3; }).remove();
+ },
+ defaults : {
+ data : false,
+ ajax : false,
+ correct_state : true
+ },
+ _fn : {
+ load_node : function (obj, s_call, e_call) { var _this = this; this.load_node_html(obj, function () { _this.__callback({ "obj" : _this._get_node(obj) }); s_call.call(this); }, e_call); },
+ _is_loaded : function (obj) {
+ obj = this._get_node(obj);
+ return obj == -1 || !obj || (!this._get_settings().html_data.ajax && !$.isFunction(this._get_settings().html_data.data)) || obj.is(".jstree-open, .jstree-leaf") || obj.children("ul").children("li").size() > 0;
+ },
+ load_node_html : function (obj, s_call, e_call) {
+ var d,
+ s = this.get_settings().html_data,
+ error_func = function () {},
+ success_func = function () {};
+ obj = this._get_node(obj);
+ if(obj && obj !== -1) {
+ if(obj.data("jstree_is_loading")) { return; }
+ else { obj.data("jstree_is_loading",true); }
+ }
+ switch(!0) {
+ case ($.isFunction(s.data)):
+ s.data.call(this, obj, $.proxy(function (d) {
+ if(d && d !== "" && d.toString && d.toString().replace(/^[\s\n]+$/,"") !== "") {
+ d = $(d);
+ if(!d.is("ul")) { d = $("<ul />").append(d); }
+ if(obj == -1 || !obj) { this.get_container().children("ul").empty().append(d.children()).find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'>&#160;</ins>").end().filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); }
+ else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.append(d).children("ul").find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'>&#160;</ins>").end().filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); obj.removeData("jstree_is_loading"); }
+ this.clean_node(obj);
+ if(s_call) { s_call.call(this); }
+ }
+ else {
+ if(obj && obj !== -1) {
+ obj.children("a.jstree-loading").removeClass("jstree-loading");
+ obj.removeData("jstree_is_loading");
+ if(s.correct_state) {
+ this.correct_state(obj);
+ if(s_call) { s_call.call(this); }
+ }
+ }
+ else {
+ if(s.correct_state) {
+ this.get_container().children("ul").empty();
+ if(s_call) { s_call.call(this); }
+ }
+ }
+ }
+ }, this));
+ break;
+ case (!s.data && !s.ajax):
+ if(!obj || obj == -1) {
+ this.get_container()
+ .children("ul").empty()
+ .append(this.data.html_data.original_container_html)
+ .find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'>&#160;</ins>").end()
+ .filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon");
+ this.clean_node();
+ }
+ if(s_call) { s_call.call(this); }
+ break;
+ case (!!s.data && !s.ajax) || (!!s.data && !!s.ajax && (!obj || obj === -1)):
+ if(!obj || obj == -1) {
+ d = $(s.data);
+ if(!d.is("ul")) { d = $("<ul />").append(d); }
+ this.get_container()
+ .children("ul").empty().append(d.children())
+ .find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'>&#160;</ins>").end()
+ .filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon");
+ this.clean_node();
+ }
+ if(s_call) { s_call.call(this); }
+ break;
+ case (!s.data && !!s.ajax) || (!!s.data && !!s.ajax && obj && obj !== -1):
+ obj = this._get_node(obj);
+ error_func = function (x, t, e) {
+ var ef = this.get_settings().html_data.ajax.error;
+ if(ef) { ef.call(this, x, t, e); }
+ if(obj != -1 && obj.length) {
+ obj.children("a.jstree-loading").removeClass("jstree-loading");
+ obj.removeData("jstree_is_loading");
+ if(t === "success" && s.correct_state) { this.correct_state(obj); }
+ }
+ else {
+ if(t === "success" && s.correct_state) { this.get_container().children("ul").empty(); }
+ }
+ if(e_call) { e_call.call(this); }
+ };
+ success_func = function (d, t, x) {
+ var sf = this.get_settings().html_data.ajax.success;
+ if(sf) { d = sf.call(this,d,t,x) || d; }
+ if(d === "" || (d && d.toString && d.toString().replace(/^[\s\n]+$/,"") === "")) {
+ return error_func.call(this, x, t, "");
+ }
+ if(d) {
+ d = $(d);
+ if(!d.is("ul")) { d = $("<ul />").append(d); }
+ if(obj == -1 || !obj) { this.get_container().children("ul").empty().append(d.children()).find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'>&#160;</ins>").end().filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); }
+ else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.append(d).children("ul").find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'>&#160;</ins>").end().filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); obj.removeData("jstree_is_loading"); }
+ this.clean_node(obj);
+ if(s_call) { s_call.call(this); }
+ }
+ else {
+ if(obj && obj !== -1) {
+ obj.children("a.jstree-loading").removeClass("jstree-loading");
+ obj.removeData("jstree_is_loading");
+ if(s.correct_state) {
+ this.correct_state(obj);
+ if(s_call) { s_call.call(this); }
+ }
+ }
+ else {
+ if(s.correct_state) {
+ this.get_container().children("ul").empty();
+ if(s_call) { s_call.call(this); }
+ }
+ }
+ }
+ };
+ s.ajax.context = this;
+ s.ajax.error = error_func;
+ s.ajax.success = success_func;
+ if(!s.ajax.dataType) { s.ajax.dataType = "html"; }
+ if($.isFunction(s.ajax.url)) { s.ajax.url = s.ajax.url.call(this, obj); }
+ if($.isFunction(s.ajax.data)) { s.ajax.data = s.ajax.data.call(this, obj); }
+ $.ajax(s.ajax);
+ break;
+ }
+ }
+ }
+ });
+ // include the HTML data plugin by default
+ $.jstree.defaults.plugins.push("html_data");
+})(jQuery);
+//*/
+
+/*
+ * jsTree themeroller plugin
+ * Adds support for jQuery UI themes. Include this at the end of your plugins list, also make sure "themes" is not included.
+ */
+(function ($) {
+ $.jstree.plugin("themeroller", {
+ __init : function () {
+ var s = this._get_settings().themeroller;
+ this.get_container()
+ .addClass("ui-widget-content")
+ .addClass("jstree-themeroller")
+ .delegate("a","mouseenter.jstree", function (e) {
+ if(!$(e.currentTarget).hasClass("jstree-loading")) {
+ $(this).addClass(s.item_h);
+ }
+ })
+ .delegate("a","mouseleave.jstree", function () {
+ $(this).removeClass(s.item_h);
+ })
+ .bind("init.jstree", $.proxy(function (e, data) {
+ data.inst.get_container().find("> ul > li > .jstree-loading > ins").addClass("ui-icon-refresh");
+ this._themeroller(data.inst.get_container().find("> ul > li"));
+ }, this))
+ .bind("open_node.jstree create_node.jstree", $.proxy(function (e, data) {
+ this._themeroller(data.rslt.obj);
+ }, this))
+ .bind("loaded.jstree refresh.jstree", $.proxy(function (e) {
+ this._themeroller();
+ }, this))
+ .bind("close_node.jstree", $.proxy(function (e, data) {
+ this._themeroller(data.rslt.obj);
+ }, this))
+ .bind("delete_node.jstree", $.proxy(function (e, data) {
+ this._themeroller(data.rslt.parent);
+ }, this))
+ .bind("correct_state.jstree", $.proxy(function (e, data) {
+ data.rslt.obj
+ .children("ins.jstree-icon").removeClass(s.opened + " " + s.closed + " ui-icon").end()
+ .find("> a > ins.ui-icon")
+ .filter(function() {
+ return this.className.toString()
+ .replace(s.item_clsd,"").replace(s.item_open,"").replace(s.item_leaf,"")
+ .indexOf("ui-icon-") === -1;
+ }).removeClass(s.item_open + " " + s.item_clsd).addClass(s.item_leaf || "jstree-no-icon");
+ }, this))
+ .bind("select_node.jstree", $.proxy(function (e, data) {
+ data.rslt.obj.children("a").addClass(s.item_a);
+ }, this))
+ .bind("deselect_node.jstree deselect_all.jstree", $.proxy(function (e, data) {
+ this.get_container()
+ .find("a." + s.item_a).removeClass(s.item_a).end()
+ .find("a.jstree-clicked").addClass(s.item_a);
+ }, this))
+ .bind("dehover_node.jstree", $.proxy(function (e, data) {
+ data.rslt.obj.children("a").removeClass(s.item_h);
+ }, this))
+ .bind("hover_node.jstree", $.proxy(function (e, data) {
+ this.get_container()
+ .find("a." + s.item_h).not(data.rslt.obj).removeClass(s.item_h);
+ data.rslt.obj.children("a").addClass(s.item_h);
+ }, this))
+ .bind("move_node.jstree", $.proxy(function (e, data) {
+ this._themeroller(data.rslt.o);
+ this._themeroller(data.rslt.op);
+ }, this));
+ },
+ __destroy : function () {
+ var s = this._get_settings().themeroller,
+ c = [ "ui-icon" ];
+ $.each(s, function (i, v) {
+ v = v.split(" ");
+ if(v.length) { c = c.concat(v); }
+ });
+ this.get_container()
+ .removeClass("ui-widget-content")
+ .find("." + c.join(", .")).removeClass(c.join(" "));
+ },
+ _fn : {
+ _themeroller : function (obj) {
+ var s = this._get_settings().themeroller;
+ obj = (!obj || obj == -1) ? this.get_container_ul() : this._get_node(obj);
+ obj = (!obj || obj == -1) ? this.get_container_ul() : obj.parent();
+ obj
+ .find("li.jstree-closed")
+ .children("ins.jstree-icon").removeClass(s.opened).addClass("ui-icon " + s.closed).end()
+ .children("a").addClass(s.item)
+ .children("ins.jstree-icon").addClass("ui-icon")
+ .filter(function() {
+ return this.className.toString()
+ .replace(s.item_clsd,"").replace(s.item_open,"").replace(s.item_leaf,"")
+ .indexOf("ui-icon-") === -1;
+ }).removeClass(s.item_leaf + " " + s.item_open).addClass(s.item_clsd || "jstree-no-icon")
+ .end()
+ .end()
+ .end()
+ .end()
+ .find("li.jstree-open")
+ .children("ins.jstree-icon").removeClass(s.closed).addClass("ui-icon " + s.opened).end()
+ .children("a").addClass(s.item)
+ .children("ins.jstree-icon").addClass("ui-icon")
+ .filter(function() {
+ return this.className.toString()
+ .replace(s.item_clsd,"").replace(s.item_open,"").replace(s.item_leaf,"")
+ .indexOf("ui-icon-") === -1;
+ }).removeClass(s.item_leaf + " " + s.item_clsd).addClass(s.item_open || "jstree-no-icon")
+ .end()
+ .end()
+ .end()
+ .end()
+ .find("li.jstree-leaf")
+ .children("ins.jstree-icon").removeClass(s.closed + " ui-icon " + s.opened).end()
+ .children("a").addClass(s.item)
+ .children("ins.jstree-icon").addClass("ui-icon")
+ .filter(function() {
+ return this.className.toString()
+ .replace(s.item_clsd,"").replace(s.item_open,"").replace(s.item_leaf,"")
+ .indexOf("ui-icon-") === -1;
+ }).removeClass(s.item_clsd + " " + s.item_open).addClass(s.item_leaf || "jstree-no-icon");
+ }
+ },
+ defaults : {
+ "opened" : "ui-icon-triangle-1-se",
+ "closed" : "ui-icon-triangle-1-e",
+ "item" : "ui-state-default",
+ "item_h" : "ui-state-hover",
+ "item_a" : "ui-state-active",
+ "item_open" : "ui-icon-folder-open",
+ "item_clsd" : "ui-icon-folder-collapsed",
+ "item_leaf" : "ui-icon-document"
+ }
+ });
+ $(function() {
+ var css_string = '' +
+ '.jstree-themeroller .ui-icon { overflow:visible; } ' +
+ '.jstree-themeroller a { padding:0 2px; } ' +
+ '.jstree-themeroller .jstree-no-icon { display:none; }';
+ $.vakata.css.add_sheet({ str : css_string, title : "jstree" });
+ });
+})(jQuery);
+//*/
+
+/*
+ * jsTree unique plugin
+ * Forces different names amongst siblings (still a bit experimental)
+ * NOTE: does not check language versions (it will not be possible to have nodes with the same title, even in different languages)
+ */
+(function ($) {
+ $.jstree.plugin("unique", {
+ __init : function () {
+ this.get_container()
+ .bind("before.jstree", $.proxy(function (e, data) {
+ var nms = [], res = true, p, t;
+ if(data.func == "move_node") {
+ // obj, ref, position, is_copy, is_prepared, skip_check
+ if(data.args[4] === true) {
+ if(data.args[0].o && data.args[0].o.length) {
+ data.args[0].o.children("a").each(function () { nms.push($(this).text().replace(/^\s+/g,"")); });
+ res = this._check_unique(nms, data.args[0].np.find("> ul > li").not(data.args[0].o), "move_node");
+ }
+ }
+ }
+ if(data.func == "create_node") {
+ // obj, position, js, callback, is_loaded
+ if(data.args[4] || this._is_loaded(data.args[0])) {
+ p = this._get_node(data.args[0]);
+ if(data.args[1] && (data.args[1] === "before" || data.args[1] === "after")) {
+ p = this._get_parent(data.args[0]);
+ if(!p || p === -1) { p = this.get_container(); }
+ }
+ if(typeof data.args[2] === "string") { nms.push(data.args[2]); }
+ else if(!data.args[2] || !data.args[2].data) { nms.push(this._get_string("new_node")); }
+ else { nms.push(data.args[2].data); }
+ res = this._check_unique(nms, p.find("> ul > li"), "create_node");
+ }
+ }
+ if(data.func == "rename_node") {
+ // obj, val
+ nms.push(data.args[1]);
+ t = this._get_node(data.args[0]);
+ p = this._get_parent(t);
+ if(!p || p === -1) { p = this.get_container(); }
+ res = this._check_unique(nms, p.find("> ul > li").not(t), "rename_node");
+ }
+ if(!res) {
+ e.stopPropagation();
+ return false;
+ }
+ }, this));
+ },
+ defaults : {
+ error_callback : $.noop
+ },
+ _fn : {
+ _check_unique : function (nms, p, func) {
+ var cnms = [], ok = true;
+ p.children("a").each(function () { cnms.push($(this).text().replace(/^\s+/g,"")); });
+ if(!cnms.length || !nms.length) { return true; }
+ $.each(nms, function (i, v) {
+ if($.inArray(v, cnms) !== -1) {
+ ok = false;
+ return false;
+ }
+ });
+ if(!ok) {
+ this._get_settings().unique.error_callback.call(null, nms, p, func);
+ }
+ return ok;
+ },
+ check_move : function () {
+ if(!this.__call_old()) { return false; }
+ var p = this._get_move(), nms = [];
+ if(p.o && p.o.length) {
+ p.o.children("a").each(function () { nms.push($(this).text().replace(/^\s+/g,"")); });
+ return this._check_unique(nms, p.np.find("> ul > li").not(p.o), "check_move");
+ }
+ return true;
+ }
+ }
+ });
+})(jQuery);
+//*/
+
+/*
+ * jsTree wholerow plugin
+ * Makes select and hover work on the entire width of the node
+ * MAY BE HEAVY IN LARGE DOM
+ */
+(function ($) {
+ $.jstree.plugin("wholerow", {
+ __init : function () {
+ if(!this.data.ui) { throw "jsTree wholerow: jsTree UI plugin not included."; }
+ this.data.wholerow.html = false;
+ this.data.wholerow.to = false;
+ this.get_container()
+ .bind("init.jstree", $.proxy(function (e, data) {
+ this._get_settings().core.animation = 0;
+ }, this))
+ .bind("open_node.jstree create_node.jstree clean_node.jstree loaded.jstree", $.proxy(function (e, data) {
+ this._prepare_wholerow_span( data && data.rslt && data.rslt.obj ? data.rslt.obj : -1 );
+ }, this))
+ .bind("search.jstree clear_search.jstree reopen.jstree after_open.jstree after_close.jstree create_node.jstree delete_node.jstree clean_node.jstree", $.proxy(function (e, data) {
+ if(this.data.to) { clearTimeout(this.data.to); }
+ this.data.to = setTimeout( (function (t, o) { return function() { t._prepare_wholerow_ul(o); }; })(this, data && data.rslt && data.rslt.obj ? data.rslt.obj : -1), 0);
+ }, this))
+ .bind("deselect_all.jstree", $.proxy(function (e, data) {
+ this.get_container().find(" > .jstree-wholerow .jstree-clicked").removeClass("jstree-clicked " + (this.data.themeroller ? this._get_settings().themeroller.item_a : "" ));
+ }, this))
+ .bind("select_node.jstree deselect_node.jstree ", $.proxy(function (e, data) {
+ data.rslt.obj.each(function () {
+ var ref = data.inst.get_container().find(" > .jstree-wholerow li:visible:eq(" + ( parseInt((($(this).offset().top - data.inst.get_container().offset().top + data.inst.get_container()[0].scrollTop) / data.inst.data.core.li_height),10)) + ")");
+ // ref.children("a")[e.type === "select_node" ? "addClass" : "removeClass"]("jstree-clicked");
+ ref.children("a").attr("class",data.rslt.obj.children("a").attr("class"));
+ });
+ }, this))
+ .bind("hover_node.jstree dehover_node.jstree", $.proxy(function (e, data) {
+ this.get_container().find(" > .jstree-wholerow .jstree-hovered").removeClass("jstree-hovered " + (this.data.themeroller ? this._get_settings().themeroller.item_h : "" ));
+ if(e.type === "hover_node") {
+ var ref = this.get_container().find(" > .jstree-wholerow li:visible:eq(" + ( parseInt(((data.rslt.obj.offset().top - this.get_container().offset().top + this.get_container()[0].scrollTop) / this.data.core.li_height),10)) + ")");
+ // ref.children("a").addClass("jstree-hovered");
+ ref.children("a").attr("class",data.rslt.obj.children(".jstree-hovered").attr("class"));
+ }
+ }, this))
+ .delegate(".jstree-wholerow-span, ins.jstree-icon, li", "click.jstree", function (e) {
+ var n = $(e.currentTarget);
+ if(e.target.tagName === "A" || (e.target.tagName === "INS" && n.closest("li").is(".jstree-open, .jstree-closed"))) { return; }
+ n.closest("li").children("a:visible:eq(0)").click();
+ e.stopImmediatePropagation();
+ })
+ .delegate("li", "mouseover.jstree", $.proxy(function (e) {
+ e.stopImmediatePropagation();
+ if($(e.currentTarget).children(".jstree-hovered, .jstree-clicked").length) { return false; }
+ this.hover_node(e.currentTarget);
+ return false;
+ }, this))
+ .delegate("li", "mouseleave.jstree", $.proxy(function (e) {
+ if($(e.currentTarget).children("a").hasClass("jstree-hovered").length) { return; }
+ this.dehover_node(e.currentTarget);
+ }, this));
+ if(is_ie7 || is_ie6) {
+ $.vakata.css.add_sheet({ str : ".jstree-" + this.get_index() + " { position:relative; } ", title : "jstree" });
+ }
+ },
+ defaults : {
+ },
+ __destroy : function () {
+ this.get_container().children(".jstree-wholerow").remove();
+ this.get_container().find(".jstree-wholerow-span").remove();
+ },
+ _fn : {
+ _prepare_wholerow_span : function (obj) {
+ obj = !obj || obj == -1 ? this.get_container().find("> ul > li") : this._get_node(obj);
+ if(obj === false) { return; } // added for removing root nodes
+ obj.each(function () {
+ $(this).find("li").addBack().each(function () {
+ var $t = $(this);
+ if($t.children(".jstree-wholerow-span").length) { return true; }
+ $t.prepend("<span class='jstree-wholerow-span' style='width:" + ($t.parentsUntil(".jstree","li").length * 18) + "px;'>&#160;</span>");
+ });
+ });
+ },
+ _prepare_wholerow_ul : function () {
+ var o = this.get_container().children("ul").eq(0), h = o.html();
+ o.addClass("jstree-wholerow-real");
+ if(this.data.wholerow.last_html !== h) {
+ this.data.wholerow.last_html = h;
+ this.get_container().children(".jstree-wholerow").remove();
+ this.get_container().append(
+ o.clone().removeClass("jstree-wholerow-real")
+ .wrapAll("<div class='jstree-wholerow' />").parent()
+ .width(o.parent()[0].scrollWidth)
+ .css("top", (o.height() + ( is_ie7 ? 5 : 0)) * -1 )
+ .find("li[id]").each(function () { this.removeAttribute("id"); }).end()
+ );
+ }
+ }
+ }
+ });
+ $(function() {
+ var css_string = '' +
+ '.jstree .jstree-wholerow-real { position:relative; z-index:1; } ' +
+ '.jstree .jstree-wholerow-real li { cursor:pointer; } ' +
+ '.jstree .jstree-wholerow-real a { border-left-color:transparent !important; border-right-color:transparent !important; } ' +
+ '.jstree .jstree-wholerow { position:relative; z-index:0; height:0; } ' +
+ '.jstree .jstree-wholerow ul, .jstree .jstree-wholerow li { width:100%; } ' +
+ '.jstree .jstree-wholerow, .jstree .jstree-wholerow ul, .jstree .jstree-wholerow li, .jstree .jstree-wholerow a { margin:0 !important; padding:0 !important; } ' +
+ '.jstree .jstree-wholerow, .jstree .jstree-wholerow ul, .jstree .jstree-wholerow li { background:transparent !important; }' +
+ '.jstree .jstree-wholerow ins, .jstree .jstree-wholerow span, .jstree .jstree-wholerow input { display:none !important; }' +
+ '.jstree .jstree-wholerow a, .jstree .jstree-wholerow a:hover { text-indent:-9999px; !important; width:100%; padding:0 !important; border-right-width:0px !important; border-left-width:0px !important; } ' +
+ '.jstree .jstree-wholerow-span { position:absolute; left:0; margin:0px; padding:0; height:18px; border-width:0; padding:0; z-index:0; }';
+ if(is_ff2) {
+ css_string += '' +
+ '.jstree .jstree-wholerow a { display:block; height:18px; margin:0; padding:0; border:0; } ' +
+ '.jstree .jstree-wholerow-real a { border-color:transparent !important; } ';
+ }
+ if(is_ie7 || is_ie6) {
+ css_string += '' +
+ '.jstree .jstree-wholerow, .jstree .jstree-wholerow li, .jstree .jstree-wholerow ul, .jstree .jstree-wholerow a { margin:0; padding:0; line-height:18px; } ' +
+ '.jstree .jstree-wholerow a { display:block; height:18px; line-height:18px; overflow:hidden; } ';
+ }
+ $.vakata.css.add_sheet({ str : css_string, title : "jstree" });
+ });
+})(jQuery);
+//*/
+
+/*
+* jsTree model plugin
+* This plugin gets jstree to use a class model to retrieve data, creating great dynamism
+*/
+(function ($) {
+ var nodeInterface = ["getChildren","getChildrenCount","getAttr","getName","getProps"],
+ validateInterface = function(obj, inter) {
+ var valid = true;
+ obj = obj || {};
+ inter = [].concat(inter);
+ $.each(inter, function (i, v) {
+ if(!$.isFunction(obj[v])) { valid = false; return false; }
+ });
+ return valid;
+ };
+ $.jstree.plugin("model", {
+ __init : function () {
+ if(!this.data.json_data) { throw "jsTree model: jsTree json_data plugin not included."; }
+ this._get_settings().json_data.data = function (n, b) {
+ var obj = (n == -1) ? this._get_settings().model.object : n.data("jstree_model");
+ if(!validateInterface(obj, nodeInterface)) { return b.call(null, false); }
+ if(this._get_settings().model.async) {
+ obj.getChildren($.proxy(function (data) {
+ this.model_done(data, b);
+ }, this));
+ }
+ else {
+ this.model_done(obj.getChildren(), b);
+ }
+ };
+ },
+ defaults : {
+ object : false,
+ id_prefix : false,
+ async : false
+ },
+ _fn : {
+ model_done : function (data, callback) {
+ var ret = [],
+ s = this._get_settings(),
+ _this = this;
+
+ if(!$.isArray(data)) { data = [data]; }
+ $.each(data, function (i, nd) {
+ var r = nd.getProps() || {};
+ r.attr = nd.getAttr() || {};
+ if(nd.getChildrenCount()) { r.state = "closed"; }
+ r.data = nd.getName();
+ if(!$.isArray(r.data)) { r.data = [r.data]; }
+ if(_this.data.types && $.isFunction(nd.getType)) {
+ r.attr[s.types.type_attr] = nd.getType();
+ }
+ if(r.attr.id && s.model.id_prefix) { r.attr.id = s.model.id_prefix + r.attr.id; }
+ if(!r.metadata) { r.metadata = { }; }
+ r.metadata.jstree_model = nd;
+ ret.push(r);
+ });
+ callback.call(null, ret);
+ }
+ }
+ });
+})(jQuery);
+//*/
+
+})(); \ No newline at end of file
diff --git a/resources/assets/javascripts/jquery/jstree/themes/default/d.gif b/resources/assets/javascripts/jquery/jstree/themes/default/d.gif
new file mode 100644
index 0000000..0e958d3
--- /dev/null
+++ b/resources/assets/javascripts/jquery/jstree/themes/default/d.gif
Binary files differ
diff --git a/resources/assets/javascripts/jquery/jstree/themes/default/d.png b/resources/assets/javascripts/jquery/jstree/themes/default/d.png
new file mode 100644
index 0000000..8540175
--- /dev/null
+++ b/resources/assets/javascripts/jquery/jstree/themes/default/d.png
Binary files differ
diff --git a/resources/assets/javascripts/jquery/jstree/themes/default/style.css b/resources/assets/javascripts/jquery/jstree/themes/default/style.css
new file mode 100644
index 0000000..6e779c2
--- /dev/null
+++ b/resources/assets/javascripts/jquery/jstree/themes/default/style.css
@@ -0,0 +1,76 @@
+/*
+ * jsTree default theme 1.0
+ * Supported features: dots/no-dots, icons/no-icons, focused, loading
+ * Supported plugins: ui (hovered, clicked), checkbox, contextmenu, search
+ */
+
+.jstree { width:90%; }
+
+.jstree-default li,
+.jstree-default ins { background-image:url("d.png"); background-repeat:no-repeat; background-color:transparent; }
+.jstree-default li { background-position:-90px 0; background-repeat:repeat-y; }
+.jstree-default li.jstree-last { background:transparent; }
+.jstree-default .jstree-open > ins { background-position:-72px 0; }
+.jstree-default .jstree-closed > ins { background-position:-54px 0; }
+.jstree-default .jstree-leaf > ins { background-position:-36px 0; }
+
+.jstree-default .jstree-hovered { background:#e7f4f9; border:1px solid #d8f0fa; padding:0 2px 0 1px; }
+.jstree-default .jstree-clicked { background:#beebff; border:1px solid #99defd; padding:0 2px 0 1px; }
+.jstree-default a .jstree-icon { background-position:-56px -19px; }
+.jstree-default a.jstree-loading .jstree-icon { background:url("throbber.gif") center center no-repeat !important; }
+
+.jstree-default.jstree-focused { background:transparent; }
+
+.jstree-default .jstree-no-dots li,
+.jstree-default .jstree-no-dots .jstree-leaf > ins { background:transparent; }
+.jstree-default .jstree-no-dots .jstree-open > ins { background-position:-18px 0; }
+.jstree-default .jstree-no-dots .jstree-closed > ins { background-position:0 0; }
+
+.jstree-default .jstree-no-icons a .jstree-icon { display:none; }
+
+.jstree-default .jstree-search { font-style:italic; }
+
+.jstree-default .jstree-no-icons .jstree-checkbox { display:inline-block; }
+.jstree-default .jstree-no-checkboxes .jstree-checkbox { display:none !important; }
+.jstree-default .jstree-checked > a > .jstree-checkbox { background-position:-38px -19px; }
+.jstree-default .jstree-unchecked > a > .jstree-checkbox { background-position:-2px -19px; }
+.jstree-default .jstree-undetermined > a > .jstree-checkbox { background-position:-20px -19px; }
+.jstree-default .jstree-checked > a > .jstree-checkbox:hover { background-position:-38px -37px; }
+.jstree-default .jstree-unchecked > a > .jstree-checkbox:hover { background-position:-2px -37px; }
+.jstree-default .jstree-undetermined > a > .jstree-checkbox:hover { background-position:-20px -37px; }
+
+#vakata-dragged.jstree-default ins { background:transparent !important; }
+#vakata-dragged.jstree-default .jstree-ok { background:url("d.png") -2px -53px no-repeat !important; }
+#vakata-dragged.jstree-default .jstree-invalid { background:url("d.png") -18px -53px no-repeat !important; }
+#jstree-marker.jstree-default { background:url("d.png") -41px -57px no-repeat !important; text-indent:-100px; }
+
+.jstree-default a.jstree-search { color:aqua; }
+.jstree-default .jstree-locked a { color:silver; cursor:default; }
+
+#vakata-contextmenu.jstree-default-context,
+#vakata-contextmenu.jstree-default-context li ul { background:#f0f0f0; border:1px solid #979797; -moz-box-shadow: 1px 1px 2px #999; -webkit-box-shadow: 1px 1px 2px #999; box-shadow: 1px 1px 2px #999; }
+#vakata-contextmenu.jstree-default-context li { }
+#vakata-contextmenu.jstree-default-context a { color:black; }
+#vakata-contextmenu.jstree-default-context a:hover,
+#vakata-contextmenu.jstree-default-context .vakata-hover > a { padding:0 5px; background:#e8eff7; border:1px solid #aecff7; color:black; -moz-border-radius:2px; -webkit-border-radius:2px; border-radius:2px; }
+#vakata-contextmenu.jstree-default-context li.jstree-contextmenu-disabled a,
+#vakata-contextmenu.jstree-default-context li.jstree-contextmenu-disabled a:hover { color:silver; background:transparent; border:0; padding:1px 4px; }
+#vakata-contextmenu.jstree-default-context li.vakata-separator { background:white; border-top:1px solid #e0e0e0; margin:0; }
+#vakata-contextmenu.jstree-default-context li ul { margin-left:-4px; }
+
+/* IE6 BEGIN */
+.jstree-default li,
+.jstree-default ins,
+#vakata-dragged.jstree-default .jstree-invalid,
+#vakata-dragged.jstree-default .jstree-ok,
+#jstree-marker.jstree-default { _background-image:url("d.gif"); }
+.jstree-default .jstree-open ins { _background-position:-72px 0; }
+.jstree-default .jstree-closed ins { _background-position:-54px 0; }
+.jstree-default .jstree-leaf ins { _background-position:-36px 0; }
+.jstree-default a ins.jstree-icon { _background-position:-56px -19px; }
+#vakata-contextmenu.jstree-default-context ins { _display:none; }
+#vakata-contextmenu.jstree-default-context li { _zoom:1; }
+.jstree-default .jstree-undetermined a .jstree-checkbox { _background-position:-20px -19px; }
+.jstree-default .jstree-checked a .jstree-checkbox { _background-position:-38px -19px; }
+.jstree-default .jstree-unchecked a .jstree-checkbox { _background-position:-2px -19px; }
+/* IE6 END */ \ No newline at end of file
diff --git a/resources/assets/javascripts/jquery/jstree/themes/default/throbber.gif b/resources/assets/javascripts/jquery/jstree/themes/default/throbber.gif
new file mode 100644
index 0000000..5b33f7e
--- /dev/null
+++ b/resources/assets/javascripts/jquery/jstree/themes/default/throbber.gif
Binary files differ
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;
+}
diff --git a/resources/assets/javascripts/mvv.js b/resources/assets/javascripts/mvv.js
new file mode 100644
index 0000000..257ad90
--- /dev/null
+++ b/resources/assets/javascripts/mvv.js
@@ -0,0 +1,831 @@
+import { $gettext } from './lib/gettext.js';
+
+jQuery(function ($) {
+ $(document).on('click', 'a.mvv-load-in-new-row', function () {
+ STUDIP.MVV.Content.loadRow($(this));
+ return false;
+ });
+
+ $(document).on('click', '.loaded-details a.cancel', function () {
+ $(this).closest('.loaded-details').prev().find('toggler').click();
+ return false;
+ });
+
+ STUDIP.MVV.Sort.init($('.sortable'));
+
+ $(document).on('change', '#mvv-chooser select', function(){
+ STUDIP.MVV.Chooser.create($(this));
+ return false;
+ });
+
+ $(document).on('click', '.mvv-item-remove', function () {
+ STUDIP.MVV.Content.removeItem(this);
+ return false;
+ });
+
+ $(document).on('click', '.mvv-item-edit', function () {
+ STUDIP.MVV.Content.editAnnotation(this);
+ return false;
+ });
+
+ $(document).on('click', '.mvv-item-edit-properties', function () {
+ $(this).parents("li").find(".mvv-item-document-comments").toggle();
+ return false;
+ });
+
+ // get the quicksearch input
+ $(document).on('click focus', '.ui-autocomplete-input', function() {
+ STUDIP.MVV.Search.qs_input = this;
+ return false;
+ });
+
+ $('.with-datepicker').datepicker();
+
+ $(document).on('change', '.mvv-inst-chooser select', function() {
+ STUDIP.MVV.LanguageChooser.showButtons($(this));
+ return false;
+ });
+
+ $(document).on('click', '.mvv-show-original', function() {
+ STUDIP.MVV.Content.showOriginal($(this));
+ return false;
+ });
+
+ $(document).on('click', '.mvv-show-all-original', function() {
+ STUDIP.MVV.Content.showAllOriginal();
+ return false;
+ });
+
+ $(document).on('click', 'a.mvv-new-tab', function(event) {
+ STUDIP.MVV.Diff.openNewTab(this);
+ return false;
+ });
+
+ $(document).on('click', 'input.mvv-qs-button', function($event) {
+ STUDIP.MVV.Search.addSelect($(this));
+ return false;
+ });
+
+ $(document).on('click', '.stgfile .remove_attachment', function($event) {
+ STUDIP.MVV.Document.remove_attachment($(this));
+ return false;
+ });
+
+ STUDIP.dialogReady(
+ function() {
+
+ var contactSearchParams = $('#search-contact-params');
+ var contactSearchSelect = $('#search-contact-select');
+ if (contactSearchParams) {
+ contactSearchSelect.select2({
+ placeholder: contactSearchSelect.data('placeholder'),
+ minimumInputLength: 3,
+ ajax: {
+ url: STUDIP.URLHelper.getURL('dispatch.php/shared/contacts/search_'
+ + contactSearchSelect.data('search_type')),
+ data: function (params) {
+ var query = {
+ term: params.term,
+ _type: params._type,
+ contact_id: contactSearchParams.data('contact')
+ }
+ return query;
+ },
+ dataType: 'json'
+ }
+ });
+ }
+
+ $('#search-file-select').select2({
+ placeholder: $gettext('Dokument suchen'),
+ minimumInputLength: 3,
+ ajax: {
+ url: STUDIP.URLHelper.getURL('dispatch.php/materialien/files/search_file'),
+ dataType: 'json'
+ }
+ });
+
+ $('#search-file-studiengang-select').select2({
+ placeholder: $gettext('Studiengang suchen'),
+ minimumInputLength: 3,
+ ajax: {
+ url: STUDIP.URLHelper.getURL('dispatch.php/materialien/files/search_studiengang'),
+ dataType: 'json'
+ }
+ });
+ $('#search-file-modul-select').select2({
+ placeholder: $gettext('Modul suchen'),
+ minimumInputLength: 3,
+ ajax: {
+ url: STUDIP.URLHelper.getURL('dispatch.php/materialien/files/search_modul'),
+ dataType: 'json'
+ }
+ });
+ $('#search-file-abschlusskategorie-select').select2({
+ placeholder: $gettext('AbschlussKategorie suchen'),
+ minimumInputLength: 3,
+ ajax: {
+ url: STUDIP.URLHelper.getURL('dispatch.php/materialien/files/search_abschlusskategorie'),
+ dataType: 'json'
+ }
+ });
+ }
+ );
+
+});
+
+/* ------------------------------------------------------------------------
+ * the local MVV namespace
+ * ------------------------------------------------------------------------ */
+window.STUDIP.MVV = window.STUDIP.MVV || {};
+
+STUDIP.MVV.Search = {
+ qs_input : null,
+ qs_selected_name : null,
+ getFocus: function (item_id, item_name) {
+ var qs_input = jQuery(STUDIP.MVV.Search.qs_input),
+ qs_item = jQuery('#'+qs_input.attr('id'));
+ if (item_id == '') {
+ STUDIP.MVV.Search.addSelect(qs_item);
+ } else {
+ qs_input.closest('form')
+ .find('.mvv-submit')
+ .show()
+ .focus();
+ }
+ return true;
+ },
+ addButton: function (item_id, item_name) {
+ var qs_input = jQuery(STUDIP.MVV.Search.qs_input),
+ qs_item = jQuery('#'+qs_input.attr('id'));
+ if (item_id == '') {
+ STUDIP.MVV.Search.addSelect(qs_item);
+ } else {
+ STUDIP.MVV.Search.addTheButton(qs_item);
+ }
+ return true;
+ },
+
+ addTheButton: function (qs_item) {
+ var add_button = jQuery('<a href="#" />').addClass('mvv-add-item'),
+ qs_name = qs_item.attr('id'),
+ target_name = qs_name.slice(0, qs_name.lastIndexOf('_')),
+ item_id = jQuery('#'+qs_name+'_realvalue').val();
+ jQuery('<img src="' + STUDIP.ASSETS_URL
+ + 'images/icons/yellow/arr_2down.svg">')
+ .attr('alt', $gettext("hinzufügen"))
+ .appendTo(add_button);
+ if (item_id == '') {
+ qs_item.siblings('.mvv-add-button').find('.mvv-add-item')
+ .fadeOut('slow', function () {
+ qs_item.val('').focus();
+ jQuery(this).remove();
+ });
+ } else {
+ add_button.click(function() {
+ if (_.isNull(STUDIP.MVV.Search.qs_selected_name)) {
+ STUDIP.MVV.Content.addItem(target_name, item_id,
+ qs_item.val());
+ } else {
+ STUDIP.MVV.Content.addItem(target_name, item_id,
+ STUDIP.MVV.Search.qs_selected_name);
+ }
+ jQuery(this).fadeOut('slow', function () {
+ qs_item.val('').focus();
+ jQuery(this).remove();
+ });
+ jQuery('#select_'+qs_name).fadeOut('fast', function(){
+ jQuery(this).next('.mvv-search-reset').fadeOut();
+ jQuery('#'+qs_name).fadeIn();
+ jQuery(this).remove();
+ });
+ return false;
+ }
+ );
+ qs_item.siblings('.mvv-add-button').first().children('.mvv-add-item')
+ .fadeOut('slow').remove();
+ qs_item.siblings('.mvv-add-button').first().append(add_button);
+ add_button.fadeIn('slow');
+ qs_item.siblings('.mvv-select-group').fadeIn();
+ add_button.focus();
+ qs_item.focus(function() {
+ add_button.fadeOut();
+ qs_item.siblings('.mvv-select-group').fadeOut();
+ });
+ }
+ return true;
+ },
+
+ addSelect: function (qs_item) {
+ var qs_input = jQuery('#' + qs_item.data('qs_name')),
+ qs_real = qs_input.prev('input'),
+ qs_name = qs_input.attr('id'),
+ qs_select = jQuery('<select/>').attr('id', 'select_' + qs_name)
+ .addClass('mvv-search-select-list'),
+ qs_id = qs_item.data('qs_id'),
+ do_submit = qs_item.data('qs_submit');
+ var reset_button = jQuery('<input type="image" />');
+ reset_button.attr({
+ src: STUDIP.ASSETS_URL+'images/icons/blue/decline.svg',
+ title: $gettext("Suche zurücksetzen")
+ }).addClass('mvv-search-reset');
+ if (!_.isUndefined(do_submit)) {
+ qs_select.change(function() {
+ var selected = qs_select.children('option:selected');
+ qs_real.val(selected.val());
+ if (do_submit === 'yes') {
+ qs_input.closest('form').submit();
+ }
+ });
+ } else {
+ qs_select.change(function() {
+ var selected = qs_select.children('option:selected');
+ STUDIP.MVV.Search.addSelected.call(
+ qs_real,
+ selected.val(),
+ selected.text().trim()
+ );
+ });
+ }
+ jQuery.ajax({
+ url: STUDIP.URLHelper.getURL(STUDIP.MVV.CONTROLLER_URL + 'qs_result'),
+ data: {'qs_id': qs_id, 'qs_term': qs_input.val()},
+ type: 'POST',
+ success: function (data) {
+ for (var i in data) {
+ var d = data[i];
+ jQuery('<option/>').attr('value', d.id).text(d.name)
+ .appendTo(qs_select);
+ }
+ qs_input.fadeOut('fast', function () {
+ var inp = jQuery(this);
+ reset_button.click(function () {
+ qs_select.fadeOut('fast', function () {
+ reset_button.hide();
+ qs_select.remove();
+ inp.val('');
+ inp.fadeIn().focus();
+ qs_item.fadeIn();
+ });
+ reset_button.remove();
+ });
+ qs_select.insertAfter(qs_input);
+ qs_item.fadeOut('fast', function () {
+ reset_button.insertAfter(this).fadeIn();
+ });
+ qs_select.fadeIn().focus();
+ });
+ }
+ });
+ },
+
+ submitSelected: function (item_id, item_name) {
+ jQuery(this).closest('form').submit();
+ },
+
+ addSelected: function (item_id, item_name) {
+ var strip_tags = /<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi;
+ var that = jQuery(this),
+ qs_name = that.attr('name'),
+ //QUICKSEARCHTODO
+ //target_name = qs_id.slice(0, qs_id.lastIndexOf('_'));
+ target_name = qs_name.split('_')[0];
+ STUDIP.MVV.Content.addItem(target_name, item_id,
+ jQuery('<div/>').html(item_name.replace(strip_tags, '')).text());
+ },
+
+ insertFachName: function (item_id, item_name) {
+ $.get(STUDIP.URLHelper.getURL(STUDIP.MVV.CONTROLLER_URL + 'fach_data'), {
+ fach_id: item_id
+ }).done(function(d) {
+ if (_.isNull(d.name)) {
+ $('input[name="name"]').attr(
+ 'placeholder',
+ $gettext('Keine Angabe beim Fach')
+ );
+ } else {
+ $('input[name="name"]').attr({
+ value: d.name,
+ placeholder: d.name,
+ 'aria-label': d.name,
+ });
+ }
+ if (_.isNull(d.name_en)) {
+ $('input[name="name_i18n[en_GB]"]').attr(
+ 'placeholder',
+ $gettext('Keine Angabe beim Fach')
+ );
+ } else {
+ $('input[name="name_i18n[en_GB]"]').attr('value', d.name_en);
+ }
+ if (_.isNull(d.name_kurz)) {
+ $('input[name="name_kurz"]').attr(
+ 'placeholder',
+ $gettext('Keine Angabe beim Fach')
+ );
+ } else {
+ $('input[name="name_kurz"]').attr('value', d.name_kurz);
+ }
+ if (_.isNull(d.name_kurz_en)) {
+ $('input[name="name_kurz_i18n[en_GB]"]').attr(
+ 'placeholder',
+ $gettext('Keine Angabe beim Fach')
+ );
+ } else {
+ $('input[name="name_kurz_i18n[en_GB]"]').attr('value', d.name_kurz_en);
+ }
+ });
+ }
+};
+
+STUDIP.MVV.Sort = {
+ i: null,
+ start: function(event, ui) {
+ STUDIP.MVV.Sort.i = jQuery(ui.item).index();
+ },
+ stop: function(event, ui) {
+ var i = jQuery(ui.item).index();
+ if(STUDIP.MVV.Sort.i !== i){
+ var newOrder = jQuery(this).sortable('toArray');
+ var tableID = jQuery(this).closest('.sortable').attr('id');
+ STUDIP.MVV.Sort.save(newOrder, tableID);
+ }
+ },
+ save: function(newOrder, tableID) {
+ jQuery.ajax({
+ url: STUDIP.URLHelper.getURL(STUDIP.MVV.CONTROLLER_URL + 'sort'),
+ data:{
+ 'list_id':tableID,
+ 'newOrder':newOrder
+ },
+ type:'POST',
+ success: function() {}
+ });
+ },
+ init: function(target) {
+ target.sortable({
+ items: '> .sort_items',
+ cursor: 'move',
+ containment: 'parent',
+ axis: 'y',
+ start: STUDIP.MVV.Sort.start,
+ stop: STUDIP.MVV.Sort.stop
+ });
+ }
+};
+
+STUDIP.MVV.Chooser = {
+ create: function (element) {
+ var parent = element.closest('form');
+ jQuery('#mvv-load-content').fadeOut().html('');
+ jQuery.ajax({
+ url: STUDIP.URLHelper.getURL(parent.attr('action')),
+ data: parent.serializeArray(),
+ type:'POST',
+ success: function(data) {
+ var next = parent.nextAll();
+ if (jQuery(data).is('form')) {
+ if (next.length !== 0) {
+ jQuery('.mvv-version-content').nextAll().fadeOut().remove();
+ jQuery('.mvv-version-content').fadeIn();
+ next.remove();
+ }
+ parent.after(data);
+ } else {
+ location.reload();
+ }
+ }
+ });
+ }
+};
+
+STUDIP.MVV.LanguageChooser = {
+ showButtons: function (element) {
+ var chooser = element.closest('.mvv-inst-chooser');
+ var sel = chooser.find(':selected');
+ chooser.find('.mvv-inst-add-button img').fadeOut();
+ if (!sel.hasClass('mvv-inst-chooser-level')) {
+ var button = chooser.find('.mvv-inst-add-button img');
+ button.fadeIn('fast').unbind('click');
+ jQuery(button).click(function() {
+ if (sel.data('fb') === '') {
+ STUDIP.MVV.Content.addItem(
+ chooser.find('select').attr('name'),
+ sel.val(), sel.text());
+ } else {
+ STUDIP.MVV.Content.addItem(
+ chooser.find('select').attr('name'),
+ sel.val(),
+ sel.data('fb') + ' - ' + sel.text());
+ }
+ });
+ }
+ }
+};
+
+STUDIP.MVV.Content = {
+ deskriptor_data: null,
+
+ get: function (id) {
+ jQuery('#mvv-load-content').load(
+ STUDIP.URLHelper.getURL(STUDIP.MVV.CONTROLLER_URL+'content/'+id), function() {
+ jQuery('#mvv-load-content').fadeIn();
+ });
+ },
+ addItem: function (target_name, item_id, item_name) {
+ var target = jQuery('#' + target_name + '_target'),
+ group_id = '',
+ li_id = item_id;
+ if (target.hasClass('mvv-assign-group')) {
+ group_id = target.siblings('.mvv-select-group').find(':selected').val();
+ li_id = target_name + '_' + group_id + '_' + li_id;
+ } else {
+ li_id = target_name + '_' + li_id;
+ }
+ if (jQuery('#' + li_id).length) {
+ jQuery('#' + li_id)
+ .effect('highlight', {color: '#ff0000'}, 1500);
+ } else {
+ var item = jQuery('<li/>').attr('id', li_id);
+ jQuery('<div class="mvv-item-list-text"/>')
+ .text(item_name).appendTo(item);
+ if (target.hasClass('sortable')) {
+ item.addClass('sort_items');
+ }
+ target.children('.mvv-item-list-placeholder').hide();
+ if (target.hasClass('mvv-assign-single')) {
+ target.children().not('.mvv-item-list-placeholder').remove();
+ jQuery('<input type="hidden" />')
+ .attr('name', target_name + '_item')
+ .val(item_id).appendTo(item);
+ } else {
+ if (target.hasClass('mvv-assign-group')) {
+ jQuery('<input type="hidden" />')
+ .attr('name', target_name+'_items_'+group_id+'[]')
+ .val(item_id).appendTo(item);
+ } else {
+ jQuery('<input type="hidden" />')
+ .attr('name', target_name + '_items[]')
+ .val(item_id).appendTo(item);
+ }
+ }
+ var button_list = jQuery('<div ' + 'class="mvv-item-list-buttons"/>')
+ .append('<a href="#" class="mvv-item-remove"><img alt="Trash" src="'
+ + STUDIP.ASSETS_URL
+ + 'images/icons/blue/trash.svg"></a>');
+ button_list.appendTo(item);
+ if (target.is('.mvv-with-annotations')) {
+ var text_area = jQuery('<textarea/>').attr('name',
+ target_name + '_' + 'annotations[' + item_id + ']');
+ jQuery('<div/>').append(text_area).appendTo(item);
+ }
+ if (target.hasClass('mvv-with-properties')) {
+ var prop_input = jQuery('<div/>').addClass('mvv-item-list-properties');
+ jQuery('<img src="' + STUDIP.ASSETS_URL + 'images/languages/lang_de.gif"/>')
+ .appendTo(prop_input);
+ jQuery('<textarea name="kommentar[' + item_id + ']"/>').appendTo(prop_input);
+ jQuery('<img src="' + STUDIP.ASSETS_URL + 'images/languages/lang_en.gif"/>')
+ .appendTo(prop_input);
+ jQuery('<textarea name="kommentar_en[' + item_id + ']"/>').appendTo(prop_input);
+ prop_input.appendTo(item);
+ }
+ if (target.hasClass('mvv-assign-group')) {
+ target = target.find('#'+target_name+'_'+group_id);
+ target.append(item);
+ target.parent().fadeIn('fast', function() {
+ item.effect('highlight', {color: '#55ff55'}, 1500);
+ });
+ } else {
+ target.append(item);
+ item.effect('highlight', {color: '#55ff55'}, 1500);
+ }
+ }
+ },
+
+ addItemFromDialog: function (data) {
+ STUDIP.MVV.Content.addItem(data.target, data.item_id, data.item_name);
+ },
+
+ removeItem: function (this_button) {
+ var item = jQuery(this_button).closest('li');
+ if (item.closest('.mvv-assigned-items').hasClass('mvv-assign-group')) {
+ if (item.siblings().length == 0) {
+ item.parent().parent('li').fadeOut();
+ }
+ if (item.parent().parent().siblings(':visible').length == 0) {
+ item.parent().parent()
+ .siblings('.mvv-item-list-placeholder').fadeIn('slow');
+ }
+ } else {
+ if (item.siblings().length < 2) {
+ item.siblings('.mvv-item-list-placeholder').fadeIn('slow');
+ }
+ }
+ item.remove();
+ },
+ editAnnotation: function (button) {
+ var this_button = jQuery(button),
+ item = this_button.closest('li'),
+ target_id = item.attr('id'),
+ target_name = target_id.slice(0, target_id.lastIndexOf('_')),
+ item_id = target_id.slice(target_id.lastIndexOf('_') + 1, target_id.length),
+ annotation = item.children('.mvv-item-list-properties').first(),
+ content = annotation.children('div').first();
+ content.hide('slow', function () {
+ jQuery('<textarea/>').attr('name', target_name + '_annotations['
+ + item_id + ']').text(content.text()).hide().appendTo(annotation)
+ .fadeIn();
+ this_button.fadeOut();
+ });
+ },
+ editProperties: function (button) {
+ var this_button = jQuery(button),
+ item = this_button.closest('li');
+ STUDIP.MVV.EditForm.openRef(item);
+ },
+ loadRow: function (element) {
+ if (element.data('busy')) {
+ return false;
+ }
+ if (element.closest('tr').next().hasClass('loaded-details')) {
+ element.closest('tbody').toggleClass('collapsed not-collapsed');
+ return false;
+ }
+ element.data('busy', true);
+ jQuery.get(element.attr('href'), '', function (response) {
+ var row = jQuery('<tr />').addClass('loaded-details nohover');
+ element.closest('tbody').append(row);
+ element.closest('tbody').children('.loaded-details').html(response);
+ element.data('busy', false);
+ jQuery('body').trigger('ajaxLoaded');
+ jQuery(row).show();
+ STUDIP.MVV.Sort.init(jQuery('.sortable'));
+ STUDIP.Table.enhanceSortableTable(row.find('.sortable-table'));
+ });
+ element.closest('tbody').toggleClass('collapsed not-collapsed');
+ return false;
+ },
+ showOriginal: function (element) {
+ if (element.data('hasData')) {
+ element.next().slideToggle('fast');
+ return false;
+ };
+ if (_.isNull(STUDIP.MVV.Content.deskriptor_data)) {
+ jQuery.ajax({
+ url: STUDIP.URLHelper.getURL(STUDIP.MVV.CONTROLLER_URL + 'show_original/'),
+ data: {
+ 'id' : STUDIP.MVV.PARENT_ID,
+ 'type': element.data('type')
+ },
+ type: 'POST',
+ async: false,
+ success: function (data) {
+ if (data.length !== 0) {
+ STUDIP.MVV.Content.deskriptor_data = data;
+ }
+ }
+ });
+ }
+ if (!_.isNull(STUDIP.MVV.Content.deskriptor_data)) {
+ var field_id = element.closest('label')
+ .find('textarea, input[type=text]')
+ .attr('id');
+ var item = jQuery('<div/>').addClass('mvv-orig-lang');
+ if (!_.isUndefined(STUDIP.MVV.Content.deskriptor_data[field_id])) {
+ if (STUDIP.MVV.Content.deskriptor_data[field_id]['empty']) {
+ item.css({
+ "color": "red",
+ "font-style": "italic"
+ });
+ }
+ item.html(STUDIP.MVV.Content.deskriptor_data[field_id]['value']);
+ } else {
+ item.html($gettext("Datenfeld in Original-Sprache nicht verfügbar."));
+ item.css({
+ "color": "red",
+ "font-style": "italic"
+ });
+ }
+ item.insertAfter(element);
+ item.slideDown('fast');
+ element.data('hasData', true);
+ }
+ return false;
+ },
+ showAllOriginal: function () {
+ elements = jQuery('.mvv-show-original');
+ _.each(elements, function (e) {
+ var element = jQuery(e);
+ if (element.next(':visible').length === 0) {
+ element.click();
+ }
+ });
+ return false;
+ }
+};
+
+STUDIP.MVV.Diff = {
+ openNewTab: function (item) {
+ var url_to_open = null,
+ new_id = null,
+ old_id = null;
+ var source = jQuery(item);
+ if (source.is('a')) {
+ url_to_open = item.href;
+ window.open(STUDIP.URLHelper.getURL(url_to_open));
+ } else {
+ url_to_open = source.closest('form').attr('action');
+ new_id = source.siblings('[name="new_id"]').attr('value');
+ old_id = source.siblings('[name="old_id"]').attr('value');
+ window.open(STUDIP.URLHelper.getURL(url_to_open,
+ {'new_id': new_id, 'old_id': old_id}));
+ }
+ return false;
+ }
+};
+
+STUDIP.MVV.Document = {
+ reload_documenttable: function(range_id, range_type) {
+ setTimeout(function() {
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/materialien/files/' + (typeof range_id != 'undefined' ? 'range' : 'index'),
+ data: {
+ 'range_id': range_id,
+ 'range_type': range_type
+ },
+ type: 'POST',
+ success: function (data) {
+ jQuery(data).each(function(){
+ jQuery('#'+ jQuery(this).attr("id")).html(jQuery(this).html());
+ });
+ }
+ })
+ }, 100);
+ },
+ remove_attachment: function(item) {
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/materialien/files/delete_attachment',
+ data: {
+ mvvfile_id: jQuery('#mvvfile_id').val(),
+ fileref_id: item.closest('li')
+ .find('input[name=document_id]')
+ .val()
+ },
+ type: 'POST'
+ });
+ item.parents('td').find('.attachments').toggle();
+ item.closest('li')
+ .fadeOut(300, function() {
+ jQuery(this).remove();
+ jQuery('#upload_chooser').show();
+ });
+ },
+ upload_from_input: function(input, file_language) {
+ STUDIP.MVV.Document.upload_files(input.files, file_language);
+ jQuery(input).val('');
+ },
+ fileIDQueue: 1,
+ upload_files: function(files, file_language) {
+ 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('mvvfile_id', jQuery('#mvvfile_id').val());
+ fd.append('range_id', jQuery('#range_id').val());
+ fd.append('file_language', file_language);
+ STUDIP.MVV.Document.upload_file(fd, statusbar);
+ }
+ },
+ upload_file: function(formdata, statusbar) {
+ $.ajax({
+ xhr: function() {
+ var xhrobj = $.ajaxSettings.xhr();
+ if (xhrobj.upload) {
+ xhrobj.upload.addEventListener(
+ 'progress',
+ function(event) {
+ var percent = 0;
+ var position = event.loaded || event.position;
+ var total = event.total;
+ if (event.lengthComputable) {
+ percent = Math.ceil((position / total) * 100);
+ }
+ //Set progress
+ statusbar.find('.progress').css({ 'min-width': percent + '%', 'max-width': percent + '%' });
+ statusbar
+ .find('.progresstext')
+ .text(percent === 100 ? jQuery('#upload_finished').text() : percent + '%');
+ },
+ false
+ );
+ }
+ return xhrobj;
+ },
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/materialien/files/upload_attachment',
+ type: 'POST',
+ contentType: false,
+ processData: false,
+ cache: false,
+ data: formdata,
+ dataType: 'json'
+ })
+ .done(function(data) {
+ statusbar.find('.progress').css({ 'min-width': '100%', 'max-width': '100%' });
+ var file = jQuery('#fileselector_'+formdata.get('file_language')).find('.stgfiles > .stgfile')
+ .first()
+ .clone();
+ file.find('.name').text(data.name);
+ if (data.size < 1024) {
+ file.find('.size').text(data.size + 'B');
+ }
+ if (data.size > 1024 && data.size < 1024 * 1024) {
+ file.find('.size').text(Math.floor(data.size / 1024) + 'KB');
+ }
+ if (data.size > 1024 * 1024 && data.size < 1024 * 1024 * 1024) {
+ file.find('.size').text(Math.floor(data.size / 1024 / 1024) + 'MB');
+ }
+ if (data.size > 1024 * 1024 * 1024) {
+ file.find('.size').text(Math.floor(data.size / 1024 / 1024 / 1024) + 'GB');
+ }
+ file.find('.icon').html(data.icon);
+ file.find('input[name=document_id]').attr('value', data.document_id);
+ jQuery('#fileviewer_'+formdata.get('file_language')).find('.stgfiles').append(file);
+ jQuery('#fileselector_'+formdata.get('file_language')).toggle();
+ jQuery('#fileselector_'+formdata.get('file_language')).parents('.attachments').toggle();
+ jQuery('#fileselector_'+formdata.get('file_language')).parents('.attachments').find('span').toggle();
+ file.fadeIn(300);
+ statusbar.find('.progresstext').text(jQuery('#upload_received_data').text());
+ statusbar.delay(1000).fadeOut(300, function() {
+ jQuery('#upload_chooser').hide();
+ jQuery(this).remove();
+ });
+ })
+ .fail(function(jqxhr, status, errorThrown) {
+ var error = jqxhr.responseJSON.error;
+
+ statusbar
+ .find('.progress')
+ .addClass('progress-error')
+ .attr('title', error);
+ statusbar.find('.progresstext').html(error);
+ statusbar.on('click', function() {
+ jQuery(this).fadeOut(300, function() {
+ jQuery(this).remove();
+ });
+ });
+ });
+ }
+};
+
+
+STUDIP.MVV.Contact = {
+ reload_contacttable: function(range_id, range_type) {
+ setTimeout(function() {
+ jQuery.ajax({
+ url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/shared/contacts/' + (typeof range_id != 'undefined' ? 'range' : 'index'),
+ data: {
+ 'range_id': range_id,
+ 'range_type': range_type
+ },
+ type: 'POST',
+ success: function (data) {
+ jQuery(data).each(function(){
+ jQuery('#'+ jQuery(this).attr("id")).html(jQuery(this).html());
+ });
+ }
+ })
+ }, 100);
+ }
+};
+
+STUDIP.MVV.Aufbaustg = {
+ create: function(df) {
+ setTimeout(function() {
+ $.ajax({
+ url: STUDIP.URLHelper.getURL('dispatch.php/studiengaenge/studiengaenge/aufbaustg_store'),
+ data: $(df).serialize(),
+ type: 'POST',
+ success: function (data) {
+ $('#mvv-aufbaustg-table').html($(data).html());
+ STUDIP.Table.enhanceSortableTable($('#mvv-aufbaustg-table').find('.sortable-table'));
+ }
+ })
+ }, 100);
+ },
+ loadTable: function(stg_id) {
+ setTimeout(function() {
+ $.ajax({
+ url: STUDIP.URLHelper.getURL('dispatch.php/studiengaenge/studiengaenge/aufbaustg_table/' + stg_id),
+ type: 'GET',
+ success: function (data) {
+ $('#mvv-aufbaustg-table').html($(data).html());
+ STUDIP.Table.enhanceSortableTable($('#mvv-aufbaustg-table').find('.sortable-table'));
+ }
+ })
+ }, 100);
+ }
+}
diff --git a/resources/assets/javascripts/mvv_course_wizard.js b/resources/assets/javascripts/mvv_course_wizard.js
new file mode 100644
index 0000000..2cda92a
--- /dev/null
+++ b/resources/assets/javascripts/mvv_course_wizard.js
@@ -0,0 +1,346 @@
+window.STUDIP.MVV = window.STUDIP.MVV || {};
+
+STUDIP.MVV.CourseWizard = {
+ /**
+ * Fetches the children of a given lvgroup.
+ * @param node the ID of the parent.
+ * @param assignable is the given lvgroup assignable?
+ * @returns {boolean}
+ */
+ getTreeChildren: function(node, assignable, classtype) {
+ var target = $('.' + (assignable ? 'lvgroup-tree-' : 'lvgroup-tree-assign-') + node);
+ if (!target.hasClass('tree-loaded')) {
+ var params =
+ 'step=' +
+ $('input[name="step"]').val() +
+ '&method=getLVGroupTreeLevel' +
+ '&parameter[]=' +
+ $('#' + node).attr('id') +
+ '&parameter[]=' +
+ classtype;
+ $.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) {
+ var items = $.parseJSON(data);
+ target.find('.tree-loading').remove();
+ if (items.length > 0) {
+ var list = target.children('ul');
+ for (i = 0; i < items.length; i++) {
+ if (items[i].assignable || items[i].has_children) {
+ list.append(STUDIP.MVV.CourseWizard.createTreeNode(items[i], assignable));
+ }
+ }
+ }
+ target.addClass('tree-loaded');
+
+ var onode = $('<input>')
+ .attr('type', 'hidden')
+ .attr('name', 'open_lvg_nodes[]')
+ .attr('value', node);
+ $('#lvgroup-tree-open-nodes').append(onode);
+ },
+ 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 lvgruppen tree for a given term and show all matching groups.
+ * @returns {boolean}
+ */
+ searchTree: function() {
+ var searchterm = $('#lvgroup-tree-search').val();
+ if (searchterm != '') {
+ $.ajax($('#studyareas').data('ajax-url'), {
+ data: {
+ step: $('input[name="step"]').val(),
+ method: 'searchLVGroupTree',
+ 'parameter[]': searchterm
+ },
+ method: 'POST',
+ beforeSend: function(xhr, settings) {
+ $('#lvgroup-tree-search-start')
+ .parent()
+ .append(
+ $('<img>')
+ .attr('src', STUDIP.ASSETS_URL + 'images/ajax-indicator-black.svg')
+ .attr('id', 'lvgroup-tree-search-loading')
+ .css('width', '16')
+ .css('height', '16')
+ );
+ },
+ success: function(data, status, xhr) {
+ $('#lvgroup-tree-search-loading').remove();
+ var items = $.parseJSON(data);
+ if (items.length > 0) {
+ $('#lvgroup-tree-search-reset')
+ .removeClass('hidden-js');
+ $('#lvgsearchresults ul').empty();
+ $('#lvgsearchresults').show();
+ for (var i = 0; i < items.length; i++) {
+ lvgroup_html = $(items[i].html_string);
+ if ($('#lvgroup-tree-assigned-' + items[i].id).length) {
+ lvgroup_html
+ .find('input')
+ .first()
+ .css('visibility', 'hidden');
+ }
+ $('#lvgsearchresults ul').append(lvgroup_html);
+ }
+ } else {
+ alert($('#studyareas').data('no-search-result'));
+ }
+ },
+ error: function(xhr, status, error) {
+ $('#lvgroup-tree-search-loading').remove();
+ alert(error);
+ }
+ });
+ }
+ return false;
+ },
+
+ /**
+ * Reset a search and empty the search result.
+ * @returns {boolean}
+ */
+ resetSearch: function() {
+ $('#lvgroup-tree-search-reset').addClass('hidden-js');
+ $('#lvgroup-tree-search').val('');
+ $('#lvgsearchresults ul').empty();
+ $('#lvgsearchresults').hide();
+ return false;
+ },
+
+ /**
+ * Creates a tree node element from given data.
+ * @param values values for the node
+ * @param assignable is the given lvgroup assignable?
+ * @returns {*|jQuery}
+ */
+ createTreeNode: function(values, assignable, selected) {
+ var item = $('<li>');
+
+ // Node in lvgroups tree.
+ if (assignable) {
+ var mvv_ids = values.id.split('-');
+
+ item.addClass('lvgroup-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.MVV.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.MVV.CourseWizard.getTreeChildren('" + values.id + "', true, '" + values.mvvclass + "')"
+ );
+ // 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(values.name);
+ label.append(openLink);
+ item.append(input);
+ item.append(label);
+ if (values.has_children) {
+ item.append('<ul>');
+ }
+ if (values.assignable) {
+ if ($('#lvgroup-tree-assigned-' + mvv_ids[0]).length > 0) {
+ assign.css('display', 'none');
+ }
+ }
+ } else {
+ if ($('#lvgroup-tree-assigned-' + mvv_ids[0]).length > 0) {
+ assign.css('display', 'none');
+ }
+ item.html(item.html() + values.name);
+ item.addClass('tree-node');
+ }
+ }
+
+ $(item).data('id', values.id);
+ return item;
+ },
+
+ /**
+ * Assign a given node to the course.
+ * @param id lvgoup ID to assign
+ * @returns {boolean}
+ */
+ assignNode: function(id) {
+ var root = $('#lvgroup-tree-assigned-selected');
+ var params = 'step=' + $('input[name="step"]').val() + '&method=getAncestorTree' + '&parameter[]=' + id;
+ $.ajax($('#studyareas').data('ajax-url'), {
+ data: params,
+ beforeSend: function(xhr, settings) {
+ STUDIP.MVV.CourseWizard.loadingOverlay($('div#assigned ul.css-tree'));
+ },
+ success: function(data, status, xhr) {
+ $('#loading-overlay').remove();
+ var items = $.parseJSON(data);
+
+ var lvgid = id.split('-');
+ if ($('#lvgroup-tree-assigned-' + lvgid).length === 0) {
+ var input = $('<input>')
+ .attr('type', 'hidden')
+ .attr('name', 'lvgroups[]')
+ .attr('value', items.id);
+ root.before(input);
+ root.append(items.html_string);
+ }
+
+ $("input[name*='assign[" + lvgid[0] + "']").each(function() {
+ $(this).hide();
+ });
+ $("svg[name*='assign[" + lvgid[0] + "']").each(function() {
+ $(this).hide();
+ });
+ },
+ error: function(xhr, status, error) {
+ alert(error);
+ }
+ });
+ return false;
+ },
+
+ /**
+ * 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);
+ },
+
+ /**
+ * Show details of a node.
+ * @param id lvgroup ID to unassign
+ * @returns {boolean}
+ */
+ showDetails: function(id) {
+ if ($('#lvgruppe_selection_detail_' + id).is(':visible')) {
+ $('#lvgruppe_selection_detail_' + id).empty();
+ $('#lvgruppe_selection_detail_' + id).hide();
+ } else {
+ $('#lvgruppe_selection_detail_' + id).empty();
+ var params = 'step=' + $('input[name="step"]').val() + '&method=getLVGroupDetails' + '&parameter[]=' + id;
+ $.ajax($('#assigned').data('ajax-url'), {
+ data: params,
+ beforeSend: function(xhr, settings) {
+ STUDIP.MVV.CourseWizard.loadingOverlay($('div#assigned ul.css-tree'));
+ },
+ success: function(data, status, xhr) {
+ $('#loading-overlay').remove();
+ var items = $.parseJSON(data);
+ $('#lvgroup-tree-assigned-' + id + ' ul').append(items.html_string);
+ },
+ error: function(xhr, status, error) {
+ alert(error);
+ }
+ });
+ $('#lvgruppe_selection_detail_' + id).show();
+ }
+ return false;
+ },
+
+ /**
+ * Show details of a searchnode.
+ * @param id lvgroup ID to unassign
+ * @returns {boolean}
+ */
+ showSearchDetails: function(id) {
+ if ($('#lvgruppe_search_' + id + ' ul').is(':visible')) {
+ $('#lvgruppe_search_' + id + ' ul').remove();
+ } else {
+ var params = 'step=' + $('input[name="step"]').val() + '&method=getLVGroupDetails' + '&parameter[]=' + id;
+ $.ajax($('#studyareas').data('ajax-url'), {
+ data: params,
+ beforeSend: function(xhr, settings) {
+ STUDIP.MVV.CourseWizard.loadingOverlay($('div#lvgsearchresults ul.css-tree'));
+ },
+ success: function(data, status, xhr) {
+ $('#loading-overlay').remove();
+ var items = $.parseJSON(data);
+ $('#lvgruppe_search_' + id).append('<ul>' + items.html_string + '</ul>');
+ },
+ error: function(xhr, status, error) {
+ alert(error);
+ }
+ });
+ }
+
+ return false;
+ },
+
+ /**
+ * Remove a node from the assigned ones.
+ * @param id lvgroup ID to unassign
+ * @returns {boolean}
+ */
+ removeLVGroup: function(id) {
+ $('#lvgroup-tree-assigned-' + id).remove();
+ $("input[name*='assign[" + id + "']").each(function() {
+ $(this).show();
+ });
+ return false;
+ }
+};
diff --git a/resources/assets/javascripts/public-path.js b/resources/assets/javascripts/public-path.js
new file mode 100644
index 0000000..1fd9fe0
--- /dev/null
+++ b/resources/assets/javascripts/public-path.js
@@ -0,0 +1 @@
+__webpack_public_path__ = __webpack_public_path__ || (window.STUDIP && window.STUDIP.ASSETS_URL)
diff --git a/resources/assets/javascripts/studip-jquery-selection-helper.js b/resources/assets/javascripts/studip-jquery-selection-helper.js
new file mode 100644
index 0000000..165e765
--- /dev/null
+++ b/resources/assets/javascripts/studip-jquery-selection-helper.js
@@ -0,0 +1,107 @@
+$.fn.extend({
+ // Returns the current position of the cursor
+ getCaretPosition: function() {
+ var that = this[0],
+ range,
+ position;
+ if (!!document.selection) {
+ that.focus();
+ range = document.selection.createRange();
+ range.moveStart('character', -that.value.length);
+ position = range.text.length;
+ } else {
+ position = that.selectionStart || 0;
+ }
+ return position;
+ },
+ // Sets the current position of the cursor
+ setCaretPosition: function(position) {
+ return $(this).setSelection(position, position);
+ },
+ // Returns the currently selected text
+ getSelection: function() {
+ var that = this[0];
+ if (!!document.selection) {
+ return document.selection.createRange().text;
+ }
+ if (!!this[0].setSelectionRange) {
+ return this[0].value.substring(this[0].selectionStart, this[0].selectionEnd);
+ }
+ return false;
+ },
+ // Sets the currently selected text
+ setSelection: function(start, end) {
+ return this.each(function() {
+ var range;
+ if (!!this.setSelectionRange) {
+ this.setSelectionRange(start, end);
+ } else if (!!this.createTextRange) {
+ this.focus();
+ range = this.createTextRange();
+ range.collapse(true);
+ if (position < 0) {
+ position = Math.max(0, this.value.length + position);
+ }
+ range.moveStart('character', start);
+ range.moveEnd('character', end);
+ range.select();
+ }
+ });
+ },
+ // Stores the current selection
+ storeSelection: function() {
+ return $(this).each(function() {
+ var selection = false,
+ position;
+ if (!!document.selection) {
+ position = $(this).getCaretPosition();
+ selection = {
+ start: position,
+ end: position + $(this).getSelection().length
+ };
+ } else if (!!this.setSelectionRange) {
+ selection = {
+ start: this.selectionStart,
+ end: this.selectionEnd
+ };
+ }
+ $(this).data('stored-selection', selection);
+ });
+ },
+ // Restores a possibly stored selection
+ restoreSelection: function() {
+ return $(this).each(function() {
+ var selection = $(this).data('stored-selection');
+ if (selection !== false) {
+ $(this).setSelection(selection.start, selection.end);
+ }
+
+ $(this).removeData('stored-selection');
+ });
+ },
+ // Replaces the currently selected text of an element with the given
+ // replacement
+ replaceSelection: function(replacement, cursor_position) {
+ return this.each(function() {
+ var scroll_top = this.scrollTop,
+ range,
+ selection_start;
+ if (!!document.selection) {
+ this.focus();
+ range = document.selection.createRange();
+ range.text = replacement;
+ range.select();
+ } else if (!!this.setSelectionRange) {
+ selection_start = this.selectionStart;
+ this.value =
+ this.value.substring(0, selection_start) + replacement + this.value.substring(this.selectionEnd);
+ this.setSelectionRange(
+ selection_start + (cursor_position || replacement.length),
+ selection_start + (cursor_position || replacement.length)
+ );
+ }
+ this.focus();
+ this.scrollTop = scroll_top;
+ });
+ }
+});
diff --git a/resources/assets/javascripts/studip-jquery-tweaks.js b/resources/assets/javascripts/studip-jquery-tweaks.js
new file mode 100644
index 0000000..b8a1448
--- /dev/null
+++ b/resources/assets/javascripts/studip-jquery-tweaks.js
@@ -0,0 +1,67 @@
+/*jslint browser: true */
+/*global jQuery */
+
+/**
+ * SVG class handling.
+ *
+ * This tweaks jQuery so that calls of addClass(), removeClass() and hasClass()
+ * don't fail on svg elements.
+ *
+ * SVGs don't have a className attribute but rather a classList object
+ * so the native jQuery methods will have no effect on SVG elements.
+ */
+(function ($) {
+ 'use strict';
+
+ var originals = {
+ addClass: $.fn.addClass,
+ removeClass: $.fn.removeClass,
+ hasClass: $.fn.hasClass
+ };
+
+ $.fn.addClass = function (value) {
+ if (jQuery.isFunction(value)) {
+ return originals.addClass.call(this, value);
+ }
+
+ this.filter('svg').each(function () {
+ var classes = (value || '').trim().split(/\s+/) || [];
+
+ this.classList.add.apply(this.classList, classes);
+ });
+ originals.addClass.call(this.not('svg'), value);
+
+ return this;
+ };
+
+ $.fn.removeClass = function (value) {
+ if (jQuery.isFunction(value)) {
+ return originals.removeClass.call(this, value);
+ }
+
+ this.filter('svg').each(function () {
+ var classes = (value || '').trim().split(/\s+/) || [];
+
+ this.classList.remove.apply(this.classList, classes);
+ });
+ originals.removeClass.call(this.not('svg'), value);
+
+ return this;
+ };
+
+ $.fn.hasClass = function (value) {
+ var svgs = $(this).filter('svg'),
+ i,
+ l = svgs.length;
+ if (l > 0) {
+ for (i = 0; i < l; i += 1) {
+ if (svgs.get(i).classList.contains(value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return originals.hasClass.call(this, value);
+ };
+
+}(jQuery)); \ No newline at end of file
diff --git a/resources/assets/javascripts/studip-jquery.multi-select.tweaks.js b/resources/assets/javascripts/studip-jquery.multi-select.tweaks.js
new file mode 100644
index 0000000..d9277bb
--- /dev/null
+++ b/resources/assets/javascripts/studip-jquery.multi-select.tweaks.js
@@ -0,0 +1,86 @@
+import { $gettext } from './lib/gettext.js';
+
+/*jslint browser: true */
+/*global jQuery */
+
+/**
+ * Neccessary tweaks/adjustments for the jQuery UI multiselect addon.
+ *
+ * The tweaks essentially enable the list elements to include images as well
+ * as newlines and to create disabled elements without the addon interfering.
+ *
+ * This is accomplished by refining the defined methods of the addon:
+ *
+ * - For icons, the generateLisFromOption() checks for a special format of
+ * the text parameter and injects a background image to the generated
+ * option element
+ * - New lines are created by adjusting the escapeHTML method, any new line
+ * character is replaced by <br>
+ * - The disabled elements require a hack involving the methods
+ * generateLisFromOption, addOption and the jQuery extension insertAt.
+ * If the item should be disabled, the index is set to -1 and handled in
+ * each and every method as it would not have been set in the first place.
+ * This way, a disabled item is always added at the end of the list but
+ * this is exactly what we want to achieve.
+ *
+ * Note:
+ *
+ * With every update of the multi select addon, this needs to be checked and
+ * eventually adjusted to the new conditions.
+ */
+(function ($, MultiSelect) {
+ 'use strict';
+
+ var originals = {
+ generateLisFromOption: MultiSelect.prototype.generateLisFromOption,
+ addOption: MultiSelect.prototype.addOption,
+ escapeHTML: MultiSelect.prototype.escapeHTML,
+ insertAt: $.fn.insertAt
+ };
+
+ MultiSelect.prototype.generateLisFromOption = function (option, index, $container) {
+ var $option = $(option),
+ chunks = $option.text().split('--');
+
+ if (index === -1) {
+ $option.prop('disabled', true);
+ index = undefined;
+ }
+
+ if (chunks.length > 1) {
+ $option.attr('style', 'background-image: url(' + chunks.shift() + ')');
+
+ $option.text(chunks.join("\n"));
+
+ if ($option.is(':disabled')) {
+ $option.attr('title', $gettext('Die Person ist bereits eingetragen.'));
+ }
+ }
+
+ originals.generateLisFromOption.call(this, $option.get(0), index, $container);
+ };
+
+ MultiSelect.prototype.addOption = function (options) {
+ if (options.disabled) {
+ options.index = -1;
+ delete options.disabled;
+ }
+ options.text = this.escapeHTML(options.text);
+ return originals.addOption.call(this, options);
+ };
+
+ MultiSelect.prototype.escapeHTML = function (text) {
+ var result = originals.escapeHTML.call(this, text);
+ return result.replace("\n", '<br>');
+ };
+
+ $.fn.insertAt = function (index, $parent) {
+ if (index === -1) {
+ index = $parent.children().length;
+ }
+
+ return originals.insertAt.call(this, index, $parent);
+ };
+
+
+}(jQuery, jQuery.fn.multiSelect.Constructor)); \ No newline at end of file
diff --git a/resources/assets/javascripts/studip-ui.js b/resources/assets/javascripts/studip-ui.js
new file mode 100644
index 0000000..657a2dd
--- /dev/null
+++ b/resources/assets/javascripts/studip-ui.js
@@ -0,0 +1,638 @@
+import { $gettext } from './lib/gettext.js';
+
+/*jslint browser: true */
+/*global jQuery, STUDIP */
+
+/**
+ * This file contains extensions/adjustments for jQuery UI.
+ */
+
+(function ($, STUDIP) {
+ /**
+ * Setup and refine date picker, add automated handling for .has-date-picker
+ * and [data-date-picker].
+ * Note: [date-datepicker] would be a way better selector but unfortunately
+ * jQuery UI's Datepicker itself stores vital data in the the "datepicker"
+ * data() variable, so we cannot use it and need to use "date-picker"
+ * instead.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license GPL2 or any later version
+ * @since Stud.IP 3.4
+ */
+
+ 'use strict';
+
+ // Exit if datepicker is undefined (which it should never be)
+ if ($.datepicker === undefined) {
+ return;
+ }
+
+ // Exit if datetimepicker is undefined (which it should never be)
+ if ($.ui.timepicker === undefined) {
+ return;
+ }
+
+ // Setup defaults and default locales
+ var defaults = {},
+ locale = {
+ closeText: $gettext('Schließen'),
+ prevText: $gettext('Zurück'),
+ nextText: $gettext('Vor'),
+ currentText: $gettext('Jetzt'),
+ monthNames: [
+ $gettext('Januar'),
+ $gettext('Februar'),
+ $gettext('März'),
+ $gettext('April'),
+ $gettext('Mai'),
+ $gettext('Juni'),
+ $gettext('Juli'),
+ $gettext('August'),
+ $gettext('September'),
+ $gettext('Oktober'),
+ $gettext('November'),
+ $gettext('Dezember')
+ ],
+ monthNamesShort: [
+ $gettext('Jan'),
+ $gettext('Feb'),
+ $gettext('Mär'),
+ $gettext('Apr'),
+ $gettext('Mai'),
+ $gettext('Jun'),
+ $gettext('Jul'),
+ $gettext('Aug'),
+ $gettext('Sep'),
+ $gettext('Okt'),
+ $gettext('Nov'),
+ $gettext('Dez')
+ ],
+ dayNames: [
+ $gettext('Sonntag'),
+ $gettext('Montag'),
+ $gettext('Dienstag'),
+ $gettext('Mittwoch'),
+ $gettext('Donnerstag'),
+ $gettext('Freitag'),
+ $gettext('Samstag')
+ ],
+ dayNamesShort: [
+ $gettext('So'),
+ $gettext('Mo'),
+ $gettext('Di'),
+ $gettext('Mi'),
+ $gettext('Do'),
+ $gettext('Fr'),
+ $gettext('Sa')
+ ],
+ weekHeader: $gettext('Wo'),
+ dateFormat: 'dd.mm.yy',
+ firstDay: 1,
+ isRTL: false,
+ showMonthAfterYear: false,
+ yearSuffix: '',
+ changeMonth: true,
+ changeYear: true,
+ timeOnlyTitle: $gettext('Zeit wählen'),
+ timeText: $gettext('Zeit'),
+ hourText: $gettext('Stunde'),
+ minuteText: $gettext('Minute'),
+ secondText: $gettext('Sekunde'),
+ millisecText: $gettext('Millisekunde'),
+ microsecText: $gettext('Mikrosekunde'),
+ timezoneText: $gettext('Zeitzone'),
+ timeFormat: $gettext('HH:mm'),
+ amNames: [$gettext('vorm.'), 'AM', 'A'],
+ pmNames: [$gettext('nachm.'), 'PM', 'P']
+ };
+ // Set dayNamesMin to dayNamesShort since they are equal
+ locale.dayNamesMin = locale.dayNamesShort;
+
+ // Setup Stud.IP's own datepicker extensions
+ STUDIP.UI = STUDIP.UI || {};
+ STUDIP.UI.Datepicker = {
+ selector: '.has-date-picker,[data-date-picker]',
+ // Initialize all datepickers that not yet been initialized (e.g. in dialogs)
+ init: function () {
+ $(this.selector).filter(function () {
+ return $(this).data('date-picker-init') === undefined;
+ }).each(function () {
+ $(this).data('date-picker-init', true).datepicker();
+ });
+ },
+ // Apply registered handlers. Take care: This happens upon before a
+ // picker is shown as well as after a date has been selected.
+ refresh: function () {
+ $(this.selector).each(function () {
+ var element = this,
+ options = $(element).data().datePicker;
+ if (options) {
+ $.each(options, function (key, value) {
+ if (STUDIP.UI.Datepicker.dataHandlers.hasOwnProperty(key)) {
+ STUDIP.UI.Datepicker.dataHandlers[key].call(element, value);
+ }
+ });
+ }
+ });
+ }
+ };
+
+ // Define handlers for any data-datepicker option
+ STUDIP.UI.Datepicker.dataHandlers = {
+ // Ensure this date is not later (<=) than another date by setting
+ // the maximum allowed date the other date.
+ // This will also set this date to the maximum allowed date if it
+ // currently later than the allowed maximum date.
+ '<=': function (selector, offset) {
+ var this_date = $(this).datepicker('getDate'),
+ max_date = null,
+ temp,
+ adjustment = 0;
+
+ if ($(this).data().datePicker.offset) {
+ temp = $(this).data().datePicker.offset;
+ adjustment = parseInt($(temp).val(), 10);
+ }
+
+ // Get max date by either actual dates or maxDate options on
+ // all matching elements
+ if (selector === 'today') {
+ max_date = new Date();
+ } else {
+ $(selector).each(function () {
+ var date = $(this).datepicker('getDate') || $(this).datepicker('option', 'maxDate');
+ if (date && (!max_date || date < max_date)) {
+ max_date = new Date(date);
+ }
+ });
+ }
+
+ // Set max date and adjust current date if neccessary
+ if (max_date) {
+ max_date.setTime(max_date.getTime() - (offset || 0) * 24 * 60 * 60 * 1000);
+
+ temp = new Date(max_date);
+ temp.setDate(temp.getDate() - adjustment);
+
+ if (this_date && this_date > max_date) {
+ $(this).datepicker('setDate', temp);
+ }
+
+ $(this).datepicker('option', 'maxDate', max_date);
+ } else {
+ $(this).datepicker('option', 'maxDate', null);
+ }
+ },
+ // Ensure this date is earlier (<) than another date by setting the
+ // maximum allowed date to the other date - 1 day.
+ // This will also set this date to the maximum allowed date - 1 day
+ // if it is currently later than the allowed maximum date.
+ '<': function (selector) {
+ STUDIP.UI.Datepicker.dataHandlers['<='].call(this, selector, 1);
+ },
+ // Ensure this date is not earlier (>=) than another date by setting
+ // the minimum allowed date to the other date.
+ // This will also set this date to the minimum allowed date if it is
+ // currently earlier than the allowed minimum date.
+ '>=': function (selector, offset) {
+ var this_date = $(this).datepicker('getDate'),
+ min_date = null,
+ temp,
+ adjustment = 0;
+
+ if ($(this).data().datePicker.offset) {
+ temp = $(this).data().datePicker.offset;
+ adjustment = parseInt($(temp).val(), 10);
+ }
+
+ // Get min date by either actual dates or minDate options on
+ // all matching elements
+ if (selector === 'today') {
+ min_date = new Date();
+ } else {
+ $(selector).each(function () {
+ var date = $(this).datepicker('getDate') || $(this).datepicker('option', 'minDate');
+ if (date && (!min_date || date > min_date)) {
+ min_date = new Date(date);
+ }
+ });
+ }
+
+ // Set min date and adjust current date if neccessary
+ if (min_date) {
+ min_date.setTime(min_date.getTime() + (offset || 0) * 24 * 60 * 60 * 1000);
+
+ temp = new Date(min_date);
+ temp.setDate(temp.getDate() + adjustment);
+
+ if (this_date && this_date < min_date) {
+ $(this).datepicker('setDate', temp);
+ }
+
+ $(this).datepicker('option', 'minDate', min_date);
+ } else {
+ $(this).datepicker('option', 'minDate', null);
+ }
+ },
+ // Ensure this date is later (>) than another date by setting the
+ // minimum allowed date to the other date + 1 day.
+ // This will also set this date to the minimum allowed date + 1 day
+ // if it is currently earlier than the allowed minimum date.
+ '>': function (selector) {
+ STUDIP.UI.Datepicker.dataHandlers['>='].call(this, selector, 1);
+ }
+ };
+
+ STUDIP.UI.DateTimepicker = {
+ selector: '.has-datetime-picker,[data-datetime-picker]',
+ // Initialize all datetimepickers that not yet been initialized (e.g. in dialogs)
+ init: function () {
+ $(this.selector).filter(function () {
+ return $(this).data('datetime-picker-init') === undefined;
+ }).each(function () {
+ $(this).data('datetime-picker-init', true).datetimepicker();
+ });
+ },
+ // Apply registered handlers. Take care: This happens upon before a
+ // picker is shown as well as after a date has been selected.
+ refresh: function () {
+ $(this.selector).each(function () {
+ var element = this,
+ options = $(element).data().datetimePicker;
+ if (options) {
+ $.each(options, function (key, value) {
+ if (STUDIP.UI.DateTimepicker.dataHandlers.hasOwnProperty(key)) {
+ STUDIP.UI.DateTimepicker.dataHandlers[key].call(element, value);
+ }
+ });
+ }
+ });
+ }
+ };
+
+ // Define handlers for any data-datepicker option
+ STUDIP.UI.DateTimepicker.dataHandlers = {
+ // Ensure this date is not later (<=) than another date by setting
+ // the maximum allowed date the other date.
+ // This will also set this date to the maximum allowed date if it
+ // currently later than the allowed maximum date.
+ '<=': function (selector, offset) {
+ var this_date = $(this).datetimepicker('getDate'),
+ max_date = null,
+ temp;
+
+ if ((offset === undefined) && $(selector).data('offset')) {
+ temp = $(selector).data('offset');
+ offset = parseInt($(temp).val(), 10);
+ }
+
+ // Get max date by either actual dates or maxDate options on
+ // all matching elements
+ if (selector === 'today') {
+ max_date = new Date();
+ max_date.setHours(0, 23, 59, 59);
+ } else {
+ $(selector).each(function () {
+ var date = $(this).datetimepicker('getDate') || $(this).datetimepicker('option', 'maxDate');
+ if (date && (!max_date || date < max_date)) {
+ max_date = new Date(date);
+ }
+ });
+ }
+
+ // Set max date and adjust current date if neccessary
+ if (max_date) {
+ max_date.setTime(max_date.getTime() - (offset || 0) * 24 * 60 * 60 * 1000);
+
+ if (this_date && this_date > max_date) {
+ $(this).datetimepicker('setDate', max_date);
+ }
+
+ $(this).datetimepicker('option', 'maxDate', max_date);
+ } else {
+ $(this).datetimepicker('option', 'maxDate', null);
+ }
+ },
+ // Ensure this date is earlier (<) than another date by setting the
+ // maximum allowed date to the other date - 1 day.
+ // This will also set this date to the maximum allowed date - 1 day
+ // if it is currently later than the allowed maximum date.
+ '<': function (selector) {
+ STUDIP.UI.DateTimepicker.dataHandlers['<='].call(this, selector, 1);
+ },
+ // Ensure this date is not earlier (>=) than another date by setting
+ // the minimum allowed date to the other date.
+ // This will also set this date to the minimum allowed date if it is
+ // currently earlier than the allowed minimum date.
+ '>=': function (selector, offset) {
+ var this_date = $(this).datetimepicker('getDate'),
+ min_date = null,
+ temp;
+
+ if ((offset === undefined) && $(selector).data('offset')) {
+ temp = $(selector).data('offset');
+ offset = parseInt($(temp).val(), 10);
+ }
+
+ // Get min date by either actual dates or minDate options on
+ // all matching elements
+ if (selector === 'today') {
+ min_date = new Date();
+ min_date.setHours(0, 0, 0);
+ } else {
+ $(selector).each(function () {
+ var date = $(this).datetimepicker('getDate') || $(this).datetimepicker('option', 'minDate');
+ if (date && (!min_date || date > min_date)) {
+ min_date = new Date(date);
+ }
+ });
+ }
+
+ // Set min date and adjust current date if neccessary
+ if (min_date) {
+ min_date.setTime(min_date.getTime() + (offset || 0) * 24 * 60 * 60 * 1000);
+
+ if (this_date && this_date < min_date) {
+ $(this).datetimepicker('setDate', min_date);
+ }
+
+ $(this).datetimepicker('option', 'minDate', min_date);
+ } else {
+ $(this).datetimepicker('option', 'minDate', null);
+ }
+ },
+ // Ensure this date is later (>) than another date by setting the
+ // minimum allowed date to the other date + 1 day.
+ // This will also set this date to the minimum allowed date + 1 day
+ // if it is currently earlier than the allowed minimum date.
+ '>': function (selector) {
+ STUDIP.UI.DateTimepicker.dataHandlers['>='].call(this, selector, 1);
+ }
+ };
+
+ STUDIP.UI.Timepicker = {
+ selector: '.has-time-picker,[data-time-picker]',
+ // Initialize all datetimepickers that not yet been initialized (e.g. in dialogs)
+ init: function () {
+ $(this.selector).filter(function () {
+ return $(this).data('time-picker-init') === undefined;
+ }).each(function () {
+ $(this).addClass('hasTimepicker').data('time-picker-init', true).timepicker();
+ });
+ },
+ // Apply registered handlers. Take care: This happens upon before a
+ // picker is shown as well as after a date has been selected.
+ refresh: function () {
+ $(this.selector).each(function () {
+ var element = this,
+ options = $(element).data().timePicker;
+ if (options) {
+ $.each(options, function (key, value) {
+ if (STUDIP.UI.Timepicker.dataHandlers.hasOwnProperty(key)) {
+ STUDIP.UI.Timepicker.dataHandlers[key].call(element, value);
+ }
+ });
+ }
+ });
+ }
+ };
+
+ STUDIP.UI.Timepicker.parseTime = (time) => {
+ const split = time.split(':');
+ return {
+ hour: parseInt(split[0], 10),
+ minute: parseInt(split[1], 10)
+ };
+ };
+ STUDIP.UI.Timepicker.createTime = (hours, minutes) => {
+ return ('0' + hours).slice(-2) + ':' + ('0' + minutes).slice(-2);
+ };
+ STUDIP.UI.Timepicker.setTime = (time) => {
+ const date = new Date();
+ const parsed = STUDIP.UI.Timepicker.parseTime(time);
+
+ date.setHours(parsed.hour);
+ date.setMinutes(parsed.minute);
+
+ return date;
+ };
+
+ // Define handlers for any data-time-picker option
+ // TODO: This don't work well if at all. We should probably switch to
+ // another (date) timepicker
+ STUDIP.UI.Timepicker.dataHandlers = {
+ // Ensure this time is not later (<=) than another time by setting
+ // the maximum allowed time on the other time.
+ // This will also set this time to the maximum allowed time if it is
+ // currently later than the allowed maximum time.
+ '<=': function (selector, offset) {
+ var this_time = this.value;
+ var max_time = null;
+ var temp;
+
+ if ((offset === undefined) && $(selector).data('offset')) {
+ temp = $(selector).data('offset');
+ offset = parseInt($(temp).val(), 10);
+ }
+
+ // Get max time by either actual times
+ $(selector).each(function () {
+ var time = this.value;
+ if (time && (!max_time || time < max_time)) {
+ max_time = time;
+ }
+ });
+
+ // Set max time and adjust current time if neccessary
+ if (max_time) {
+ const parsed = STUDIP.UI.Timepicker.parseTime(max_time);
+ max_time = STUDIP.UI.Timepicker.createTime(
+ Math.min(23, Math.max(0, parsed.hour - (offset || 0))),
+ parsed.minute
+ );
+
+ console.log('max time:', this_time, max_time);
+ if (this_time && this_time > max_time) {
+ $(this).timepicker(STUDIP.UI.Timepicker.parseTime(max_time));
+ }
+
+ $(this).timepicker({
+ maxTime: STUDIP.UI.Timepicker.setTime(max_time)
+ });
+ } else {
+ $(this).timepicker({
+ maxTime: null
+ });
+ }
+ },
+ // Ensure this date is earlier (<) than another date by setting the
+ // maximum allowed date to the other date - 1 day.
+ // This will also set this date to the maximum allowed date - 1 day
+ // if it is currently later than the allowed maximum date.
+ '<': function (selector) {
+ STUDIP.UI.Timepicker.dataHandlers['<='].call(this, selector, 1);
+ },
+ // Ensure this date is not earlier (>=) than another date by setting
+ // the minimum allowed date to the other date.
+ // This will also set this date to the minimum allowed date if it is
+ // currently earlier than the allowed minimum date.
+ '>=': function (selector, offset) {
+ var this_time = this.value;
+ var min_time = null;
+ var temp;
+
+ if ((offset === undefined) && $(selector).data('offset')) {
+ temp = $(selector).data('offset');
+ offset = parseInt($(temp).val(), 10);
+ }
+
+ // Get min time by either actual times
+ $(selector).each(function () {
+ var time = this.value;
+ if (time && (!min_time || time > min_time)) {
+ min_time = time;
+ }
+ });
+
+ // Set min time and adjust current time if neccessary
+ if (min_time) {
+ const parsed = STUDIP.UI.Timepicker.parseTime(min_time);
+ min_time = STUDIP.UI.Timepicker.createTime(
+ Math.min(23, Math.max(0, parsed.hour + (offset || 0))),
+ parsed.minute
+ );
+
+ console.log('min time:', this_time, min_time);
+ if (this_time && this_time < min_time) {
+ $(this).timepicker(STUDIP.UI.Timepicker.parseTime(min_time));
+ }
+
+ $(this).timepicker({
+ minTime: STUDIP.UI.Timepicker.setTime(min_time)
+ });
+ } else {
+ $(this).timepicker({
+ minTime: null
+ });
+ }
+ },
+ // Ensure this date is later (>) than another date by setting the
+ // minimum allowed date to the other date + 1 day.
+ // This will also set this date to the minimum allowed date + 1 day
+ // if it is currently earlier than the allowed minimum date.
+ '>': function (selector) {
+ STUDIP.UI.Timepicker.dataHandlers['>='].call(this, selector, 1);
+ }
+ };
+
+ // Apply defaults including date picker handlers
+ defaults = Object.assign({}, locale, {
+ beforeShow (input) {
+ STUDIP.UI.Datepicker.refresh();
+ STUDIP.UI.DateTimepicker.refresh();
+ STUDIP.UI.Timepicker.refresh();
+
+ if ($(input).parents('.ui-dialog').length > 0) {
+ return;
+ }
+
+ $(input).css({
+ 'position': 'relative',
+ 'z-index': 1002
+ });
+ },
+ onSelect: function (value, instance) {
+ if (value !== instance.lastVal) {
+ $(this).change();
+ }
+ }
+ });
+
+ $.datepicker.setDefaults(Object.assign({}, defaults, {
+ beforeShow (input) {
+ // Don't lose original behaviour
+ defaults.beforeShow(input);
+
+ if ($(input).parents('.ui-dialog').length > 0) {
+ $('.ui-dialog-content').bind('scroll.datepicker-scroll', _.debounce($.proxy(DpHideOnScroll, null, input), 100, {leading:true, trailing:false}));
+ }
+ $(window).bind('scroll.datepicker-scroll', _.debounce($.proxy(DpHideOnScroll, null, input), 100, {leading:true, trailing:false}));
+
+ if ($(input).closest('.sidebar').length === 0) {
+ return;
+ }
+
+ const button = input.nextElementSibling;
+ if (button && button.matches('input[type="submit"]')) {
+ button.style.position = 'relative';
+ button.style.zIndex = input.style.zIndex;
+ }
+ },
+ onClose (date, inst) {
+ $(this).one('click.picker', function () {
+ $(this).datepicker('show');
+ }).on('blur', function () {
+ $(this).off('click.picker');
+ });
+
+ if ($(this).parents('.ui-dialog').length > 0) {
+ $('.ui-dialog-content').unbind('scroll.datepicker-scroll');
+ } else {
+ $(window).unbind('scroll.datepicker-scroll');
+ }
+ }
+ }));
+
+ var DpHideOnScroll = function () {
+ var input = arguments[0];
+ $(input).blur();
+ $(input).datepicker('hide');
+ }
+
+ $.timepicker.setDefaults(Object.assign({}, defaults, {
+ timeFormat: 'HH:mm'
+ }));
+
+ // Attach global focus handler on date picker elements
+ $(document).on('focus', STUDIP.UI.Datepicker.selector, () => {
+ STUDIP.UI.Datepicker.init();
+ });
+
+ // Attach global focus handler on datetime picker elements
+ $(document).on('focus', STUDIP.UI.DateTimepicker.selector, () => {
+ STUDIP.UI.DateTimepicker.init();
+ });
+
+ // Attach global focus handler on time picker elements
+ $(document).on('focus', STUDIP.UI.Timepicker.selector, (event) => {
+ if (!$(event.target).attr('pattern')) {
+ $(event.target).attr('pattern', '^[012]\\d:[0-5]\\d$');
+ }
+
+ STUDIP.UI.Timepicker.init();
+ }).on('keyup', STUDIP.UI.Timepicker.selector, (event) => {
+ const input = event.target;
+ input.value = input.value.replace(/:+$/, ':');
+
+ const value = input.value.trim();
+ if (value.length === 2 && event.which !== 8) {
+ input.value += ':';
+ }
+ }).on('blur', STUDIP.UI.Timepicker.selector, (event) => {
+ if (event.target.checkValidity()) {
+ return;
+ }
+
+ const input = event.target;
+ let value = input.value.trim();
+ value = value.replace(/:/g, '');
+ if (['0', '1', '2'].indexOf(value.substr(0, 1)) === -1) {
+ value = '0' + value;
+ }
+ value = (value + '00').substr(0, 4);
+
+ input.value = value.substr(0, 2) + ':' + value.substr(2, 2);
+ });
+
+}(jQuery, STUDIP));
diff --git a/resources/assets/javascripts/vendor/qrcode-04f46c6.js b/resources/assets/javascripts/vendor/qrcode-04f46c6.js
new file mode 100644
index 0000000..36bd8dc
--- /dev/null
+++ b/resources/assets/javascripts/vendor/qrcode-04f46c6.js
@@ -0,0 +1,1487 @@
+/**
+ * @fileoverview
+ * - Using the 'QRCode for Javascript library'
+ * - Fixed dataset of 'QRCode for Javascript library' for support full-spec.
+ * - this library has no dependencies.
+ *
+ * @author davidshimjs
+ * @see <a href="http://www.d-project.com/" target="_blank">http://www.d-project.com/</a>
+ * @see <a href="http://jeromeetienne.github.com/jquery-qrcode/" target="_blank">http://jeromeetienne.github.com/jquery-qrcode/</a>
+ */
+
+//---------------------------------------------------------------------
+// QRCode for JavaScript
+//
+// Copyright (c) 2009 Kazuhiko Arase
+//
+// URL: http://www.d-project.com/
+//
+// Licensed under the MIT license:
+// http://www.opensource.org/licenses/mit-license.php
+//
+// The word "QR Code" is registered trademark of
+// DENSO WAVE INCORPORATED
+// http://www.denso-wave.com/qrcode/faqpatent-e.html
+//
+//---------------------------------------------------------------------
+function QR8bitByte(data) {
+ this.mode = QRMode.MODE_8BIT_BYTE;
+ this.data = data;
+ this.parsedData = [];
+
+ // Added to support UTF-8 Characters
+ for (var i = 0, l = this.data.length; i < l; i++) {
+ var byteArray = [];
+ var code = this.data.charCodeAt(i);
+
+ if (code > 0x10000) {
+ byteArray[0] = 0xf0 | ((code & 0x1c0000) >>> 18);
+ byteArray[1] = 0x80 | ((code & 0x3f000) >>> 12);
+ byteArray[2] = 0x80 | ((code & 0xfc0) >>> 6);
+ byteArray[3] = 0x80 | (code & 0x3f);
+ } else if (code > 0x800) {
+ byteArray[0] = 0xe0 | ((code & 0xf000) >>> 12);
+ byteArray[1] = 0x80 | ((code & 0xfc0) >>> 6);
+ byteArray[2] = 0x80 | (code & 0x3f);
+ } else if (code > 0x80) {
+ byteArray[0] = 0xc0 | ((code & 0x7c0) >>> 6);
+ byteArray[1] = 0x80 | (code & 0x3f);
+ } else {
+ byteArray[0] = code;
+ }
+
+ this.parsedData.push(byteArray);
+ }
+
+ this.parsedData = Array.prototype.concat.apply([], this.parsedData);
+
+ if (this.parsedData.length != this.data.length) {
+ this.parsedData.unshift(191);
+ this.parsedData.unshift(187);
+ this.parsedData.unshift(239);
+ }
+}
+
+QR8bitByte.prototype = {
+ getLength: function(buffer) {
+ return this.parsedData.length;
+ },
+ write: function(buffer) {
+ for (var i = 0, l = this.parsedData.length; i < l; i++) {
+ buffer.put(this.parsedData[i], 8);
+ }
+ }
+};
+
+function QRCodeModel(typeNumber, errorCorrectLevel) {
+ this.typeNumber = typeNumber;
+ this.errorCorrectLevel = errorCorrectLevel;
+ this.modules = null;
+ this.moduleCount = 0;
+ this.dataCache = null;
+ this.dataList = [];
+}
+
+QRCodeModel.prototype = {
+ addData: function(data) {
+ var newData = new QR8bitByte(data);
+ this.dataList.push(newData);
+ this.dataCache = null;
+ },
+ isDark: function(row, col) {
+ if (
+ row < 0 ||
+ this.moduleCount <= row ||
+ col < 0 ||
+ this.moduleCount <= col
+ ) {
+ throw new Error(row + "," + col);
+ }
+ return this.modules[row][col];
+ },
+ getModuleCount: function() {
+ return this.moduleCount;
+ },
+ make: function() {
+ this.makeImpl(false, this.getBestMaskPattern());
+ },
+ makeImpl: function(test, maskPattern) {
+ this.moduleCount = this.typeNumber * 4 + 17;
+ this.modules = new Array(this.moduleCount);
+ for (var row = 0; row < this.moduleCount; row++) {
+ this.modules[row] = new Array(this.moduleCount);
+ for (var col = 0; col < this.moduleCount; col++) {
+ this.modules[row][col] = null;
+ }
+ }
+ this.setupPositionProbePattern(0, 0);
+ this.setupPositionProbePattern(this.moduleCount - 7, 0);
+ this.setupPositionProbePattern(0, this.moduleCount - 7);
+ this.setupPositionAdjustPattern();
+ this.setupTimingPattern();
+ this.setupTypeInfo(test, maskPattern);
+ if (this.typeNumber >= 7) {
+ this.setupTypeNumber(test);
+ }
+ if (this.dataCache == null) {
+ this.dataCache = QRCodeModel.createData(
+ this.typeNumber,
+ this.errorCorrectLevel,
+ this.dataList
+ );
+ }
+ this.mapData(this.dataCache, maskPattern);
+ },
+ setupPositionProbePattern: function(row, col) {
+ for (var r = -1; r <= 7; r++) {
+ if (row + r <= -1 || this.moduleCount <= row + r) continue;
+ for (var c = -1; c <= 7; c++) {
+ if (col + c <= -1 || this.moduleCount <= col + c) continue;
+ if (
+ (0 <= r && r <= 6 && (c == 0 || c == 6)) ||
+ (0 <= c && c <= 6 && (r == 0 || r == 6)) ||
+ (2 <= r && r <= 4 && 2 <= c && c <= 4)
+ ) {
+ this.modules[row + r][col + c] = true;
+ } else {
+ this.modules[row + r][col + c] = false;
+ }
+ }
+ }
+ },
+ getBestMaskPattern: function() {
+ var minLostPoint = 0;
+ var pattern = 0;
+ for (var i = 0; i < 8; i++) {
+ this.makeImpl(true, i);
+ var lostPoint = QRUtil.getLostPoint(this);
+ if (i == 0 || minLostPoint > lostPoint) {
+ minLostPoint = lostPoint;
+ pattern = i;
+ }
+ }
+ return pattern;
+ },
+ createMovieClip: function(target_mc, instance_name, depth) {
+ var qr_mc = target_mc.createEmptyMovieClip(instance_name, depth);
+ var cs = 1;
+ this.make();
+ for (var row = 0; row < this.modules.length; row++) {
+ var y = row * cs;
+ for (var col = 0; col < this.modules[row].length; col++) {
+ var x = col * cs;
+ var dark = this.modules[row][col];
+ if (dark) {
+ qr_mc.beginFill(0, 100);
+ qr_mc.moveTo(x, y);
+ qr_mc.lineTo(x + cs, y);
+ qr_mc.lineTo(x + cs, y + cs);
+ qr_mc.lineTo(x, y + cs);
+ qr_mc.endFill();
+ }
+ }
+ }
+ return qr_mc;
+ },
+ setupTimingPattern: function() {
+ for (var r = 8; r < this.moduleCount - 8; r++) {
+ if (this.modules[r][6] != null) {
+ continue;
+ }
+ this.modules[r][6] = r % 2 == 0;
+ }
+ for (var c = 8; c < this.moduleCount - 8; c++) {
+ if (this.modules[6][c] != null) {
+ continue;
+ }
+ this.modules[6][c] = c % 2 == 0;
+ }
+ },
+ setupPositionAdjustPattern: function() {
+ var pos = QRUtil.getPatternPosition(this.typeNumber);
+ for (var i = 0; i < pos.length; i++) {
+ for (var j = 0; j < pos.length; j++) {
+ var row = pos[i];
+ var col = pos[j];
+ if (this.modules[row][col] != null) {
+ continue;
+ }
+ for (var r = -2; r <= 2; r++) {
+ for (var c = -2; c <= 2; c++) {
+ if (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) {
+ this.modules[row + r][col + c] = true;
+ } else {
+ this.modules[row + r][col + c] = false;
+ }
+ }
+ }
+ }
+ }
+ },
+ setupTypeNumber: function(test) {
+ var bits = QRUtil.getBCHTypeNumber(this.typeNumber);
+ for (var i = 0; i < 18; i++) {
+ var mod = !test && ((bits >> i) & 1) == 1;
+ this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod;
+ }
+ for (var i = 0; i < 18; i++) {
+ var mod = !test && ((bits >> i) & 1) == 1;
+ this.modules[i % 3 + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
+ }
+ },
+ setupTypeInfo: function(test, maskPattern) {
+ var data = (this.errorCorrectLevel << 3) | maskPattern;
+ var bits = QRUtil.getBCHTypeInfo(data);
+ for (var i = 0; i < 15; i++) {
+ var mod = !test && ((bits >> i) & 1) == 1;
+ if (i < 6) {
+ this.modules[i][8] = mod;
+ } else if (i < 8) {
+ this.modules[i + 1][8] = mod;
+ } else {
+ this.modules[this.moduleCount - 15 + i][8] = mod;
+ }
+ }
+ for (var i = 0; i < 15; i++) {
+ var mod = !test && ((bits >> i) & 1) == 1;
+ if (i < 8) {
+ this.modules[8][this.moduleCount - i - 1] = mod;
+ } else if (i < 9) {
+ this.modules[8][15 - i - 1 + 1] = mod;
+ } else {
+ this.modules[8][15 - i - 1] = mod;
+ }
+ }
+ this.modules[this.moduleCount - 8][8] = !test;
+ },
+ mapData: function(data, maskPattern) {
+ var inc = -1;
+ var row = this.moduleCount - 1;
+ var bitIndex = 7;
+ var byteIndex = 0;
+ for (var col = this.moduleCount - 1; col > 0; col -= 2) {
+ if (col == 6) col--;
+ while (true) {
+ for (var c = 0; c < 2; c++) {
+ if (this.modules[row][col - c] == null) {
+ var dark = false;
+ if (byteIndex < data.length) {
+ dark = ((data[byteIndex] >>> bitIndex) & 1) == 1;
+ }
+ var mask = QRUtil.getMask(maskPattern, row, col - c);
+ if (mask) {
+ dark = !dark;
+ }
+ this.modules[row][col - c] = dark;
+ bitIndex--;
+ if (bitIndex == -1) {
+ byteIndex++;
+ bitIndex = 7;
+ }
+ }
+ }
+ row += inc;
+ if (row < 0 || this.moduleCount <= row) {
+ row -= inc;
+ inc = -inc;
+ break;
+ }
+ }
+ }
+ }
+};
+QRCodeModel.PAD0 = 0xec;
+QRCodeModel.PAD1 = 0x11;
+QRCodeModel.createData = function(typeNumber, errorCorrectLevel, dataList) {
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel);
+ var buffer = new QRBitBuffer();
+ for (var i = 0; i < dataList.length; i++) {
+ var data = dataList[i];
+ buffer.put(data.mode, 4);
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber));
+ data.write(buffer);
+ }
+ var totalDataCount = 0;
+ for (var i = 0; i < rsBlocks.length; i++) {
+ totalDataCount += rsBlocks[i].dataCount;
+ }
+ if (buffer.getLengthInBits() > totalDataCount * 8) {
+ throw new Error(
+ "code length overflow. (" +
+ buffer.getLengthInBits() +
+ ">" +
+ totalDataCount * 8 +
+ ")"
+ );
+ }
+ if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
+ buffer.put(0, 4);
+ }
+ while (buffer.getLengthInBits() % 8 != 0) {
+ buffer.putBit(false);
+ }
+ while (true) {
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(QRCodeModel.PAD0, 8);
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(QRCodeModel.PAD1, 8);
+ }
+ return QRCodeModel.createBytes(buffer, rsBlocks);
+};
+QRCodeModel.createBytes = function(buffer, rsBlocks) {
+ var offset = 0;
+ var maxDcCount = 0;
+ var maxEcCount = 0;
+ var dcdata = new Array(rsBlocks.length);
+ var ecdata = new Array(rsBlocks.length);
+ for (var r = 0; r < rsBlocks.length; r++) {
+ var dcCount = rsBlocks[r].dataCount;
+ var ecCount = rsBlocks[r].totalCount - dcCount;
+ maxDcCount = Math.max(maxDcCount, dcCount);
+ maxEcCount = Math.max(maxEcCount, ecCount);
+ dcdata[r] = new Array(dcCount);
+ for (var i = 0; i < dcdata[r].length; i++) {
+ dcdata[r][i] = 0xff & buffer.buffer[i + offset];
+ }
+ offset += dcCount;
+ var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
+ var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1);
+ var modPoly = rawPoly.mod(rsPoly);
+ ecdata[r] = new Array(rsPoly.getLength() - 1);
+ for (var i = 0; i < ecdata[r].length; i++) {
+ var modIndex = i + modPoly.getLength() - ecdata[r].length;
+ ecdata[r][i] = modIndex >= 0 ? modPoly.get(modIndex) : 0;
+ }
+ }
+ var totalCodeCount = 0;
+ for (var i = 0; i < rsBlocks.length; i++) {
+ totalCodeCount += rsBlocks[i].totalCount;
+ }
+ var data = new Array(totalCodeCount);
+ var index = 0;
+ for (var i = 0; i < maxDcCount; i++) {
+ for (var r = 0; r < rsBlocks.length; r++) {
+ if (i < dcdata[r].length) {
+ data[index++] = dcdata[r][i];
+ }
+ }
+ }
+ for (var i = 0; i < maxEcCount; i++) {
+ for (var r = 0; r < rsBlocks.length; r++) {
+ if (i < ecdata[r].length) {
+ data[index++] = ecdata[r][i];
+ }
+ }
+ }
+ return data;
+};
+var QRMode = {
+ MODE_NUMBER: 1 << 0,
+ MODE_ALPHA_NUM: 1 << 1,
+ MODE_8BIT_BYTE: 1 << 2,
+ MODE_KANJI: 1 << 3
+};
+var QRErrorCorrectLevel = { L: 1, M: 0, Q: 3, H: 2 };
+var QRMaskPattern = {
+ PATTERN000: 0,
+ PATTERN001: 1,
+ PATTERN010: 2,
+ PATTERN011: 3,
+ PATTERN100: 4,
+ PATTERN101: 5,
+ PATTERN110: 6,
+ PATTERN111: 7
+};
+var QRUtil = {
+ PATTERN_POSITION_TABLE: [
+ [],
+ [6, 18],
+ [6, 22],
+ [6, 26],
+ [6, 30],
+ [6, 34],
+ [6, 22, 38],
+ [6, 24, 42],
+ [6, 26, 46],
+ [6, 28, 50],
+ [6, 30, 54],
+ [6, 32, 58],
+ [6, 34, 62],
+ [6, 26, 46, 66],
+ [6, 26, 48, 70],
+ [6, 26, 50, 74],
+ [6, 30, 54, 78],
+ [6, 30, 56, 82],
+ [6, 30, 58, 86],
+ [6, 34, 62, 90],
+ [6, 28, 50, 72, 94],
+ [6, 26, 50, 74, 98],
+ [6, 30, 54, 78, 102],
+ [6, 28, 54, 80, 106],
+ [6, 32, 58, 84, 110],
+ [6, 30, 58, 86, 114],
+ [6, 34, 62, 90, 118],
+ [6, 26, 50, 74, 98, 122],
+ [6, 30, 54, 78, 102, 126],
+ [6, 26, 52, 78, 104, 130],
+ [6, 30, 56, 82, 108, 134],
+ [6, 34, 60, 86, 112, 138],
+ [6, 30, 58, 86, 114, 142],
+ [6, 34, 62, 90, 118, 146],
+ [6, 30, 54, 78, 102, 126, 150],
+ [6, 24, 50, 76, 102, 128, 154],
+ [6, 28, 54, 80, 106, 132, 158],
+ [6, 32, 58, 84, 110, 136, 162],
+ [6, 26, 54, 82, 110, 138, 166],
+ [6, 30, 58, 86, 114, 142, 170]
+ ],
+ G15:
+ (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0),
+ G18:
+ (1 << 12) |
+ (1 << 11) |
+ (1 << 10) |
+ (1 << 9) |
+ (1 << 8) |
+ (1 << 5) |
+ (1 << 2) |
+ (1 << 0),
+ G15_MASK: (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1),
+ getBCHTypeInfo: function(data) {
+ var d = data << 10;
+ while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) {
+ d ^=
+ QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15));
+ }
+ return ((data << 10) | d) ^ QRUtil.G15_MASK;
+ },
+ getBCHTypeNumber: function(data) {
+ var d = data << 12;
+ while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) {
+ d ^=
+ QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18));
+ }
+ return (data << 12) | d;
+ },
+ getBCHDigit: function(data) {
+ var digit = 0;
+ while (data != 0) {
+ digit++;
+ data >>>= 1;
+ }
+ return digit;
+ },
+ getPatternPosition: function(typeNumber) {
+ return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1];
+ },
+ getMask: function(maskPattern, i, j) {
+ switch (maskPattern) {
+ case QRMaskPattern.PATTERN000:
+ return (i + j) % 2 == 0;
+ case QRMaskPattern.PATTERN001:
+ return i % 2 == 0;
+ case QRMaskPattern.PATTERN010:
+ return j % 3 == 0;
+ case QRMaskPattern.PATTERN011:
+ return (i + j) % 3 == 0;
+ case QRMaskPattern.PATTERN100:
+ return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0;
+ case QRMaskPattern.PATTERN101:
+ return (i * j) % 2 + (i * j) % 3 == 0;
+ case QRMaskPattern.PATTERN110:
+ return ((i * j) % 2 + (i * j) % 3) % 2 == 0;
+ case QRMaskPattern.PATTERN111:
+ return ((i * j) % 3 + (i + j) % 2) % 2 == 0;
+ default:
+ throw new Error("bad maskPattern:" + maskPattern);
+ }
+ },
+ getErrorCorrectPolynomial: function(errorCorrectLength) {
+ var a = new QRPolynomial([1], 0);
+ for (var i = 0; i < errorCorrectLength; i++) {
+ a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0));
+ }
+ return a;
+ },
+ getLengthInBits: function(mode, type) {
+ if (1 <= type && type < 10) {
+ switch (mode) {
+ case QRMode.MODE_NUMBER:
+ return 10;
+ case QRMode.MODE_ALPHA_NUM:
+ return 9;
+ case QRMode.MODE_8BIT_BYTE:
+ return 8;
+ case QRMode.MODE_KANJI:
+ return 8;
+ default:
+ throw new Error("mode:" + mode);
+ }
+ } else if (type < 27) {
+ switch (mode) {
+ case QRMode.MODE_NUMBER:
+ return 12;
+ case QRMode.MODE_ALPHA_NUM:
+ return 11;
+ case QRMode.MODE_8BIT_BYTE:
+ return 16;
+ case QRMode.MODE_KANJI:
+ return 10;
+ default:
+ throw new Error("mode:" + mode);
+ }
+ } else if (type < 41) {
+ switch (mode) {
+ case QRMode.MODE_NUMBER:
+ return 14;
+ case QRMode.MODE_ALPHA_NUM:
+ return 13;
+ case QRMode.MODE_8BIT_BYTE:
+ return 16;
+ case QRMode.MODE_KANJI:
+ return 12;
+ default:
+ throw new Error("mode:" + mode);
+ }
+ } else {
+ throw new Error("type:" + type);
+ }
+ },
+ getLostPoint: function(qrCode) {
+ var moduleCount = qrCode.getModuleCount();
+ var lostPoint = 0;
+ for (var row = 0; row < moduleCount; row++) {
+ for (var col = 0; col < moduleCount; col++) {
+ var sameCount = 0;
+ var dark = qrCode.isDark(row, col);
+ for (var r = -1; r <= 1; r++) {
+ if (row + r < 0 || moduleCount <= row + r) {
+ continue;
+ }
+ for (var c = -1; c <= 1; c++) {
+ if (col + c < 0 || moduleCount <= col + c) {
+ continue;
+ }
+ if (r == 0 && c == 0) {
+ continue;
+ }
+ if (dark == qrCode.isDark(row + r, col + c)) {
+ sameCount++;
+ }
+ }
+ }
+ if (sameCount > 5) {
+ lostPoint += 3 + sameCount - 5;
+ }
+ }
+ }
+ for (var row = 0; row < moduleCount - 1; row++) {
+ for (var col = 0; col < moduleCount - 1; col++) {
+ var count = 0;
+ if (qrCode.isDark(row, col)) count++;
+ if (qrCode.isDark(row + 1, col)) count++;
+ if (qrCode.isDark(row, col + 1)) count++;
+ if (qrCode.isDark(row + 1, col + 1)) count++;
+ if (count == 0 || count == 4) {
+ lostPoint += 3;
+ }
+ }
+ }
+ for (var row = 0; row < moduleCount; row++) {
+ for (var col = 0; col < moduleCount - 6; col++) {
+ if (
+ qrCode.isDark(row, col) &&
+ !qrCode.isDark(row, col + 1) &&
+ qrCode.isDark(row, col + 2) &&
+ qrCode.isDark(row, col + 3) &&
+ qrCode.isDark(row, col + 4) &&
+ !qrCode.isDark(row, col + 5) &&
+ qrCode.isDark(row, col + 6)
+ ) {
+ lostPoint += 40;
+ }
+ }
+ }
+ for (var col = 0; col < moduleCount; col++) {
+ for (var row = 0; row < moduleCount - 6; row++) {
+ if (
+ qrCode.isDark(row, col) &&
+ !qrCode.isDark(row + 1, col) &&
+ qrCode.isDark(row + 2, col) &&
+ qrCode.isDark(row + 3, col) &&
+ qrCode.isDark(row + 4, col) &&
+ !qrCode.isDark(row + 5, col) &&
+ qrCode.isDark(row + 6, col)
+ ) {
+ lostPoint += 40;
+ }
+ }
+ }
+ var darkCount = 0;
+ for (var col = 0; col < moduleCount; col++) {
+ for (var row = 0; row < moduleCount; row++) {
+ if (qrCode.isDark(row, col)) {
+ darkCount++;
+ }
+ }
+ }
+ var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5;
+ lostPoint += ratio * 10;
+ return lostPoint;
+ }
+};
+var QRMath = {
+ glog: function(n) {
+ if (n < 1) {
+ throw new Error("glog(" + n + ")");
+ }
+ return QRMath.LOG_TABLE[n];
+ },
+ gexp: function(n) {
+ while (n < 0) {
+ n += 255;
+ }
+ while (n >= 256) {
+ n -= 255;
+ }
+ return QRMath.EXP_TABLE[n];
+ },
+ EXP_TABLE: new Array(256),
+ LOG_TABLE: new Array(256)
+};
+for (var i = 0; i < 8; i++) {
+ QRMath.EXP_TABLE[i] = 1 << i;
+}
+for (var i = 8; i < 256; i++) {
+ QRMath.EXP_TABLE[i] =
+ QRMath.EXP_TABLE[i - 4] ^
+ QRMath.EXP_TABLE[i - 5] ^
+ QRMath.EXP_TABLE[i - 6] ^
+ QRMath.EXP_TABLE[i - 8];
+}
+for (var i = 0; i < 255; i++) {
+ QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i;
+}
+function QRPolynomial(num, shift) {
+ if (num.length == undefined) {
+ throw new Error(num.length + "/" + shift);
+ }
+ var offset = 0;
+ while (offset < num.length && num[offset] == 0) {
+ offset++;
+ }
+ this.num = new Array(num.length - offset + shift);
+ for (var i = 0; i < num.length - offset; i++) {
+ this.num[i] = num[i + offset];
+ }
+}
+QRPolynomial.prototype = {
+ get: function(index) {
+ return this.num[index];
+ },
+ getLength: function() {
+ return this.num.length;
+ },
+ multiply: function(e) {
+ var num = new Array(this.getLength() + e.getLength() - 1);
+ for (var i = 0; i < this.getLength(); i++) {
+ for (var j = 0; j < e.getLength(); j++) {
+ num[i + j] ^= QRMath.gexp(
+ QRMath.glog(this.get(i)) + QRMath.glog(e.get(j))
+ );
+ }
+ }
+ return new QRPolynomial(num, 0);
+ },
+ mod: function(e) {
+ if (this.getLength() - e.getLength() < 0) {
+ return this;
+ }
+ var ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0));
+ var num = new Array(this.getLength());
+ for (var i = 0; i < this.getLength(); i++) {
+ num[i] = this.get(i);
+ }
+ for (var i = 0; i < e.getLength(); i++) {
+ num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio);
+ }
+ return new QRPolynomial(num, 0).mod(e);
+ }
+};
+function QRRSBlock(totalCount, dataCount) {
+ this.totalCount = totalCount;
+ this.dataCount = dataCount;
+}
+QRRSBlock.RS_BLOCK_TABLE = [
+ [1, 26, 19],
+ [1, 26, 16],
+ [1, 26, 13],
+ [1, 26, 9],
+ [1, 44, 34],
+ [1, 44, 28],
+ [1, 44, 22],
+ [1, 44, 16],
+ [1, 70, 55],
+ [1, 70, 44],
+ [2, 35, 17],
+ [2, 35, 13],
+ [1, 100, 80],
+ [2, 50, 32],
+ [2, 50, 24],
+ [4, 25, 9],
+ [1, 134, 108],
+ [2, 67, 43],
+ [2, 33, 15, 2, 34, 16],
+ [2, 33, 11, 2, 34, 12],
+ [2, 86, 68],
+ [4, 43, 27],
+ [4, 43, 19],
+ [4, 43, 15],
+ [2, 98, 78],
+ [4, 49, 31],
+ [2, 32, 14, 4, 33, 15],
+ [4, 39, 13, 1, 40, 14],
+ [2, 121, 97],
+ [2, 60, 38, 2, 61, 39],
+ [4, 40, 18, 2, 41, 19],
+ [4, 40, 14, 2, 41, 15],
+ [2, 146, 116],
+ [3, 58, 36, 2, 59, 37],
+ [4, 36, 16, 4, 37, 17],
+ [4, 36, 12, 4, 37, 13],
+ [2, 86, 68, 2, 87, 69],
+ [4, 69, 43, 1, 70, 44],
+ [6, 43, 19, 2, 44, 20],
+ [6, 43, 15, 2, 44, 16],
+ [4, 101, 81],
+ [1, 80, 50, 4, 81, 51],
+ [4, 50, 22, 4, 51, 23],
+ [3, 36, 12, 8, 37, 13],
+ [2, 116, 92, 2, 117, 93],
+ [6, 58, 36, 2, 59, 37],
+ [4, 46, 20, 6, 47, 21],
+ [7, 42, 14, 4, 43, 15],
+ [4, 133, 107],
+ [8, 59, 37, 1, 60, 38],
+ [8, 44, 20, 4, 45, 21],
+ [12, 33, 11, 4, 34, 12],
+ [3, 145, 115, 1, 146, 116],
+ [4, 64, 40, 5, 65, 41],
+ [11, 36, 16, 5, 37, 17],
+ [11, 36, 12, 5, 37, 13],
+ [5, 109, 87, 1, 110, 88],
+ [5, 65, 41, 5, 66, 42],
+ [5, 54, 24, 7, 55, 25],
+ [11, 36, 12],
+ [5, 122, 98, 1, 123, 99],
+ [7, 73, 45, 3, 74, 46],
+ [15, 43, 19, 2, 44, 20],
+ [3, 45, 15, 13, 46, 16],
+ [1, 135, 107, 5, 136, 108],
+ [10, 74, 46, 1, 75, 47],
+ [1, 50, 22, 15, 51, 23],
+ [2, 42, 14, 17, 43, 15],
+ [5, 150, 120, 1, 151, 121],
+ [9, 69, 43, 4, 70, 44],
+ [17, 50, 22, 1, 51, 23],
+ [2, 42, 14, 19, 43, 15],
+ [3, 141, 113, 4, 142, 114],
+ [3, 70, 44, 11, 71, 45],
+ [17, 47, 21, 4, 48, 22],
+ [9, 39, 13, 16, 40, 14],
+ [3, 135, 107, 5, 136, 108],
+ [3, 67, 41, 13, 68, 42],
+ [15, 54, 24, 5, 55, 25],
+ [15, 43, 15, 10, 44, 16],
+ [4, 144, 116, 4, 145, 117],
+ [17, 68, 42],
+ [17, 50, 22, 6, 51, 23],
+ [19, 46, 16, 6, 47, 17],
+ [2, 139, 111, 7, 140, 112],
+ [17, 74, 46],
+ [7, 54, 24, 16, 55, 25],
+ [34, 37, 13],
+ [4, 151, 121, 5, 152, 122],
+ [4, 75, 47, 14, 76, 48],
+ [11, 54, 24, 14, 55, 25],
+ [16, 45, 15, 14, 46, 16],
+ [6, 147, 117, 4, 148, 118],
+ [6, 73, 45, 14, 74, 46],
+ [11, 54, 24, 16, 55, 25],
+ [30, 46, 16, 2, 47, 17],
+ [8, 132, 106, 4, 133, 107],
+ [8, 75, 47, 13, 76, 48],
+ [7, 54, 24, 22, 55, 25],
+ [22, 45, 15, 13, 46, 16],
+ [10, 142, 114, 2, 143, 115],
+ [19, 74, 46, 4, 75, 47],
+ [28, 50, 22, 6, 51, 23],
+ [33, 46, 16, 4, 47, 17],
+ [8, 152, 122, 4, 153, 123],
+ [22, 73, 45, 3, 74, 46],
+ [8, 53, 23, 26, 54, 24],
+ [12, 45, 15, 28, 46, 16],
+ [3, 147, 117, 10, 148, 118],
+ [3, 73, 45, 23, 74, 46],
+ [4, 54, 24, 31, 55, 25],
+ [11, 45, 15, 31, 46, 16],
+ [7, 146, 116, 7, 147, 117],
+ [21, 73, 45, 7, 74, 46],
+ [1, 53, 23, 37, 54, 24],
+ [19, 45, 15, 26, 46, 16],
+ [5, 145, 115, 10, 146, 116],
+ [19, 75, 47, 10, 76, 48],
+ [15, 54, 24, 25, 55, 25],
+ [23, 45, 15, 25, 46, 16],
+ [13, 145, 115, 3, 146, 116],
+ [2, 74, 46, 29, 75, 47],
+ [42, 54, 24, 1, 55, 25],
+ [23, 45, 15, 28, 46, 16],
+ [17, 145, 115],
+ [10, 74, 46, 23, 75, 47],
+ [10, 54, 24, 35, 55, 25],
+ [19, 45, 15, 35, 46, 16],
+ [17, 145, 115, 1, 146, 116],
+ [14, 74, 46, 21, 75, 47],
+ [29, 54, 24, 19, 55, 25],
+ [11, 45, 15, 46, 46, 16],
+ [13, 145, 115, 6, 146, 116],
+ [14, 74, 46, 23, 75, 47],
+ [44, 54, 24, 7, 55, 25],
+ [59, 46, 16, 1, 47, 17],
+ [12, 151, 121, 7, 152, 122],
+ [12, 75, 47, 26, 76, 48],
+ [39, 54, 24, 14, 55, 25],
+ [22, 45, 15, 41, 46, 16],
+ [6, 151, 121, 14, 152, 122],
+ [6, 75, 47, 34, 76, 48],
+ [46, 54, 24, 10, 55, 25],
+ [2, 45, 15, 64, 46, 16],
+ [17, 152, 122, 4, 153, 123],
+ [29, 74, 46, 14, 75, 47],
+ [49, 54, 24, 10, 55, 25],
+ [24, 45, 15, 46, 46, 16],
+ [4, 152, 122, 18, 153, 123],
+ [13, 74, 46, 32, 75, 47],
+ [48, 54, 24, 14, 55, 25],
+ [42, 45, 15, 32, 46, 16],
+ [20, 147, 117, 4, 148, 118],
+ [40, 75, 47, 7, 76, 48],
+ [43, 54, 24, 22, 55, 25],
+ [10, 45, 15, 67, 46, 16],
+ [19, 148, 118, 6, 149, 119],
+ [18, 75, 47, 31, 76, 48],
+ [34, 54, 24, 34, 55, 25],
+ [20, 45, 15, 61, 46, 16]
+];
+QRRSBlock.getRSBlocks = function(typeNumber, errorCorrectLevel) {
+ var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel);
+ if (rsBlock == undefined) {
+ throw new Error(
+ "bad rs block @ typeNumber:" +
+ typeNumber +
+ "/errorCorrectLevel:" +
+ errorCorrectLevel
+ );
+ }
+ var length = rsBlock.length / 3;
+ var list = [];
+ for (var i = 0; i < length; i++) {
+ var count = rsBlock[i * 3 + 0];
+ var totalCount = rsBlock[i * 3 + 1];
+ var dataCount = rsBlock[i * 3 + 2];
+ for (var j = 0; j < count; j++) {
+ list.push(new QRRSBlock(totalCount, dataCount));
+ }
+ }
+ return list;
+};
+QRRSBlock.getRsBlockTable = function(typeNumber, errorCorrectLevel) {
+ switch (errorCorrectLevel) {
+ case QRErrorCorrectLevel.L:
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
+ case QRErrorCorrectLevel.M:
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
+ case QRErrorCorrectLevel.Q:
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
+ case QRErrorCorrectLevel.H:
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
+ default:
+ return undefined;
+ }
+};
+function QRBitBuffer() {
+ this.buffer = [];
+ this.length = 0;
+}
+QRBitBuffer.prototype = {
+ get: function(index) {
+ var bufIndex = Math.floor(index / 8);
+ return ((this.buffer[bufIndex] >>> (7 - index % 8)) & 1) == 1;
+ },
+ put: function(num, length) {
+ for (var i = 0; i < length; i++) {
+ this.putBit(((num >>> (length - i - 1)) & 1) == 1);
+ }
+ },
+ getLengthInBits: function() {
+ return this.length;
+ },
+ putBit: function(bit) {
+ var bufIndex = Math.floor(this.length / 8);
+ if (this.buffer.length <= bufIndex) {
+ this.buffer.push(0);
+ }
+ if (bit) {
+ this.buffer[bufIndex] |= 0x80 >>> (this.length % 8);
+ }
+ this.length++;
+ }
+};
+var QRCodeLimitLength = [
+ [17, 14, 11, 7],
+ [32, 26, 20, 14],
+ [53, 42, 32, 24],
+ [78, 62, 46, 34],
+ [106, 84, 60, 44],
+ [134, 106, 74, 58],
+ [154, 122, 86, 64],
+ [192, 152, 108, 84],
+ [230, 180, 130, 98],
+ [271, 213, 151, 119],
+ [321, 251, 177, 137],
+ [367, 287, 203, 155],
+ [425, 331, 241, 177],
+ [458, 362, 258, 194],
+ [520, 412, 292, 220],
+ [586, 450, 322, 250],
+ [644, 504, 364, 280],
+ [718, 560, 394, 310],
+ [792, 624, 442, 338],
+ [858, 666, 482, 382],
+ [929, 711, 509, 403],
+ [1003, 779, 565, 439],
+ [1091, 857, 611, 461],
+ [1171, 911, 661, 511],
+ [1273, 997, 715, 535],
+ [1367, 1059, 751, 593],
+ [1465, 1125, 805, 625],
+ [1528, 1190, 868, 658],
+ [1628, 1264, 908, 698],
+ [1732, 1370, 982, 742],
+ [1840, 1452, 1030, 790],
+ [1952, 1538, 1112, 842],
+ [2068, 1628, 1168, 898],
+ [2188, 1722, 1228, 958],
+ [2303, 1809, 1283, 983],
+ [2431, 1911, 1351, 1051],
+ [2563, 1989, 1423, 1093],
+ [2699, 2099, 1499, 1139],
+ [2809, 2213, 1579, 1219],
+ [2953, 2331, 1663, 1273]
+];
+
+function _isSupportCanvas() {
+ return typeof CanvasRenderingContext2D != "undefined";
+}
+
+// android 2.x doesn't support Data-URI spec
+function _getAndroid() {
+ var android = false;
+ var sAgent = navigator.userAgent;
+
+ if (/android/i.test(sAgent)) {
+ // android
+ android = true;
+ var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i);
+
+ if (aMat && aMat[1]) {
+ android = parseFloat(aMat[1]);
+ }
+ }
+
+ return android;
+}
+
+var svgDrawer = (function() {
+ var Drawing = function(el, htOption) {
+ this._el = el;
+ this._htOption = htOption;
+ };
+
+ Drawing.prototype.draw = function(oQRCode) {
+ var _htOption = this._htOption;
+ var _el = this._el;
+ var nCount = oQRCode.getModuleCount();
+ var nWidth = Math.floor(_htOption.width / nCount);
+ var nHeight = Math.floor(_htOption.height / nCount);
+
+ this.clear();
+
+ function makeSVG(tag, attrs) {
+ var el = document.createElementNS("http://www.w3.org/2000/svg", tag);
+ for (var k in attrs)
+ if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]);
+ return el;
+ }
+
+ var svg = makeSVG("svg", {
+ viewBox: "0 0 " + String(nCount) + " " + String(nCount),
+ width: "100%",
+ height: "100%",
+ fill: _htOption.colorLight
+ });
+ svg.setAttributeNS(
+ "http://www.w3.org/2000/xmlns/",
+ "xmlns:xlink",
+ "http://www.w3.org/1999/xlink"
+ );
+ _el.appendChild(svg);
+
+ svg.appendChild(
+ makeSVG("rect", {
+ fill: _htOption.colorLight,
+ width: "100%",
+ height: "100%"
+ })
+ );
+ svg.appendChild(
+ makeSVG("rect", {
+ fill: _htOption.colorDark,
+ width: "1",
+ height: "1",
+ id: "template"
+ })
+ );
+
+ for (var row = 0; row < nCount; row++) {
+ for (var col = 0; col < nCount; col++) {
+ if (oQRCode.isDark(row, col)) {
+ var child = makeSVG("use", { x: String(col), y: String(row) });
+ child.setAttributeNS(
+ "http://www.w3.org/1999/xlink",
+ "href",
+ "#template"
+ );
+ svg.appendChild(child);
+ }
+ }
+ }
+ };
+ Drawing.prototype.clear = function() {
+ while (this._el.hasChildNodes()) this._el.removeChild(this._el.lastChild);
+ };
+ return Drawing;
+})();
+
+var useSVG = document.documentElement.tagName.toLowerCase() === "svg";
+
+// Drawing in DOM by using Table tag
+var Drawing = useSVG
+ ? svgDrawer
+ : !_isSupportCanvas()
+ ? (function() {
+ var Drawing = function(el, htOption) {
+ this._el = el;
+ this._htOption = htOption;
+ };
+
+ /**
+ * Draw the QRCode
+ *
+ * @param {QRCode} oQRCode
+ */
+ Drawing.prototype.draw = function(oQRCode) {
+ var _htOption = this._htOption;
+ var _el = this._el;
+ var nCount = oQRCode.getModuleCount();
+ var nWidth = Math.floor(_htOption.width / nCount);
+ var nHeight = Math.floor(_htOption.height / nCount);
+ var aHTML = ['<table style="border:0;border-collapse:collapse;">'];
+
+ for (var row = 0; row < nCount; row++) {
+ aHTML.push("<tr>");
+
+ for (var col = 0; col < nCount; col++) {
+ aHTML.push(
+ '<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:' +
+ nWidth +
+ "px;height:" +
+ nHeight +
+ "px;background-color:" +
+ (oQRCode.isDark(row, col)
+ ? _htOption.colorDark
+ : _htOption.colorLight) +
+ ';"></td>'
+ );
+ }
+
+ aHTML.push("</tr>");
+ }
+
+ aHTML.push("</table>");
+ _el.innerHTML = aHTML.join("");
+
+ // Fix the margin values as real size.
+ var elTable = _el.childNodes[0];
+ var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2;
+ var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2;
+
+ if (nLeftMarginTable > 0 && nTopMarginTable > 0) {
+ elTable.style.margin =
+ nTopMarginTable + "px " + nLeftMarginTable + "px";
+ }
+ };
+
+ /**
+ * Clear the QRCode
+ */
+ Drawing.prototype.clear = function() {
+ this._el.innerHTML = "";
+ };
+
+ return Drawing;
+ })()
+ : (function() {
+ // Drawing in Canvas
+ function _onMakeImage() {
+ this._elImage.src = this._elCanvas.toDataURL("image/png");
+ this._elImage.style.display = "block";
+ this._elCanvas.style.display = "none";
+ }
+
+ /**
+ * Check whether the user's browser supports Data URI or not
+ *
+ * @private
+ * @param {Function} fSuccess Occurs if it supports Data URI
+ * @param {Function} fFail Occurs if it doesn't support Data URI
+ */
+ function _safeSetDataURI(fSuccess, fFail) {
+ var self = this;
+ self._fFail = fFail;
+ self._fSuccess = fSuccess;
+
+ // Check it just once
+ if (self._bSupportDataURI === null) {
+ var el = document.createElement("img");
+ var fOnError = function() {
+ self._bSupportDataURI = false;
+
+ if (self._fFail) {
+ self._fFail.call(self);
+ }
+ };
+ var fOnSuccess = function() {
+ self._bSupportDataURI = true;
+
+ if (self._fSuccess) {
+ self._fSuccess.call(self);
+ }
+ };
+
+ el.onabort = fOnError;
+ el.onerror = fOnError;
+ el.onload = fOnSuccess;
+ el.src =
+ "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data.
+ return;
+ } else if (self._bSupportDataURI === true && self._fSuccess) {
+ self._fSuccess.call(self);
+ } else if (self._bSupportDataURI === false && self._fFail) {
+ self._fFail.call(self);
+ }
+ }
+
+ /**
+ * Drawing QRCode by using canvas
+ *
+ * @constructor
+ * @param {HTMLElement} el
+ * @param {Object} htOption QRCode Options
+ */
+ var Drawing = function(el, htOption) {
+ this._bIsPainted = false;
+ this._android = _getAndroid();
+
+ this._htOption = htOption;
+ this._elCanvas = document.createElement("canvas");
+ this._elCanvas.width = htOption.width;
+ this._elCanvas.height = htOption.height;
+ el.appendChild(this._elCanvas);
+ this._el = el;
+ this._oContext = this._elCanvas.getContext("2d");
+ this._bIsPainted = false;
+ this._elImage = document.createElement("img");
+ this._elImage.alt = "Scan me!";
+ this._elImage.style.display = "none";
+ this._el.appendChild(this._elImage);
+ this._bSupportDataURI = null;
+ };
+
+ /**
+ * Draw the QRCode
+ *
+ * @param {QRCode} oQRCode
+ */
+ Drawing.prototype.draw = function(oQRCode) {
+ var _elImage = this._elImage;
+ var _oContext = this._oContext;
+ var _htOption = this._htOption;
+
+ var nCount = oQRCode.getModuleCount();
+ var nWidth = _htOption.width / nCount;
+ var nHeight = _htOption.height / nCount;
+ var nRoundedWidth = Math.round(nWidth);
+ var nRoundedHeight = Math.round(nHeight);
+
+ _elImage.style.display = "none";
+ this.clear();
+
+ for (var row = 0; row < nCount; row++) {
+ for (var col = 0; col < nCount; col++) {
+ var bIsDark = oQRCode.isDark(row, col);
+ var nLeft = col * nWidth;
+ var nTop = row * nHeight;
+ _oContext.strokeStyle = bIsDark
+ ? _htOption.colorDark
+ : _htOption.colorLight;
+ _oContext.lineWidth = 1;
+ _oContext.fillStyle = bIsDark
+ ? _htOption.colorDark
+ : _htOption.colorLight;
+ _oContext.fillRect(nLeft, nTop, nWidth, nHeight);
+
+ // 안티 앨리어싱 방지 처리
+ _oContext.strokeRect(
+ Math.floor(nLeft) + 0.5,
+ Math.floor(nTop) + 0.5,
+ nRoundedWidth,
+ nRoundedHeight
+ );
+
+ _oContext.strokeRect(
+ Math.ceil(nLeft) - 0.5,
+ Math.ceil(nTop) - 0.5,
+ nRoundedWidth,
+ nRoundedHeight
+ );
+ }
+ }
+
+ this._bIsPainted = true;
+ };
+
+ /**
+ * Make the image from Canvas if the browser supports Data URI.
+ */
+ Drawing.prototype.makeImage = function() {
+ if (this._bIsPainted) {
+ _safeSetDataURI.call(this, _onMakeImage);
+ }
+ };
+
+ /**
+ * Return whether the QRCode is painted or not
+ *
+ * @return {Boolean}
+ */
+ Drawing.prototype.isPainted = function() {
+ return this._bIsPainted;
+ };
+
+ /**
+ * Clear the QRCode
+ */
+ Drawing.prototype.clear = function() {
+ this._oContext.clearRect(
+ 0,
+ 0,
+ this._elCanvas.width,
+ this._elCanvas.height
+ );
+ this._bIsPainted = false;
+ };
+
+ /**
+ * @private
+ * @param {Number} nNumber
+ */
+ Drawing.prototype.round = function(nNumber) {
+ if (!nNumber) {
+ return nNumber;
+ }
+
+ return Math.floor(nNumber * 1000) / 1000;
+ };
+
+ return Drawing;
+ })();
+
+/**
+ * Get the type by string length
+ *
+ * @private
+ * @param {String} sText
+ * @param {Number} nCorrectLevel
+ * @return {Number} type
+ */
+function _getTypeNumber(sText, nCorrectLevel) {
+ var nType = 1;
+ var length = _getUTF8Length(sText);
+
+ for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) {
+ var nLimit = 0;
+
+ switch (nCorrectLevel) {
+ case QRErrorCorrectLevel.L:
+ nLimit = QRCodeLimitLength[i][0];
+ break;
+ case QRErrorCorrectLevel.M:
+ nLimit = QRCodeLimitLength[i][1];
+ break;
+ case QRErrorCorrectLevel.Q:
+ nLimit = QRCodeLimitLength[i][2];
+ break;
+ case QRErrorCorrectLevel.H:
+ nLimit = QRCodeLimitLength[i][3];
+ break;
+ }
+
+ if (length <= nLimit) {
+ break;
+ } else {
+ nType++;
+ }
+ }
+
+ if (nType > QRCodeLimitLength.length) {
+ throw new Error("Too long data");
+ }
+
+ return nType;
+}
+
+function _getUTF8Length(sText) {
+ var replacedText = encodeURI(sText)
+ .toString()
+ .replace(/\%[0-9a-fA-F]{2}/g, "a");
+ return replacedText.length + (replacedText.length != sText ? 3 : 0);
+}
+
+/**
+ * @class QRCode
+ * @constructor
+ * @example
+ * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie");
+ *
+ * @example
+ * var oQRCode = new QRCode("test", {
+ * text : "http://naver.com",
+ * width : 128,
+ * height : 128
+ * });
+ *
+ * oQRCode.clear(); // Clear the QRCode.
+ * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode.
+ *
+ * @param {HTMLElement|String} el target element or 'id' attribute of element.
+ * @param {Object|String} vOption
+ * @param {String} vOption.text QRCode link data
+ * @param {Number} [vOption.width=256]
+ * @param {Number} [vOption.height=256]
+ * @param {String} [vOption.colorDark="#000000"]
+ * @param {String} [vOption.colorLight="#ffffff"]
+ * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H]
+ */
+var QRCode = function(el, vOption) {
+ this._htOption = {
+ width: 256,
+ height: 256,
+ typeNumber: 4,
+ colorDark: "#000000",
+ colorLight: "#ffffff",
+ correctLevel: QRErrorCorrectLevel.H
+ };
+
+ if (typeof vOption === "string") {
+ vOption = {
+ text: vOption
+ };
+ }
+
+ // Overwrites options
+ if (vOption) {
+ for (var i in vOption) {
+ this._htOption[i] = vOption[i];
+ }
+ }
+
+ if (typeof el == "string") {
+ el = document.getElementById(el);
+ }
+
+ if (this._htOption.useSVG) {
+ Drawing = svgDrawer;
+ }
+
+ this._android = _getAndroid();
+ this._el = el;
+ this._oQRCode = null;
+ this._oDrawing = new Drawing(this._el, this._htOption);
+
+ if (this._htOption.text) {
+ this.makeCode(this._htOption.text);
+ }
+};
+
+/**
+ * Make the QRCode
+ *
+ * @param {String} sText link data
+ */
+QRCode.prototype.makeCode = function(sText) {
+ this._oQRCode = new QRCodeModel(
+ _getTypeNumber(sText, this._htOption.correctLevel),
+ this._htOption.correctLevel
+ );
+ this._oQRCode.addData(sText);
+ this._oQRCode.make();
+ this._el.title = sText;
+ this._oDrawing.draw(this._oQRCode);
+ this.makeImage();
+};
+
+/**
+ * Make the Image from Canvas element
+ * - It occurs automatically
+ * - Android below 3 doesn't support Data-URI spec.
+ *
+ * @private
+ */
+QRCode.prototype.makeImage = function() {
+ if (
+ typeof this._oDrawing.makeImage == "function" &&
+ (!this._android || this._android >= 3)
+ ) {
+ this._oDrawing.makeImage();
+ }
+};
+
+/**
+ * Clear the QRCode
+ */
+QRCode.prototype.clear = function() {
+ this._oDrawing.clear();
+};
+
+/**
+ * @name QRCode.CorrectLevel
+ */
+QRCode.CorrectLevel = QRErrorCorrectLevel;
+
+export default QRCode;