diff options
Diffstat (limited to 'resources/assets/javascripts')
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><</button></li>').appendTo(switcher); + $('<li><button class="switch-right">></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 += '×tamp=' + 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., '× 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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'); + }, + // 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> </ins><a class='jstree-loading' href='#'><ins class='jstree-icon'> </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'> </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'> </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'> </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'> </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,"<").replace(/>/ig,">"); } + $.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("»") + .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'> </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) > 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> </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> </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]) > 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> </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> </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, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + }; + $.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 += "> </ins><a href='#' rel='" + i + "'>"; + if(val.submenu) { + str += "<span style='float:" + ($.vakata.context.rtl ? "left" : "right") + ";'>»</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'> </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'> </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'> </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'> </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'> </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'> </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;'> </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 Binary files differnew file mode 100644 index 0000000..0e958d3 --- /dev/null +++ b/resources/assets/javascripts/jquery/jstree/themes/default/d.gif diff --git a/resources/assets/javascripts/jquery/jstree/themes/default/d.png b/resources/assets/javascripts/jquery/jstree/themes/default/d.png Binary files differnew file mode 100644 index 0000000..8540175 --- /dev/null +++ b/resources/assets/javascripts/jquery/jstree/themes/default/d.png 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 Binary files differnew file mode 100644 index 0000000..5b33f7e --- /dev/null +++ b/resources/assets/javascripts/jquery/jstree/themes/default/throbber.gif 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' + '¶meter[]=' + 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' + + '¶meter[]=' + + $('#' + 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' + '¶meter[]=' + 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' + '¶meter[]=' + 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( + [ + 'À', + 'Á', + 'Â', + 'Ã', + 'Ä', + 'Å', + 'Æ', + 'È', + 'É', + 'Ê', + 'Ë', + 'Ì', + 'Í', + 'Ï', + 'Î', + '', + 'Ý', + + 'à', + 'á', + 'â', + 'ã', + 'ä', + 'å', + 'æ', + 'è', + 'é', + 'ê', + 'ë', + 'ì', + 'í', + 'ï', + 'î', + '', + 'ý', + + 'Ò', + 'Ó', + 'Ô', + 'Õ', + 'Ö', + 'Ø', + 'Œ', + 'Ù', + 'Ú', + 'Û', + 'Ü', + '', + 'Ç', + 'Ñ', + 'Ŵ', + '', + 'Ŷ', + + 'ò', + 'ó', + 'ô', + 'õ', + 'ö', + 'ø', + 'œ', + 'ù', + 'ú', + 'û', + 'ü', + '', + 'ç', + 'ñ', + 'ŵ', + '', + 'ŷ', + + 'ß', + 'Ð', + 'ð', + 'Þ', + 'þ', + '', + '', + '`', + '´', + '^', + '¨', + '', + '¸', + '~', + '≈', + '', + 'ÿ' + ], + (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; + })(), + [ + 'ª', + 'º', + '°', + '¹', + '²', + '³', + '¼', + '½', + '¾', + '‘', + '’', + '“', + '”', + '«', + '»', + '¡', + '¿', + + '@', + '§', + '¶', + 'µ', + '[', + ']', + '{', + '}', + '|', + '¦', + '–', + '—', + '¯', + '‚', + '‛', + '„', + '…', + + '€', + '¢', + '£', + '¥', + '¤', + '©', + '®', + '™', + + '¬', + '·', + '×', + '÷', + + '►', + '•', + '→', + '⇒', + '⇔', + '♦', + + '±', // ± + '∩', // ∩ INTERSECTION + '∪', // ∪ UNION + '∞', // ∞ INFINITY + 'ℇ', // ℇ EULER CONSTANT + '∀', // ∀ FOR ALL + '∁', // ∁ COMPLEMENT + '∂', // ∂ PARTIAL DIFFERENTIAL + '∃', // ∃ THERE EXISTS + '∄', // ∄ THERE DOES NOT EXIST + '∅', // ∅ EMPTY SET + '∆', // ∆ INCREMENT + '∇', // ∇ NABLA + '⊂', // ⊂ SUBSET OF + '⊃', // ⊃ SUPERSET OF + '⊄', // ⊄ NOT A SUBSET OF + '⊆', // ⊆ SUBSET OF OR EQUAL TO + '⊇', // ⊇ SUPERSET OF OR EQUAL TO + '∈', // ∈ ELEMENT OF + '∉', // ∉ NOT AN ELEMENT OF + '∧', // ∧ LOGICAL AND + '∨', // ∨ LOGICAL OR + '≤', // ≤ LESS-THAN OR EQUAL TO + '≥', // ≥ GREATER-THAN OR EQUAL TO + '∎', // ∎ END OF PROOF + '∏', // ∏ N-ARY PRODUCT + '∑', // ∑ N-ARY SUMMATION + '√', // √ SQUARE ROOT + '∫', // ∫ INTEGRAL + '∴', // ∴ THEREFORE + '∵', // ∵ BECAUSE + '≠', // ≠ NOT EQUAL TO + '≢', // ≢ NOT IDENTICAL TO + '≣', // ≣ STRICTLY EQUIVALENT TO + '⊢', // ⊢ RIGHT TACK + '⊣', // ⊣ LEFT TACK + '⊤', // ⊤ DOWN TACK + '⊥', // ⊥ UP TACK + '⊧', // ⊧ MODELS + '⊨', // ⊨ TRUE + '⊬', // ⊬ DOES NOT PROVE + '⊭', // ⊭ NOT TRUE + '⋮', // ⋮ VERTICAL ELLIPSIS + '⋯', // ⋯ MIDLINE HORIZONTAL ELLIPSIS + '⧼', // ⧼ LEFT-POINTING CURVED ANGLE BRACKET + '⧽', // ⧽ RIGHT-POINTING CURVED ANGLE BRACKET + 'ⁿ', // ⁿ SUPERSCRIPT LATIN SMALL LETTER N + '⊕', // ⊕ CIRCLED PLUS + '⊗', // ⊗ CIRCLED TIMES + '⊙' // ⊙ 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' + + '¶meter[]=' + + $('#' + node).attr('id') + + '¶meter[]=' + + 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' + '¶meter[]=' + 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' + '¶meter[]=' + 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' + '¶meter[]=' + 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; |
