diff options
| author | Jan-Hendrik Willms <tleilax+github@gmail.com> | 2021-07-22 16:07:19 +0200 |
|---|---|---|
| committer | Jan-Hendrik Willms <tleilax+github@gmail.com> | 2021-07-22 16:19:12 +0200 |
| commit | a3da1483a9e689846179159355badfec8073dbec (patch) | |
| tree | 770dcca6bdf5f6f2a11b0e7fcbbeda6919a3fc52 /resources | |
current code from svn, revision 62608
Diffstat (limited to 'resources')
440 files changed, 70900 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; diff --git a/resources/assets/stylesheets/fullcalendar.scss b/resources/assets/stylesheets/fullcalendar.scss new file mode 100644 index 0000000..9862337 --- /dev/null +++ b/resources/assets/stylesheets/fullcalendar.scss @@ -0,0 +1,204 @@ +@import "scss/variables"; +@import "scss/buttons"; +@import "mixins.scss"; + +a.fc-event, td.fc-event { + border-radius: 0; + + .fc-time { + background-color: rgba(255, 255, 255, 0.2); + font-weight: bold; + } +} + +.fc button.fc-button { + @include button(); + border-radius: 0; +} + +.fc-button-primary:not(:disabled):active, +.fc-button-primary:not(:disabled).fc-button-active, +.fc button.fc-button.fc-state-active { + -webkit-box-shadow: none; + box-shadow: none; + + background-color: $base-color !important; + color: $white; +} + +/* adjust height: */ +/* .fc-scroller.fc-time-grid-container { + height: auto !important; + min-height: 0 !important; +}*/ + +.fullcalendar-header { + &.fullcalendar-dialog{ + width: calc(100% - 550px); + vertical-align: middle; + display: inline-block; + margin-right: 275px; + } +} + +.fullcalendar-dialogwidget-container { + border-left: 0; + display: inline-block; + flex: 0 0 auto; + margin-bottom: 1em; + position: relative; + + $width: 270px; + + padding-bottom: 7px; + width: $width; + z-index: 2; + + + .fullcalendar-dialogwidget-widget { + background: $white; + border: 1px solid $content-color-40; + margin: 15px 0px 0; + } + + .fullcalendar-dialogwidget-widget-header { + @include clearfix(); + background: $content-color-20; + color: $base-color; + font-weight: bold; + padding: 4px; + } + + select.fullcalendar-dialogwidget-selectlist { + overflow-y: auto; + width: 100%; + } + + .fullcalendar-dialogwidget-widget-content { + border-top: 1px solid $content-color-40; + padding: 4px; + transition: all 0.5s; + } +} + +.institute-plan .fc-slats tr { + height: 100px; +} + +#external-events{ + td.fc-event { + border-radius: 0; + margin: 2px 0px; + background-color: $content-color; + border: 1px solid $brand-color-light; + } +} + +.institute-plan { + .fc-bg td.fc-today { + background: none; + } + + th.fc-day-header, .fc-axis, th.fc-resource-cell { + background-color: $content-color-10; + } +} + +.calendar-caption { + background-color: transparent; + padding-top: 0; + color: $base-gray; + font-size: 1.4em; + text-align: left; + margin-bottom: -10px; +} + +#event-color-picker { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + column-gap: 16px; + width: 200px; + height: 204px; + margin: 10px 0px; + margin-left: calc(50% - 100px); + + input { + opacity: 0; + position: absolute; + + + label { + cursor: pointer; + + &::before { + background-repeat: no-repeat; + content: ' '; + display: inline-block; + margin: 0px 1px 1px 1px; + vertical-align: text-top; + background-image: none; + background-size: 100%; + height: 100%; + width: calc(100% - 4px); + } + } + + &:checked + label::before { + @include background-icon(checkbox-checked, info_alt, 100%); + } + } +} + +.event-colorpicker { + background: none; + border: 0; + cursor: pointer; + padding: 0; + + width: 20px; + height: 20px; + + position: absolute; + top: 0px; + right: 0px; + + &.white { + @include background-icon(group4, info_alt, 100%); + } + &.black { + @include background-icon(group4, info, 100%); + } +} + +.fc[data-fullcalendar="1"].print-view { + position: absolute; + top: 0px; + left: 0px; + height: 2000px; + width: 2000px; + + .fc-resource-cell img, + .event-colorpicker { + display: none; + } + th span a { + color: $black; + } + + td.fc-today { + background: none; + } + .fc-now-indicator { + border: 0; + } + + &.without-weekend { + .fc-day-header, + .fc-day, + .fc-content-skeleton td { + &:last-child, + &:nth-last-child(2) { + display: none; + } + } + } +} diff --git a/resources/assets/stylesheets/jquery-ui.structure.css b/resources/assets/stylesheets/jquery-ui.structure.css new file mode 100644 index 0000000..23f58da --- /dev/null +++ b/resources/assets/stylesheets/jquery-ui.structure.css @@ -0,0 +1,866 @@ +.ui-draggable-handle { + touch-action: none; +} +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { + display: none; +} +.ui-helper-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} +.ui-helper-reset { + margin: 0; + padding: 0; + border: 0; + outline: 0; + line-height: 1.3; + text-decoration: none; + font-size: 100%; + list-style: none; +} +.ui-helper-clearfix:before, +.ui-helper-clearfix:after { + content: ""; + display: table; + border-collapse: collapse; +} +.ui-helper-clearfix:after { + clear: both; +} +.ui-helper-zfix { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 0; +} + +.ui-front { + z-index: 100; +} + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { + cursor: default !important; + pointer-events: none; +} + + +/* Icons +----------------------------------*/ +.ui-icon { + display: inline-block; + vertical-align: middle; + margin-top: -.25em; + position: relative; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; +} + +.ui-widget-icon-block { + left: 50%; + margin-left: -8px; + display: block; +} + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.ui-resizable { + position: relative; +} +.ui-resizable-handle { + position: absolute; + font-size: 0.1px; + display: block; + touch-action: none; +} +.ui-resizable-disabled .ui-resizable-handle, +.ui-resizable-autohide .ui-resizable-handle { + display: none; +} +.ui-resizable-n { + cursor: n-resize; + height: 7px; + width: 100%; + top: -5px; + left: 0; +} +.ui-resizable-s { + cursor: s-resize; + height: 7px; + width: 100%; + bottom: -5px; + left: 0; +} +.ui-resizable-e { + cursor: e-resize; + width: 7px; + right: -5px; + top: 0; + height: 100%; +} +.ui-resizable-w { + cursor: w-resize; + width: 7px; + left: -5px; + top: 0; + height: 100%; +} +.ui-resizable-se { + cursor: se-resize; + width: 12px; + height: 12px; + right: 1px; + bottom: 1px; +} +.ui-resizable-sw { + cursor: sw-resize; + width: 9px; + height: 9px; + left: -5px; + bottom: -5px; +} +.ui-resizable-nw { + cursor: nw-resize; + width: 9px; + height: 9px; + left: -5px; + top: -5px; +} +.ui-resizable-ne { + cursor: ne-resize; + width: 9px; + height: 9px; + right: -5px; + top: -5px; +} +.ui-selectable { + touch-action: none; +} +.ui-selectable-helper { + position: absolute; + z-index: 100; + border: 1px dotted black; +} +.ui-sortable-handle { + touch-action: none; +} +.ui-accordion .ui-accordion-header { + display: block; + cursor: pointer; + position: relative; + margin: 2px 0 0 0; + padding: .5em .5em .5em .7em; + font-size: 100%; +} +.ui-accordion .ui-accordion-content { + padding: 1em 2.2em; + border-top: 0; + overflow: auto; +} +.ui-autocomplete { + position: absolute; + top: 0; + left: 0; + cursor: default; +} +.ui-menu { + list-style: none; + padding: 0; + margin: 0; + display: block; + outline: 0; +} +.ui-menu .ui-menu { + position: absolute; +} +.ui-menu .ui-menu-item { + margin: 0; + cursor: pointer; + /* support: IE10, see #8844 */ + list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); +} +.ui-menu .ui-menu-item-wrapper { + position: relative; + padding: 3px 1em 3px .4em; +} +.ui-menu .ui-menu-divider { + margin: 5px 0; + height: 0; + font-size: 0; + line-height: 0; + border-width: 1px 0 0 0; +} +.ui-menu .ui-state-focus, +.ui-menu .ui-state-active { + margin: -1px; +} + +/* icon support */ +.ui-menu-icons { + position: relative; +} +.ui-menu-icons .ui-menu-item-wrapper { + padding-left: 2em; +} + +/* left-aligned */ +.ui-menu .ui-icon { + position: absolute; + top: 0; + bottom: 0; + left: .2em; + margin: auto 0; +} + +/* right-aligned */ +.ui-menu .ui-menu-icon { + left: auto; + right: 0; +} +.ui-button { + padding: .4em 1em; + display: inline-block; + position: relative; + line-height: normal; + margin-right: .1em; + cursor: pointer; + vertical-align: middle; + text-align: center; + user-select: none; + + /* Support: IE <= 11 */ + overflow: visible; +} + +.ui-button, +.ui-button:link, +.ui-button:visited, +.ui-button:hover, +.ui-button:active { + text-decoration: none; +} + +/* to make room for the icon, a width needs to be set here */ +.ui-button-icon-only { + width: 2em; + box-sizing: border-box; + text-indent: -9999px; + white-space: nowrap; +} + +/* no icon support for input elements */ +input.ui-button.ui-button-icon-only { + text-indent: 0; +} + +/* button icon element(s) */ +.ui-button-icon-only .ui-icon { + position: absolute; + top: 50%; + left: 50%; + margin-top: -8px; + margin-left: -8px; +} + +.ui-button.ui-icon-notext .ui-icon { + padding: 0; + width: 2.1em; + height: 2.1em; + text-indent: -9999px; + white-space: nowrap; + +} + +input.ui-button.ui-icon-notext .ui-icon { + width: auto; + height: auto; + text-indent: 0; + white-space: normal; + padding: .4em 1em; +} + +/* workarounds */ +/* Support: Firefox 5 - 40 */ +input.ui-button::-moz-focus-inner, +button.ui-button::-moz-focus-inner { + border: 0; + padding: 0; +} +.ui-controlgroup { + vertical-align: middle; + display: inline-block; +} +.ui-controlgroup > .ui-controlgroup-item { + float: left; + margin-left: 0; + margin-right: 0; +} +.ui-controlgroup > .ui-controlgroup-item:focus, +.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus { + z-index: 9999; +} +.ui-controlgroup-vertical > .ui-controlgroup-item { + display: block; + float: none; + width: 100%; + margin-top: 0; + margin-bottom: 0; + text-align: left; +} +.ui-controlgroup-vertical .ui-controlgroup-item { + box-sizing: border-box; +} +.ui-controlgroup .ui-controlgroup-label { + padding: .4em 1em; +} +.ui-controlgroup .ui-controlgroup-label span { + font-size: 80%; +} +.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item { + border-left: none; +} +.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item { + border-top: none; +} +.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content { + border-right: none; +} +.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content { + border-bottom: none; +} + +/* Spinner specific style fixes */ +.ui-controlgroup-vertical .ui-spinner-input { + + /* Support: IE8 only, Android < 4.4 only */ + width: 75%; + width: calc( 100% - 2.4em ); +} +.ui-controlgroup-vertical .ui-spinner .ui-spinner-up { + border-top-style: solid; +} + +.ui-checkboxradio-label .ui-icon-background { + box-shadow: inset 1px 1px 1px #ccc; + border-radius: .12em; + border: none; +} +.ui-checkboxradio-radio-label .ui-icon-background { + width: 16px; + height: 16px; + border-radius: 1em; + overflow: visible; + border: none; +} +.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon, +.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon { + background-image: none; + width: 8px; + height: 8px; + border-width: 4px; + border-style: solid; +} +.ui-checkboxradio-disabled { + pointer-events: none; +} +.ui-datepicker { + width: 17em; + padding: .2em .2em 0; + display: none; +} +.ui-datepicker .ui-datepicker-header { + position: relative; + padding: .2em 0; +} +.ui-datepicker .ui-datepicker-prev, +.ui-datepicker .ui-datepicker-next { + position: absolute; + top: 2px; + width: 1.8em; + height: 1.8em; +} +.ui-datepicker .ui-datepicker-prev-hover, +.ui-datepicker .ui-datepicker-next-hover { + top: 1px; +} +.ui-datepicker .ui-datepicker-prev { + left: 2px; +} +.ui-datepicker .ui-datepicker-next { + right: 2px; +} +.ui-datepicker .ui-datepicker-prev-hover { + left: 1px; +} +.ui-datepicker .ui-datepicker-next-hover { + right: 1px; +} +.ui-datepicker .ui-datepicker-prev span, +.ui-datepicker .ui-datepicker-next span { + display: block; + position: absolute; + left: 50%; + margin-left: -8px; + top: 50%; + margin-top: -8px; +} +.ui-datepicker .ui-datepicker-title { + margin: 0 2.3em; + line-height: 1.8em; + text-align: center; +} +.ui-datepicker .ui-datepicker-title select { + font-size: 1em; + margin: 1px 0; +} +.ui-datepicker select.ui-datepicker-month, +.ui-datepicker select.ui-datepicker-year { + width: 45%; +} +.ui-datepicker table { + width: 100%; + font-size: .9em; + border-collapse: collapse; + margin: 0 0 .4em; +} +.ui-datepicker th { + padding: .7em .3em; + text-align: center; + font-weight: bold; + border: 0; +} +.ui-datepicker td { + border: 0; + padding: 1px; +} +.ui-datepicker td span, +.ui-datepicker td a { + display: block; + padding: .2em; + text-align: right; + text-decoration: none; +} +.ui-datepicker .ui-datepicker-buttonpane { + background-image: none; + margin: .7em 0 0 0; + padding: 0 .2em; + border-left: 0; + border-right: 0; + border-bottom: 0; +} +.ui-datepicker .ui-datepicker-buttonpane button { + float: right; + margin: .5em .2em .4em; + cursor: pointer; + padding: .2em .6em .3em .6em; + width: auto; + overflow: visible; +} +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { + float: left; +} + +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { + width: auto; +} +.ui-datepicker-multi .ui-datepicker-group { + float: left; +} +.ui-datepicker-multi .ui-datepicker-group table { + width: 95%; + margin: 0 auto .4em; +} +.ui-datepicker-multi-2 .ui-datepicker-group { + width: 50%; +} +.ui-datepicker-multi-3 .ui-datepicker-group { + width: 33.3%; +} +.ui-datepicker-multi-4 .ui-datepicker-group { + width: 25%; +} +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { + border-left-width: 0; +} +.ui-datepicker-multi .ui-datepicker-buttonpane { + clear: left; +} +.ui-datepicker-row-break { + clear: both; + width: 100%; + font-size: 0; +} + +/* RTL support */ +.ui-datepicker-rtl { + direction: rtl; +} +.ui-datepicker-rtl .ui-datepicker-prev { + right: 2px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next { + left: 2px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-prev:hover { + right: 1px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next:hover { + left: 1px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane { + clear: right; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button { + float: left; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current, +.ui-datepicker-rtl .ui-datepicker-group { + float: right; +} +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { + border-right-width: 0; + border-left-width: 1px; +} + +/* Icons */ +.ui-datepicker .ui-icon { + display: block; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; + left: .5em; + top: .3em; +} +.ui-dialog { + position: absolute; + top: 0; + left: 0; + padding: .2em; + outline: 0; +} +.ui-dialog .ui-dialog-titlebar { + padding: .4em 1em; + position: relative; +} +.ui-dialog .ui-dialog-title { + float: left; + margin: .1em 0; + white-space: nowrap; + width: 90%; + overflow: hidden; + text-overflow: ellipsis; +} +.ui-dialog .ui-dialog-titlebar-close { + position: absolute; + right: .3em; + top: 50%; + width: 20px; + margin: -10px 0 0 0; + padding: 1px; + height: 20px; +} +.ui-dialog .ui-dialog-content { + position: relative; + border: 0; + padding: .5em 1em; + background: none; + overflow: auto; +} +.ui-dialog .ui-dialog-buttonpane { + text-align: left; + border-width: 1px 0 0 0; + background-image: none; + margin-top: .5em; + padding: .3em 1em .5em .4em; +} +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { + float: right; +} +.ui-dialog .ui-dialog-buttonpane button { + margin: .5em .4em .5em 0; + cursor: pointer; +} +.ui-dialog .ui-resizable-n { + height: 2px; + top: 0; +} +.ui-dialog .ui-resizable-e { + width: 2px; + right: 0; +} +.ui-dialog .ui-resizable-s { + height: 2px; + bottom: 0; +} +.ui-dialog .ui-resizable-w { + width: 2px; + left: 0; +} +.ui-dialog .ui-resizable-se, +.ui-dialog .ui-resizable-sw, +.ui-dialog .ui-resizable-ne, +.ui-dialog .ui-resizable-nw { + width: 7px; + height: 7px; +} +.ui-dialog .ui-resizable-se { + right: 0; + bottom: 0; +} +.ui-dialog .ui-resizable-sw { + left: 0; + bottom: 0; +} +.ui-dialog .ui-resizable-ne { + right: 0; + top: 0; +} +.ui-dialog .ui-resizable-nw { + left: 0; + top: 0; +} +.ui-draggable .ui-dialog-titlebar { + cursor: move; +} +.ui-progressbar { + height: 2em; + text-align: left; + overflow: hidden; +} +.ui-progressbar .ui-progressbar-value { + margin: -1px; + height: 100%; +} +.ui-progressbar .ui-progressbar-overlay { + background: url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw=="); + height: 100%; + opacity: 0.25; +} +.ui-progressbar-indeterminate .ui-progressbar-value { + background-image: none; +} +.ui-selectmenu-menu { + padding: 0; + margin: 0; + position: absolute; + top: 0; + left: 0; + display: none; +} +.ui-selectmenu-menu .ui-menu { + overflow: auto; + overflow-x: hidden; + padding-bottom: 1px; +} +.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup { + font-size: 1em; + font-weight: bold; + line-height: 1.5; + padding: 2px 0.4em; + margin: 0.5em 0 0 0; + height: auto; + border: 0; +} +.ui-selectmenu-open { + display: block; +} +.ui-selectmenu-text { + display: block; + margin-right: 20px; + overflow: hidden; + text-overflow: ellipsis; +} +.ui-selectmenu-button.ui-button { + text-align: left; + white-space: nowrap; + width: 14em; +} +.ui-selectmenu-icon.ui-icon { + float: right; + margin-top: 0; +} +.ui-slider { + position: relative; + text-align: left; +} +.ui-slider .ui-slider-handle { + position: absolute; + z-index: 2; + width: 1.2em; + height: 1.2em; + cursor: default; + touch-action: none; +} +.ui-slider .ui-slider-range { + position: absolute; + z-index: 1; + font-size: .7em; + display: block; + border: 0; + background-position: 0 0; +} + +/* support: IE8 - See #6727 */ +.ui-slider.ui-state-disabled .ui-slider-handle, +.ui-slider.ui-state-disabled .ui-slider-range { + filter: inherit; +} + +.ui-slider-horizontal { + height: .8em; +} +.ui-slider-horizontal .ui-slider-handle { + top: -.3em; + margin-left: -.6em; +} +.ui-slider-horizontal .ui-slider-range { + top: 0; + height: 100%; +} +.ui-slider-horizontal .ui-slider-range-min { + left: 0; +} +.ui-slider-horizontal .ui-slider-range-max { + right: 0; +} + +.ui-slider-vertical { + width: .8em; + height: 100px; +} +.ui-slider-vertical .ui-slider-handle { + left: -.3em; + margin-left: 0; + margin-bottom: -.6em; +} +.ui-slider-vertical .ui-slider-range { + left: 0; + width: 100%; +} +.ui-slider-vertical .ui-slider-range-min { + bottom: 0; +} +.ui-slider-vertical .ui-slider-range-max { + top: 0; +} +.ui-spinner { + position: relative; + display: inline-block; + overflow: hidden; + padding: 0; + vertical-align: middle; +} +.ui-spinner-input { + border: none; + background: none; + color: inherit; + padding: .222em 0; + margin: .2em 0; + vertical-align: middle; + margin-left: .4em; + margin-right: 2em; +} +.ui-spinner-button { + width: 1.6em; + height: 50%; + font-size: .5em; + padding: 0; + margin: 0; + text-align: center; + position: absolute; + cursor: default; + display: block; + overflow: hidden; + right: 0; +} +/* more specificity required here to override default borders */ +.ui-spinner a.ui-spinner-button { + border-top-style: none; + border-bottom-style: none; + border-right-style: none; +} +.ui-spinner-up { + top: 0; +} +.ui-spinner-down { + bottom: 0; +} +.ui-tabs { + position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ + padding: .2em; +} +.ui-tabs .ui-tabs-nav { + margin: 0; + padding: .2em .2em 0; +} +.ui-tabs .ui-tabs-nav li { + list-style: none; + float: left; + position: relative; + top: 0; + margin: 1px .2em 0 0; + border-bottom-width: 0; + padding: 0; + white-space: nowrap; +} +.ui-tabs .ui-tabs-nav .ui-tabs-anchor { + float: left; + padding: .5em 1em; + text-decoration: none; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active { + margin-bottom: -1px; + padding-bottom: 1px; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor, +.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor, +.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor { + cursor: text; +} +.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor { + cursor: pointer; +} +.ui-tabs .ui-tabs-panel { + display: block; + border-width: 0; + padding: 1em 1.4em; + background: none; +} +.ui-tooltip { + padding: 8px; + position: absolute; + z-index: 9999; + max-width: 300px; +} +body .ui-tooltip { + border-width: 2px; +} diff --git a/resources/assets/stylesheets/less/admin.less b/resources/assets/stylesheets/less/admin.less new file mode 100644 index 0000000..e26e175 --- /dev/null +++ b/resources/assets/stylesheets/less/admin.less @@ -0,0 +1,154 @@ +.drag-and-drop { display: none; } +.js .drag-and-drop { + display: block; + margin: 5px; + overflow: hidden; + padding: 10px; + padding-left: 0px; + position: relative; + text-align: center; + background-color: @content-color-20; + .background-icon('upload', 'clickable', 50); + background-repeat: no-repeat; + background-position: center 10px; + padding-top: 70px; + color: @base-color; + cursor: pointer; + + input[type=file] { + border: 0; + font-size: 5em; + margin: 0; + opacity: 0; + padding: 0; + position: absolute; + right: 0; + top: 0; + } + + &.hovered { + background-color: @base-color; + .background-icon('upload', 'info_alt', 50); + color: white; + } +} +.js .widget-links .drag-and-drop { + margin-left: -15px; +} + +fieldset.attribute_table { + border-collapse: collapse; + + ul { + margin: 0px; + } + .sem_class_name .sem_class_edit { + display: none; + } + .sem_class_name:hover .sem_class_edit { + display: inline; + } + ul#sem_type_list > li { + height: 20px; + } + ul#sem_type_list > li .sem_type_delete, ul#sem_type_list > li .sem_type_edit { + display: none; + } + ul#sem_type_list > li:hover .sem_type_delete, ul#sem_type_list > li:hover .sem_type_edit { + display: inline; + } + div[container] { + display: inline-block; + width: 150px; + max-width: 150px; + overflow: hidden; + border: thin solid #cccccc; + vertical-align: top; + margin-top: 3px; + margin-bottom: 3px; + } + div[container] > h2 { + display: block; + width: 100%; + background-color: #dddddd; + font-size: 12px; + text-align: center; + margin: 0px; + } + div[container] > div.droparea { + min-height: 30px; + padding: 1px; + } + div[container] > div.droparea > div.plugin { + cursor: move; + border: thin solid #cccccc; + border-radius: 5px; + background-color: #dddddd; + margin: 5px; + margin-left: 7px; + margin-right: 7px; + display: inline-block; + min-width: 132px; + max-width: 132px; + } + div[container] > div.droparea > div > h2 { + display: block; + width: 100%; + background-color: #cccccc; + font-size: 12px; + text-align: center; + margin: 0px; + } + div[container] > div.droparea > div.deactivated { + opacity: 0.5; + } + div[container]#plugins { + width: 99%; + max-width: 99%; + margin-left: auto; + margin-right: auto; + } + hr { + height: 1px; + color: #aaaaaa; + background-color: #aaaaaa; + border: none; + } + div[container]#deactivated_modules { + width: 99%; + max-width: 99%; + margin-left: auto; + margin-right: auto; + } + div[container]#deactivated_modules .plugin > div { + display: none; + } +} + +.course-admin { + .course-completion { + .hide-text(); + .square(16px); + background-repeat: no-repeat; + display: block; + } + + th .course-completion { + .background-icon('radiobutton-checked', 'clickable'); + } + + td .course-completion { + .background-icon('radiobutton-checked', 'status-red'); + + &[data-course-completion="1"] { + .background-icon('radiobutton-checked', 'status-yellow'); + } + &[data-course-completion="2"] { + .background-icon('radiobutton-checked', 'status-green'); + } + + &.ajaxing { + background-image: url("@{image-path}/ajax_indicator_small.gif"); + } + } +} diff --git a/resources/assets/stylesheets/less/ajax.less b/resources/assets/stylesheets/less/ajax.less new file mode 100644 index 0000000..0826be4 --- /dev/null +++ b/resources/assets/stylesheets/less/ajax.less @@ -0,0 +1,43 @@ +/* --- AJAX indicator ------------------------------------------------------- */ +#ajax_notification { + background-color: #7387ac; + bottom: 0; + color: #fff; + display: none; + font-size: 1.3em; + font-weight: bold; + height: 20px; + margin: 0; + padding: 5px 0 0; + position: fixed; + text-align: center; + width: 100%; + + img { vertical-align: middle; } +} + +.ajax_notification { + position: relative; + + .notification { + background: rgba(255, 255, 255, 0.5) url("@{image-path}/ajax_indicator_small.gif") center center no-repeat; + border: 1px solid #ccc; + border-radius: 8px; + display: inline-block; + height: 16px; + margin: 0 3px; + opacity: 1; + position: absolute; + width: 16px; + } +} + +.ajaxing { + background: url("@{image-path}/ajax_indicator_small.gif") center no-repeat; + display: inline-block; + .size(16px, 16px); + .hide-text; + + img, image, svg { display: none; } +} + diff --git a/resources/assets/stylesheets/less/article.less b/resources/assets/stylesheets/less/article.less new file mode 100644 index 0000000..deba2a1 --- /dev/null +++ b/resources/assets/stylesheets/less/article.less @@ -0,0 +1,152 @@ +article.studip { + border-color: @content-color-40; + border-style: solid; + border-width: 1px; + margin-bottom: 10px; + transition: all 300ms ease 0s; + padding: 10px; + + &:last-child { + margin-bottom: 0; + } + + > header { + display: flex; + justify-content: flex-end; + align-items: center; + flex-wrap: wrap; + + padding: 2px; + background-color: @content-color-20; + margin: -10px; + margin-bottom: 10px; + + > * { + /* Try to get header aligned by forcing children into centering */ + display: flex; + align-items: center; + + &:first-child { + flex: 1; + } + } + + h1 { + padding: 5px; + margin: 0; + color: @base-color; + border-bottom: none; + font-size: medium; + + > a { + display: flex; + align-items: top; + } + + &, + > a { + > img, + > svg { + margin-right: 5px; + margin-top: 2px; + } + } + } + + > nav { + display: flex; + align-items: center; + padding: 2px; + + > * { + border-right: 1px solid @content-color; + padding-right: 4px; + margin-right: 4px; + + &:last-child { + border-right: none; + padding-right: 0; + margin-right: 0; + } + + &.nowrap { + white-space: nowrap; + } + } + + } + } + + &.toggle { + > header { + h1 > a { + .icon('before', 'arr_1right', 'clickable'); + &::before { + flex: 0 0 auto; + margin-right: 5px; +// margin-top: 2px; + transition: all 200ms ease 0s; + } + width: 100%; + } + margin-bottom: -10px; + + > *:first-child { + cursor: pointer; + } + } + &:not(.open) > *:not(header) { + display: none; + } + + &.open { + > header { + h1 > a { + &::before { + transform: rotate(90deg); + } + } + margin-bottom: 10px; + } + } + } + + > footer { + text-align: center; + border-color: @content-color-40; + border-top-style: solid; + border-width: 1px; + margin: -10px; + margin-top: 10px; + + &:empty { + display: none !important; + border: 0 !important; + } + } + + &.padding-less { + padding: 0; + + header { + margin: 0; + } + + > footer { + margin: 0; + } + } +} + +article.new { + &.toggle { + > header { + h1 > a { + .icon('before', 'arr_1right', 'new'); + &::before { + margin-right: 5px; + } + } + } + } +} diff --git a/resources/assets/stylesheets/less/autocomplete.less b/resources/assets/stylesheets/less/autocomplete.less new file mode 100644 index 0000000..2d0df42 --- /dev/null +++ b/resources/assets/stylesheets/less/autocomplete.less @@ -0,0 +1,31 @@ +/* --- Autocompleter -------------------------------------------------------- */ +div.autocomplete { position: absolute; } + +.ac_odd { background-color: #eee; } +.ac_over { background-color: #ffb; } + +.ac_results { + background-color: white; + border: 1px solid #888; + margin: 0; + padding: 0; + position: absolute; + z-index: 99999; + + ul { + list-style: none outside none; + margin: 0; + padding: 0; + width: 100%; + } + li { + list-style-type: none; + cursor: pointer; + display: block; + font-size: 0.75em; + margin: 0; + min-height: 2em; + padding: 2px; + text-align: left; + } +} diff --git a/resources/assets/stylesheets/less/avatar.less b/resources/assets/stylesheets/less/avatar.less new file mode 100644 index 0000000..1511ec3 --- /dev/null +++ b/resources/assets/stylesheets/less/avatar.less @@ -0,0 +1,166 @@ +@import (inline) "../../../../node_modules/cropperjs/dist/cropper.css"; + +/* --- Avatars of users, courses and institutes ----------------------------- */ +.avatar-small { + vertical-align: middle; + .size(25px, 25px); +} +.avatar-medium { + max-width: 100px; + height: 100px; +} +.avatar-normal { + max-width: 250px; + height: 250px; +} + +.course-avatar-small { + vertical-align: middle; + max-width: 25px; + height: 25px; +} +.course-avatar-medium { + max-width: 180px; + height: 60px; +} + +.license-avatar-normal { + max-height: 100px; + width: 300px; +} +.license-avatar-medium { + height: 40px; + max-width: 120px; +} +.license-avatar-small { + max-height: 20px; + width: 60px; +} + +div.avatar-widget { + position: relative; + + .profile-avatar { + vertical-align: middle; + margin: 5px 0px; + position: relative; + } + + div.avatar-overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + + margin: 0; + padding: 0; + + transition: opacity .5s ease-in-out; + background-color: fadeout(@base-color, 33.3%); + opacity: 0; + + &:hover { + opacity: 1; + } + + &.dragging { + left: -2px; + top: -2px; + background-color: fadeout(@base-color-40, 33.3%); + border: 2px dashed @base-color; + opacity: 1; + } + + a { + color: @white; + display: block; + height: 100%; + width: 100%; + + span { + position: absolute; + bottom: 1em; + left: 0; + width: 100%; + } + } + } +} + +#custom_avatar label { + display: block; + font-weight: bold; +} + +div#avatar { + img { + max-width: 100%; + } +} + +form.settings-avatar { + + .avatar-normal { + display: block; + margin-left: auto; + margin-right: auto; + padding: 2em; + } + + .file-upload { + flex: 1 1 auto; + position: relative; + top: 2em; + } + + .form-text { + color: initial; + } + + .media-breakpoint-small-up({ .form-group { + display: flex; + align-items: normal; + } }); + + .media-breakpoint-small-down({ + .file-upload { + position: initial; + left: 0; + top: 0; + } + .form-group { + display: initial; + } + }); + + .cropper-container { + margin-left: auto; + margin-right: auto; + + .cropper-view-box { + outline: 1px solid @base-color-80; + } + + .cropper-line, .cropper-point { + background-color: @base-color-80; + } + } + + #avatar-buttons { + padding-left: 5px; + padding-right: 5px; + text-align: left; + width: 150px; + + a { + align-items: center; + display: flex; + margin-bottom: 10px; + + img { + padding-right: 5px; + } + } + } +} diff --git a/resources/assets/stylesheets/less/badges.less b/resources/assets/stylesheets/less/badges.less new file mode 100644 index 0000000..897f51a --- /dev/null +++ b/resources/assets/stylesheets/less/badges.less @@ -0,0 +1,33 @@ +#header { + .badge { + position: relative; + } + .badge:after { + content: attr(data-badge-number); + position: absolute; + top: 0px; + right: 15px; + display: inline-block; + max-width: 30px; + width: auto; + overflow: hidden; + + margin: 0px; + padding: 1px 5px; + /* border: 2px solid white; */ + + background-color: #D60000; + color: white; + + font-size: 10px; + font-weight: bold; + text-align: center; + text-overflow: ellipsis; + vertical-align: top; + white-space: nowrap; + text-shadow: rgba(0, 0, 0, 0.496094) 0px -1px 0px; + + border-radius: 9px; + box-shadow: black 0px 1px 3px; + } +} diff --git a/resources/assets/stylesheets/less/big-image-handler.less b/resources/assets/stylesheets/less/big-image-handler.less new file mode 100644 index 0000000..1d913ea --- /dev/null +++ b/resources/assets/stylesheets/less/big-image-handler.less @@ -0,0 +1,40 @@ +.oversized-image { + cursor: zoom-in; +} +.oversized-image-zoom { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + + height: 100%; + width: 100%; + z-index: 10000; + + background: fadeout(#000, 30%); + cursor: zoom-out; + + display: flex; + justify-content: center; + align-items: center; + + // The actual oversized image is loaded as a background image so we can + // use the background-size option "contain" which will ensure that the + // image will be visible even on small displays. + span { + background-color: fadeout(#000, 30%); + background-position: center; + background-repeat: no-repeat; + background-size: contain; + border: 1px solid #fff; + box-shadow: 0 0 20px fadeout(#fff, 50%); + display: block; + max-width: 98%; + max-height: 98%; + + img { + opacity: 0; + } + } +}
\ No newline at end of file diff --git a/resources/assets/stylesheets/less/breakpoints.less b/resources/assets/stylesheets/less/breakpoints.less new file mode 100644 index 0000000..e4b1c0c --- /dev/null +++ b/resources/assets/stylesheets/less/breakpoints.less @@ -0,0 +1,5 @@ +//** Major Breakpoints +@major-breakpoint-tiny: 0; +@major-breakpoint-small: 576px; +@major-breakpoint-medium: 768px; +@major-breakpoint-large: 1200px; diff --git a/resources/assets/stylesheets/less/buttons.less b/resources/assets/stylesheets/less/buttons.less new file mode 100644 index 0000000..37e5615 --- /dev/null +++ b/resources/assets/stylesheets/less/buttons.less @@ -0,0 +1,173 @@ +/* Stud.IP button styles */ +.button() { + background: white; + border: 1px solid @base-color; + border-radius: 0; + box-sizing: border-box; + color: @base-color; + cursor: pointer; + display: inline-block; + font-family: @font-family-base; + font-size: 14px; + line-height: 130%; + margin: 0.8em 0.6em 0.8em 0; + min-width: 100px; + overflow: visible; + padding: 5px 15px; + position: relative; + text-align: center; + text-decoration: none; + vertical-align: middle; + white-space: nowrap; + width: auto; + + &:hover, &:active { + background: @base-color; + color: white; + outline: 0; + } + &:focus { + outline: dotted 1px #000; + } + &::-moz-focus-inner { + border: 0; + } + + &.disabled, &[disabled] { + box-shadow: none; + background: @light-gray-color-20; + cursor: default; + opacity: 0.65; + + &:hover { + color: @base-color; + } + } + + transition: none; +} + +a.button, button.button { + .button; +} + +.button-with-empty-icon { + white-space: nowrap; + + &::before { + background-repeat: no-repeat; + content: " "; + float: left; + height: 16px; + margin: 1px 5px 0 -8px; + width: 16px; + } +} + +.button-with-icon(@icon, @role, @roleOnHover) { + &:extend(.button-with-empty-icon); + &::before { + &:extend(.button-with-empty-icon::before); + .background-icon(@icon, @role); + } + + &:hover::before { + .background-icon(@icon, @roleOnHover); + } + + &.disabled, + &[disabled] { + &:hover::before { + .background-icon(@icon, @role); + } + } +} + +.button.accept { + .button-with-icon("accept", "clickable", "info_alt"); +} + +.button.cancel { + .button-with-icon("decline", "clickable", "info_alt"); +} + +.button.edit { + .button-with-icon("edit", "clickable", "info_alt"); +} + +.button.move-up { + .button-with-icon("arr_1up", "clickable", "info_alt"); +} + +.button.move-down { + .button-with-icon("arr_1down", "clickable", "info_alt"); +} + +.button.add { + .button-with-icon("add", "clickable", "info_alt"); +} + +.button.download { + .button-with-icon("download", "clickable", "info_alt"); +} + +.button.search { + .button-with-icon("search", "clickable", "info_alt"); +} + +.button.refresh { + .button-with-icon("refresh", "clickable", "info_alt"); +} + +.button.sort { + .button-with-icon("arr_1sort", "clickable", "info_alt"); +} + +.button.trash { + .button-with-icon("trash", "clickable", "info_alt"); +} + +/* Grouped Buttons */ +.button-group { + display: inline-block; + list-style: none; + margin: 0 0.8em 0 0; + padding: 0; + vertical-align: middle; + + button, .button { + float: left; + margin-left: 5px; + margin-right: 0; + } +} + + +/* Other button styles */ + +button, +.button { + &.undecorated { + background: none; + border: 0; + margin: 0; + padding: 0; + + &[formaction] { + cursor: pointer; + color: @base-color; + + transition: color 0.3s; + + &:hover, + &:active { + color: @active-color; + text-decoration: none; + } + + &[disabled] { + pointer-events: none; + } + } + } +} diff --git a/resources/assets/stylesheets/less/calendar.less b/resources/assets/stylesheets/less/calendar.less new file mode 100644 index 0000000..8af7a3a --- /dev/null +++ b/resources/assets/stylesheets/less/calendar.less @@ -0,0 +1,587 @@ +// TODO: LESSify + +/* --- Styles fuer Terminkalender ------------------------------------------- */ +a.day { + font-weight: bold; +} + +a.sday { + color: @red; + font-weight: bold; +} + +a.hday { + color: @red-80; + font-weight: bold; +} + +span.kwmin { + color: @dark-gray-color-80; + font-weight: bold; +} + +a.lightday { + color: @base-color-40; + font-weight: bold; +} + +a.lightsday { + color: @red-40; + font-weight: bold; +} + +.inday { + font-size: 8pt; +} + +.precol1w { + font-size: 12pt; + font-weight: bold; + color: @light-gray-color; + text-align: center; + vertical-align: top; +} + +.precol2w { + font-size: 8pt; + font-weight: bold; + color: @light-gray-color; + text-align: center; +} + +td.calhead, div.calhead { + font-size: 18pt; + font-weight: bold; + color: @light-gray-color; + text-align: center; +} + +a:link.calhead { + color: @base-color-60; + white-space: nowrap; + font-weight: bold; +} + +a:hover.calhead { + color: @red-60; +} + +.calhead label { + cursor: pointer; + &:hover { + color: @base-color-40; + } + + .media-breakpoint-small-down({ + .button(); + + img { + padding-left: 0.5em; + vertical-align: middle; + } + }) +} + +.celltoday { + background-color: @red-20; +} + +td.weekend { + background-color: @dark-gray-color-15; +} + +td.weekday { + background-color: @dark-gray-color-5; +} + +td.current { + padding: 2px; + border: 2px solid @red; +} + +table.calendar-week, table.calendar-day { + border-spacing: 0; + table-layout: fixed; + td { + padding: 0; + } +} + +table.calendar-month { + width: 100%; + tr td { + max-width: 90px; + min-width: 90px; + vertical-align: top; + } +} + +td.month { + background-color: fadeout(darken(@dark-gray-color-15, 5), 30); + padding: 3px; + div { + width: 90%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +td.lightmonth { + background-color: fadeout(darken(@dark-gray-color-5, 5), 30); + padding: 3px; + div { + width: 90%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +table.calendar-month td.calendar-month-week { + text-align: center; + vertical-align: middle; + height: 80px; + width: 80px; +} + +td.weekdayevents { + width: 90px; +} + +nav.calendar-nav { + display: flex; + align-items: center; + padding-bottom: 1em; + + > div { + flex: 1 1 auto; + } + + .calhead { + color: @base-color; + } +} + +.calendar-day-edit { + text-align: right; + font-size: 0.8em; +} + +.calendar-week tbody tr, .calendar-day tbody tr { + transition: background-color 0.3s; + &:hover { + background-color: fadeout(@dark-gray-color-5, 40%); + } + & td { + padding: 3px; + border-bottom: 1px solid @dark-gray-color-5; + } +} + +.calendar-day-event-title { + overflow: hidden; + text-overflow: ellipsis; + a { + color: @dark-gray-color; + } +} + +.calendar-category-mixin(@color-bg) { + vertical-align: top; + font-size: 11px; + color: @white; + padding: 0; + /* necessary for All-day Events */ + a { + color: contrast(@color-bg, black, white, 60%); + font-weight: 600; + } +} + +.calendar-single-year { + .calendar-single-year--table { + + > thead th { + min-width: 5em; + text-align: left; + } + + .yday { + white-space: nowrap; + } + } +} +/* --- Coloring Styles for Personal TERMIN Categories ---------------------------------------------- */ +select, ul, span { + option, li, span, input[type="radio"] { + &.calendar-category1 { + color: @calendar-category-1; + } + &.calendar-category2 { + color: @calendar-category-2; + } + &.calendar-category3 { + color: @calendar-category-3; + } + &.calendar-category4 { + color: @calendar-category-4; + } + &.calendar-category5 { + color: @calendar-category-5; + } + &.calendar-category6 { + color: @calendar-category-6; + } + &.calendar-category7 { + color: @calendar-category-7; + } + &.calendar-category8 { + color: @calendar-category-8; + } + &.calendar-category9 { + color: @calendar-category-9; + } + &.calendar-category10 { + color: @calendar-category-10; + } + &.calendar-category11 { + color: @calendar-category-11; + } + &.calendar-category12 { + color: @calendar-category-12; + } + &.calendar-category13 { + color: @calendar-category-13; + } + &.calendar-category14 { + color: @calendar-category-14; + } + &.calendar-category15 { + color: @calendar-category-15; + } + &.calendar-category255 { + color: @calendar-category-255; + } + } +} + +table.calendar-week, table.calendar-day { + & tbody tr td { + &.calendar-day-event { + div:first-child { + background-color: @calendar-day-event; + overflow: hidden; + } + background: @calendar-day-event-aux; + border: solid 1px @calendar-day-event; + .calendar-category-mixin(@calendar-day-event-aux); + } + &.calendar-category1, &.calendar-course-category5 { + div:first-child { + background-color: @calendar-category-1; + overflow: hidden; + } + background: @calendar-category-1-aux; + border: solid 1px @calendar-category-1; + .calendar-category-mixin(@calendar-category-1-aux); + } + &.calendar-category2, &.calendar-course-category1 { + div:first-child { + background-color: @calendar-category-2; + overflow: hidden; + } + background: @calendar-category-2-aux; + border: solid 1px @calendar-category-2; + .calendar-category-mixin(@calendar-category-2-aux); + } + &.calendar-category3, &.calendar-course-category2 { + div:first-child { + background-color: @calendar-category-3; + overflow: hidden; + } + background: @calendar-category-3-aux; + border: solid 1px @calendar-category-3; + .calendar-category-mixin(@calendar-category-3-aux); + } + &.calendar-category4, &.calendar-course-category3 { + div:first-child { + background-color: @calendar-category-4; + overflow: hidden; + } + background: @calendar-category-4-aux; + border: solid 1px @calendar-category-4; + .calendar-category-mixin(@calendar-category-4-aux); + } + &.calendar-category5, &.calendar-course-category4 { + div:first-child { + background-color: @calendar-category-5; + overflow: hidden; + } + background: @calendar-category-5-aux; + border: solid 1px @calendar-category-5; + .calendar-category-mixin(@calendar-category-5-aux); + } + &.calendar-category6, &.calendar-course-category6 { + div:first-child { + background-color: @calendar-category-6; + overflow: hidden; + } + background: @calendar-category-6-aux; + border: solid 1px @calendar-category-6; + .calendar-category-mixin(@calendar-category-6-aux); + } + &.calendar-category7, &.calendar-course-category8 { + div:first-child { + background-color: @calendar-category-7; + overflow: hidden; + } + background: @calendar-category-7-aux; + border: solid 1px @calendar-category-7; + .calendar-category-mixin(@calendar-category-7-aux); + } + &.calendar-category8, &.calendar-course-category9 { + div:first-child { + background-color: @calendar-category-8; + overflow: hidden; + } + background: @calendar-category-8-aux; + border: solid 1px @calendar-category-8; + .calendar-category-mixin(@calendar-category-8-aux); + } + &.calendar-category9, &.calendar-course-category10 { + div:first-child { + background-color: @calendar-category-9; + overflow: hidden; + } + background: @calendar-category-9-aux; + border: solid 1px @calendar-category-9; + .calendar-category-mixin(@calendar-category-9-aux); + } + &.calendar-category10, &.calendar-course-category11 { + div:first-child { + background-color: @calendar-category-10; + overflow: hidden; + } + background: @calendar-category-10-aux; + border: solid 1px @calendar-category-10; + .calendar-category-mixin(@calendar-category-10-aux); + } + &.calendar-category11, &.calendar-course-category12 { + div:first-child { + background-color: @calendar-category-11; + overflow: hidden; + } + background: @calendar-category-11-aux; + border: solid 1px @calendar-category-11; + .calendar-category-mixin(@calendar-category-11-aux); + } + &.calendar-category12, &.calendar-course-category13 { + div:first-child { + background-color: @calendar-category-12; + overflow: hidden; + } + background: @calendar-category-12-aux; + border: solid 1px @calendar-category-12; + .calendar-category-mixin(@calendar-category-12-aux); + } + &.calendar-category13, &.calendar-course-category14 { + div:first-child { + background-color: @calendar-category-13; + overflow: hidden; + } + background: @calendar-category-13-aux; + border: solid 1px @calendar-category-13; + .calendar-category-mixin(@calendar-category-13-aux); + } + &.calendar-category14, &.calendar-course-category15 { + div:first-child { + background-color: @calendar-category-14; + overflow: hidden; + } + background: @calendar-category-14-aux; + border: solid 1px @calendar-category-14; + .calendar-category-mixin(@calendar-category-14-aux); + } + &.calendar-category15, &.calendar-course-category7 { + div:first-child { + background-color: @calendar-category-15; + overflow: hidden; + } + background: @calendar-category-15-aux; + border: solid 1px @calendar-category-15; + .calendar-category-mixin(@calendar-category-15-aux); + } + &.calendar-category255, &.calendar-course-category255 { + div:first-child { + background-color: @calendar-category-255; + overflow: hidden; + } + background: @calendar-category-255-aux; + border: solid 1px @calendar-category-255; + .calendar-category-mixin(@calendar-category-255-aux); + } + /* Termin von im Stundenplan vorgemerkter Kurs */ + &.calendar-course-category256 { + div:first-child { + background-color: #2D2C64; + overflow: hidden; + } + background: mix(#2D2C64, #fff, 60%); + border: solid 1px #2D2C64; + .calendar-category-mixin(mix(#2D2C64, #fff, 60%)); + } + } +} + +a.calendar-event-text1, +a.Calendar-course-event-text5 { + color: @calendar-category-1; +} + +a.calendar-event-text2, +a.Calendar-course-event-text1{ + color: @calendar-category-2; +} + +a.calendar-event-text3, +a.Calendar-course-event-text2 { + color: @calendar-category-3; +} + +a.calendar-event-text4, +a.Calendar-course-event-text3 { + color: @calendar-category-4; +} + +a.calendar-event-text5, +a.Calendar-course-event-text4 { + color: @calendar-category-5; +} + +a.calendar-event-text6, +a.Calendar-course-event-text6 { + color: @calendar-category-6; +} + +a.calendar-event-text7 { + color: @calendar-category-7; +} + +a.calendar-event-text8 { + color: @calendar-category-8; +} + +a.calendar-event-text9 { + color: @calendar-category-9; +} + +a.calendar-event-text10 { + color: @calendar-category-10; +} + +a.calendar-event-text11 { + color: @calendar-category-11; +} + +a.calendar-event-text12 { + color: @calendar-category-12; +} + +a.calendar-event-text13 { + color: @calendar-category-13; +} + +a.calendar-event-text14 { + color: @calendar-category-14; +} + +a.calendar-event-text15, +a.Calendar-course-event-text7 { + color: @calendar-category-15; +} + +a.calendar-event-text255, +a.Calendar-course-event-text255 { + color: @calendar-category-255; +} +.calendar-tooltip { + display: none; + font-size: 0.8em; +} + +.calendar-group-events { + background: linear-gradient(to right, #8E8EB2, #BABAD4) repeat-x #8E8EB2; + border: solid 1px #505064; +} + +#exc-dates { + padding: 2px; + list-style-type: none; + width: 7.5em; + min-height: 5em; + max-height: 10em; + overflow: auto; + border: 1px solid #888; + + img { + vertical-align: text-top; + } + input:checked ~ span { + text-decoration: line-through; + opacity: 0.6; + } +} + +/* --- Styles fuer TerminZeile ---------------------------------------------- */ +table.tabdaterow { + background-color: white; +} + +td.tddaterowp { + border: 1px solid #FFFFFF; + background-color: #f0f0f0; + font-weight: bold; + color: #009900; + font-size: 8pt; +} + +td.tddaterowpx { + border: 1px solid #D00000; + background-color: #f0f0f0; + font-weight: bold; + color: #009900; + font-size: 8pt; +} + + +.recurrences { + width: 100%; + float: none; + list-style: none; + text-align: left; + position: relative; + padding: 0; + margin: 0; + li { + display: block; + width: 100%; + } + input.rec-select { + } + .rec-label { + cursor: pointer; + } + .rec-label:hover { + } + .rec-content { + display: none; + position: relative; + padding-left: 3em; + } + [id^="rec"]:checked + label.rec-label { + } + [id^="rec"]:checked ~ [id^="rec-content"] { + display: block; + } +} diff --git a/resources/assets/stylesheets/less/clipboard.less b/resources/assets/stylesheets/less/clipboard.less new file mode 100644 index 0000000..55e591f --- /dev/null +++ b/resources/assets/stylesheets/less/clipboard.less @@ -0,0 +1,91 @@ +.clipboard-selector { + width: calc(100% - 5em); + margin-bottom: 0.25em; + margin-right: 1em; +} + +.clipboard-name { + height: 1.7em; + padding: 1px 8px; + width: calc(100% - 5.5em); + margin-bottom: 0.25em; +} + +.selected-element-transporter { + padding: 0.5em; + text-align: center; +} + +.dragged-clipboard-item { + position: fixed; + z-index: @drag_and_drop_z_index; + border: @drag_and_drop_border; + color: @base-color; + font-weight: bold; + font-size: 16px; + background-color: @white; +} + +div.clipboard-area-container { + margin-bottom: 0.5em; + overflow-y: scroll; + max-height: 15em; +} + +table.clipboard-area { + width: 100%; + border: 1px solid @content-color-40; + min-height: 8em; + height: 8em; + padding: 0.25em; +} + +table.clipboard-area tr.empty-clipboard-message > td { + padding: 0.5em; + text-align: center; +} + +.clipboard-item { + + > td.item-name, > td.item-actions { + vertical-align: top; + } + + > .item-name { + width: 80%; + } + + > .item-actions { + width: 20%; + } +} + +.clipboard-widget { + form .apply-button { + width: 100%; + margin-bottom: 0.15em; + } + + form.new-clipboard-form { + input[type=text][name=name] { + display: inline-block; + width: calc(100% - 2em); + } + } + +} + + +.animated-drop { + animation: drop-animation 0.5s; +} + +@keyframes drop-animation { + 0% { + background-color: @yellow-60; + } + + 100% { + background-color: @white; + } +} diff --git a/resources/assets/stylesheets/less/comments.less b/resources/assets/stylesheets/less/comments.less new file mode 100644 index 0000000..a336763 --- /dev/null +++ b/resources/assets/stylesheets/less/comments.less @@ -0,0 +1,32 @@ +section.comments { + text-align: left; + border-color: @content-color-40; + border-top-style: none; + border-width: 1px; + background-color: white; + padding: 5px; + + h1 { + font-size: 1em; + font-weight: bold; + border: none; + padding: 0; + } + article.comment { + border: 0; + border-top: 1px solid @light-gray-color-40; + max-width: 1260px; + margin: auto; + margin-bottom: 4px; + + h1 { + margin-bottom: 0px; + } + + time { + float: right; + font-size: 0.8em; + color: @light-gray-color-40; + } + } +} diff --git a/resources/assets/stylesheets/less/consultation.less b/resources/assets/stylesheets/less/consultation.less new file mode 100644 index 0000000..76e2468 --- /dev/null +++ b/resources/assets/stylesheets/less/consultation.less @@ -0,0 +1,39 @@ +.consultation-note { + border-bottom: 1px solid @light-gray-color-40; + font-size: @font-size-small; + margin-bottom: 2px; + padding-bottom: 2px; + + &-below { + border-bottom: 0; + margin-bottom: 0; + padding-bottom: 0; + + border-top: 1px solid @light-gray-color-40; + margin-top: 2px; + padding-top: 2px; + } + + &.shortened { + .icon('before', 'info-circle', 'info', 12, 5px); + transition: opacity 300ms; + &:not(:hover)::after { + opacity: 0.5; + } + } +} +.consultation-free { + color: @green; +} +.consultation-occupied { + color: @red; +} + +.consultation-overview { + .block-is-expired th { + font-style: italic; + } + .slot-is-expired td { + background-color: @dark-gray-color-10; + } +} diff --git a/resources/assets/stylesheets/less/contacts.less b/resources/assets/stylesheets/less/contacts.less new file mode 100644 index 0000000..cb04db0 --- /dev/null +++ b/resources/assets/stylesheets/less/contacts.less @@ -0,0 +1,50 @@ +// Turns a vertical list into a horizontal one spaced with separators +.contact-legend { + color: #555; + text-align: center; + + ul, li { + list-style: none; + margin: 0; + padding: 0; + } + ul { display: inline; } + li { + border-left: 1px solid #333; + display: inline-block; + padding: 0 0.5em; + + &:first-child { border-left: 0; } + + img { vertical-align: text-top; } + } +} + +// Prefixed table is neccessary to override some other previously defined rules +table.contact-header { + margin: auto; + + img { vertical-align: text-top; } + td { + background-color: #f2f2f2; + padding: 3px 0.5em; + text-align: center; + vertical-align: middle; + + &:hover { background-color: #ced8f2; } + + // Active state + &.active { + background-color: #e2e2e2; + border: 1px solid #888; + + &:hover { background-color: #b7c2e2; } + + a { + color: #f00; + font-weight: bold; + } + } + &.empty a { color: #999; } + } +} diff --git a/resources/assets/stylesheets/less/content.less b/resources/assets/stylesheets/less/content.less new file mode 100644 index 0000000..7f8cf6c --- /dev/null +++ b/resources/assets/stylesheets/less/content.less @@ -0,0 +1,79 @@ +.content_title { + background-color: #e3eaf6; + background-image: linear-gradient(#cdd9ed, #e3eaf6 40%, #e3eaf6); + background-repeat: no-repeat; + border-top: 1px solid @content-color; + line-height: 17pt; + height: 25px; +} + +.content_body { + background-color: #f8f8f8; +} + +.content_body_panel { + background-color: #dfe2e9; + border-left: 1px solid #ccc; +} + +.content_seperator, +.content_seperator td { + background-color: #adadad; + background-image: linear-gradient(#e0e0e0, #bcbcbc 15%, #adadad); + background-repeat: no-repeat; + border-top: 1px solid #c7c7c7; + height: 15px; +} + +// Display formatted content as inline so we don't break any existing code +// that expects formatReady() to output an inline elements instead of a +// block element. +// If browser support for "display: contents" is given, this should be changed +// to that instead. +.formatted-content { + display: inline; + + a { + word-break: break-all; + + &.link-extern, + &.link-intern { + display: inline-block; + max-width: 90%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; + } + } + + img { + height: auto; + max-width: 100%; + } + + pre { // pre-formatted content breaks the Stud.IP Layout! + white-space: pre-wrap; + } + + h1, h2 { + border-bottom: 1px solid rgba(0,0,0,0.2); + } +} + +// Emphasize tt tags a little bit so ##monospace## blocks will stick out. +.formatted-content tt { + background-color: rgba(255, 255, 255, 0.5); + border: 1px solid rgba(0, 0, 0, 0.5); + padding: 0 0.5ex; +} + +// Margin for lists in user content +.formatted-content ul, +.formatted-content ol { + margin: 0.5em 0; + ul, + ol { + margin-top: 0; + margin-bottom: 0; + } +} diff --git a/resources/assets/stylesheets/less/content_box.less b/resources/assets/stylesheets/less/content_box.less new file mode 100644 index 0000000..9485c9a --- /dev/null +++ b/resources/assets/stylesheets/less/content_box.less @@ -0,0 +1,241 @@ +section.contentbox { + border-color: @content-color-40; + border-style: solid; + border-width: 1px; + margin-bottom: 10px; + transition: all 300ms ease 0s; + + header { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + + padding: 2px; + background-color: @content-color-20; + + span.title { + font-size: medium; + color: @base-color; + + &.no-overflow { + width: calc(100% - 1.5em); + overflow: hidden; + white-space: nowrap; + + &:after { + content: ""; + width: 4em; + background: linear-gradient(to right, transparent, @content-color-20); + position: absolute; + height: 1.5em; + right: 2.5em; + } + } + } + + h1 { + flex: 1 0 0; + padding: 5px; + margin: 0; + color: @base-color; + border-bottom: none; + font-size: medium; + > a { + img, svg { + margin-right: 5px; + } + } + } + + > nav { + align-items: center; + display: flex; + flex: 0 0 auto; + justify-content: flex-end; + padding: 2px; + text-align: right; + + > *:not(:first-child) { + padding-left: 0.25em; + } + } + } + footer:empty { + display: none !important; + } + footer { + text-align: center; + border-color: @content-color-40; + border-top-style: solid; + border-width: 1px; + background-color: white;} + + section { + padding: 10px; + } + + + table.default { + margin-bottom: 0; + } + + table.default tbody tr:last-child td { + border-bottom: none; + } + + > article { + border-color: @content-color-40; + border-style: solid; + border-width: 1px; + margin: 10px; + + > p, > section, > footer, > div { + max-height: 0px; + opacity: 0; + overflow: auto; + transition: opacity 0.3s; + } + + + > p, > footer, > div { + padding: 0 10px 0 10px; + } + + div, p { + margin: 0; + } + + section { + border-width: 0; + margin-top: 0; + margin-bottom: 0; + padding: 0; + + article { + border: none; + } + + header { + background: transparent; + + h1 { + text-align: center; + font-size: small; + width: 100%; + font-weight: bold; + color: black; + } + } + + > article { + padding: 5px; + } + + article time { + float: right; + font-size: smaller; + margin: 2px; + } + } + + header { + h1 a { + .icon('before', "arr_1right", 'clickable'); + &:before { + transition: all 200ms ease 0s; + margin-right: 2px; + } + display: flex; + align-items: center; + } + nav { + a, span { + display: inline-block; + vertical-align: middle; + } + + span { + &:last-child { + border-right: none; + padding-right: 0px; + } + display: inline-block; + border-right: 1px solid @content-color; + padding: 0px 5px; + } + a { + padding-left: 5px; + align-items: center; + display: flex; + } + } + } + + footer { + text-align: center; + border-color: @content-color-40; + border-top-style: none; + border-width: 1px; + background-color: white; + h1 { + font-size: 1em; + font-weight: bold; + border: none; + padding: 0; + } + article.comment { + border: 0; + border-top: 1px solid @light-gray-color-40; + max-width: 1260px; + margin: auto; + margin-bottom: 4px; + text-align: left; + h1 { + margin-bottom: 0px; + } + time { + float:right; + font-size: 0.8em; + color: @light-gray-color-40; + } + } + } + + &:not(.open) header ~ * { + max-height: 0px; + opacity: 0; + overflow: auto; + transition: opacity 0.3s; + } + + &.open { + > p, > section, > footer, > div { + max-height: none; + opacity: 1; + transition: opacity 0.3s; + } + + footer { + border-top-style: solid; + } + + header h1 a::before { + transform: rotate(90deg); + } + } + + &.new { + header h1 a { + .icon('before', "arr_1right", 'new'); + } + } + + &.indented { + margin-left: calc(10px + 1em); + > header { + background-color: mix(@content-color, #fff, 10%); + } + } + } +} diff --git a/resources/assets/stylesheets/less/copyable-links.less b/resources/assets/stylesheets/less/copyable-links.less new file mode 100644 index 0000000..0084128 --- /dev/null +++ b/resources/assets/stylesheets/less/copyable-links.less @@ -0,0 +1,61 @@ +// Defines a css animation keyframes specific for this section with stop points +// at 1/3 and 2/3. This way, the animation runs for a third of the allocated +// time, shows the desired state for a third of the time and reverts for - you +// guessed it - a third of the time. +.keyframes(@name, @rules-inital, @rules-target) { + @keyframes @name { + 0% { @rules-inital(); } + 33% { @rules-target(); } + 66% { @rules-target(); } + 100% { @rules-inital(); } + } +} + +.copyable-link-animation { + @animation-name: copyable-links-confirmation; + @animation-duration: 2s; + + // Position confirmation message above the link + position: relative; + + div { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + text-align: center; + + .icon('before', 'check-circle', 'status-green', 16px, 5px); + } + + // Flip the link and confirmation message along the x axis + a, + div { + backface-visibility: hidden; + pointer-events: none; + } + + a { + .keyframes(~"@{animation-name}-front", { + opacity: 1; + transform: rotateX(0); + }, { + opacity: 0; + transform: rotateX(180deg); + }); + animation: ~"@{animation-name}-front" @animation-duration linear; + } + + div { + .keyframes(~"@{animation-name}-back", { + opacity: 0; + transform: rotateX(180deg); + }, { + opacity: 1; + transform: rotateX(0); + }); + animation: ~"@{animation-name}-back" @animation-duration linear; + } +} diff --git a/resources/assets/stylesheets/less/coursewizard.less b/resources/assets/stylesheets/less/coursewizard.less new file mode 100644 index 0000000..a6a00d2 --- /dev/null +++ b/resources/assets/stylesheets/less/coursewizard.less @@ -0,0 +1,84 @@ +option { + &.faculty { + font-weight: bold; + } + &.sub_institute { + padding-left: 15px; + } +} + +div { + &#wizard-participating, &#wizard-lecturers, &#wizard-deputies, &#wizard-tutors { + margin-top: 5px; + margin-left: 25px; + div.description { + font-style: italic; + } + } + &#assigned { + float:left; + padding-right: 10px; + width: ~"calc(50% - 10px)"; + } + &#studyareas { + border-left: 1px solid #666666; + float: left; + padding-left: 10px; + width: ~"calc(50% - 20px)"; + } +} + +/* +change order for AdvancedBasicDataWizardStep +so we do not have to copy the basicdata/index.php +*/ +form.course-wizard-step-0 { + display: flex; + flex-direction: column; + + > * { + order: 1; + } + section:nth-of-type(2) { + order: 2; + } + section:nth-of-type(3) { + order: 3; + } + section:nth-of-type(4) { + order: 5; + } + section:nth-of-type(5) { + order: 9; + } + section:nth-of-type(6) { + order: 10; + } + section:nth-of-type(7) { + order: 11; + } + section:nth-of-type(8) { + order: 12; + } + section:nth-of-type(9) { + order: 13; + } + section:nth-of-type(10) { + order: 11 + } + section:nth-of-type(11) { + order: 4; + } + section:nth-of-type(12) { + order: 6; + } + section:nth-of-type(13) { + order: 7; + } + section:nth-of-type(14) { + order: 8; + } + footer { + order: 100; + } +}
\ No newline at end of file diff --git a/resources/assets/stylesheets/less/cronjobs.less b/resources/assets/stylesheets/less/cronjobs.less new file mode 100644 index 0000000..d81ebd7 --- /dev/null +++ b/resources/assets/stylesheets/less/cronjobs.less @@ -0,0 +1,106 @@ +/* CSS */ +.cron-task { + label { + cursor: pointer; + display: block; + padding: 5px; + } + td { + padding: 0; + vertical-align: middle; + } + tr ~ tr { + display: none; + } + .selected { + td { background-color: fadeout(@active-color, 75%); } + tr ~ tr { + display: table-row; + td { + background-color: #fff; + } + td[colspan] { + background-color: inherit; + padding: 0; + } + } + } + .parameters { + border: 1px solid #888; + border-bottom: 0; + border-top: 0; + padding: 0.5em; + + h3 { + margin: 0; + padding: 0; + } + + input[type=text], input[type=number], select, textarea { + width: 200px; + } + } + .parameter { + &.required { + font-weight: bold; + } + label { + padding: 0; + } + } + tbody:last-child .parameters { + border-bottom: 1px solid #888; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + } +} + +.cronjob-filters { + margin-bottom: 1em; + + select { width: 100%; } + thead th { + text-align: right; + &:first-child { text-align: left; } + } + tfoot td { text-align: center; } +} +.crontab, .crontab li { + list-style: none; + margin: 0; + padding: 0; +} +.crontab li { + display: inline-block; + padding-right: 5px; + text-align: center; +} +.crontab span { + display: block; + text-align: right; +} + +.inactivatible td { + color: #888; +} + +.cronjobs-edit { + h1 { + margin: 0 0 0.5em; + } + + > table > thead > tr > th { .table_header_bold; } + > table { + margin-bottom: 1em; + } + td { vertical-align: top; } +} +.cron-schedule tbody tr td label { + display: inline; + font-weight: normal; + white-space: nowrap; +} + +.cron-item input[type=number] { + width: 2em; +} diff --git a/resources/assets/stylesheets/less/css_tree.less b/resources/assets/stylesheets/less/css_tree.less new file mode 100644 index 0000000..a4551be --- /dev/null +++ b/resources/assets/stylesheets/less/css_tree.less @@ -0,0 +1,115 @@ +@css-tree-delay: 300ms; +@css-tree-distance: 8px; +@css-tree-border: 1px solid @light-gray-color-80; + +.css-tree { + &, ul { + list-style: none; + margin: 0; + padding: 0; + } + ul { + margin-left: @css-tree-distance; + position: relative; + + &:before { + border-left: @css-tree-border; + content: ''; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 0; + } + } + li { + overflow: hidden; + padding-left: (@css-tree-distance + 2px); + position: relative; + + &.css-tree-hidden { + display: none; + } + } + ul li:before { + content: ''; + display: block; + height: 0; + width: @css-tree-distance; + position: absolute; + border-top: @css-tree-border; + left: 0; + top: @css-tree-distance; + } + ul li:last-child:before { + background: #fff; + height: auto; + top: 10px; + bottom: 0; + } +} +.css-tree.collapsable { + input[type=checkbox] { + display: none; + + label { + .icon('before', 'arr_1right', 'clickable'); + cursor: pointer; + + &:before { + transition: transform @css-tree-delay; + vertical-align: baseline; + } + } + ~ ul { + max-height: 0; + opacity: 0; + + transition: all @css-tree-delay; + } + ~ input[type=radio] + label { + margin-left: 0; + } + + &:checked { + + label::before { + transform: rotate(90deg); + } + ~ ul { + max-height: 10000px; + opacity: 1; + } + } + } +} +.css-tree.selectable { + input[type=checkbox] { + + label { + .hide-text(); + } + ~ input[type=radio] + label { + margin-left: 0; + } + } + + input[type=radio] { + display: none; + + + label { + color: @brand-color-dark; + border-radius: 2px; + cursor: pointer; + padding: 0 3px; + margin-left: 1px; + } + + &:checked + label { + font-weight: bold; + background: @content-color-40; + } + + &[disabled] + label { + color: #888; + } + } +} diff --git a/resources/assets/stylesheets/less/dashboard.less b/resources/assets/stylesheets/less/dashboard.less new file mode 100644 index 0000000..e769929 --- /dev/null +++ b/resources/assets/stylesheets/less/dashboard.less @@ -0,0 +1,196 @@ +.dashboard-documents-compact { + list-style: none; + padding-left: 0; + + > li { + padding: .5em 0; + display: flex; + } + + > li:nth-child(n+2) { + border-top: 1px solid #d4dbe5; + } +} + +.document-icon { + padding-right: 0.5em; + align-self: center; +} + +.document-data { + flex: 1; + display: flex; + flex-wrap: wrap; + + span { + border-right: 1px solid #d4dbe5; + margin-right: 0.3em; + padding-right: 0.4em; + } + + span:not(.document-name) { + color: @dark-gray-color-75; + } + + .document-name, span:last-child { + border: none; + margin-right: 0; + padding-right: 0; + } +} + +.document-name { + flex: 1 1 100%; +} + +.document-chdate { + white-space: nowrap; +} + +.document-range { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +// tiny +.document-range, .document-size { + display: none; + + .media-breakpoint-medium-up({ display: inline; }); +} + +// small: nur volle breite +.media-breakpoint-small-up({ +.grid-stack-one-column-mode { + .document-range, .document-size { + display: inline; + } +} +}); + +.files-search-active-filters { + + font-size: 0.8em; + padding-bottom: 1em; + + ul { + .list-inline(); + + img { + margin-left: 0.25em; + vertical-align: text-bottom; + } + + .files-search-active-filter { + background-color: @light-gray-color-20; + padding: 0.25em 0.25em 0.25em 0.6em; + font-size: 0.9em; + margin-bottom: 1px; + } + } + + /* not within a caption */ + form.files-search-search + & { + font-size: calc(1.4em * 0.8); + } +} + +.files-search-results { + + .files-search-actions { + min-width: 3em; + text-align: right; + } + + a.files-search-more { + font-size: 1.1em; + } +} + +#files_dashboard-index, +#files_dashboard-search { + form { + label, .input-group { + margin-top: 0; + } + } +} + +form div.files-search { + &.input-group { + display: flex; + align-items: stretch; + width: 100%; + margin-top: 1ex; + margin-bottom: 15px; + + input[type="text"] { + flex: 1 1 auto; + display: block; + width: 1%; + line-height: 1.5; + padding: .375rem .75rem; + margin: 0; + } + + .input-group-append { + align-items: stretch; + display: flex; + + .button { + margin: 0; + line-height: 1.5; + background-color: @content-color-20; + color: @brand-color-dark; + min-width:auto; + border: 1px solid @light-gray-color-40; + border-left: none; + } + + img { + vertical-align: middle; + } + } + } + + .input-group-append a.button.reset { + .button-with-icon("refresh", "clickable", "clickable"); + .hide-text(); + top: 2px; + } +} + +.media-breakpoint-tiny-down({ + +.files-search-search { + margin-bottom: 0; +} + +.files-search-active-filters { + padding-bottom: 0; +} +}); + + +.files-search-active-filters { + li:first-child { + .hidden-tiny-down(); + } +} + +.files-search-results { + caption span { + .hidden-tiny-down(); + } + + th:nth-child(3), td:nth-child(3) { + .hidden-tiny-down(); + } + + th:nth-child(1), td:nth-child(1), + th:nth-child(5), td:nth-child(5), + th:nth-child(6), td:nth-child(6) { + .hidden-small-down(); + } +} diff --git a/resources/assets/stylesheets/less/deprecated.less b/resources/assets/stylesheets/less/deprecated.less new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/resources/assets/stylesheets/less/deprecated.less diff --git a/resources/assets/stylesheets/less/dialog.less b/resources/assets/stylesheets/less/dialog.less new file mode 100644 index 0000000..be88b26 --- /dev/null +++ b/resources/assets/stylesheets/less/dialog.less @@ -0,0 +1,307 @@ +.ui-widget-overlay { + background: fadeout(mix(@base-gray, #fff, 85%), 21%); + opacity: 1; + position: fixed; +} + +.ui-dialog.ui-widget.ui-widget-content { + border: 0; + padding: 3px; + box-shadow: 0px 0px 8px rgba(0,0,0,0.5); + + .hide-in-dialog { + display: none; + } + + .ui-dialog-titlebar { + background: @brand-color-darker; + border: 0; + color: @contrast-content-white; + font-size:1.3em; + font-weight: normal; + } + + .ui-dialog-titlebar-close { + .square(32px); + background: inherit; + border: 0; + line-height:32px; + margin-top:-16px; + padding: 0; + text-align:center; + + &:hover { + .square(32px); + background: inherit; + border: 0; + margin-top:-16px; + padding: 0; + } + .ui-icon { + .square(16px); + .background-icon('decline', 'info_alt'); + background-position: 0; + display:inline-block; + margin: 0; + + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + + &:hover{ + .background-icon('decline', 'info_alt'); + } + } + } + &.no-close .ui-dialog-titlebar-close { + display: none; + } + + .ui-dialog-buttonpane { + padding: 0.5em 0.4em; + margin: 0 1em; + border-color: @base-color-20; + + .ui-dialog-buttonset { + text-align: center; + float: none; + white-space: nowrap; + } + .ui-button { + .button(); // Include button mixin + font-weight: normal; + + &.accept, + &.cancel { + padding-right: 23px; + } + &:last-child { + margin-right: 0; + } + + &.accept { + .button-with-icon("accept", "clickable", "info_alt"); + } + + &.cancel { + .button-with-icon("decline", "clickable", "info_alt"); + } + + &.download { + .button-with-icon("download2", "clickable", "info_alt"); + } + + &.disabled, + &[disabled] { + background: @light-gray-color-20; + cursor: default; + opacity: 0.65; + + &:hover { + color: @base-color; + } + } + + &::before { + margin-left: -5px; + margin-top: 2px; + } + } + + .ui-button-text-only .ui-button-text { + padding: 0; + white-space: nowrap; + } + } +} +.ui-dialog-titlebar-wiki { + .background-icon('question-circle', 'info_alt', 24); + background-position: center; + background-repeat: no-repeat; + display: inline-block; + position: absolute; + .square(32px); + margin-top: -16px; + top: 50%; + right: 34px; // This is ugly but hard to avoid since the close button's position on a dialog is also hardcoded +} + +// Centered content in dialog +.studip-dialog-centered .ui-dialog-content { + box-sizing: border-box; + display: table !important; // jQuery UI sets these values directly on + width: 100% !important; // the element, thus we need to force it! +} +.studip-dialog-centered-helper { + display: table-cell; + text-align: center; + vertical-align: middle; +} + + +// Confirmation dialog (like createQuestion) +.ui-dialog.ui-widget.ui-widget-content.studip-confirmation { + min-width: 30em; + + .ui-dialog-titlebar { + background-color: @yellow; + color: black; + text-align: left; + } + .ui-dialog-titlebar-close { + background: transparent; + border: 0; + + .ui-icon, .ui-icon:hover { + .background-icon('decline', 'clickable'); + background-position: 0; + } + } + + .ui-dialog-content { + box-sizing: border-box; + .background-icon('question-circle-full', 'status-yellow'); + background-position: 12px 8px; + background-repeat: no-repeat; + background-size: 32px; + padding: 15px 15px 15px 55px; + max-height: 60vh; + } + + .ui-dialog-buttonpane { + text-align: center; + + .ui-dialog-buttonset { + float: none; + > * { + display: inline-block; + } + } + } +} + +.ui-dialog.studip-lightbox { + @arrow-distance: 8px; + @arrow-size: 32px; + @arrow-zoom: 16px; + .wrapper { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + background-repeat: no-repeat; + background-position: center; + background-size: contain; + + .next, + .previous { + transition: opacity 300ms; + background-repeat: no-repeat; + display: block; + opacity: 0.1; + + position: absolute; + top: 0; + bottom: 0; + + outline: none; + + &:hover { + opacity: 1; + } + + } + .previous { + left: 0; + right: 50%; + + .icon('before', 'arr_1left', 'clickable', @arrow-size); + &::before { + position: absolute; + left: @arrow-distance; + top: 50%; + transform: translate(0, -50%); + z-index: 2; + } + + &::after { + .square((@arrow-size + @arrow-zoom)); + + position: absolute; + left: 0; + top: 50%; + transform: translate(0, -50%); + + background-color: @white; + content: ''; + display: block; + + z-index: 1; + } + } + .next { + right: 0; + left: 50%; + + .icon('before', 'arr_1right', 'clickable', @arrow-size); + &::before { + position: absolute; + right: @arrow-distance; + top: 50%; + transform: translate(0, -50%); + z-index: 2; + } + + &::after { + .square((@arrow-size + @arrow-zoom)); + + position: absolute; + right: 0; + top: 50%; + transform: translate(0, -50%); + + background-color: @white; + content: ''; + display: block; + + z-index: 1; + } + } + + &.first .previous, + &.last .next { + display: none; + } + } +} + +.ui-dialog.studip-dialog.ui-widget.ui-widget-content { + .ui-dialog-buttonpane .ui-dialog-buttonset { + white-space:normal; + + html.responsive-display & { + @gap: 10px; + + display: flex; + flex-wrap: wrap; + justify-content: space-space-between; + margin: -@gap 0 0 -@gap; + + .ui-button { + flex: 1; + margin: @gap 0 0 @gap; + } + } + } +} + + +h2.dialog-subtitle { + font-weight: normal; + font-size: 1.4em; + border-bottom: none; + margin-top: 0.25em; + margin-bottom: 0.25em; +} diff --git a/resources/assets/stylesheets/less/documents.less b/resources/assets/stylesheets/less/documents.less new file mode 100644 index 0000000..c2b0a49 --- /dev/null +++ b/resources/assets/stylesheets/less/documents.less @@ -0,0 +1,110 @@ +.documents { + .chdir-up a { + display: block; + } + .options { + text-align: right; + } + .bread-crumbs { + display: inline-block; + min-height: 1.5em; + width: 40px; + z-index: 1; + + > a, ul { + background-color: #fff; + padding: 5px; + } + > a { + padding-bottom: 2px; + } + + &.extendable:hover { + > a, > ul { + box-shadow: 0 4px 3px #aaa; + } + ul { + display: flex; + flex-direction: column-reverse; + } + } + + ul { + display: none; + list-style: none; + margin: 0; + position: absolute; + } + li { + font-size: 0.85em; + line-height: 1.5em; + + a { + .background-icon('folder-parent', 'clickable', 24); + background-position: left center; + background-repeat: no-repeat; + padding-left: 30px; + } + &:first-child a { + .background-icon('folder-empty', 'clickable', 24); + } + } + } +} + +.document-dialog { + @info-width: 150px; + .clearfix; + > aside { + float: left; + width: @info-width; + } + .document-dialog-icon { + text-align: center; + } + > div { + border-left: 1px dashed #888; + margin-left: @info-width; + min-height: 100%; + max-height: 100%; + overflow-y: auto; + } + dl { + dt:after { + content: ':'; + } + dd { + margin: 0 0 0.5em 0.5em; + padding: 0; + &:last-child { + margin-bottom: 0; + } + } + } +} + +.documents.dragging { + [data-file]:not([data-folder]) { + background-color: @light-gray-color-40; + opacity: 0.6; + } +} +.documents { + [data-folder].dropping { + background-color: @red-40; + } +} + +.document-draggable-helper { + background-color: @activity-color-40 !important; + opacity: 1 !important; + td { + border-bottom: 0 !important; + } +} + +fieldset.document-admin-search label { + box-sizing: border-box; + display: inline-block; + width: 49%; +} diff --git a/resources/assets/stylesheets/less/enrolment.less b/resources/assets/stylesheets/less/enrolment.less new file mode 100644 index 0000000..9aae6d2 --- /dev/null +++ b/resources/assets/stylesheets/less/enrolment.less @@ -0,0 +1,115 @@ +#enrollment { + ul { + border-top: 1px solid @base-color; + list-style: none inside; + margin: 0px; + overflow-x: auto; + padding: 0px; + + .media-breakpoint-medium-up({ + max-height: 200px; + }); + + li { + border-bottom: 1px solid @base-color; + padding: 5px; + + &.ui-draggable.ui-draggable-handle { + cursor: move; + } + + .actions { + cursor: pointer; + float: right; + white-space: nowrap; + } + + &::after { + content: ''; + display: block; + clear: both; + } + } + + &.ui-sortable li.empty { + cursor: no-drop; + } + } + + li.empty:not(:only-child) { + display: none; + } + + #available-courses li.visible, + #selected-courses li { + &:hover { + background-color: @base-color-20; + } + } + + #available-courses li.ui-draggable.ui-draggable-dragging, + #selected-courses li.ui-sortable-helper { + background-color: @base-color-20; + border: 1px solid @base-color; + list-style: none inside; + padding: 5px; + width: auto; + } + + #available-courses li:not(.visible) { + display: none; + } + + #selected-courses li { + list-style-type: decimal; + + &.ui-sortable-placeholder, + &.empty { + list-style-type: none; + } + + &.ui-sortable-placeholder { + background-color: @yellow-20; + } + } + + .ui-sortable-helper .delete { + display:none; + } + + input[name="filter"] { + margin-bottom: 20px; + } + + .ui-state-highlight { + background: @red; + border: 0; + height: 30px; + padding: 10px; + } + + // Show available and selected courses next to each others + .priority-lists { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; + + .available, + .selected { + flex: 1; + } + .available { + margin-right: 0.5em; + } + .selected { + margin-left: 0.5em; + } + // + // .available #available-courses, + // .selected #selected-courses { + // position: sticky; + // top: 0; + // } + } +} diff --git a/resources/assets/stylesheets/less/evaluation.less b/resources/assets/stylesheets/less/evaluation.less new file mode 100644 index 0000000..92d595d --- /dev/null +++ b/resources/assets/stylesheets/less/evaluation.less @@ -0,0 +1,40 @@ +/* classes for the evaluation modules in Stud.IP ---------------------------- */ +.eval_title { + font-size: 1.2em; + font-weight: bold; + color: #24437c; +} + +.eval_error { + color: #E00000; +} + +.eval_success { + color: #008000; +} + +.eval_info { + color: #333333; +} + +.eval_metainfo { + font-size: 0.8em; +} + +.eval_highlight { + background-color: #b8c3e8; +} + +.eval_gray { + background: #d5d5dd none; +} +.evaluation_item { + box-sizing: border-box; + margin: 3px; +} + +h3.eval { + font-size: 1.3em; + color: #000000; + font-weight: bold; +} diff --git a/resources/assets/stylesheets/less/feedback.less b/resources/assets/stylesheets/less/feedback.less new file mode 100644 index 0000000..f8c297d --- /dev/null +++ b/resources/assets/stylesheets/less/feedback.less @@ -0,0 +1,105 @@ +article.studip.feedback-container { + header { + h1 { + a { + word-break: break-all; + } + } + } +} +article.studip.feedback-stream { + h1 { + span { + padding: 0; + margin-right: 8px; + font-weight: normal; + white-space: nowrap; + } + > img:not(:first-child), > .feedback-star-rating{ + margin-left: 8px; + } + } + h2 { + border-bottom: none; + } +} +.feedback-entry-add { + .rating { + label.checked img, label.hover img { + opacity: 1; + } + label img, label.out img { + opacity: .2; + } + label { + font-size: 0; + cursor: pointer; + } + input { + display: none; + } + } +} +.feedback-entries { + .feedback-entry { + margin-top: 10px; + padding: 5px; + background-color: @content-color-10; + border: 1px solid @content-color-40; + border-width: 1px 0; + + header { + background: transparent; + padding: 0; + margin: 0 !important; + h1 { + border: 0; + padding-left: 0; + > span { + font-weight: bold; + } + .avatar-small, span { + margin-right: 5px; + } + } + } + .rating { + white-space: nowrap; + font-size: 0; + .inactive { + opacity: .2; + } + } + .date { + color: #999; + text-align: right; + font-size: 12px; + } + } +} +.ui-dialog-content { + .feedback-elements { + margin-top: 10px; + } + article.feedback-stream { + header { + background: transparent; + margin: -10px; + } + } +} +table.feedback { + img { + vertical-align: middle; + } + > tfoot > tr > td { + padding: 5px; + } +} +.percentage-bar { + margin-left: -5px; + padding: 0 5px; + color: @content-color-10; + background-color: @base-color; + min-width: 20px; +} diff --git a/resources/assets/stylesheets/less/files.less b/resources/assets/stylesheets/less/files.less new file mode 100644 index 0000000..6f1e3af --- /dev/null +++ b/resources/assets/stylesheets/less/files.less @@ -0,0 +1,475 @@ +.file_uploader { + display: none; +} +.file_upload_window { + .filenames li { + display: flex; + justify-content: space-between; + + span { + flex: 1; + &.upload-progress { + flex: 0; + } + } + + &:only-child .upload-progress { + display: none; + } + } +} +.uploadbar { + position: relative; + + img { + margin: 10px; + z-index: 1; + } + + &.uploadbar-outer { + border: @base-color solid 1px; + } + &.uploadbar-inner { + position: absolute; + top: 0; + right: 100%; + bottom: 0; + left: 0; + background-color: @base-color; + overflow: hidden; + white-space: nowrap; + + display: flex; + justify-content: space-between; + + transition: right 200ms; + + img { + background-color: @base-color; + flex: 0; + outline: 10px solid @base-color; + } + .ufo { + animation: ufoflight 1.5s linear infinite; + z-index: 0; + } + } + + .upload-progress { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + mix-blend-mode: luminosity; + + color: (#fff - @base-color); + font-size: large; + //text-shadow: 0 1px 0 @base-color, + // 1px 1px 0 @base-color, + // 1px 0 0 @base-color, + // 1px -1px 0 @base-color, + // 0 -1px 0 @base-color, + // -1px 1px 0 @base-color, + // 0 1px 0 @base-color, + // -1px -1px 0 @base-color; + } +} + +@keyframes ufoflight { + 25% { + transform: translateX(-2px) translateY(4px); + } + 50% { + transform: translateX(-0px) translateY(8px); + } + 75% { + transform: translateX(-2px) translateY(4px); + } + 100% { + transform: translateX(0px) translateY(0px); + } +} + +.subfolders .empty { + display: none; + &:only-child { + display: table-row; + } +} + +/* for file/edit view and file/new_edit_folder_form view: */ +div.file_select_possibilities, .folder_type_select_possibilities { + @width: 100px; + @height: 100px; + + display: flex; + flex-direction: column; + > div { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-items: stretch; + > a, > button, > div.clickable { + cursor: pointer; + background-color: transparent; + margin: 10px; + border: thin solid @content-color-20; + padding: 10px; + width: @width; + min-width: @width; + max-width: @width; + height: @height; + min-height: @height; + max-height: @height; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + text-align: center; + > img { + margin-left: auto; + margin-right: auto; + } + } + + > .important-item { + min-width: calc(100% - 1.5em); + background-color: #E7EBF1; + border-color: @base-color-60; + display: flex; + flex-direction: row; + height: initial; + max-height: initial; + + > .icon { + width: 10em; + } + + > .description { + width: 100%; + text-align: left; + margin-left: 1em; + color: @black; + } + } + } + + > label.content_terms_of_use_entry:not(.undecorated) + { + width: 40px; + min-width: 40px; + max-width: 40px; + height: 40px; + min-height: 40px; + max-height: 40px; + + img { + width:100%; + height:100%; + display:block; + } + } + + > button { + box-sizing: content-box; + cursor: pointer; + color: yellow; + } + + + > label:not(.undecorated) { + display: flex; + justify-content: center; + font-size:0.7em; + cursor: pointer; + + img { + width:40%; + height:40%; + display:block; + } + } + + + /* for file/edit view only: */ + + input[name=content_terms_of_use_id] { + display: none; + } + + input[name=content_terms_of_use_id]:checked + label { + background-color: @brand-color-darker; + color: @contrast-content-white; + + img { + filter: invert(100%) brightness(200%); + } + } + + + /* for file/new_edit_folder_form view only: */ + + input[name=folder_type] { + display: none; + } + + input[name=folder_type]:checked + label { + background-color: @brand-color-darker; + color: @contrast-content-white; + + img { + filter: invert(100%) brightness(200%); + } + } + +} + +div.file_select_possibilities.content_terms_of_use_icons { + justify-content: left; +} + + +#file_edit_window, #file_details_window { + display: flex; + justify-content: space-between; + align-items: flex-start; + align-content: flex-start; +} + + +table.documents tfoot td.sticky { + position: sticky; + bottom: 0; +} + +table.documents { + tfoot .footer-items { + display: flex; + flex-direction: row; + & > .bulk-buttons { + flex-grow: 1; + } + } +} + + +/* for file/edit and folder/edit only: */ +@media screen and (max-width: 800px) { + /* mobile view: */ + #file_aside, #folder_aside { + display: block; + + div.file-icon, div.folder-icon { + img { + width: 30%; + height: 100%; + max-height: 10em; + margin-right: 1em; + } + } + + h3 { + font-size: 140%; + padding-top: 1em; + } + + dl { + display: table; + } + } + + #file_management_forms { + display: table; + width: 100%; + } + + .file_preview { + max-width: 100%; + } + + #file_edit_window, #file_details_window { + flex-direction: column; + } + + #file_aside, #folder_aside { + width: 100%; + max-width: none; + } + + #file_management_forms { + width: 100%; + max-width: none; + } + + div#preview_container { + .file_preview { + max-width: 100%; + } + + iframe.file_preview { + width: 100%; + height: 20em; + } + } +} + +@media screen and (min-width: 801px) { + /* desktop view: */ + + #file_aside, #folder_aside { + width: calc(30% - 10px); + max-width: calc(30% - 10px); +/* overflow: hidden; */ + + div.file-icon, div.folder-icon { + img { + margin-left: 20%; + width: 60%; + max-height: 16em; + height: 100%; + } + } + + h3 { + font-size: 1.1em; + } + } + + #file_management_forms, div#preview_container { + width: calc(70% - 10px); + max-width: calc(70% - 10px); + + .file_preview { + max-width: 100%; + } + + iframe.file_preview { + width: 100%; + height: 40em; + } + } + +} + +form.default fieldset.select_terms_of_use { + > legend { + margin: 0px; + width: 100%; + } + border: none; + padding: 0px; + margin-left: 0px; + margin-right: 0px; + + > input[type=radio] { + display: none; + } + > label { + cursor: pointer; + border: 1px solid @content-color-40; + transition: background-color 200ms; + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px; + padding-bottom: 2px; + margin-bottom: 0; + border-top: none; + > .text { + width: 100%; + margin-left: 10px; + } + > .arrow { + margin-right: 5px; + } + > .check { + display: none; + } + } + > label:first-of-type { + border-top: 1px solid @content-color-40; + } + > div { + border: 1px solid @content-color-40; + border-top: none; + display: none; + padding: 10px; + + } + > input[type=radio]:checked + label { + background-color: @content-color-20; + transition: background-color 200ms; + > .arrow { + display: none; + } + > .check { + display: inline-block; + } + } + > input[type=radio]:checked + label + div { + display: block; + .description { + animation-duration: 400ms; + animation-name: terms_of_use_fadein; + } + } +} + +@keyframes terms_of_use_fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +table.documents { + td, th { + vertical-align: top; + + &:first-child { + padding-left: 8px; + } + } + + &.flat td.filter-match { + background-color: @yellow-20; + } + + tr:target { + background-color: @activity-color-20; + } + + tbody.subfolders tr, tbody.files tr { + height: 43px; + } +} + + +/* +For the file and folder table in the selection dialogs +when adding a file or copying/moving things: +*/ +td.document-icon { + max-width: 40px; + width: 40px; +} + + +/* Rules for the library search dialog: */ + +h2.search-result-info { + font-weight: normal; + font-size: 1.4em; + color: @base-gray; + border-bottom: none; + margin-top: 0; +} diff --git a/resources/assets/stylesheets/less/font-face-lato.less b/resources/assets/stylesheets/less/font-face-lato.less new file mode 100644 index 0000000..fc4bb87 --- /dev/null +++ b/resources/assets/stylesheets/less/font-face-lato.less @@ -0,0 +1,41 @@ +@font-face { + font-family: 'Lato'; + src: url('../fonts/LatoLatin/LatoLatin-Light.eot'); /* IE9 Compat Modes */ + src: url('../fonts/LatoLatin/LatoLatin-Light.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('../fonts/LatoLatin/LatoLatin-Light.woff2') format('woff2'), /* Modern Browsers */ + url('../fonts/LatoLatin/LatoLatin-Light.woff') format('woff'), /* Modern Browsers */ + url('../fonts/LatoLatin/LatoLatin-Light.ttf') format('truetype'); + font-display: auto; + font-style: normal; + font-weight: 300; + text-rendering: optimizeLegibility; + unicode-range: U+000-5FF; /* Latin glyphs */ +} + +@font-face { + font-family: 'Lato'; + src: url('../fonts/LatoLatin/LatoLatin-Regular.eot'); /* IE9 Compat Modes */ + src: url('../fonts/LatoLatin/LatoLatin-Regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('../fonts/LatoLatin/LatoLatin-Regular.woff2') format('woff2'), /* Modern Browsers */ + url('../fonts/LatoLatin/LatoLatin-Regular.woff') format('woff'), /* Modern Browsers */ + url('../fonts/LatoLatin/LatoLatin-Regular.ttf') format('truetype'); + font-display: auto; + font-style: normal; + font-weight: 400; + text-rendering: optimizeLegibility; + unicode-range: U+000-5FF; /* Latin glyphs */ +} + +@font-face { + font-family: 'Lato'; + src: url('../fonts/LatoLatin/LatoLatin-Bold.eot'); /* IE9 Compat Modes */ + src: url('../fonts/LatoLatin/LatoLatin-Bold.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('../fonts/LatoLatin/LatoLatin-Bold.woff2') format('woff2'), /* Modern Browsers */ + url('../fonts/LatoLatin/LatoLatin-Bold.woff') format('woff'), /* Modern Browsers */ + url('../fonts/LatoLatin/LatoLatin-Bold.ttf') format('truetype'); + font-display: auto; + font-style: normal; + font-weight: 700; + text-rendering: optimizeLegibility; + unicode-range: U+000-5FF; /* Latin glyphs */ +} diff --git a/resources/assets/stylesheets/less/forms.less b/resources/assets/stylesheets/less/forms.less new file mode 100644 index 0000000..f3fa397 --- /dev/null +++ b/resources/assets/stylesheets/less/forms.less @@ -0,0 +1,553 @@ +form.default { + @gap: 1.5ex; + + @max-width-s: 8em; + @max-width-m: 48em; + @max-width-l: 100%; + + div.select2-wrapper { + display: block ! important; + } + + section { + &:not(.contentbox) { + padding-top: @gap; + + label:first-of-type { + margin-top: 0px; + } + } + } + + fieldset > section:last-child { + margin-bottom: @gap; + } + + ol.default { + padding-left: 20px; + + li { + padding: 2px 0px; + } + } + + span.empty { + color: @light-gray-color-40; + font-style: italic; + } + + input[type=date], input[type=email], input[type=number], + input[type=password], input[type=text], input[type=time], input[type=url], input[type=tel], + textarea, select { + box-sizing: border-box; + + border: 1px solid @light-gray-color-40; + color: @dark-gray-color; + max-width: @max-width-m; + padding: 5px; + vertical-align: middle; + width: 100%; + transition: all 0.3s ease-out; + + &:focus { + border-color: @brand-color-dark; + } + + &.size-s { + max-width: @max-width-s; + } + + &.size-m { + } + + &.size-l { + max-width: @max-width-l; + } + + &[readonly] { + background-color: @light-gray-color-20; + } + } + + .quicksearch_container { + max-width: @max-width-m; + } + + input[type=date], input[type=number], input[type=time], input[type=tel]:not(.size-m) { + max-width: @max-width-s; + } + + textarea { + min-height: 6em; + } + + label:not(.undecorated) { + display: block; + margin-bottom: @gap; + max-width: 100%; + text-indent: 0.25ex; + vertical-align: top; + + input[type=date], input[type=email], input[type=number], + input[type=password], input[type=text], input[type=time], input[type=tel], input[type=url], + textarea, select, .ckplaceholder { + display: block; + margin-top: 0.5ex; + } + } + + .label-text { + display: inline-block; + text-indent: 0.25ex; + } + + /* we have to use specific css selectors, otherwise the settings are + overwritten by other rules */ + label.col-1, label.col-2, label.col-3, label.col-4, label.col-5, + div.col-1, div.col-2, div.col-3, div.col-4, div.col-5, + section.col-1, section.col-2, section.col-3, section.col-4, section.col-5 { + display: inline-block; + padding-right: 1em; + vertical-align: top; + word-break: break-all; + } + + label, + div, + section { + &.col-1 { + width: 14%; + } + &.col-2 { + width: 29%; + } + &.col-3 { + width: 45%; + } + &.col-4 { + width: 60%; + } + &.col-5 { + width: 75%; + } + } + + div.col-1, + div.col-2, + div.col-3, + div.col-4, + div.col-5 { + margin-top: 2ex; + } + + fieldset { + box-sizing: border-box; + border: solid 1px @content-color-40; + margin: 0 0 10px; + padding: 10px; + padding-top: @gap; + + > legend { + box-sizing: border-box; + background-color: @fieldset-header; + border: 1px solid @content-color-40; + border-bottom: 0; + color: @brand-color-dark; + font-size: 12pt; + font-weight: bold; + line-height: 2em; + margin: 0 -11px; + padding: 0; + text-indent: 10px; + width: calc(100% + 22px); + } + + // Insert invisible element that corrects double padding/margin at the + // bottom + &:not(.collapsed) > label:last-child::after { + content: ''; + display: block; + margin-top: -@gap; + } + } + + .selectbox { + padding:5px; + max-height:200px; + overflow:auto; + + > fieldset { + border:none; + margin:0px; + padding:0px; + } + } + + .required { + font-weight: bold; + &::after { + content: "*"; + color: red; + } + } + + input[type=checkbox], input[type=radio] { + vertical-align: text-bottom; + } + + select[disabled] { + background-color: @dark-gray-color-15; + } + + .tooltip.tooltip-icon::before { + vertical-align: text-bottom; + } + + footer { + background-color: @content-color-20; + border-top: 1px solid @brand-color-darker; + clear: both; + margin-left: 0; + padding: 10px; + padding-top: 5px; + padding-bottom: 5px; + + .button { + margin-bottom: 0; + margin-top: 0; + } + } + + //Special inputs + + label.file-upload { + .background-icon('upload', 'clickable'); + + background-repeat: no-repeat; + background-position: top left; + background-size: 20px 20px; + cursor: pointer; + padding-left: 30px; + color: @base-color; + + input[type=file] { + display: none; + } + .filename { + padding-left: 0.5em; + color: @light-gray-color-80; + } + } + + label.with-action { + span:first-of-type { + display: block; + } + + > input[type=image], > img { + vertical-align: text-bottom; + margin-left: 5px; + } + + input[type=date], input[type=email], input[type=number], + input[type=password], input[type=text], input[type=time], input[type=url], input[type=tel], + textarea, select { + max-width: calc(@max-width-m - 2em); + width: calc(100% - 2em); + display: inline-block; + + transition: all 0.3s ease-out; + + &:focus { + border-color: @brand-color-dark; + } + + &.size-s { + max-width: calc(@max-width-s - 2em); + } + + &.size-m { + } + + &.size-l { + max-width: calc(@max-width-l - 2em); + } + } + } + + // Group elements in a row + .hgroup, + .hgroup-btn { + display: flex; + align-items: center; + flex-wrap: wrap; + max-width: @max-width-m; + + &.size-s { + max-width: @max-width-s; + } + &.size-l { + max-width: @max-width-l; + } + + > * { + box-sizing: border-box; + flex: 1 0 auto; + + &:not(:first-child) { + margin-left: 3px; + } + &:not(:last-child) { + margin-right: 3px; + } + } + + label { + margin-top: 0; + } + + &, label:not(.undecorated) { + input[type=date], input[type=email], input[type=number], + input[type=password], input[type=text], input[type=time], input[type=tel], input[type=url], + textarea, select { + display: inline-block; + margin-top: 0; + width: auto; + } + } + + .button { + margin-bottom: 0; + margin-top: 0; + } + } + + .hgroup-btn { + align-items: baseline; + .form-control { + flex: 1; + } + .button { + flex: 0; + } + } + + // Collapsable fieldsets + .js &.collapsable fieldset, fieldset.collapsable { + legend { + box-sizing: border-box; + .background-icon('arr_1down', 'clickable', 20); + background-position: 6px center; + background-repeat: no-repeat; + cursor: pointer; + padding-left: 20px; + } + + &.collapsed { + legend { + .background-icon('arr_1right', 'clickable', 20); + margin-bottom: 0; + } + padding-bottom: 0; + padding-top: 0; + *:not(legend) { + display: none; + } + } + } + + // Length hint display for input[maxlength] + .length-hint-wrapper { + position: relative; + white-space: nowrap; + } + .length-hint { + position: absolute; + bottom: 100%; + right: 0; + + color: @light-gray-color; + font-size: 0.8em; + } + + // Display small forms as inline + &.inline { + label { + display: inline; + max-width: inherit; + vertical-align: middle; + width: auto; + } + input, textarea, select, button { + display: inline-block;; + } + } + + label.packed { + display: flex; + + > * { + flex: 1; + max-width: none; + } + button { + flex: 0 0 auto; + margin: 0; + } + } + + .invalid { + border: 2px dotted red ! important; + } // an invalid form entry + + .invalid_message { + display: none; + font-weight: bold; + color: red; + } + + .select2-container { + margin-top: 0.5ex; + } + + //hidden radio buttons with icon: + + input[type="radio"].hidden-checkbox, input[type="checkbox"].hidden-checkbox { + display:none; + + & + label { + cursor: pointer; + + & .hidden-content { + cursor: initial; + } + + & .hidden-checkbox-checked-icon { + display: inline; + visibility: hidden; + } + + & .hidden-content { + display: none; + } + } + + &:checked + label { + & .hidden-checkbox-checked-icon { + visibility: visible; + } + + & .hidden-content { + display: block; + } + } + } + + &.show_validation_hints { + :invalid, .invalid { + .icon('before', 'exclaim-circle', 'attention', 16, 5px); + display: inline-block; + } + textarea:invalid, input[type=text]:invalid { + border-left: 4px solid @red; + } + } +} + +form.narrow { + label.col-1, label.col-2, label.col-3, label.col-4, label.col-5, + div.col-1, div.col-2, div.col-3, div.col-4, div.col-5, + section.col-1, section.col-2, section.col-3, section.col-4, section.col-5 + { + padding-right: 0px; + } +} + +.studip-checkbox { + display: none; + + label { + .icon('before', 'checkbox-unchecked', 'clickable', 16, 5px); + cursor: pointer; + } + &:checked + label { + .icon('before', 'checkbox-checked', 'clickable', 16, 5px); + } + &:indeterminate + label { + .icon('before', 'checkbox-indeterminate', 'clickable', 16, 5px); + } + + &[disabled] { + + label { + .icon('before', 'checkbox-unchecked', 'inactive', 16, 5px); + } + + &:checked + label { + .icon('before', 'checkbox-checked', 'inactive', 16, 5px); + } + } +} + +// give forms some optimized styling for very narrow screen sizes +.media-breakpoint-tiny-down({ + form.default { + label.col-1, div.col-1, section.col-1, + label.col-2, div.col-2, section.col-2, + label.col-3, div.col-3, section.col-3, + label.col-4, div.col-4, section.col-4, + label.col-5, div.col-5, section.col-5 { + min-width: 100%; + } + } +}); + +table.hide_datafield_title { + .datafield_title { + display: none; + } +} + +.content-title { + background-color: transparent; + padding-top: 0px; + color: @base-gray; + font-size: 1.4em; + text-align: left; +} + +@media (max-width: 580px) { + .ms-selectable, + .ms-selection { + width: 100% ! important; + } +} + +// Adjustments for dialog +.ui-dialog { + form.default > fieldset:first-of-type:last-of-type { + border: 0; + padding: 0; + + legend { + display: none; + } + } +} + +form.inline { + display: inline; + input.icon-role-clickable { + cursor: pointer; + } +} + +@media (min-width: 800px) { + form.default .form-columns { + display: flex; + flex-direction: row; + + .column { + flex-grow: 1; + margin-right: 1em; + } + } +} diff --git a/resources/assets/stylesheets/less/fullcalendar-print.less b/resources/assets/stylesheets/less/fullcalendar-print.less new file mode 100644 index 0000000..f6e690b --- /dev/null +++ b/resources/assets/stylesheets/less/fullcalendar-print.less @@ -0,0 +1,116 @@ + +a.fc-event { + border-radius: 0; + color: black; + + .fc-time { + text-decoration: underline; + color: black; + } +} + +div.fc-toolbar { + display: none; +} + +/* the "now" indicator: */ +.fc-now-indicator.fc-now-indicator-line, +.fc-now-indicator.fc-now-indicator-arrow { + display: none; +} + +/* adjust height: */ +.fc-resourceTimelineDay-view { + .fc-widget-header{ + .fc-scroller { + height: 40px !important; + .fc-content > table{ + height: 26px !important; + } + } + } +} +.fc-resourceTimelineWeek-view { + .fc-widget-header{ + .fc-scroller { + height: 60px !important; + .fc-content > table{ + height: 26px !important; + } + } + } +} +.fc-scroller.fc-time-grid-container { + height: auto !important; + min-height: 0 !important; +} + +/* disable page break for calendar: */ +div.fc-view-container { + page-break-inside: avoid; +} + +/* a potential existing map key: */ +.map-key-list { + display: none; +} + +.studip-fullcalendar-header { + &.fullcalendar-dialog{ + display: block; + width: 100%; + margin-top: 2em; + margin-right: 0em; + } +} + +.fullcalendar-dialogwidget-container { + display:none !important; +} + +div.fc-timeGrid-view th.fc-axis, td.fc-axis { + display: table-cell !important; +} + +div.fc-slats, div.fc-time-grid hr { + display: block !important; +} + +section.fc th, section.fc td, section.fc hr, section.fc thead, section.fc tbody, .fc-row { + background: none !important; +} + +div.fc-content-skeleton { + & table { + height: 100% !important; + } + height: 100%; +} + +div.fc-time-grid a.fc-event { + position: absolute !important; + margin: 0px 0px 1px 0px !important; + + img { + filter: brightness(0); + } +} + +a.fc-timeline-event { + img { + filter: brightness(0); + } +} + +.fc-slats table tr { + line-height: 1.7em; + line-height: 22px; +} + +.fc-resource-area { + .fc-cell-content { + a > img { + display: none; + } + } +} diff --git a/resources/assets/stylesheets/less/fullcalendar.less b/resources/assets/stylesheets/less/fullcalendar.less new file mode 100644 index 0000000..1a94655 --- /dev/null +++ b/resources/assets/stylesheets/less/fullcalendar.less @@ -0,0 +1,101 @@ +a.fc-event { + border-radius: 0; + + .fc-time { + background-color: rgba(255, 255, 255, 0.2); + font-weight: bold; + } +} + +.fc button.fc-button { + .button; + border-radius: 0; +} + +.fc-button-primary:not(:disabled):active, +.fc-button-primary:not(:disabled).fc-button-active, +.fc button.fc-button.fc-state-active { + -webkit-box-shadow: none; + box-shadow: none; + + background-color: @base-color !important; + color: white; +} + +/* adjust height: */ +/* .fc-scroller.fc-time-grid-container { + height: auto !important; + min-height: 0 !important; +}*/ + +.studip-fullcalendar-header { + &.fullcalendar-dialog{ + width: calc(100% - 550px); + vertical-align: middle; + display: inline-block; + margin-right: 275px; + } +} + +.fullcalendar-dialogwidget-container { + border-left: 0; + display: inline-block; + flex: 0 0 auto; + margin-bottom: 1em; + position: relative; + + @width: 270px; + + padding-bottom: 7px; + width: @width; + z-index: 2; + + + .fullcalendar-dialogwidget-widget { + background: #fff; + border: 1px solid @content-color-40; + margin: 15px 0px 0; + } + + .fullcalendar-dialogwidget-widget-header { + .clearfix(); + background: @content-color-20; + color: @base-color; + font-weight: bold; + padding: 4px; + } + + select.fullcalendar-dialogwidget-selectlist { + overflow-y: auto; + width: 100%; + } + + .fullcalendar-dialogwidget-widget-content { + border-top: 1px solid @content-color-40; + padding: 4px; + transition: all 0.5s; + } +} + +.fc { + .fc-toolbar.fc-header-toolbar { + margin-bottom: 0.5em; + } + + .fc-button-group { + height: 30px; + + .fc-button { + margin-top: 0; + margin-bottom: 0; + } + } +} + +.studip-fullcalendar-header { + /* This shall look like a table caption. */ + background-color: transparent; + color: @base-gray; + font-size: 1.4em; + text-align: left; +} diff --git a/resources/assets/stylesheets/less/globalsearch.less b/resources/assets/stylesheets/less/globalsearch.less new file mode 100644 index 0000000..79ef97a --- /dev/null +++ b/resources/assets/stylesheets/less/globalsearch.less @@ -0,0 +1,333 @@ +#quicksearch_item { + align-self: flex-start; +} +#globalsearch-searchbar { + @width: 423px; + @hidden-width: 215px; + @transition-duration: 300ms; + + position: relative; + top: 4px; + white-space: nowrap; + + // Reset alignments among browsers + > * { + box-sizing: border-box; + } + + // Defines the clear icon for the input + #globalsearch-clear { + .square(16px); + margin-left: -22px; + vertical-align: middle; + + opacity: 0; + transition: opacity @transition-duration; + } + &.has-value #globalsearch-clear { + opacity: 1; + } + + // The actual search input + #globalsearch-input { + height: 29px; + padding-left: 5px; + width: @hidden-width; + transition: width @transition-duration; + } + &.is-visible #globalsearch-input { + width: @width; + } + + // Search icon + #globalsearch-icon { + margin-left: 5px; + position: relative; + top: 3px; + } + + // Hint toggle text + #globalsearch-togglehints { + font-size: @font-size-small; + margin: 0; + + .icon('before', 'arr_1right', 'clickable', @font-size-small, 2px); + + // This is only neccessary to remove the whitespace in front of the text + // Otherwise, the text would jump when getting replaced + display: flex; + align-items: center; + + + #globalsearch-hints { + display: none; + } + + &.open { + &::before { + transform: rotate(90deg); + } + + + #globalsearch-hints { + display: block; + white-space: normal !important; + } + } + } + + // List display + #globalsearch-list { + background-color: @white; + box-shadow: 1px 1px 1px @light-gray-color-80; + color: @text-color; + display: none; + max-height: 90vh; + overflow: auto; + padding: 5px; + position: absolute; + width: @width; + + a { + color: @base-color; + + &:hover { + color: @active-color; + } + } + + section { + color: @text-color; + + header { + color: @base-color; + margin: 5px; + margin-bottom: 0; + } + + p { + font-size: 12px; + margin-left: 15px; + margin-right: 10px; + } + } + } + &.is-visible #globalsearch-list { + display: block; + } + + // "Searching..." info + #globalsearch-searching { + @icon-size: 32px; + + color: @dark-gray-color-45; + display: none; + text-align: center; + + background-image: url("@{image-path}/ajax-indicator-black.svg"); + background-position: center bottom; + background-repeat: no-repeat; + background-size: @icon-size; + margin-bottom: 10px; + padding-bottom: (@icon-size + 5px); + } + &.is-searching { + #globalsearch-searching { + display: block; + } + #globalsearch-results { + display: none; + } + } + + #globalsearch-results { + &:empty { + display: none; + } + + article { + border: 1px solid @content-color-40; + margin: 3px; + margin-bottom: 8px; + margin-top: 8px; + + > header { + background-color: @content-color-20; + color: @base-color; + + display: flex; + flex-direction: row; + flex-wrap: nowrap; + + font-weight: bold; + padding: 3px; + + div.globalsearch-category { + flex: auto; + } + + div.globalsearch-more-results { + font-size: @font-size-small; + font-weight: normal; + line-height: @font-size-h3; + margin-bottom: auto; + margin-top: auto; + text-align: right; + width: 100px; + } + } + + section { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + + padding: 6px 6px 6px 0; + + border-top: 1px solid @content-color-40; + transition: background-color @transition-duration; + + &:hover { + background-color: fadeout(@light-gray-color, 80%); + } + + &.globalsearch-extended-result { + display: none; + } + + & > a { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + margin: 0; + width: 100%; + } + + .globalsearch-result-img { + flex: 0; + margin-left: 6px; + margin-right: 6px; + + img { + .square(36px); + } + } + + .globalsearch-result-data { + flex: 1; + overflow: hidden; + margin-right: 6px; + white-space: nowrap; + + .globalsearch-result-title { + font-size: @font-size-base; + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + } + + .globalsearch-result-details { + color: @dark-gray-color-80; + font-size: @font-size-small; + } + } + + .globalsearch-result-time { + color: @dark-gray-color-80; + flex: 0; + font-size: @font-size-small; + text-align: right; + white-space: nowrap; + } + + .globalsearch-result-expand { + flex: auto; + margin: 20px 0 0 -32px; + + a { + .background-icon('arr_1right', 'clickable', 24); + .square(24px); + display: inline-block; + } + } + } + } + } +} + +#search_legend { + input { + position: absolute; + right: 0.5em; + top: 0.438em; + } +} + +html.responsive-display { + #quicksearch_item { + align-self: center; + } + #globalsearch-searchbar { + position: static; + top: 0; + + #globalsearch-input { + width: 80vw; + } + #globalsearch-icon { + left: calc(100% - 16px); + margin-left: 0; + } + #globalsearch-list { + @padding: 5px; + + position: absolute; + left: @padding; + top: calc(@bar-bottom-container-height + @padding); + width: calc(100vw - (2 * @padding)); + } + + #globalsearch-clear { + opacity: 1; + } + } +} +html:not(.size-large) { + &:not(.globalsearch-visible) { + #globalsearch-list, + #globalsearch-clear { + display: none; + } + } + + &.globalsearch-visible { + #barBottomright ul { + li { + display: none; + } + #quicksearch_item, + #sidebar-menu { + display: initial; + } + } + + .helpbar { + z-index: 0; + } + + #layout_page { + position: relative; + filter: blur(1px); + + &::before { + content: ' '; + display: block; + position: absolute; + top: -1px; + right: -1px; + bottom: -1px; + left: -1px; + background: fadeout(@base-color, 50%); + z-index: 1; + } + } + } +} diff --git a/resources/assets/stylesheets/less/gradebook.less b/resources/assets/stylesheets/less/gradebook.less new file mode 100644 index 0000000..eb6e144 --- /dev/null +++ b/resources/assets/stylesheets/less/gradebook.less @@ -0,0 +1,142 @@ +.progress { + display: flex; + height: 20px; + overflow: hidden; + font-size: 15px; + background-color: @light-gray-color-20; + margin: 0.5em 0; +} + +.progress-bar { + display: flex; + flex-direction: column; + justify-content: center; + color: white; + text-align: center; + white-space: nowrap; + background-color: @base-color; +} + +.gradebook-lecturer-overview-definition { + white-space: nowrap; +} + +.gradebook-lecturer-overview .gradebook-column-total, +.gradebook-lecturer-overview .gradebook-column-category { + border-left: 1px solid @light-gray-color-20; +} + +.gradebook-lecturer-overview .gradebook-column-category { + text-align: right; +} + +form.gradebook-lecturer-weights fieldset { + display: flex; + flex-wrap: wrap; +} + +form.gradebook-lecturer-weights label.gradebook-weight { + white-space: nowrap; + padding-right: 2px; + flex: 1 0 auto; + + > div { + display: flex; + flex-direction: row; + align-items: center; + } + + output { + color: @light-gray-color; + } + + output:before { + content: "~"; + } + output:after { + content: " %"; + } +} + +form.gradebook-lecturer-weights input[type="number"] { + max-width: 6em; +} + +.gradebook-student-name { + white-space: nowrap; +} + +.gradebook-definition-name { + font-weight: bold; +} + +article.gradebook-student h1, +article.gradebook-student h2 +{ + border-bottom: none; +} + +article.gradebook-student > header { + margin-bottom: 2.5em; +} + +section.gradebook-student-category { + margin-bottom: 3em; +} + +section.gradebook-student-category > header { + display: flex; + align-items: baseline; + margin-bottom: 0.5em; +} + +section.gradebook-student-category header .progress { + flex: 1; + margin-left: 1em; +} + +.gradebook-lecturer-custom-definitions .gradebook-lecturer-blank-slate { + text-align: center; + // border-left: 1px solid @dark-gray-color-15; + // border-bottom: 1px solid @brand-color-darker; +} + +table.default .gradebook-grade-input, +table.default .gradebook-inline-actions { + padding-left: 1em; +} + +.gradebook-inline-actions, +.gradebook-grade-input label { + white-space: nowrap; +} + +.gradebook-grade-input label { + margin-left: 1em; + margin-right: 1em; + display: block; +} + +.gradebook-grade-input input { + min-width: 5em; +} + +.gradebook-inline-actions .action-menu-icon { + vertical-align: text-bottom; +} + +th.gradebook-inline-actions .action-menu-item { + font-weight: 400; +} + +.gradebook-lecturer-custom-definitions input[type="number"] { + max-width: 3em; +} + +table.gradebook-lecturer-custom-definitions { + margin: 0; +} + +form.default footer.gradebook-lecturer-custom-definitions-actions { + border-top: none; +} diff --git a/resources/assets/stylesheets/less/header.less b/resources/assets/stylesheets/less/header.less new file mode 100644 index 0000000..d200787 --- /dev/null +++ b/resources/assets/stylesheets/less/header.less @@ -0,0 +1,211 @@ +/* --- header.css ----------------------------------------------------------- */ +#layout_wrapper { + box-sizing: border-box; + padding-top: @bar-bottom-container-height; +} +#barBottomContainer { + background-color: @base-color; + border: 1px @brand-color-darker; + color: @contrast-content-white; + border-bottom-style: solid; + height: @bar-bottom-container-height; + + width: 100%; + position: relative; + + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + + transform: translate(0, 0) !important; // We need !important due to the horizontal scroll handler + position: fixed; + top: 0px; + z-index: 10000; + +} +#barBottomLeft, +#barTopFont { + flex: 0 0 auto; + padding: 0px 15px; + z-index: 2; +} + + +#layout_footer, +#barBottomright { + > ul > li > a { + color: @contrast-content-white; + margin: 0 6px; + text-decoration: none; + &:hover { + color: @contrast-content-hovergray; + } + } +} + +#layout_footer { + > ul { + display: flex; + align-items: center; + flex-wrap: wrap; + justify-content: space-between; + list-style-type: none; + min-height: 30px; + padding: 0px; + > li { + margin: 2px; + } + } +} + +#barBottomright { + flex: 0 1 auto; + justify-self: flex-end; + > ul { + display: flex; + align-items: center; + justify-content: space-between; + list-style-type: none; + height: 40px; + padding: 0px; + > li { + margin: 2px; + padding: 0px 10px; + } + } +} + +#barTopAvatar { + display: inline-flex; +} + +#header_avatar_menu { + height: 30px; + margin: 0; + vertical-align: text-bottom; + z-index: 1003; + + .action-menu-icon { + border: 1px solid @dark-gray-color-40; + background-color: @dark-gray-color-5; + height: 28px; + margin: 0; + position: relative; + width: 28px; + z-index: 1; + + img { + height: 100%; + width: 100%; + } + + // Add arrow on the right + margin-right: 32px; + .icon('after', 'arr_1down', 'info_alt'); + + &::after { + background-position: center; + background-repeat: no-repeat; + padding: 7px 8px; + position: absolute; + left: 100%; + top: 0; + } + } +} +.action-menu.avatar-menu { + z-index: 1002; + + .action-menu-title{ + margin: 0em 0 0.3em; + } + + .action-menu-content { + position: absolute; + top: 41px; + right: 0; + + /*padding: 4px 28px 4px 8px;*/ + background: #fff; + box-shadow: 1px 1px 1px @dark-gray-color-60; + text-align: left; + white-space: nowrap; + + a:link, + a:visited { + color: @base-color; + } + a:hover, + a:active { + color: @active-color; + } + + div { + color: #000; + } + } +} + +.header_avatar_container { + align-items: center; +} + +#barTopFont { + flex: 1; + color: @white; + margin-left: 0px; + z-index: 1002; + line-height: @bar-bottom-container-height; + white-space: nowrap; +} + +.studip-logo { + .hide-text(); + background-repeat: no-repeat; + + .retina-background-image('logos/studip4-logo.png', 'logos/studip4-logo@2x.png', 130px, 92px); + background-image: none, url("@{image-path}/logos/studip4-logo.svg"); + + background-size: 130px 92px; + display: block; + width: 130px; + height: 81px; + margin-top: -10px; +} + +#barTopStudip { + margin-left: 20px; + margin-right: 12px; +} + +#barTopLogo { + left: 0; + position: absolute; + top: 0; +} + + +#flex-header { + height: @header-height; + background-color: @dark-gray-color-5; + + position: relative; + z-index: 3; +} + +// Slide menu in header navigation +#barBottomLeft { + box-sizing: border-box; + overflow-x: hidden; + padding: 0; + text-indent: -150px; + transition: all 500ms; +} +body.fixed { + #barBottomLeft { + overflow-x: visible; + padding: 0 15px; + text-indent: 0; + } +} diff --git a/resources/assets/stylesheets/less/helpbar.less b/resources/assets/stylesheets/less/helpbar.less new file mode 100644 index 0000000..63cc79c --- /dev/null +++ b/resources/assets/stylesheets/less/helpbar.less @@ -0,0 +1,169 @@ +@helpbar-width: 250px; + +.helpbar-container { + clear: both; + height: 28px; + position: relative; + top: 1px; + min-width: 32px; + /*width: 100%;*/ + right: 12px; + + float: right; + + #helpbar-sticky { + display: none; + } + + h2, h3 { + border-bottom: 0; + color: #fff; + font-size: 1em; + font-weight: normal; + margin: 0; + padding: 0; + } + h2 { + font-size: 1.2em; + } + h3 { + border-bottom: 1px dotted @base-color-80; + font-size: 1.1em; + margin-bottom: 2px; + padding-bottom: 2px; + } +} +.helpbar { + box-sizing: border-box; + + @border-width: 4px; + + position: absolute; + right: -2px; + top: 0px; + + width: @helpbar-width; + z-index: 1000; + + &:before { + border-bottom: 2px solid @base-color-80; + border-left: @border-width solid transparent; + border-right: @border-width solid transparent; + content: ''; + display: none; + position: absolute; + top: -1px; + left: 0; + right: 0; + } + + .helpbar-title { + .background-icon('question-circle', 'clickable', 24); + background-position: right top; + background-repeat: no-repeat; + margin: 2px 4px -4px; + overflow: hidden; + padding: 0px 0 2px 0px; + + label { + cursor: pointer; + display: block; + padding: 3px 4px; + font-weight: 700; + margin: 0em 0 0.3em; + } + } + .helpbar-widgets { + color: #fff; + list-style: none; + margin: 0px 8px; + padding: 0; + + a { + vertical-align: text-bottom; + } + a:link, a:visited { + color: #fff; + } + a:hover, a:active { + color: @light-gray-color-40; + text-decoration: underline; + } + > li { + border-top: 1px solid @content-color; + padding: 3px 0px; + margin: 0 .25em; + } + } + .help-tours { + list-style: none; + margin: 0; + padding: 0; + a { + .background-icon('play', 'info_alt'); + background-position: left 2px; + background-repeat: no-repeat; + display: block; + padding-left: 20px; + &.tour-paused { + .background-icon('pause', 'info_alt'); + } + &.tour-completed { + .background-icon('accept', 'info_alt'); + } + } + } + + a.link-extern { + .icon('before', 'link-extern', 'info_alt'); + } + a.link-intern { + .icon('before', 'link-intern', 'info_alt'); + } +} + +.helpbar { + @transition: all 300; + + width: 32px; + text-indent: -@helpbar-width; + transition: @transition; + + .helpbar-widgets { + max-height: 0; + transition: @transition; + overflow: hidden; + } + + #helpbar-sticky:checked + & { + text-indent: 0; + background-color: #28497c; + width: @helpbar-width; + .helpbar-title { + .background-icon('decline-circle', 'info_alt', 24); + } + .helpbar-widgets { + max-height: 1000px; + } + } +} + + +section.big-help-box { + background-color: #d4dbe5; + border: 1px solid #7e92b0; + padding: 0.5em; + margin-top: 0.5em; + text-align: center; + display: flex; + flex-direction: row; + + .icon { + flex-grow: 1; + } + + .text { + flex-grow: 5; + } +} + diff --git a/resources/assets/stylesheets/less/i18n.less b/resources/assets/stylesheets/less/i18n.less new file mode 100644 index 0000000..d2315e2 --- /dev/null +++ b/resources/assets/stylesheets/less/i18n.less @@ -0,0 +1,39 @@ +div.i18n_group { + @max-width: 28px; + position: relative; + + > select.i18n { + border: 0; + border-right: 1px solid @light-gray-color-40; + border-bottom: 1px solid @light-gray-color-40; + border-radius: 0; + box-sizing: border-box; + margin: 0 !important; + position: absolute; + top: 1px; + left: 1px; + height: 31px; + overflow: hidden; + max-width: @max-width; + z-index: 2; // stay above ckeditor toolbar + + appearance: none; + + background-position: left 4px center; + background-repeat: no-repeat; + background-size: 20px auto; + padding: 0px 2px 0px 28px !important; + + > option { + background-position: left 1px center; + background-repeat: no-repeat; + padding-left: 28px; + } + } + + > div.i18n { + input[type=text], > textarea, .editor_toolbar .buttons, .cktoolbar .cke_inner { + padding-left: (@max-width + 6px) !important; + } + } +} diff --git a/resources/assets/stylesheets/less/ilias-interface.less b/resources/assets/stylesheets/less/ilias-interface.less new file mode 100644 index 0000000..a404ba8 --- /dev/null +++ b/resources/assets/stylesheets/less/ilias-interface.less @@ -0,0 +1,22 @@ +/* --- Styles for ilias interface ------------------------------------------- */ +//TODO: lessify +#ilias_module_details_window, #ilias_module_edit_window { + display: flex; + justify-content: space-between; + align-items: flex-start; + align-content: flex-start; +} +#ilias_module_aside { + width: calc(30% - 10px); + max-width: calc(30% - 10px); +} +#ilias_module_aside div.ilias-module-icon img { + margin-left: 20%; + width: 60%; + max-height: 16em; + height: 100%; +} +#ilias_module_preview { + width: calc(70% - 10px); + max-width: calc(70% - 10px); +} diff --git a/resources/assets/stylesheets/less/index.less b/resources/assets/stylesheets/less/index.less new file mode 100644 index 0000000..a3b37ed --- /dev/null +++ b/resources/assets/stylesheets/less/index.less @@ -0,0 +1,132 @@ +div.index_container { + margin: 0 0; + top: 111px; + bottom: 0px; + width: 100%; + height: ~"calc(100% - 110px)"; + + ul#tabs { + display: none; + } + + #background-desktop { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; + } + + #background-mobile { + display: none; + z-index: -1; + } + + div.messagebox { + margin-left: 50px; + margin-top: 50px; + width: 428px; + } + + div.index_main { + background-color: rgba(255, 255, 255, 0.8); + box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5); + margin-left: 50px; + margin-top: 50px; + width: 500px; + + & > div { + padding: 20px; + } + + form { + padding: 20px; + padding-bottom: 0; + } + + footer { + overflow: auto; + padding: 20px; + + div#languages { + border-top: 1px solid @light-gray-color; + font-size: 0.9em; + padding: 10px; + + a { + padding-right: 10px; + } + } + + div.login_info { + border-top: 1px solid @light-gray-color; + font-size: 0.8em; + div { + text-align: right; + float: left; + padding: 5px; + + &:last-child { + float:right; + } + } + } + & > a { + margin-left: 12px; + } + } + nav { + h1 { + border-bottom: 0px; + } + padding: 10px; + margin-left: 20px; + margin-top: 20px; + display: inline-block; + width: 450px; + div.login_link { + display: inline-block; + width: 180px; + vertical-align: top; + padding-right: 25px; + a { + font-size: 1.5em; + p { + font-size: 0.5em; + color: @light-gray-color; + } + } + } + } + + } +} + +#index, +#login { + .messagebox { + margin-bottom: -25px; + } + + #layout_footer { + position: relative; + top: -33px; + } +} + +@-moz-document url-prefix() { + div.index_container { + height: calc(100% - 145px); + } + + #index, + #login { + height: calc(100% - 34px); + + #layout_footer { + position: inherit; + top: 0; + } + } +} diff --git a/resources/assets/stylesheets/less/inline-editing.less b/resources/assets/stylesheets/less/inline-editing.less new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/resources/assets/stylesheets/less/inline-editing.less diff --git a/resources/assets/stylesheets/less/jquery-ui/custom.less b/resources/assets/stylesheets/less/jquery-ui/custom.less new file mode 100644 index 0000000..269904e --- /dev/null +++ b/resources/assets/stylesheets/less/jquery-ui/custom.less @@ -0,0 +1,437 @@ +/*! + * jQuery UI CSS Framework 1.12.0 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/category/theming/ + * + * To view and modify this theme, visit http://jqueryui.com/themeroller/?bgShadowXPos=&bgOverlayXPos=&bgErrorXPos=&bgHighlightXPos=&bgContentXPos=&bgHeaderXPos=&bgActiveXPos=&bgHoverXPos=&bgDefaultXPos=&bgShadowYPos=&bgOverlayYPos=&bgErrorYPos=&bgHighlightYPos=&bgContentYPos=&bgHeaderYPos=&bgActiveYPos=&bgHoverYPos=&bgDefaultYPos=&bgShadowRepeat=&bgOverlayRepeat=&bgErrorRepeat=&bgHighlightRepeat=&bgContentRepeat=&bgHeaderRepeat=&bgActiveRepeat=&bgHoverRepeat=&bgDefaultRepeat=&iconsHover=url(%22images%2Fui-icons_555555_256x240.png%22)&iconsHighlight=url(%22images%2Fui-icons_777620_256x240.png%22)&iconsHeader=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsError=url(%22images%2Fui-icons_cc0000_256x240.png%22)&iconsDefault=url(%22images%2Fui-icons_777777_256x240.png%22)&iconsContent=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsActive=url(%22images%2Fui-icons_ffffff_256x240.png%22)&bgImgUrlShadow=&bgImgUrlOverlay=&bgImgUrlHover=&bgImgUrlHighlight=&bgImgUrlHeader=&bgImgUrlError=&bgImgUrlDefault=&bgImgUrlContent=&bgImgUrlActive=&opacityFilterShadow=Alpha(Opacity%3D30)&opacityFilterOverlay=Alpha(Opacity%3D30)&opacityShadowPerc=30&opacityOverlayPerc=30&iconColorHover=%23555555&iconColorHighlight=%23777620&iconColorHeader=%23444444&iconColorError=%23cc0000&iconColorDefault=%23777777&iconColorContent=%23444444&iconColorActive=%23ffffff&bgImgOpacityShadow=0&bgImgOpacityOverlay=0&bgImgOpacityError=95&bgImgOpacityHighlight=55&bgImgOpacityContent=75&bgImgOpacityHeader=75&bgImgOpacityActive=65&bgImgOpacityHover=75&bgImgOpacityDefault=75&bgTextureShadow=flat&bgTextureOverlay=flat&bgTextureError=flat&bgTextureHighlight=flat&bgTextureContent=flat&bgTextureHeader=flat&bgTextureActive=flat&bgTextureHover=flat&bgTextureDefault=flat&cornerRadius=3px&fwDefault=normal&ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&cornerRadiusShadow=8px&thicknessShadow=5px&offsetLeftShadow=0px&offsetTopShadow=0px&opacityShadow=.3&bgColorShadow=%23666666&opacityOverlay=.3&bgColorOverlay=%23aaaaaa&fcError=%235f3f3f&borderColorError=%23f1a899&bgColorError=%23fddfdf&fcHighlight=%23777620&borderColorHighlight=%23dad55e&bgColorHighlight=%23fffa90&fcContent=%23333333&borderColorContent=%23dddddd&bgColorContent=%23ffffff&fcHeader=%23333333&borderColorHeader=%23dddddd&bgColorHeader=%23e9e9e9&fcActive=%23ffffff&borderColorActive=%23003eff&bgColorActive=%23007fff&fcHover=%232b2b2b&borderColorHover=%23cccccc&bgColorHover=%23ededed&fcDefault=%23454545&borderColorDefault=%23c5c5c5&bgColorDefault=%23f6f6f6 + */ + + +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Arial,Helvetica,sans-serif; + font-size: 1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Arial,Helvetica,sans-serif; + font-size: 1em; +} +.ui-widget.ui-widget-content { + border: 1px solid #c5c5c5; +} +.ui-widget-content { + border: 1px solid #dddddd; + background: #ffffff; + color: #333333; +} +.ui-widget-content a { + color: #333333; +} +.ui-widget-header { + border: 1px solid #dddddd; + background: #e9e9e9; + color: #333333; + font-weight: bold; +} +.ui-widget-header a { + color: #333333; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default, +.ui-button, + +/* We use html here because we need a greater specificity to make sure disabled +works properly when clicked or hovered */ +html .ui-button.ui-state-disabled:hover, +html .ui-button.ui-state-disabled:active { + border: 1px solid #c5c5c5; + background: #f6f6f6; + font-weight: normal; + color: #454545; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited, +a.ui-button, +a:link.ui-button, +a:visited.ui-button, +.ui-button { + color: #454545; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus, +.ui-button:hover, +.ui-button:focus { + border: 1px solid #cccccc; + background: #ededed; + font-weight: normal; + color: #2b2b2b; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited, +.ui-state-focus a, +.ui-state-focus a:hover, +.ui-state-focus a:link, +.ui-state-focus a:visited, +a.ui-button:hover, +a.ui-button:focus { + color: #2b2b2b; + text-decoration: none; +} + +.ui-visual-focus { + box-shadow: 0 0 3px 1px rgb(94, 158, 214); +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active, +a.ui-button:active, +.ui-button:active, +.ui-button.ui-state-active:hover { + border: 1px solid #003eff; + background: #007fff; + font-weight: normal; + color: #ffffff; +} +.ui-icon-background, +.ui-state-active .ui-icon-background { + border: #003eff; + background-color: #ffffff; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #ffffff; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #dad55e; + background: #fffa90; + color: #777620; +} +.ui-state-checked { + border: 1px solid #dad55e; + background: #fffa90; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #777620; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #f1a899; + background: #fddfdf; + color: #5f3f3f; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #5f3f3f; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #5f3f3f; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + background-image: none; +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +// .ui-icon, +// .ui-widget-content .ui-icon { +// background-image: url("images/ui-icons_444444_256x240.png"); +// } +// .ui-widget-header .ui-icon { +// background-image: url("images/ui-icons_444444_256x240.png"); +// } +// .ui-button .ui-icon { +// background-image: url("images/ui-icons_777777_256x240.png"); +// } +// .ui-state-hover .ui-icon, +// .ui-state-focus .ui-icon, +// .ui-button:hover .ui-icon, +// .ui-button:focus .ui-icon, +// .ui-state-default .ui-icon { +// background-image: url("images/ui-icons_555555_256x240.png"); +// } +// .ui-state-active .ui-icon, +// .ui-button:active .ui-icon { +// background-image: url("images/ui-icons_ffffff_256x240.png"); +// } +// .ui-state-highlight .ui-icon, +// .ui-button .ui-state-highlight.ui-icon { +// background-image: url("images/ui-icons_777620_256x240.png"); +// } +// .ui-state-error .ui-icon, +// .ui-state-error-text .ui-icon { +// background-image: url("images/ui-icons_cc0000_256x240.png"); +// } + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-caret-1-n { background-position: 0 0; } +.ui-icon-caret-1-ne { background-position: -16px 0; } +.ui-icon-caret-1-e { background-position: -32px 0; } +.ui-icon-caret-1-se { background-position: -48px 0; } +.ui-icon-caret-1-s { background-position: -65px 0; } +.ui-icon-caret-1-sw { background-position: -80px 0; } +.ui-icon-caret-1-w { background-position: -96px 0; } +.ui-icon-caret-1-nw { background-position: -112px 0; } +.ui-icon-caret-2-n-s { background-position: -128px 0; } +.ui-icon-caret-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -65px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -65px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 1px -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 3px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 3px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 3px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 3px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #aaaaaa; + opacity: .003; +} +.ui-widget-shadow { + box-shadow: 0px 0px 5px #666666; +} diff --git a/resources/assets/stylesheets/less/jquery-ui/studip.less b/resources/assets/stylesheets/less/jquery-ui/studip.less new file mode 100644 index 0000000..0f58608 --- /dev/null +++ b/resources/assets/stylesheets/less/jquery-ui/studip.less @@ -0,0 +1,203 @@ +.ui-front { + z-index: @z-index; +} +.ui-widget_start { + font-family: Arial, Helvetica, sans-serif; + font-size: 1.0em; + padding: 0; +} + +.ui-widget { + font-family: inherit; + font-size: 1.0em; + + input, + select, + textarea, + button { + font-family: inherit; + } +} + +.ui-widget-content { + background: #fff; +} + +.ui-widget-header { + background-color: @brand-color-lighter; + background-image: none; +} +.ui-widget_columnl { + float: left; + width: 100%; +} +.ui-widget_columnr { + float: right; + /*width: 39%; */ +} +.ui-widgetContainer { + background-image: none; + color: white; + padding: 2%; +} + +.ui-widget_head { + background-color: @brand-color-lighter; + color: white; + font-size: 1.3em; + line-height: 30px; + text-align: center; + + &:hover { + cursor:move; + } + + h1 { + color: black; + line-height: 100px; + text-align: center; + } +} + + +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-left, +.ui-corner-bottom, +.ui-corner-tr, +.ui-corner-br, +.ui-corner-bl, +.ui-corner-tl { + border-radius: 0; +} + +.ui-state-active, +.ui-state-focus, +.ui-state-hover, +.ui-autocomplete .ui-state-hover, +.ui-state-hover:hover, +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active { + background-color: @brand-color-lighter; + color: white; +} + +.ui-accordion .ui-accordion-header { + &, + .ui-state-default, + .ui-state-active, + .ui-state-hover { + background: @content-color-20; + border-radius: 0; + border: none; + border-top: 1px solid @light-gray-color-20; + border-bottom: 1px solid @light-gray-color-20; + color: #000; + font-size: 10pt; + margin: 0; + padding: 5px 5px 5px 30px; + text-align: left; + } +} + +.ui-accordion .ui-accordion-content { + background: #ffffff; + margin: 0; + padding: 0; + border: 0; + border-bottom: 1px solid @light-gray-color-20; +} + +.ui-state-hover, .ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, .ui-state-focus, +.ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { + background-image: none; +} + +.ui-autocomplete { + border: 1px solid @dark-gray-color-45; + padding: 1px; + + .ui-menu-item { + .ui-menu-item-wrapper { + display: block; + overflow: hidden; + text-overflow: ellipsis; + + &.ui-state-active { + background: @base-color; + border: 0; + margin: 0; + } + } + } +} + +.ui-dialog { + .ui-resizable-n, .ui-resizable-s { + height: 4px; + } + .ui-resizable-e, .ui-resizable-w { + width: 4px; + } +} + +/* --- textarea resizer ----------------------------------------------------- */ +textarea.ui-resizable-handle.ui-resizable-s { + background: #eee url("@{image-path}/vendor/handle_background.png") no-repeat center; + bottom: 0; + cursor: s-resize; + height: 12px; +} + +// Date picker +.ui-datepicker-header .ui-icon { + background-image: url(../images/vendor/jquery-ui/ui-icons_ffffff_256x240.png); +} + +.ui-datepicker-calendar .ui-state-active { + color: @brand-color-light; +} + +.ui-datepicker-calendar .ui-state-active.ui-state-hover { + color: white; +} + +.hasDatepicker, +[data-date-picker], +.has-date-picker, +[data-datetime-picker], +.has-datetime-picker { + .background-icon('schedule', 'clickable', 20); + background-position: right 3px center; + background-repeat: no-repeat; + min-width: 12ex; + border: 1px solid @light-gray-color-40; + &:focus { + border-color: @brand-color-dark; + } +} +.hasTimepicker, +[data-time-picker], +.has-time-picker { + .background-icon('date', 'clickable', 20); + background-position: right 3px center; + background-repeat: no-repeat; + min-width: 10ex; + border: 1px solid @light-gray-color-40; + &:focus { + border-color: @brand-color-dark; + } +} +[data-datetime-picker], +.has-datetime-picker { + min-width: 21ex; +} + +.ui-slider { + .ui-slider-range { + background-color: @base-color; + } +} diff --git a/resources/assets/stylesheets/less/layouts.less b/resources/assets/stylesheets/less/layouts.less new file mode 100644 index 0000000..78d2243 --- /dev/null +++ b/resources/assets/stylesheets/less/layouts.less @@ -0,0 +1,264 @@ +// TODO: LESSify + +@page-margin: 15px; + +@sidebar-width: 250px; +@sidebar-padding: 12px; +@sidebar-border-width: 1px; + +@content-width: 400px; +@content-margin: 12px; + +@site-width: (@page-margin * 2 + @sidebar-width + @sidebar-padding * 2 + @sidebar-border-width * 2 + @content-width + @content-margin * 2); +@page-width: (@sidebar-width + @sidebar-padding * 2 + @sidebar-border-width * 2 + @content-width + @content-margin * 2); + + +/* --- Layouts -------------------------------------------------------------- */ +#layout_page { + border-radius: 0 0 2px 2px; + box-sizing: border-box; + clear: both; + margin: @page-margin; + background-color: #fff; + margin: 0px; +} + +// for old pages without template layout +#layout_table { + background-color: @light-gray-color-60; + border: 20px solid #fff; + margin: 0; + padding: 0; + width: 100%; + + td { vertical-align: top; } +} + +#layout_container { + background-color: white; + border-radius: 0 0 2px 2px; + /*margin: 0;*/ + padding-top: 15px; +} + +#page_title_container { + float: left; + background-color: #fff; + line-height: 20px; + margin-left: 15px; + margin-right: 15px; + min-height: 45px; +} + +.secondary-navigation { + position: relative; + .colorblock { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: @page-margin; + } + + #layout_context_title { + font-size: 1.1em; + height: 30px; + padding-left: @page-margin; + max-height: 30px; + overflow: hidden; + + background: linear-gradient(to bottom, @dark-gray-color-5, @dark-gray-color-10); + + > .context_icon { + margin: 7px 1px 0 9px; + vertical-align: text-bottom; + } + } + + .tabs_wrapper { + padding-left: 27px; + } + + .contextless & { + .colorblock { + width: 0px; + } + + #layout_context_title { + color: rgba(0, 0, 0, 0); + height: 0px; + max-height: 35px; + // text-indent: -100%; + // + > .context_icon { + opacity: 0; + } + } + + .tabs_wrapper { + padding-left: 0px; + } + } +} +#current_page_title { + font-weight: bold; + font-size: 1.4em; + position: relative; + top: 20px; +} + +#layout_page.oversized { + overflow: visible; +} + + +#layout_content { + box-sizing: border-box; + .scrollbox-horizontal(); + padding: 0 @content-margin 47px @content-margin; + vertical-align: top; + min-width: @content-width; + + .oversized & { + overflow: visible; + } +} + +#layout_sidebar { + background: inherit; + border-left: 1px dashed @brand-color-darker; + max-width: @sidebar-width; + width: @sidebar-width; + min-width: @sidebar-width; + padding: @sidebar-padding; +} + +#layout_wrapper { + .clearfix; + clear: both; + min-width: 800px; // 800px breite ist minimum + min-height: 100%; + width: 100%; + height: auto !important; + height: 100%; + margin: 0 auto; +} + +#layout_footer { + background-color: @base-color; + color: @contrast-content-white; + display: flex; + padding: 2px 0px; + flex: 0 1 auto; // fourth row of flex-main + min-width: @site-width; + width: 100%; +} +#footer { + flex: 1; + margin-left: 8px; + margin-top: 2px; + line-height: 28px; +} + +#layout_wrapper { // the main flex, dividing all elements into flexing rows + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-content: stretch; + align-items: stretch; + justify-content: flex-start; + + min-width: @site-width; + + #flex-header { // first row of flex-main + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-content: stretch; + align-items: stretch; + justify-content: flex-start; + + width: 100%; + min-width: @site-width; + + flex: 0 1 auto; + border-bottom: 1px solid @light-gray-color-40; + + #barTopMenu { // column 1 of flex-header + flex: 1 1 auto; + } + + #barTopStudip { // column 2 of flex-header + flex: 0 1 auto; + } + } + + #barBottomContainer { // second row of flex-main + flex: 0 1 auto; + + min-width: @site-width; + z-index: 1001; // High enough so it will be above the sidebar + } + + #layout_page { // third row of flex-main + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-content: stretch; + align-items: stretch; + justify-content: flex-start; + + flex: 10 1 auto; + + min-width: @page-width; + + .tabs_wrapper { + display: flex; + flex-direction: row; + align-items: stretch; + justify-content: space-between; + background-color: @dark-gray-color-10; + font-size: 0.9em; + border-bottom: 1px solid @dark-gray-color-40; + } + + #tabs { // row 1 of layout_page + width: 100%; + flex: 0 1 auto; + padding-left: @page-margin; + transition: margin-left; + transition-duration: 300ms; + transition-delay: 500ms; + } + + #layout_container { // row 3 of layout_page + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-content: stretch; + align-items: stretch; + justify-content: flex-start; + + flex: 1 1 auto; + min-width: @page-width; + + #layout_content { // column 1 of layout_container + flex: 1 1 auto; + } + + #layout_sidebar { // column 2 of layout_container + flex: 0 1 auto; + } + + } + } + +} + +@-moz-document url-prefix() { + .flex-container { + width: 100%; + box-sizing: border-box; + } + +} diff --git a/resources/assets/stylesheets/less/links.less b/resources/assets/stylesheets/less/links.less new file mode 100644 index 0000000..a6d6158 --- /dev/null +++ b/resources/assets/stylesheets/less/links.less @@ -0,0 +1,49 @@ +/* --- Links ---------------------------------------------------------------- */ +a, a:link, a:visited { + color: @base-color; + text-decoration: none; + + &.index { color: #444; } + &.printhead { color: #339; } + &.tree { color: #000; } + &.toolbar { + color: #91a2b6; + font-size: 9px; + } +} +a[href] { + transition: color 0.3s; +} +a[disabled] { + pointer-events: none; +} + +a:hover, a:active, a:hover.index, a:active.index, a:hover.tree { + color: @active-color; + text-decoration: none; +} + +a:hover.toolbar { + color: #eee; +} + +a.link-intern, a.link-extern { + white-space: nowrap; +} + +a.link-intern { + .icon('before', 'link-intern', 'clickable', 16, 2px); +} +a.link-extern { + .icon('before', 'link-extern', 'clickable', 16, 2px); +} +a.link-add { + .icon('before', 'add', 'clickable', 16, 2px); +} +a.link-edit { + .icon('before', 'edit', 'clickable', 16, 2px); +} + +a img { + border: 0; +} diff --git a/resources/assets/stylesheets/less/lists.less b/resources/assets/stylesheets/less/lists.less new file mode 100644 index 0000000..4e10367 --- /dev/null +++ b/resources/assets/stylesheets/less/lists.less @@ -0,0 +1,107 @@ +// Lists +// ------------------------- + +// Unordered and Ordered lists +ul, +ol { + margin-top: 0; + margin-bottom: 0; + ul, + ol { + margin-bottom: 0; + } +} + + +// List options + +// Unstyled keeps list items block level, just removes default browser padding and list-style +.list-unstyled { + padding-left: 0; + list-style: none; +} + +// Inline turns list items into inline-block +.list-inline { + .list-unstyled(); + margin-left: -5px; + + > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; + } +} + +//comma separated +.list-csv { + .list-inline(); + margin-left: 0; + + > li { + padding-left: 0; + + &::after { + content: ","; + } + &:last-child::after { + content: ""; + } + } +} +.list-pipe-separated { + .list-inline(); + display: flex; // Prevents the mystery gap between elements + + > li { + border-right: 1px solid @dark-gray-color; + &:last-child { + border-right: 0; + } + } +} + +dl { + dt { + font-weight: bold; + } +} + +// reset the visualization of different levels of unordered lists +.formatted-content ul { + list-style-type: disc; + + ul { + list-style-type: circle; + + ul { + list-style-type: square; + } + } +} + +ul.default { + list-style: inside; + margin: 0; + padding: 0; + + li:only-child { + list-style: none; + } + li:not(:last-child) { + margin-bottom: 0.25em; + } +} + +dl.default { + display: grid; + grid-column-gap: 1ex; + grid-template-columns: max-content auto; + dt { + font-weight: normal; + grid-column-start: 1; + } + dd { + grid-column-start: 2; + } +} diff --git a/resources/assets/stylesheets/less/messagebox.less b/resources/assets/stylesheets/less/messagebox.less new file mode 100644 index 0000000..c1c2609 --- /dev/null +++ b/resources/assets/stylesheets/less/messagebox.less @@ -0,0 +1,136 @@ +/* --- MessageBoxes --------------------------------------------------------- */ +div.messagebox { + background: no-repeat 10px 10px; + border: 2px solid; + font-size: 12pt; + font-weight: bold; + margin: 5px 0; + padding: 15px 15px 15px 55px; + position: relative; + text-align: left; + + &:first-child { + margin-top: 0; + } + + .messagebox_buttons { + position: absolute; + right: 3px; + top: 3px; + + a { + background: transparent no-repeat center center; + background-size: 16px 16px; + + display: inline-block; + margin: 1px; + .size(16px, 16px); + + &.close, &.details { + span { display: none; } + } + &.close { + .background-icon('decline', 'clickable'); + } + &.details { + .background-icon('arr_eol-down', 'clickable'); + } + } + } + + &.details_hidden { + .messagebox_buttons a.details { + .background-icon('arr_eol-up', 'clickable'); + } + .messagebox_details { height: 0; } + } +} + +div.messagebox_details { + font-weight: normal; + overflow: hidden; +} + +// Messagebox definitions + +.messagebox (@name, @color, @background-color) { + .messagebox (@name, @color, @background-color, @color, @name); +} + +.messagebox (@name, @color, @background-color, @border-color) { + .messagebox (@name, @color, @background-color, @border-color, @name); +} + +.messagebox (@name, @color, @background-color, @border-color, @image) { + // Also generates the neccessary selector not only the rules + div.messagebox_@{name} { + color: @color; + background-color: @background-color; + background-image: url("@{image-path}/messagebox/@{image}.png"); + background-size: 32px 32px; + border-color: @border-color; + } +} + +.messagebox(info, #000, white, @base-color); +.messagebox(success, #000, white, @dark-green); +.messagebox(error, #000, white, @red); +.messagebox(exception, @red, @red-20, @red); +.messagebox(warning, #000, white, @yellow-60, 'advice'); + +// Define modal messagebox +.modaloverlay { + background: fadeout(@base-color, 50%); + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + + display: flex; + align-items: center; + justify-content: center; + + padding: 10vh 20vw; + + .messagebox { + display: inline-block; + zoom: 1; // IE :( + box-sizing: border-box; +// position: relative; + vertical-align: middle; + margin: auto; + + position: relative; + max-height: 50%; + min-width: 30em; + max-width: 50%; + width: auto; + + color: #000; + border-color: @yellow; + background-color: white; + background-image: url("@{image-path}/messagebox/question.png"); + background-size: 32px 32px; + box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5); + + .content { + max-height: 200px; + overflow: auto; + text-align: left; + } + .buttons { + margin-top: 1em; + text-align: center; + } + } + .ui-dialog { + display: inline-block; + zoom: 1; // IE :( + box-sizing: border-box; + position: relative; + vertical-align: middle; + margin: auto; + } +} diff --git a/resources/assets/stylesheets/less/messages.less b/resources/assets/stylesheets/less/messages.less new file mode 100644 index 0000000..b22aefd --- /dev/null +++ b/resources/assets/stylesheets/less/messages.less @@ -0,0 +1,112 @@ +#reloader.more { + display: none; +} +.unread { + font-weight: bold; +} +.dropping { + background-color: @content-color; + a { + color: white; + } +} +a.message-tag { + white-space: nowrap; + .icon('before', 'tag', 'clickable'); +} + +#message-move-handle { + background-color: rgba(255, 255, 255, 0.3); + padding: 3px; + border-radius: 5px; + border: thin solid #000; + + .icon('before', 'mail', 'clickable', 20, 2px); +} +#messages-tags.dragging .sidebar-widget-content { + background-color: @activity-color-20; +} + +#statusbar_container { + > .statusbar { + border: thin solid lightgrey; + min-width: 100%; + max-width: 100%; + background-color: @content-color-40; + > .progress { + background-color: @content-color; + width: 100%; + min-width: 0%; + max-width: 0%; + height: 20px; + line-height: 20px; + &.progress-error { + background-color: @red; + } + } + > .progresstext { + margin-top: -20px; + text-align: center; + color: white; + height: 20px; + line-height: 20px; + } + } +} + +#message_metadata tr { + vertical-align: top; +} + +#adressees { + max-height: 120px; + overflow: auto; + li.adressee { + white-space: nowrap; + } +} + +.message_body { + background-color: @content-color-20; + margin: 3px; + padding: 10px; +} + +.responsive_author { + margin: 0; + font-size: 0.8em; + color: @base-gray; +} + +.message-search-wrapper { + display: flex; + justify-content: flex-start; + margin-top: 1ex; + + > * { + margin-right: 1em; + } +} +ul.message-options { + list-style: none; + margin: 1em 0 0; + padding: 0; + text-align: center; + + > li { + display: inline-block; + min-width: 70px; + } +} + +#messages { + td.title { + > a { + display: block; + > div.message-indicators { + float:right; + margin-right: 5px; + } + } + } +} diff --git a/resources/assets/stylesheets/less/mobile.less b/resources/assets/stylesheets/less/mobile.less new file mode 100644 index 0000000..2e8ca73 --- /dev/null +++ b/resources/assets/stylesheets/less/mobile.less @@ -0,0 +1,58 @@ +.media-breakpoint-small-down({ + #layout_wrapper { min-width: 0 !important; } + #login div.index_container .messagebox, + #index div.index_container .messagebox, + #request_new_password div.index_container .messagebox, + #web_migrate div.index_container .messagebox { + margin-top: 110px; + margin-left: auto; + margin-right: auto; + } +}); + +.media-breakpoint-tiny-down({ + #barTopStudip img { + height: 33px; + margin-top: 5px; + width: 153px; + } + #index, + #login, + #request_new_password, + #web_migrate { + div.index_container { + display: flex; + flex-direction: column; + position: static; + top: 40px; + + .messagebox { + margin-bottom: 0; + margin-top: 0; + width: calc(100% - 74px); + } + + #background-desktop { + display: none; + } + + #background-mobile { + display: inherit; + width: 100%; + height: calc(100% - 34px); + } + + div.index_main { + margin-top: 0; + padding-left: 20px; + padding-right: 20px; + position: relative; + width: ~"calc(100% - 40px)"; + + nav { + display: inline; + } + } + } + } +}); diff --git a/resources/assets/stylesheets/less/mvv.less b/resources/assets/stylesheets/less/mvv.less new file mode 100644 index 0000000..62fac4b --- /dev/null +++ b/resources/assets/stylesheets/less/mvv.less @@ -0,0 +1,690 @@ +dl { + &.mvv-form { + margin: 0; + + dt { + font-weight: bold; + padding-left: 15px; + padding-top: 5px; + + label&:after { + content: ":"; + } + } + + dd { + padding: 10px 10px 10px 30px; + border-bottom: 1px solid @dark-gray-color-20; + + label { + display: inline-block; + padding: 10px; + } + + div.mvv-fachsemester label { + display: inline; + padding: 0 10px 0 0; + } + } + + label img { + vertical-align: baseline; + } + + div.studip { + width: 75%; + display: inline; + } + + blockquote { + border: 1px dashed @dark-gray-color-80; + margin: 3px; + padding: 3px; + font-size: 0.9em; + flex: 1 0 auto; + + &:hover { + background-color: @yellow-20; + border-color: @red; + } + } + + } + + &.mvv-details { + margin: 0; + + dt { + font-weight: bold; + padding: 5px 0 0 5px; + } + + dd { + margin: 0; + padding: 5px 0 0 15px; + } + } +} + +span.mvv-chooser-id { + display: none; +} + +table { + + tr td.ellipsis { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &.default { + + > tbody { + &.collapsed > tr > td { + border-bottom: 1px solid @dark-gray-color-20; + padding: 5px; + &:first-child { + padding-left: 0; + } + } + + &.not-collapsed { + > tr > td { + border-bottom: 1px solid @dark-gray-color-20; + padding: 5px; + &:first-child { + padding-left: 0; + } + } + > tr.loaded-details > td { + padding: 0px 0px 5px 20px; + } + } + + &:last-of-type > tr.last-child > td { + border-bottom: 1px solid @dark-gray-color-20; + } + + &.ui-sortable-helper { + display: table; + } + + &.ui-sortable-placeholder { + display: block; + } + } + + > tbody.sort_items { + > tr.header-row > td:first-child { + background: #fff url("@{image-path}/anfasser_24.png") no-repeat left center; + cursor: move; + padding-left: 10px; + } + &.empty > tr.header-row > td:first-child { + &:extend(& > tr.header-row > td:first-child); + padding-left: 30px; + } + } + + } + + &.mvv-form tr td { + &:first-child { + vertical-align: top; + font-weight: bold; + } + } + + &.mvv-modul-details { + margin: 10px; + width: 99%; + + td { + vertical-align: top; + } + + th { + text-align: left; + vertical-align: top; + + .mvv-modul-details-head { + text-align: center; + } + } + + table th { + text-align: left; + vertical-align: top; + } + + input[type=checkbox].mvv-cb-more { + display: none; + &:checked ~ ul li { + &:nth-child(n+6) { + height: 0; + visibility: hidden; + } + & label.cb-more-label { + display: block; + } + } + & ~ ul label.cb-more-label { + display: none; + } + } + } + + &.mvv-semsterdata { + width: 100%; + border-collapse: collapse; + border: none; + height: 2em; + + td { + border: none; + border-right:1px solid @dark-gray-color-20; + text-align: center; + margin: 0px; + padding:0px; + + &.type{ + font-size: 0.5em; + &.soll{ + color:@red; + } + &.kann{ + color:@dark-green; + } + } + } + + th { + border: none; + border-right:1px solid @dark-gray-color-20; + text-align: center; + margin: 0px; + padding:0px; + } + } + +} + +ul { + &.mvv-result-list { + list-style-type: none; + padding: 0; + + dt { + padding: 1em; + margin: 0; + } + + li { + padding: 10px 20px; + margin: 0; + } + + dd { + margin: 0; + } + + &.even { + background-color: @dark-gray-color-10; + + &:hover { + background-color: @content-color-60; + } + } + + &.odd { + background-color: @dark-gray-color-5; + + &:hover { + background-color: @content-color-40; + } + } + } + + &.mvv-modul li { + .icon('before', 'learnmodule', 'info', 16, 2px); + padding-left: 20px; + } + + &.mvv-persons { + width: 100%; + + & li { + .icon('before', 'person', 'info', 16, 2px); + padding-left: 20px; + } + } + + &.mvv-faecher li { + .icon('before', 'file', 'info', 16, 2px); + padding-left: 20px; + } + + &.mvv-dokumente li { + > div:first-child { + .icon('before', 'file', 'info', 16, 2px); + } + } + + &.mvv-institute li { + .icon('before', 'institute', 'info', 16, 2px); + padding-left: 20px; + } + + &.mvv-languages li { + .icon('before', 'consultation', 'info', 16, 2px); + padding-left: 20px; + } + + &.mvv-assigned-items { + max-width: 48em; + width: 100%; + list-style-type: none; + margin: 0.5em 0 0; + padding: 0; +// padding: 0px 0px 0px 10px; + + li { + border-bottom: solid @dark-gray-color-45 1px; + padding-top: 5px; + padding-left: 5px; + margin-bottom: 5px; + margin-left: 0; + display: flex; + flex-wrap: wrap; + + &.sort_items { + background: #fff url("@{image-path}/anfasser_24.png") no-repeat left center; + cursor: move; + padding-left: 10px; + } + + } + + &.ui-autocomplete { + max-width: 700px; + } + + } + + li.mvv-item-list-placeholder { + background-image: none !important; + border: none !important; + font-weight: normal !important; +// padding-left: 20px; + } +} + +div { + &.mvv-item-list-properties { + width: 100%; + align-self: baseline; + padding-left: 40px; + div { + font-style: italic; + font-size: 0.9em; + max-height: 1.2em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + &.mvv-item-list-text { + flex: 9; + } + + &.mvv-item-list-buttons { + flex: 1; + text-align: right; + } + + &.mvv-edit-form-new { + padding: 10px; + } + + &.mvv-property-en { + background: url("@{image-path}/languages/lang_en.gif") no-repeat left center; + padding-left: 20px; + font-style: italic; + height: 1.5em; + text-overflow: ellipsis; + } + + &.mvv-property-de { + background: url("@{image-path}/languages/lang_de.gif") no-repeat left center; + padding-left: 20px; + font-style: italic; + height: 1.5em; + text-overflow: ellipsis; + } +} + +select.mvv-search-select-list { + display: none; + max-width: 40em; +} + +#mvv-chooser { + + div { + float: left; + width: 19%; + } + + ul { + list-style: none inside; + margin: 5px; + padding: 0; + + li { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + border-top: 1px solid @dark-gray-color-60; + padding: 3px 20px 3px 2px; + height: 1.3em; + + &:hover { + background:@dark-gray-color-10; + cursor: pointer; + font-weight: bold; + } + + &.selected { + .background-icon('arr_2right', 'inactive', 16); + background-position: right; + background-repeat: no-repeat; + cursor: pointer; + font-weight: bold; + + &.last { + .background-icon('accept', 'inactive', 16); + background-position: right; + background-repeat: no-repeat; + } + + &:after { + float: right; + } + } + } + } +} + +#mvv-chooser-toggle { + .icon('before', 'arr_2up', 'clickable', 16, 2px); + width: 20px; + height: 20px; + float: right; + cursor: pointer; + display: none; +} + +#exposeMask { + position: fixed !important; + bottom: 0px; +} + +.mvv-add-button { + width: 20px; + display: inline-block; + + a { + cursor: pointer; + display: none; + vertical-align: middle; + } +} + +.mvv-chooser-hidden { + .icon('before', 'arr_2down', 'clickable', 16, 2px); +} + +.mvv-hideable-hidden { + display: none; +} + +.mvv-include-edit { + background: url("{@plugin-path}/images/edit-top.png") no-repeat center top; + width: 580px; + margin: 0px; + padding-top: 20px; +} + +.mvv-edit-form-ovl { + width: 560px; + + input[type=text], textarea { + width: 90%; + vertical-align: top; + } +} + +.mvv-include-content { + margin: 0 15px; + position: relative; + height: 400px; + overflow: auto; +} + +.mvv-edit-bottom { + background: url("{@plugin-path}/images/edit-bottom.png") no-repeat center bottom; + padding-top: 20px; +} + +.mvv-include-close { + background: url("{@plugin-path}/images/close.png") no-repeat; + cursor: pointer; + height: 35px; + position: absolute; + right: -30px; + top: -13px; + width: 35px; + z-index: 1000; +} + +.mvv-include-background { + background: url("{@plugin-path}/images/edit-bg.png") repeat-y 0 0; +} + +.mvv-search-reset { + display: none; + cursor: pointer; +} +table.default { + thead tr th, + tbody tr td { + &.mvv-search-modules-row { + padding-left: 25px; + } + } +} +.sortable a { + cursor: pointer; +} + +.ui-resizable-handle { + z-index: 999; +} + +.ui-menu-item a { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.clear{ + clear:both; +} + +form.default .mvv-inst-chooser select { + width: 20em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-left: 10px; +} + +.mvv-inst-next-button { + width: 20px; + height: 20px; + display: inline-block; + vertical-align: middle; + + img { + display: none; + cursor: pointer; + } +} + +.mvv-inst-add-button { + width: 20px; + height: 20px; + vertical-align: middle; + display: inline-block; + + img { + display: none; + cursor: pointer; + } +} + +.mvv-select-group { + padding-bottom: 25px; + + li { + font-weight: bold; + + ul { + padding: 10px 0 0 10px; + list-style-type: none; + + li { + font-weight: normal; + } + } + } +} + +.mvv-orig-lang { + display: none; + border: 1px solid @light-gray-color-40; + margin: 3px; + padding: 3px; + font-size: 0.9em; + background-color: #fff; + max-height: 10em; + overflow: auto; +} + +#lvgruppe_selection { + padding: 1em; + + h3 { + margin-top: 1em; + } + + &.odd { + background-color: @dark-gray-color-5; + } + + &.even { + background-color: @content-color-20; + } +} + +#lvgruppe_selection_chosen { + width: 49%; + float: left; + margin: 0; + padding: 0; + + ul { + /* list-style: none; */ + padding: 0; + margin: 0; + + li { + list-style: none; + padding: 0 0 0 1em; + margin: 0; + } + } +} + +#lvgruppe_selection_none, +#lvgruppe_selection_at_least_one { + font-style: italic; +} + +#lvgruppe_selection_selectables { + width: 49%; + margin: 0 0 0 50%; + padding: 0; + + ul { + padding: 0; + margin: 0; + + li { + list-style: none; + padding: 0 0 0 1em; + margin: 0; + } + } +} + + +#lvgruppe_selection_selected li ul li { + list-style: inside; + padding-bottom: 0.3em; +} + +#admin_seminare_assi #lvgruppe_selection { + font-size: 0.8em; + + h3 { + font-weight: normal; + } +} + +.mvv-no-entry { + font-style: italic; +} + +.mvv-content-overlay { + width: 1100px; + position: absolute; + background-color: #fff; + height: 80%; +} + +.mvv-content-overlay-close{ + right: -15px; +} + +.mvv-content-overlay-inner { + overflow: auto; + position: absolute; + left: 18px; + right: 18px; + top: 18px; + bottom: 18px; +} + +.quicksearch_frame { + white-space: nowrap; +} + +.difflog { + color: brown; + font-size: smaller; + vertical-align: text-top; + text-decoration: none; +} diff --git a/resources/assets/stylesheets/less/navigation-hoverborder.less b/resources/assets/stylesheets/less/navigation-hoverborder.less new file mode 100644 index 0000000..73d3f1f --- /dev/null +++ b/resources/assets/stylesheets/less/navigation-hoverborder.less @@ -0,0 +1,100 @@ +@transition-duration: 300ms; +.border-beneath(@color, @margin: 2px, @height: 3px) when (@color = 'dark') { + .border-beneath(@dark-gray-color-80, @height, @margin); +} +.border-beneath(@color, @margin: 2px, @height: 3px) when (@color = 'light') { + .border-beneath(@dark-gray-color-40, @height, @margin); +} +.border-beneath(@color, @margin: 2px, @height: 3px) { + border-bottom: 0; + padding-bottom: 0; + position: relative; + + &::after { + position: absolute; + top: 100%; + left: 0; + right: 0; + opacity: 1; + content: ''; + display: block; + background-color: @color; + height: @height; + margin-top: @margin; + + transition: left @transition-duration, + right @transition-duration, + opacity @transition-duration; + } +} +.border-shrink() { + left: 50%; + right: 50%; + opacity: 0; +} + +body:not(.fixed) #barTopMenu { + > li.active { + .border-beneath('dark'); + } + > li:not(.active) { + .border-beneath('light'); + &:not(:hover)::after { + .border-shrink(); + } + } + + &:hover > li:not(:hover)::after { + .border-shrink(); + } + + .action-menu-icon { + transform: rotate(-90deg); + } + + .overflow li:hover { + .border-beneath('light', 2px, 3px); + &::after { + transform: translate(0, -4px); + } + } +} + +#tabs { + > li { + &, &.current, &:hover { + line-height: 25px; + } + &.current { + .border-beneath('dark', -2px, 3px); + } + &:not(.current) { + .border-beneath('light', -2px, 3px); + &:not(:hover)::after { + .border-shrink(); + } + } + } + + &:hover > li:not(:hover)::after { + .border-shrink(); + } +} + +/*#barTopAvatar { + &:not(.fixed) { + &, &.active, &:hover { + line-height: 25px; + padding-top: 3px; + } + &.active { + .border-beneath('dark', 1px); + } + &:not(.active) { + .border-beneath('light', 1px); + &:not(:hover)::after { + .border-shrink(); + } + } + } +}*/ diff --git a/resources/assets/stylesheets/less/navigation.less b/resources/assets/stylesheets/less/navigation.less new file mode 100644 index 0000000..1b50c29 --- /dev/null +++ b/resources/assets/stylesheets/less/navigation.less @@ -0,0 +1,316 @@ +/* --- main navigation ----------------------------------------------------- */ +body:not(.fixed) #barTopMenu { + align-self: flex-end; + + margin: 0 0 4px 5px; + padding: 0; + z-index: 1000; + font-size: 0; + + > li { + display: inline-block; + list-style-type: none; + width: 64px; + height: 55px; + padding: 0; + z-index: 2; + font-size: @font-size-base; + } + a { + color: @base-color; + display: block; + padding: 0 0px; + text-align: center; + line-height: 1em; + + // Icon state: normal + span { + background: no-repeat 0 0; + display: inline-block; + .square(32px); + + // Icon state: new + &.new { + background-position: -64px 0; + } + } + + img { + margin: 8px 0px; + .square(32px); + } + &[data-badge]:not([data-badge="0"]) { + position: relative; + + &::before { + position: absolute; + left: 50%; + top: 0; + + margin-left: 5px; + .square(16px); + + background-clip: content-box; + background-color: @red; + border: 3px solid @dark-gray-color-5; + border-radius: 50%; + color: white; + content: attr(data-badge); + display: inline-block; + font-size: 10px; + z-index: 2; + } + } + } + + > li > a, + > li > label { + .navtitle { + position: absolute; + white-space: nowrap; + + left: 50%; + transform: translate(-50%, 0); + + opacity: 0; + margin-top: -10px; + font-size: 0.9em; + } + } + + img { + filter: hue-rotate(350deg) saturate(8.7%) brightness(177.3%) !important; + } + + // Hide all navigation item title on hover and display only the currently + // hovered one + .navtitle { + transition: opacity 300ms; // Smooth change when entering + } + &:hover { + > li.active .navtitle { + opacity: 0; + } + > li:hover .navtitle { + opacity: 1; + transition: opacity 0ms; // Quick change when leaving + } + } + + // Recolor on hover and for active items + li:hover, + li.active { + .navtitle { + opacity: 1; + } + > a { + img { + filter: hue-rotate(0deg) saturate(100%) brightness(100%) !important; + } + } + + // Icon state: hover + span { background-position: -32px 0; } + // Icon state: hover and new + span.new { background-position: -96px 0; } + } + + > .overflow { + position: relative; + + // Hide overflow and touch toggle + > input[type="checkbox"] { + display: none; + } + + // Rotate icon + > label img { + transition: transform 300ms; + transform: rotate(90deg); + } + + // Define transition duration for possible badge on overflow + > label > a[data-badge]::before { + transition: opacity 300ms; + } + + // Display menu on activation + &:hover label, + input[type="checkbox"]:checked { + ~ ul { + display: block; + } + img, + ~ label img { + transform: rotate(180deg); + } + > a[data-badge]::before { + opacity: 0; + } + } + + > ul { + display: none; + + position: absolute; + right: 0; + top: 100%; + z-index: 10; + + list-style: none; + margin: 5px 0 0; + padding: 4px 4px; + + background-color: @dark-gray-color-5; + border: 1px solid @dark-gray-color-40; + border-top: 0; + + min-width: 150px; + max-width: 250px; + overflow: hidden; + + li { + display: block; + line-height: 1; + a { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + + padding: 4px 0; + + &[data-badge]:not([data-badge="0"])::before { + left: 21px; + } + } + img { + flex: 1 0 20px; + .square(20px); + margin: 0 0.25em; + + } + .navtitle { + flex: 1 0 70%; + text-align: left; + white-space: nowrap; + //margin-top: -10px; + } + br { + display: none; + } + } + } + } + &:not(.overflown) > .overflow { + display: none; + } +} + +// Toggle mechanism for touch/hover +#barTopMenu-toggle { + display: none; +} +label[for="barTopMenu-toggle"] { + .background-icon('hamburger', 'info_alt', 16); + background-position: 0px center; + background-repeat: no-repeat; + + color: @white; + line-height: @bar-bottom-container-height; + overflow: hidden; + padding-left: (5px + 16px + 5px); // padding + icon + next padding + white-space: nowrap; + + height: 0; + max-height: 0; + opacity: 0; + transition: all 300ms; + + // 1/4 of the screen's width, creates a bigger hover area + width: 25vw; + +} +html.no-touch { + #barTopMenu-toggle, + label[for="barTopMenu-toggle"] { + pointer-events: none; + } +} + +body.fixed { + #flex-header { + height: @header-height; + } + + label[for="barTopMenu-toggle"] { + opacity: 1; + max-height: @bar-bottom-container-height; + height: auto; + } + #barTopMenu { + background-color: @base-color; + + list-style: none; + margin: 0px 0px 0px -15px; + padding: 0px; + position: absolute; + + width: fit-content; + + // Hide menu + display: none; + + img { + filter: contrast(0) brightness(2); + + .square(16px); + margin-right: 0.8em; + } + + li { + padding: 0.25em 15px; + > a { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + + color: @white; + } + + &:hover { + background-color: @base-color-80; + } + + &.overflow { + padding: 0; + + &:hover { + background-color: inherit; + } + + input[type="checkbox"], + label { + display: none; + } + + ul { + list-style: none; + margin: 0; + padding: 0; + } + + &:last-child { + padding-bottom: 10px; + } + } + + + } + } + #barBottomLeft:hover #barTopMenu, + #barTopMenu-toggle:checked ~ #barTopMenu { + display: block; + } +} diff --git a/resources/assets/stylesheets/less/news.less b/resources/assets/stylesheets/less/news.less new file mode 100644 index 0000000..c8e5252 --- /dev/null +++ b/resources/assets/stylesheets/less/news.less @@ -0,0 +1,93 @@ +/* --- news --------------------------------------------------- */
+#news_dialog_content {
+ overflow-y: auto;
+ padding-right: 15px;
+ padding-top: 10px
+}
+
+.news_dialog_content {
+ button {
+ vertical-align:middle;
+ }
+
+ select {
+ width: 45%;
+ }
+
+ option {
+ height: 16px;
+ }
+}
+
+.news_admin {
+ button {
+ vertical-align:middle;
+ }
+}
+
+.news_reset_filter {
+ float: right;
+}
+
+img.news_area_icon {
+ vertical-align: middle;
+}
+
+div.news_area_title {
+ display: inline;
+ height: 32px;
+ vertical-align: middle;
+}
+input.news_topic {
+ width: 90%;
+}
+
+textarea.news_body {
+ resize: vertical;
+ width: 90%;
+ font-size: 10pt;
+}
+
+select.news_area_options {
+ min-width: 200px;
+ width: 100%;
+ height: 116px;
+}
+
+optgroup.news_area_title {
+ text-indent: 26px;
+ background-repeat: no-repeat;
+
+ option {
+ text-indent: 6px;
+ }
+}
+
+input.news_search_term {
+ width: 45%;
+}
+
+div.news_area_selectable {
+ display: inline-block;
+ float: left;
+ width: 45%;
+ height: 100%;
+}
+
+div.news_area_actions {
+ display: inline-block;
+ width: 8%;
+ text-align: center;
+}
+
+div.news_area_selected {
+ display: inline-block;
+ float: right;
+ width: 45%;
+ height: 100%;
+}
+
+div.news_dialog_buttons {
+ margin-right: 15px;
+ border-top: 1px solid #d1d1d1;
+}
diff --git a/resources/assets/stylesheets/less/opengraph.less b/resources/assets/stylesheets/less/opengraph.less new file mode 100644 index 0000000..23dde88 --- /dev/null +++ b/resources/assets/stylesheets/less/opengraph.less @@ -0,0 +1,106 @@ +.opengraph-area { + margin: 10px auto 5px; + max-width: 700px; + + .switcher { + list-style: none; + text-align: right; + + li { + border-top: thin solid @dark-gray-color-20; + display: inline-block; + padding: 5px; + + &:first-child { + border-left: thin solid @dark-gray-color-20; + } + &:last-child { + border-right: thin solid @dark-gray-color-20; + } + } + .switch-left, .switch-right { + .hide-text(); + .square(20px); + background-position: center; + background-repeat: no-repeat; + padding: 0; + + &:not([disabled]) { + cursor: pointer; + } + } + .switch-left { + .background-icon('arr_1left', 'clickable'); + &[disabled] { + .background-icon('arr_1left', 'inactive'); + } + } + .switch-right { + .background-icon('arr_1right', 'clickable'); + &[disabled] { + .background-icon('arr_1right', 'inactive'); + } + } + } + + .js & .opengraph.hidden, + .js &:not(.handled) .opengraph:not(:first-of-type) { + // The second selector prevents flash of content before everything + // is setup + display: none; + } +} + +.opengraph { + @padding: 10px; + @height: 120px; + + .clearfix; + + font-size: 0.8em; + border: 1px solid @dark-gray-color-20; + padding: @padding; + min-height: @height; + + .flash-embedder { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100px; + background-position: center center; + background-repeat: no-repeat; + background-size: 100% auto; + .play { + border-radius: 100px; + transition: background-color 300ms; + background-color: rgba(0, 0, 0, 0.7); + padding: 10px; + } + &:hover .play { + background-color: rgba(0, 0, 0, 1); + } + } + .video .flash-embedder { + height: 200px; + } + + a.info { + box-sizing: border-box; + color: black; + display: block; + word-break: normal !important; + &:hover { + color: black; + } + } + .image { + .square(@height); + background-size: contain; + background-position: left center; + background-repeat: no-repeat; + display: inline-block; + float: left; + margin-right: @padding; + } +} diff --git a/resources/assets/stylesheets/less/overlapping.less b/resources/assets/stylesheets/less/overlapping.less new file mode 100644 index 0000000..5b56758 --- /dev/null +++ b/resources/assets/stylesheets/less/overlapping.less @@ -0,0 +1,81 @@ +div.mvv-ovl-modulteil { + position: absolute; + top: 0px; + left: 30px; + right: 230px; + border-bottom: solid 1px @light-gray-color-40; +} +.mvv-ovl-selection { + margin-bottom: 25px; +} +.mvv-ovl-base-abschnitt { + position: relative; + width: 100%; + height: 30px; + margin-bottom: 5px; + color: @dark-gray-color; + font-weight: 700; + font-size: 16px; + border-bottom: 1px solid @light-gray-color-40; + h2 { + position: absolute; + left: 5px; + border: none; + margin: 7px 0px; + } + & > div { + position: absolute; + left: unset; + right: 0px; + div { + display: inline-block; + width: 25px; + margin-top: 5px; + } + } +} +ul.mvv-ovl-conflict { + width: 100%; + .mvv-ovl-base-modulteil, .mvv-ovl-comp-modulteil { + > div { + position: absolute; + top: 0px; + right: 0px; + text-align: right; + border-bottom: solid 1px @light-gray-color-40; + &:first-of-type { + left: 30px; + width: auto; + text-align: left; + border-bottom: solid 1px @light-gray-color-40; + } + & > div { + display: inline-block; + width: 25px; + text-align: left; + } + } + } + .mvv-ovl-version { + font-size: 1.2em; + } +} +.mvv-ovl-base-course { + position: absolute; + width: 5px; + color: red; + left: 10px; + ~ label { + padding-left: 4px; + } +} +.mvv-overlapping-exclude { + cursor: pointer; + width: 16px; + height: 16px; + position: absolute; + background: rgba(255, 255, 255, 0.5) url("@{image-path}/icons/blue/visibility-visible.svg") center center no-repeat; + &.mvv-overlapping-invisible { + background: rgba(255, 255, 255, 0.5) url("@{image-path}/icons/blue/visibility-invisible.svg") center center no-repeat; + } +} diff --git a/resources/assets/stylesheets/less/pagination.less b/resources/assets/stylesheets/less/pagination.less new file mode 100644 index 0000000..c6b3498 --- /dev/null +++ b/resources/assets/stylesheets/less/pagination.less @@ -0,0 +1,59 @@ +.audible { + position: absolute; + left: -999em; +} +.pagination, +.pagination li { + line-height: 1.2em; + list-style: none; + margin: 0; + padding: 0; +} +.pagination { + li { + display: inline-block; + + &:not(:first-of-type) { + &::before { + content: ' | '; + font-weight: normal; + } + } + } + + .divider--template { + display: none; + } + + .pagination--link { + background-color: transparent; + border: 0; + color: @base-color; + cursor: pointer; + padding: 0; + } + + .current .pagination--link { + font-weight: bold; + color: #000; + } + + .prev, + .next { + .pagination--link { + .hide-text; + background-position: center; + background-repeat: no-repeat; + display: inline-block; + height: 16px; + width: 16px; + vertical-align: top; + } + } + .prev .pagination--link { + .background-icon('arr_1left'); + } + .next .pagination--link { + .background-icon('arr_1right'); + } +} diff --git a/resources/assets/stylesheets/less/personal-notifications.less b/resources/assets/stylesheets/less/personal-notifications.less new file mode 100644 index 0000000..10554a3 --- /dev/null +++ b/resources/assets/stylesheets/less/personal-notifications.less @@ -0,0 +1,238 @@ +#notification_marker { + width: 48px; + margin-left: 0px; + padding-left: 0px; + margin-right: 0px; + padding-right: 0px; + height: 28px; + font-size: 0.8em; + color: @base-color; + text-align: center; + line-height: 28px; + vertical-align: text-bottom; + background-color: @dark-gray-color-10; + border: 1px solid @dark-gray-color-40; + + &.alert { + background-color: @red; + color: @white; + } +} + +#notification_container { + @arrow-height: 10px; + + @list-width: 400px; + + width: 49px; + height: 30px; + /*border: thin solid @dark-gray-color-20;*/ + color: @base-color; + vertical-align: text-bottom; + background-color: @base-color; + position: relative; + + // Insert invisible padding on top of the arrow in order to try to + // close the "mouse trap gap" created by the arrow as well as an invisible + // 25px border to the left + &:hover::before { + content: ""; + display: block; + position: absolute; + bottom: -@arrow-height; + left: (-@list-width); + right: 0; + height: @arrow-height; + } + &:hover::after { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + right: 100%; + width: 25px; + } + + .list, li&:hover .list { display: none; } + &.hoverable:hover { + .list { display: block; } + } + #notification_list { + z-index: 1001; + margin-top: 10px; + ul { + width: 100%; + padding: 0; + } + .more { + font-size: 0.8em; + text-align: center; + } + } + .list { + // Creates an arrow pointing from the list to the triggering element + #arrow > .top-border(10px, @white, 1px, @light-gray-color-80); + + background-color: @white; + border-left: thin solid @light-gray-color-60; + border-top: thin solid @light-gray-color-60; + border-collapse: collapse; + color: @black; + display: none; + font-size: 1em; + position: absolute; + width: @list-width; + max-width: @list-width; + box-shadow: 1px 1px 1px @light-gray-color-80; + + // Without this, buttons or message boxes would appear on top of the list + z-index: 2; + &::before, + &::after { + left: (@list-width - 20px); + } + + // Positions: below or left'ish or right'ish to the triggering element + &.below { + left: (-@list-width + 44px); + } + &.left { + right: 0px; + &:before { + left: auto; + right: 4px; + } + } + &.right { + left: 0px; + &:before { left: 4px; } + } + + // List item + .item { + @padding: 5px; + border-top: thin solid @light-gray-color-60; + line-height: 20px; + display: block; + height: auto; + padding: @padding; + white-space: normal; + + &:hover { + background-color: fadeout(@light-gray-color, 80%); + } + + &:only-child:hover { + #arrow > .top(10px, fadeout(@light-gray-color, 80%)); + margin-top: 0; + &::before { + left: (@list-width - 20px); + z-index: 2; + } + } + + // First child: no top-border + &:first-child { + border-top: 0; + } + .content { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + + .avatar { + @avatar-size: 40px; + margin-right: 10px; + margin-left: 0px; + background-position: center center; + background-size: 100%; + background-repeat: no-repeat; + width: @avatar-size; + height: @avatar-size; + min-width: @avatar-size; + } + } + } + + a { + color: @brand-color-dark; + display: block; + padding: 0px; + &:hover { color: @active-color; } + } + + .options { + cursor: pointer; + float: right; + padding-top: 4px; + > img { + vertical-align: top; + } + + &.hidden { visibility: hidden; } + } + .item:hover .options.hidden { visibility: visible; } + } + + a.mark-all-as-read, + a.enable-desktop-notifications { + background-color: @dark-gray-color-15; + border-bottom: thin solid @dark-gray-color-45; + max-height: 31px; + padding: 5px 5px 5px 14px; + z-index: 3; + } + + a.mark-all-as-read { + // Creates an arrow pointing from the list to the triggering element + #arrow > .top-border(10px, @light-gray-color-20, 1px, @light-gray-color-80); + &::before, + &::after { + left: (@list-width - 20px); + z-index: 2; + } + + .background-icon('accept', 'clickable'); + background-repeat: no-repeat; + background-position: right 8px center; + + &:hover { + .background-icon('accept', 'attention'); + } + + margin: 0; + + // Create blind effect to hide/display this links smoothly + transition: all 300ms; + + &.notification_hidden { + border-bottom-width: 0; + max-height: 0; + opacity: 0; + padding-bottom: 0; + padding-top: 0; + pointer-events: none; + + + .enable-desktop-notifications { + // Creates an arrow pointing from the list to the triggering element + #arrow > .top-border(10px, @light-gray-color-20, 1px, @light-gray-color-80); + &::before, + &::after { + left: (@list-width - 20px); + z-index: 2; + } + margin: 0; + } + } + } + a.enable-desktop-notifications { + .background-icon('notification', 'clickable'); + background-repeat: no-repeat; + background-position: right 8px center; + + &:hover { + .background-icon('notification', 'attention'); + } + } +} diff --git a/resources/assets/stylesheets/less/plus.less b/resources/assets/stylesheets/less/plus.less new file mode 100644 index 0000000..0d090be --- /dev/null +++ b/resources/assets/stylesheets/less/plus.less @@ -0,0 +1,84 @@ +.plus { + + td.element { + padding-bottom: 2em; + } + + .element_header { + display: inline-block; + width: 250px; + margin-left: 5px; + } + + .element_description { + display: inline-block; + margin-left: 20px; + } + + .plugin_icon { + width: 16px; + height: 16px; + } + + .shortdesc { + margin-left: 3px; + } + + .plus_expert { + margin-left: 20px; + width: 97%; + + display: flex; + flex-wrap: wrap; + } + + .screenshot_holder { + width: 250px; + flex: 0 250px; + margin-right: 5mm; + box-sizing: border-box; + } + + .big_thumb { + max-width: 250px; + max-height: 250px; + padding-top: 5mm; + } + + .small_thumb { + margin-left: 2px; + margin-top: 5px; + max-height: 25px; + } + + .thumb_holder { + width: 250px; + text-align: center; + background-color: @content-color-20; + border-top: 1px solid fadeout(@brand-color-lighter, 80%); + border-bottom: 1px solid fadeout(@brand-color-lighter, 80%); + } + + .descriptionbox { + flex: 1 305px; + max-width: 45em; + } + + .keywords { + padding: 5mm; + left: 5mm; + position: relative; + } + + .longdesc { + overflow: hidden; + } + + .helplink { + float: right; + } + + article.studip > section:not(:last-child) { + border-bottom: 1px solid @table-header-color; + } +} diff --git a/resources/assets/stylesheets/less/profile.less b/resources/assets/stylesheets/less/profile.less new file mode 100644 index 0000000..c791451 --- /dev/null +++ b/resources/assets/stylesheets/less/profile.less @@ -0,0 +1,107 @@ +.profile-sidebar-details { + margin-left: 0.5em; +} + +.profile-view { + display: flex; +} +.profile-view-aside { + flex: 1 0 auto; +} +.profile-view-main { + flex: 1 1 100%; + padding: 0 1em; +} +.profile-view-actions { + .list-unstyled(); + + img { + vertical-align: text-top; + } +} + +.media-breakpoint-tiny-down({ + + table.settings-privacy { + &, thead, tbody, th, td, tr { + display: block; + } + + > tbody > tr > td { + border: none !important; + padding-left: 10%; + } + + .visibility-homepage-element { + margin-top: 2em; + } + + .visibility-homepage-element-name { + font-weight: 600; + margin-right: .75em; + } + + tbody td, + tbody td:first-child { + width: auto; + } + } +}); + + + +#select_fach_abschluss { + margin: 1em 0; + min-width: 300px; + + tbody { + td { + display: block; + white-space: nowrap; + + &:last-child { + padding-right: .5em; + } + + &::before { + content: attr(data-label); + font-weight: bold; + width: 6.5em; + display: inline-block; + } + } + + th, td { + text-align: left; + } + } +} + +#select_fach_abschluss > tbody > tr:last-child > td { + border-bottom: 1px solid @table-header-color; +} + +.media-breakpoint-small-up({ + #select_fach_abschluss tbody { + td::before { + display: none; + } + + th, td { + display: table-cell; + padding: .25em .5em; + } + th:first-child, + td:first-child { + padding-left: 0; + } + th:last-child, + td:last-child { + padding-right: 0; + } + + td:last-child { + text-align: center; + } + } +}); diff --git a/resources/assets/stylesheets/less/qrcode-print.less b/resources/assets/stylesheets/less/qrcode-print.less new file mode 100644 index 0000000..7b3ccd0 --- /dev/null +++ b/resources/assets/stylesheets/less/qrcode-print.less @@ -0,0 +1,24 @@ +@media print { + div#qr_code { + text-align: center; + + h1.title { + margin-bottom: 10mm; + } + + div.code img { + width: 170mm; + height: 170mm; + } + + .PrintAction { + display: none; + } + + div.url { + margin-top: 10mm; + font-size: 14px; + font-weight: bold; + } + } +} diff --git a/resources/assets/stylesheets/less/qrcode.less b/resources/assets/stylesheets/less/qrcode.less new file mode 100644 index 0000000..bde88bb --- /dev/null +++ b/resources/assets/stylesheets/less/qrcode.less @@ -0,0 +1,65 @@ +#qr_code { + display: none; + background-color: white; + width: 100%; + height: 100%; + flex-direction: column; + justify-content: space-around; + align-items: center; + color: #444444; + + .code > div { + margin-left: auto; + margin-right: auto; + text-align: center; + } + + .code img { + width: 70vh; + height: 70vh; + } + + .header { + background-image: url("@{image-path}/logos/logoklein.png"); + height: 100px; + width: 100%; + background-repeat: no-repeat; + background-position: center center; + } + + h1 { + text-align: center; + font-size: 1.5em; + } + + &.print-view { + .code img { + width: 170mm; + height: 170mm; + } + + .PrintAction { + display: none; + } + + .url { + font-size: 14px; + } + } +} + +#qr_code:-moz-full-screen { + display: flex; +} + +#qr_code:-webkit-full-screen { + display: flex; +} + +#qr_code:-ms-fullscreen { + display: flex; +} + +#qr_code:fullscreen { + display: flex; +} diff --git a/resources/assets/stylesheets/less/questionnaire.less b/resources/assets/stylesheets/less/questionnaire.less new file mode 100644 index 0000000..3b26c55 --- /dev/null +++ b/resources/assets/stylesheets/less/questionnaire.less @@ -0,0 +1,192 @@ +.questionnaire_edit { + section { + border: thin solid black; + margin: 3px; + } + .options { + padding: 0px; + list-style-type: none; + > li { + margin-top: 5px; + margin-bottom: 5px; + > .move { + cursor: move; + display: inline-block; + vertical-align: middle; + } + > input { + display: inline-block; + vertical-align: middle; + } + > input[type=text] { + width: calc(~"100% - 70px"); + } + .delete { + display: inline-block; + vertical-align: middle; + cursor: pointer; + } + .add { + display: none; + vertical-align: middle; + cursor: pointer; + } + } + > li:last-child .delete { + display: none; + } + > li:last-child .add { + display: inline-block; + } + > li:only-child .move { + display: none; + } + + } + .all_questions { + .question:first-child .move_up { + display: none; + } + .question:last-child .move_down { + display: none; + } + } + .add_questions { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: stretch; + border: thin dashed @content-color-40; + > a { + background-color: transparent; + margin: 10px; + border: thin solid @content-color-20; + padding: 5px; + width: 100px; + min-width: 100px; + max-width: 100px; + height: 100px; + min-height: 100px; + max-height: 100px; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + text-align: center; + > img { + margin-left: auto; + margin-right: auto; + } + } + } + .questionnaire_metadata { + margin-top: 10px; + } +} + +.questionnaire_results { + > article { + padding: 7px; + > :first-child { + margin-top: 0px; + } + } + .ct-label { + color: rgba(0, 0, 0, 0.8); + text-shadow: -1px 0px white, 0px 1px white, 1px 0px white, 0px -1px white; + font-size: 1rem; + fill: black; + } + .ct-series-a .ct-bar, .ct-series-a .ct-line, .ct-series-a .ct-point, .ct-series-a .ct-slice-donut { + //Balkenfarbe + stroke: @red; + } + //Tortenst�cke: + .ct-series-a .ct-area, .ct-series-a .ct-slice-pie { + fill: @red; + } + .ct-series-b .ct-area, .ct-series-b .ct-slice-pie { + fill: @brand-color-dark; + } + .ct-series-b .ct-label, .ct-series-a .ct-label { + //color: white; + //text-shadow: -1px 0 @light-gray-color, 0 1px @light-gray-color, 1px 0 @light-gray-color, 0 -1px @light-gray-color; + //fill: white; + } + .ct-series-c .ct-area, .ct-series-c .ct-slice-pie { + fill: @activity-color; + } + .ct-series-d .ct-area, .ct-series-d .ct-slice-pie { + fill: @content-color; + } + .ct-series-e .ct-area, .ct-series-e .ct-slice-pie { + fill: @orange; + } + + table tbody tr:last-child td { + border-bottom: 0; + } +} + + +.questionnaire_answer > article { + padding: 7px; + border: none; + border-top: 1px solid @content-color-40; + > :first-child { + margin-top: 0px; + } + .invalidation_notice { + color: @red; + } +} + +.courseselector, +.instituteselector, +.statusgroupselector { + > li > label { + cursor: pointer; + > input:checked + span { + text-decoration: line-through; + } + } +} + +.questionnaire .terms, .questionnaire_results .terms { + text-align: center; + border-top: thin solid @content-color-40; + color: @light-gray-color; + margin: 0 -10px; +} + +#qr_code { + display: none; + background-color: white; + width: 100%; + height: 100%; + flex-direction: column; + justify-content: space-around; + align-items: center; + color: #444444; + .code > div { + margin-left: auto; + margin-right: auto; + text-align: center; + } + .code img { + width: 70vh; + height: 70vh; + } + + .header { + background-image: url("@{image-path}/logos/logoklein.png"); + height: 100px; + width: 100%; + background-repeat: no-repeat; + background-position: center center; + } +} +#qr_code:fullscreen { + display: flex; +} diff --git a/resources/assets/stylesheets/less/quicksearch.less b/resources/assets/stylesheets/less/quicksearch.less new file mode 100644 index 0000000..35d7ae7 --- /dev/null +++ b/resources/assets/stylesheets/less/quicksearch.less @@ -0,0 +1,136 @@ +/* --- Quicksearch ---------------------------------------------------------- */ +form#search_sem_quick_search_frame { + display: flex; + align-items: center; +} + +input.quicksearchbox { + background-color: @dark-gray-color-10; + border: 1px solid @dark-gray-color-40; + color: @base-color; + font-size: 14px; + width: 250px; + height: 19px; + padding-left: 6px; +} + +div.quicksearch_frame { + text-indent: 0; + + input[type="text"] { + box-sizing: border-box; + border-width: 1px 30px 1px 1px; + border-style: solid; + border-color: @base-color-60; + border-image: none; + display: inline-block !important; + } + + input[name=course_search_button] { + margin-left: -34px !important; + margin-top: 0px !important; + } + + input[type=submit] { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; + width: 29px; + height: 24px; + .background-icon('search', 'info_alt'); + background-position: center; + background-repeat: no-repeat; + vertical-align: middle; + margin-left: -32px; + margin-top: 1px; + } +} + +.extendedLayout, +.studip-quicksearch { + .ui-autocomplete { + max-height: 275px; + overflow-y: auto; + overflow-x: hidden; + z-index: 99999; + } + + .ui-menu-item { + white-space: nowrap; + } + + .ui-menu-item a img { + float: left; + width: 40px; + height: 40px; + margin-right: 5px; + } +} +.quicksearchbutton { + border: 0; + margin-left: 6px; + padding: 0; + vertical-align: text-bottom; +} +.ui-autocomplete { + z-index: 99999; +} + +.quicksearch_select { + width: calc(100% - 32px); + + + input[type=submit] { + float: none; + margin-left: 0px; + height: 30px; + background-color: #7E92B0; + background-position: center center; + } +} + +.quicksearch_container { + display: inline-flex; + flex-direction: row-reverse; + width: 100%; + + .dropdownmenu { + max-width: 0px; + max-height: 0px; + overflow: visible; + position: relative; + top: 31px; + + .autocomplete__results { + list-style-type: none; + padding: 1px; + border: 1px solid @light-gray-color-40; + background-color: white; + z-index: 99999; + width: 200px; + max-height: 275px; + width: 600px; + overflow-x: auto; + overflow-y: hidden; + + > li { + padding: 5px; + cursor: pointer; + display: flex; + align-items: flex-start; + + &:hover, &.autocomplete__result--selected { + background-color: @base-color; + color: white; + } + + img { + max-width: 40px; + max-height: 40px; + margin-right: 5px; + } + } + } + } +} diff --git a/resources/assets/stylesheets/less/raumzeit.less b/resources/assets/stylesheets/less/raumzeit.less new file mode 100644 index 0000000..96d8303 --- /dev/null +++ b/resources/assets/stylesheets/less/raumzeit.less @@ -0,0 +1,115 @@ +ul.termin_related { + padding: 0; + margin: 5px 0 10px 0; + li { + padding: 0; + margin: 0; + list-style: none; + position: relative; + width: 325px; + } +} + +div.at_least_one_teacher { + width: 325px; +} + +.is_ex_termin { + color: @dark-gray-color-80; + text-decoration: line-through; +} + +.contentbox.timesrooms { + > form { + article { + border-color: @content-color-40; + border-style: solid; + border-width: 1px; + margin: 10px; + + > section { + max-height: 0px; + opacity: 0; + padding: 0; + transition: opacity 0.3s; + } + + &:not(.open) header ~ * { + max-height: 0px; + opacity: 0; + overflow: auto; + transition: opacity 0.3s; + } + + &.open { + > p, > section, > footer, > div { + max-height: none; + opacity: 1; + transition: opacity 0.3s; + } + + footer { + border-top-style: solid; + } + + header h1 a::before { + transform: rotate(90deg); + } + } + + // Flex aligment center so that elements won't stick to the top + header { + align-items: center; + } + + // Show visual toggle indicator + header h1 a { + .icon('before', 'arr_1right', 'clickable'); + } + + &.open { + header h1 a::before { + transform: rotate(90deg); + } + } + } + } + article header { + &.red { + border-left: 3px solid @red; + } + &.yellow { + border-left: 3px solid @activity-color; + } + &.green { + border-left: 3px solid @green; + } + &.red .tooltip-icon { + .icon('before', 'arr_1right', 'clickable'); + .icon('before', 'radiobutton-checked', 'status-red'); + } + &.yellow .tooltip-icon { + .icon('before', 'radiobutton-checked', 'status-yellow'); + } + &.green .tooltip-icon { + .icon('before', 'radiobutton-checked', 'status-green'); + } + } + form.default { + td label { + margin-top: 0; + } + tfoot select { + max-width: 30em; + } + } +} + +.times-rooms-grid .selectbox input[type="radio"]:checked + label { + font-weight: bold; + text-decoration: underline; +} + +.bookable_rooms_action { + cursor: pointer; +} diff --git a/resources/assets/stylesheets/less/resources-print.less b/resources/assets/stylesheets/less/resources-print.less new file mode 100644 index 0000000..889f1d4 --- /dev/null +++ b/resources/assets/stylesheets/less/resources-print.less @@ -0,0 +1,12 @@ +@media print { + #booking-plan-header-semrow-line #booking-plan-header-semweek-part, + #booking-plan-header-resource-name-line, + #booking-plan-header-seats-line, + #booking-plan-header-administration_url-line { + display: none; + } + + section.booking-plan-area { + page-break-inside: avoid; + } +} diff --git a/resources/assets/stylesheets/less/responsive.less b/resources/assets/stylesheets/less/responsive.less new file mode 100644 index 0000000..93bbfa8 --- /dev/null +++ b/resources/assets/stylesheets/less/responsive.less @@ -0,0 +1,502 @@ +@import (reference) "breakpoints.less"; +@import (reference) "visibility.less"; + +@header-bar-container-height:40px; + +@responsive-menu-width: 270px; +@responsive-menu-shadow-width: 6px; +@responsive-menu-shadow-color: rgba(0, 0, 0, 0.5); + +// Responsive main navigation (hamburger navigation to the left) +#responsive-container { + display: none; + user-select: none; + + input[type="checkbox"] { + display: none; + } + label[for="responsive-toggle"]:first-child { + .icon('before', 'hamburger-icon', 'info_alt', 20); + cursor: pointer; + } + + ul, li { + list-style-type: none; + margin: 0; + padding: 0; + } + li { + border-top: 1px solid @brand-color-lighter; + } + a { + color: white; + &:hover { + color: white; + } + } + + .nav-label { + .hide-text(); + .icon('before', 'arr_1right', 'info_alt', 24); + &:before { + transition: transform 300ms; + vertical-align: text-bottom; + } + + position: absolute; + left: 5px; + top: 5px; + + border-right: 1px solid @brand-color-lighter; + padding-right: 2px; + } + + // Create second, invisible toggle that closes the menu when clicked/touched + // outside of the menu + label[for="responsive-toggle"]:last-child { + display: none; + + position: fixed; + top: @header-bar-container-height; + right: 0; + bottom: 0; + left: @responsive-menu-width; + height: 100vh; + } +} + +#responsive-navigation { + #gradient > .horizontal(@brand-color-darker, @brand-color-light); + background-clip: content-box; + transition: left 300ms; + + + left: (-@responsive-menu-width - @responsive-menu-shadow-width); + &.visible { + left: 0; + + + label[for="responsive-toggle"] { + display: initial; + } + } + + position: fixed; + top: @header-bar-container-height; // + 1px white border + bottom: 0; + height: calc(100vh - @header-bar-container-height); + + box-sizing: border-box; + max-width: @responsive-menu-width; + width: @responsive-menu-width; + margin-bottom: @header-bar-container-height; + + border-right: @responsive-menu-shadow-width solid @responsive-menu-shadow-color; + overflow: auto; + + > li { + &:first-child { + border-top: none; + } + + .icon { + .square(26px); + display: inline-block; + padding-right: 8px; + vertical-align: text-bottom; + width: 26px; + } + + > .navigation-item { + font-size: 1.3em; + margin: 10px; + } + } + + .navigation-item { + position: relative; + } + + .nav-title { + display: block; + padding-left: 34px; + a { + display: block; + padding: 5px; + } + } + + ul { + transition: max-height 400ms ease; + + max-height: 0px; + overflow: hidden; + > li { + background-color: @brand-color-lighter; + > .navigation-item { + padding: 10px; + } + } + + .icon { + display: none; + } + } + input:checked + label::before { + transform: rotate(90deg); + } + + input:checked + label + ul { + max-height: 600px; + > li { + background-color: mix(rgba(0, 0, 0, 0.2), @brand-color-lighter); + } + } +} + +// Responsive sidebar menu (small hamburger menu to the right) +#barBottomright #sidebar-menu { + .icon('before', 'hamburger-icon-small', 'info_alt', 20); + cursor: pointer; + display: none; + margin: 0 2px; + text-align: right; + vertical-align: top; +} + + +/* @deprecated use .hidden-medium-up */ +.media-breakpoint-medium-up({ + .responsive-visible { + display: none; + } +}); + +.responsive-display { + // Hide sidebar from view until .responsified + &:not(.responsified) { + #layout-sidebar { + display: none; + } + } + + .media-breakpoint-small-down({ + #header, #flex-header, #barTopFont, #barTopMenu, + #barBottomLeft .current_page, #barBottommiddle, #barBottomLeft, #barBottomArrow, + #tabs, .sidebar-image, #sidebar-navigation:not(.show), #barTopFont, #footer, .sidebar-widget-header, + .tabs_wrapper .colorblock { + display: none !important; + } + + #layout_wrapper #layout_page { + .secondary-navigation { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + + background-color: @dark-gray-color-10; + border-bottom: 1px solid @dark-gray-color-40; + + .colorblock, + #layout_context_title, + .context_icon, + .tabs_wrapper { + transition: unset; + } + + #layout_context_title, + .tabs_wrapper { + background: transparent; + border-width: 0; + flex: 1; + } + + #layout_context_title { + flex: 1; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + + .tabs_wrapper { + flex: 0; + align-self: flex-end; + } + } + } + } + #layout_wrapper #layout_page .tabs_wrapper { + justify-content: flex-end; + .helpbar-container { + top: 0px; + right: 6px; + } + } + .responsive-hidden { + display: none; + } + #notification_marker { + display: inline-block; + margin-top: 0; + vertical-align: initial; + + width: 22px; + padding-left: 5px; + padding-right: 5px; + height: 20px; + line-height: 20px; + } + + #barTopAvatar { + position: relative; + bottom: 0px; + right: 0px; + line-height: 20px !important; + + #header_avatar_menu { + display: none; + } + + &::after { + display: none !important; + } + } + + #barBottomContainer { + box-sizing: border-box; + height: @header-bar-container-height; + position: fixed; + top: 0; + margin-left: 0px; + margin-right: 0px; + width: 100%; + } + + #barBottomright, #barBottomright ul { + box-sizing: border-box; + flex: 1; + } + + #barBottomright { + flex: 1 !important; + #sidebar-menu { + display: inline-block; + } + .list { + &::before, &::after { + display: none; + } + @width: 300px; + @arrow-height: 10px; + + margin-top: 2px; + width: @width; + max-width: @width; + + &.below { + left: (-@width + 90); + &:before { + left: (@width - 90); + } + } + + } + + > ul > li { + flex: 1 0 auto; + + &:first-child { + flex: 1 1 100%; + } + } + } + + #notification_container { + position: inherit !important; + /*top: 8px;*/ + width: 32px; + height: 20px; + } + + #responsive-container { + display: block; + } + + #layout_page { + margin-left: 0; + margin-right: 0; + } + + #layout_page, #layout_container, #barBottomContainer, #flex-header, #layout_content { + min-width: inherit !important; + } + + #layout_container { + margin-left: 0px; + margin-right: 0px; + } + + #layout_content { + margin: 0px 4px; + } + + .visible-sidebar { + right: 0 !important; + transition: right 300ms; + } + #layout-sidebar.visible-sidebar #sidebar-shadow-toggle { + display: initial; + } + + #layout-sidebar { + #gradient > .horizontal(@brand-color-light, @brand-color-darker); + background-clip: content-box; + transition: right 300ms; + + position: fixed; + top: @header-bar-container-height; + right: (-@responsive-menu-width - @responsive-menu-shadow-width); + left: auto; + bottom: 0; + + margin-right: 0px; + + width: @responsive-menu-width; + + overflow: hidden; + overflow-y: auto; + z-index: 10000; + + border-left: @responsive-menu-shadow-width solid @responsive-menu-shadow-color; + + .sidebar { + box-sizing: border-box; + + &:before { + border: 0 !important; + } + + top: auto !important; + width: @responsive-menu-width !important; + + background: inherit; + border: 0; + } + + .widget-links li.active { + &:before, &:after { + display: none; + } + margin-right: 0; + } + + // Create second, invisible toggle that closes the menu when + // clicked/touched outside of the menu + #sidebar-shadow-toggle { + position: fixed; + top: @header-bar-container-height; + right: @responsive-menu-width; + bottom: 0; + left: 0; + height: 100vh; + + display: none; + } + } + #index, + #login, + #request_new_password, + #web_migrate { + div.index_container { + height: calc(100% - 74px); + position: static; + top: 0; + + div.messagebox, + div.index_main { + margin: 1em auto; + } + } + + #background-desktop, + #background-mobile { + position: fixed; + } + } + + #layout_footer { + display: block; + min-width: 0; + width: 100vw; + } + }); + + .media-breakpoint-tiny-down({ + #index, + #login, + #request_new_password, + #web_migrate { + div.index_container { + div.messagebox, + div.index_main { + margin: 0 auto; + } + } + } + }); +} + +// Hide duplicated avatar menu in sidebar as default +.sidebar-avatar-menu { + display: none; + margin-top: 0 !important; +} + +.responsive-display { + .sidebar-avatar-menu { + display: block; + } + + #quicksearch_item { + padding: 0; + } + #search_sem_quick_search_frame { + display: flex; + flex-direction: row; + justify-content: flex-end; + + .quicksearchbox { + transition: all 300ms; + opacity: 0; + max-width: 0; + } + + &.open { + .quicksearchbox { + opacity: 1; + max-width: 1000px; + width: 100% !important; + } + } + } + + #barBottomright { + ul { + li:first-child { + flex: 1 0 auto; + } + li#quicksearch_item { + flex: 1 1 100%; + } + } + } + + table.default tfoot .button { + margin-top: 0.5em; + margin-bottom: 0.5em; + } + + .ui-dialog.ui-widget.ui-widget-content.studip-confirmation { + min-width: 20vw; + max-width: 100vw; + } +} diff --git a/resources/assets/stylesheets/less/schedule.less b/resources/assets/stylesheets/less/schedule.less new file mode 100644 index 0000000..4364390 --- /dev/null +++ b/resources/assets/stylesheets/less/schedule.less @@ -0,0 +1,439 @@ +/* --- ablaufplan / dates --------------------------------------------------- */ +.dates_items th, .dates_items td { + border-bottom: 3px solid #fff; +} + +.dates_opened td { + border-bottom: 3px solid #f3f5f8; +} + +.dates_content td { + padding: 10px; +} + +#schedule { + width: 100%; + height: 100%; +} + +#schedule_headings { + margin-left: 41px; + background-color: #e8eef7; +} + +table.schedule_headings td { + background-color: #e8eef7; +} + +div.schedule_day { + border-right: 3px double #ddd; + position: relative; +} + +div.schedule_marker { + border-bottom: 1px dotted #ddd; + border-top: 1px solid #ddd; + padding: 0; +} + +div.schedule_hours { + border-top: 1px solid #ddd; + border-right: 1px solid #ddd; + color: black; + padding-bottom: 1px; + padding-right: 3px; +} + +div.schedule_entry { + font-size: 10px; + margin: 0; + overflow: hidden; + padding: 0 0 2px; + position: absolute; + + &.clickable { cursor: pointer; } + + dl { + color: white; + height: 100%; + margin: 0; + + &.hover:hover { opacity: 0.7; } + + a { + color: white; + &:hover { text-decoration: underline; } + } + dd { + margin: 0; + overflow: hidden; + padding: 2px; + word-wrap: break-word; + font-weight: 600; + } + } +} + +div.snatch { + bottom: 4px; + cursor: ns-resize; + padding-bottom: 2px; + position: absolute; + text-align: center; + width: 100%; + + div { + border-top: 3px double white; + cursor: ns-resize; + height: 0; + margin-left: auto; + margin-right: auto; + width: 10px; + } +} + +#schedule_new_entry { + background-color: #e8eef7; + border: 2px solid #e0e0f0; + height: 230px; + position: absolute; + width: 400px; + z-index: 3; +} +#schedule_entry_new { + dl { + border: 1px solid #5c2d64; + background-color: #7c5783; + } + dt { background-color: #5c2d64; } +} + +div.schedule_edit_entry, #schedule_settings { + background-color: #E8EEF7; + border: 2px solid #E0E0F0; + height: auto; + left: 50%; + margin-left: -25%; + max-height: 80em; + min-height: 15em; + min-width: 700px; + padding-bottom: 1em; + position: absolute; + top: 180px; + width: 50%; + z-index: 4; +} + +div.schedule_edit_entry > form { + margin-right: 10px; + padding-left: 10px; + padding-top: 10px; +} + +#schedule_entry_hours { + display: inline; + padding: 2px; +} + +.schedule_icons { + position: absolute; + right: 0; + top: 0; +} + +div.invisible_entry { + opacity: 0.8; +} + +span.invisible_entry { + background-color: #600; + font-style: italic; +} + +div.schedule_settings { + float: left; + margin-left: 10px; + + div { + font-weight: bold; + } +} + +.schedule-dialog { + display: block; + outline: 0px none; + z-index: 1002; + + position: absolute; + height: 400px; + width: 600px; + top: 50%; + left: 50%; + + margin: -200px 0 0 -300px; +} + +td.schedule-adminbind { + & > span { + margin-right: 10px; + } +} + +#color_picker { + div { + display: flex; + flex-wrap: wrap; + } + + span { + flex: 0 0 auto; + + padding: 3px; + vertical-align: middle; + } + + input[type="radio"] { + display: none; + + &:checked + label { + outline: 1px solid @black; + + position: relative; + .icon('before', 'accept', 'info', 24px); + &::before { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + filter: drop-shadow(0 0 2px @white); + } + } + } + + label { + border: 1px solid @white; + display: inline-block; + height: 32px; + width: 32px; + + &.schedule-category1 { + background-color: @calendar-category-1; + } + &.schedule-category2 { + background-color: @calendar-category-2; + } + &.schedule-category3 { + background-color: @calendar-category-3; + } + &.schedule-category4 { + background-color: @calendar-category-4; + } + &.schedule-category5 { + background-color: @calendar-category-5; + } + &.schedule-category6 { + background-color: @calendar-category-6; + } + &.schedule-category7 { + background-color: @calendar-category-7; + } + &.schedule-category8 { + background-color: @calendar-category-8; + } + &.schedule-category9 { + background-color: @calendar-category-9; + } + &.schedule-category10 { + background-color: @calendar-category-10; + } + &.schedule-category11 { + background-color: @calendar-category-11; + } + &.schedule-category12 { + background-color: @calendar-category-12; + } + &.schedule-category13 { + background-color: @calendar-category-13; + } + &.schedule-category14 { + background-color: @calendar-category-14; + } + &.schedule-category15 { + background-color: @calendar-category-15; + } + &.schedule-category255 { + background-color: @calendar-category-255; + } + } +} + +div.schedule_entry { + + dl { + &.schedule-category1 { + background-color: @calendar-category-1-aux; + border: 1px solid @calendar-category-1; + dt { + background-color: @calendar-category-1; + color: contrast(@calendar-category-1, black, white); + } + dd { + color: contrast(@calendar-category-1-aux, black, white); + } + } + &.schedule-category2 { + background-color: @calendar-category-2-aux; + border: 1px solid @calendar-category-2; + dt { + background-color: @calendar-category-2; + color: contrast(@calendar-category-2, black, white); + } + dd { + color: contrast(@calendar-category-2-aux, black, white); + } + } + &.schedule-category3 { + background-color: @calendar-category-3-aux; + border: 1px solid @calendar-category-3; + dt { + background-color: @calendar-category-3; + color: contrast(@calendar-category-3, black, white); + } + dd { + color: contrast(@calendar-category-3-aux, black, white); + } + } + &.schedule-category4 { + background-color: @calendar-category-4-aux; + border: 1px solid @calendar-category-4; + dt { + background-color: @calendar-category-4; + color: contrast(@calendar-category-4, black, white); + } + dd { + color: contrast(@calendar-category-4-aux, black, white); + } + } + &.schedule-category5 { + background-color: @calendar-category-5-aux; + border: 1px solid @calendar-category-5; + dt { + background-color: @calendar-category-5; + color: contrast(@calendar-category-5, black, white); + } + dd { + color: contrast(@calendar-category-5-aux, black, white); + } + } + &.schedule-category6 { + background-color: @calendar-category-6-aux; + border: 1px solid @calendar-category-6; + dt { + background-color: @calendar-category-6; + color: contrast(@calendar-category-6, black, white); + } + dd { + color: contrast(@calendar-category-6-aux, black, white); + } + } + &.schedule-category7 { + background-color: @calendar-category-7-aux; + border: 1px solid @calendar-category-7; + dt { + background-color: @calendar-category-7; + color: contrast(@calendar-category-7, black, white); + } + dd { + color: contrast(@calendar-category-7-aux, black, white); + } + } + &.schedule-category8 { + background-color: @calendar-category-8-aux; + border: 1px solid @calendar-category-8; + dt { + background-color: @calendar-category-8; + color: contrast(@calendar-category-8, black, white); + } + dd { + color: contrast(@calendar-category-8-aux, black, white); + } + } + &.schedule-category9 { + background-color: @calendar-category-9-aux; + border: 1px solid @calendar-category-9; + dt { + background-color: @calendar-category-9; + color: contrast(@calendar-category-9, black, white); + } + dd { + color: contrast(@calendar-category-9-aux, black, white); + } + } + &.schedule-category10 { + background-color: @calendar-category-10-aux; + border: 1px solid @calendar-category-10; + dt { + background-color: @calendar-category-10; + color: contrast(@calendar-category-10, black, white); + } + dd { + color: contrast(@calendar-category-10-aux, black, white); + } + } + &.schedule-category11 { + background-color: @calendar-category-11-aux; + border: 1px solid @calendar-category-11; + dt { + background-color: @calendar-category-11; + color: contrast(@calendar-category-11, black, white); + } + dd { + color: contrast(@calendar-category-11-aux, black, white); + } + } + &.schedule-category12 { + background-color: @calendar-category-12-aux; + border: 1px solid @calendar-category-12; + dt { + background-color: @calendar-category-12; + color: contrast(@calendar-category-12, black, white); + } + dd { + color: contrast(@calendar-category-12-aux, black, white); + } + } + &.schedule-category13 { + background-color: @calendar-category-13-aux; + border: 1px solid @calendar-category-13; + dt { + background-color: @calendar-category-13; + color: contrast(@calendar-category-13, black, white); + } + dd { + color: contrast(@calendar-category-13-aux, black, white); + } + } + &.schedule-category14 { + background-color: @calendar-category-14-aux; + border: 1px solid @calendar-category-14; + dt { + background-color: @calendar-category-14; + color: contrast(@calendar-category-14, black, white); + } + dd { + color: contrast(@calendar-category-14-aux, black, white); + } + } + &.schedule-category15 { + background-color: @calendar-category-15-aux; + border: 1px solid @calendar-category-15; + dt { + background-color: @calendar-category-15; + color: contrast(@calendar-category-15, black, white); + } + dd { + color: contrast(@calendar-category-15-aux, black, white); + } + } + } +} diff --git a/resources/assets/stylesheets/less/scroll-to-top.less b/resources/assets/stylesheets/less/scroll-to-top.less new file mode 100644 index 0000000..c3a7bc1 --- /dev/null +++ b/resources/assets/stylesheets/less/scroll-to-top.less @@ -0,0 +1,27 @@ +body #scroll-to-top { + @scroll-to-top-height: 45px; + @scroll-to-top-width: 45px; + @scroll-to-top-margin: 35px; + width: @scroll-to-top-height; + height: @scroll-to-top-width; + margin-right: @scroll-to-top-margin; + margin-bottom: @scroll-to-top-margin; + padding: 10px; + background: @base-color; + border: .05rem solid transparent; + background-clip: padding-box; + cursor: pointer; + box-sizing: border-box; + position: fixed; + right: 0; + bottom: 0; + transition: all 250ms ease-in-out; + z-index: 1; + &:hover { + background: @brand-color-darker; + border-radius: .12rem; + } + &.hide { + bottom: calc( 0px - @scroll-to-top-height - @scroll-to-top-margin); + } +} diff --git a/resources/assets/stylesheets/less/search.less b/resources/assets/stylesheets/less/search.less new file mode 100644 index 0000000..12319dc --- /dev/null +++ b/resources/assets/stylesheets/less/search.less @@ -0,0 +1,230 @@ +label.inactive-settings-category { + color:red; +} + +#search { + // "Searching..." info + #searching-gif { + @icon-size: 32px; + + color: @dark-gray-color-45; + display: none; + text-align: center; + + background-image: url("@{image-path}/ajax-indicator-black.svg"); + background-position: center bottom; + background-repeat: no-repeat; + background-size: @icon-size; + margin-bottom: 10px; + padding-bottom: (@icon-size + 5px); + } + &.is-searching { + #searching-gif { + display: block; + } + #search-results { + display: none; + } + } + + #search-no-result { + display: none; + } + + #search-term-invalid { + display: none; + } + + #search-results { + &:empty { + display: none; + } + + article { + border: 1px solid @content-color-40; + margin-bottom: 8px; + margin-top: 8px; + + > header { + background-color: @content-color-20; + color: @base-color; + + display: flex; + flex-direction: row; + flex-wrap: nowrap; + + font-weight: bold; + padding: 3px; + + div.search-category { + flex: auto; + } + + div.search-more-results { + font-size: @font-size-base; + font-weight: normal; + line-height: @font-size-h3; + margin-bottom: auto; + margin-top: auto; + margin-right: 5px; + text-align: right; + } + } + + section { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + + padding: 8px; + transition: background-color @transition-duration; + + &.search-is-subcourse { + padding-left: 30px; + } + + &:not(:first-child) { + border-top: 1px solid @content-color-40; + } + + &:hover { + background-color: fadeout(@light-gray-color, 80%); + } + + &.search-extended-result { + display: none; + } + + & > a { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + margin: 0; + width: 100%; + } + + .search-result-img { + flex: 0; + margin: 0; + margin-right: 8px; + + img { + .square(36px); + vertical-align: middle; + } + } + + .search-result-data { + flex: 1; + overflow: hidden; + margin-right: 6px; + + .search-has-subcourses { + float: left; + padding-right: 5px; + } + + .search-result-title { + font-size: @font-size-base; + font-weight: bold; + } + + .search-result-details { + color: @dark-gray-color-80; + font-size: @font-size-small; + } + } + + .search-result-information { + display: flex; + flex: 1; + overflow: hidden; + margin-right: 6px; + flex-direction: column; + + + .search-result-time { + color: @dark-gray-color-80; + flex: 1; + font-size: @font-size-small; + text-align: right; + white-space: nowrap; + } + + .search-result-additional { + color: @dark-gray-color-80; + font-size: @font-size-small; + text-align: right; + } + + .search-result-admission-state { + text-align: right; // keep it simple in order to support by older browsers + flex: 1; + } + } + + .search-result-expand { + flex: auto; + margin: 20px 0 0 -32px; + + a { + .background-icon('arr_1right', 'clickable', 24); + .square(24px); + display: inline-block; + } + } + } + } + } +} + +a.no-result { + color: grey; + pointer-events: none; + cursor: default; +} +div#div-search-input { + margin-top: 0; + margin-bottom: 16px; + + // visual adjustments for the reset button + button#reset-search { + background-color: transparent; + border: 0; + padding-left: 10px; + } + +} + +#search-active-filters { + display: flex; + flex-direction: row; + align-items: baseline; + margin: 10px 0; + h5 { + margin-right: 10px; + } + .filter-items { + .button { + background-color: @content-color-20; + color: @brand-color-dark; + min-width:auto; + border: 0; + white-space: nowrap; + padding: 8px; + margin: 0 5px; + &::before { + background-repeat: no-repeat; + content: " "; + float: right; + height: 16px; + width: 16px; + .background-icon('trash', 'clickable'); + } + + &:hover::before { + .background-icon('trash', 'attention'); + } + } + } +} diff --git a/resources/assets/stylesheets/less/selects.less b/resources/assets/stylesheets/less/selects.less new file mode 100644 index 0000000..72aff70 --- /dev/null +++ b/resources/assets/stylesheets/less/selects.less @@ -0,0 +1,204 @@ +@select-border: 1px solid @light-gray-color-40; +@select-border-focus: 1px solid @brand-color-dark; +@select-border-radius: 0; +@select-height-default: 30px; +@select-arrow-size: 10; + +select { + // Reset appearance + box-sizing: border-box; + appearance: none; + background-color: #fff; + font-size: 1em; + vertical-align: baseline; + &::-ms-expand { + display: none; + } + + border: @select-border; + border-radius: @select-border-radius; + padding: 1px 8px 1px 8px; + position: relative; + white-space: nowrap; + + &:not([multiple]):not([size]) { + // Allow all other paddings to be overwritten but the right padding + // to ensure the icon is always clearly visible + padding-right: 20px; + + .background-icon('arr_1down', 'clickable', @select-arrow-size); + background-position: right 4px center; + background-repeat: no-repeat; + + height: @select-height-default; + line-height: 1; + overflow: hidden; + text-overflow: ellipsis; + } + + &:focus { + border: @select-border-focus; + } +} + +@import (inline) "~select2/dist/css/select2.css"; + +// The wrapper is neccessary for the validation error messages to appear +// at the correct position +.select2-wrapper { + display: inline-block; + position: relative; +} + +// Resets select2's styles on the hidden select element itself and +// position it exactly over the newly created select2. +.select2-hidden-accessible { + box-sizing: border-box; + border: initial !important; + clip: initial !important; + height: initial !important; + margin: initial !important; + padding: initial !important; + opacity: 0; + width: initial !important; + + pointer-events: none; // Ignore all user interaction with this element + + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.select2-container--default { + .select2-selection--single, + .select2-selection--multiple { + border: @select-border; + border-radius: @select-border-radius; + min-height: @select-height-default; + + .select2-selection__content { + font-weight: normal; // Reset due to form.default label = bold + overflow: hidden; + text-overflow: ellipsis; + } + .select2-selection__choice { + border-radius: 0px; + padding-top: 5px; + padding-bottom: 5px; + } + } + + .select2-selection--single { + .select2-selection__clear { + .background-icon('decline', 'clickable', @select-arrow-size); + background-position: right center; + background-repeat: no-repeat; + color: transparent; + display: inline-block; + float: none; + width: unit((@select-arrow-size + 5), px); + } + + .select2-selection__rendered { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + > * { + flex: 1 1 auto; + } + .select2-selection__content { + order: 1; + } + .select2-selection__clear { + order: 2; + } + } + } + + .select2-selection--multiple { + padding-right: unit((@select-arrow-size + 5), px); + .background-icon('arr_1down', 'clickable', @select-arrow-size); + background-position: right 4px top unit((@select-height-default / 2 - 4), px); + background-repeat: no-repeat; + + .select2-selection__choice__remove { + .background-icon('decline', 'clickable', @select-arrow-size); + background-position: right center; + background-repeat: no-repeat; + color: transparent !important; + display: inline-block; + float: none; + width: unit((@select-arrow-size + 5), px); + } + + .select2-selection__choice { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + > * { + flex: 1 1 auto; + } + .select2-selection__content { + order: 1; + } + .select2-selection__choice__remove { + order: 2; + } + } + } + + .select2-selection__arrow { + .background-icon('arr_1down', 'clickable', @select-arrow-size); + background-position: right 4px center; + background-repeat: no-repeat; + + b { + visibility: hidden; + } + } + + .select2-results > .select2-results__options { + max-height: 40vh; + } + + .select2-results__option { + overflow: hidden; + padding: 3px 6px; + text-overflow: ellipsis; + white-space: nowrap; + } + + &.select2-container--open { + .select2-selection--single, + .select2-selection--multiple { + border: @select-border-focus; + } + } + +} + +.nested-select { + .select2-results > ul > li { + &.nested-item-header { + font-weight: bold; + } + &.nested-item, &.nested-item.nested-level-1 { + text-indent: 2ex; + } + &.nested-item.nested-level-2 { + text-indent: 4ex; + } + &.nested-item.nested-level-3 { + text-indent: 6ex; + } + &.nested-item.nested-level-4 { + text-indent: 8ex; + } + } + + &.institute-list .select2-results > ul > li:not(.nested-item) { + font-weight: bold; + } +} diff --git a/resources/assets/stylesheets/less/skiplinks.less b/resources/assets/stylesheets/less/skiplinks.less new file mode 100644 index 0000000..f6df62b --- /dev/null +++ b/resources/assets/stylesheets/less/skiplinks.less @@ -0,0 +1,43 @@ +/* skiplink-area highlighting -------------------------------------------- */ +#skip_link_navigation { + background-color: #fff; + border: 2px solid #f60; + left: -600px; + margin: 0; + padding: 10px; + position: fixed; + top: 20px; + z-index: 3000; + + ul { + list-style-type: none; + margin: 0; + padding: 0; + } +} + +.skip_target { + position: absolute; + .hide-text(); +} + +#skiplink_list { + display: none; +} + +body.enable-skiplinks { + *:focus, + .focus_box, + .studip-checkbox:focus + label { + &:not(:empty) { + outline: 2px dashed @orange; + } + } + + #tabs { + a:focus { + position: relative; + z-index: 100; + } + } +} diff --git a/resources/assets/stylesheets/less/smileys.less b/resources/assets/stylesheets/less/smileys.less new file mode 100644 index 0000000..19df389 --- /dev/null +++ b/resources/assets/stylesheets/less/smileys.less @@ -0,0 +1,163 @@ +.smiley-tabs { + display: flex; + justify-content: space-between; + margin: 0 0 0.5em; + line-height: 1.8em; + width: 100%; + + &, li { + list-style: none; + padding: 0; + } + li { + &:extend(#tabs li all); + + flex-grow: 1; + display: inline-block; + float: none; + margin: 0; + white-space: nowrap; + + &.favorites a { + .icon('before', 'smiley', 'info_alt'); + } + &.favorites.current a { + .icon('before', 'smiley', 'info'); + } + } + a { + &:extend(#tabs a all); + + display: block; + padding-left: 0; + padding-right: 0; + text-align: center; + } +} + +.smiley-container { + border-collapse: collapse; + width: 100%; + > tbody > tr > td { + padding: 0 1em 0 0; + &:last-child { + padding-right: 0; + } + } +} +.smiley-container, .smiley-column { + table-layout: fixed; +} +.smiley-column { + margin-right: 1em; + &:last-child { + margin-right: 0; + } +} + +.smiley-icon { + img { + height: auto; + max-width: 100%; + width: auto; + } +} + +.smiley-toggle { + .square(16px); + .hide-text; + .background-icon('checkbox-unchecked', 'clickable'); + background-position: center; + background-repeat: no-repeat; + display: inline-block; + vertical-align: middle; + + &.favorite { + .background-icon('checkbox-checked', 'clickable'); + } + &.ajax { + background-image: url("@{image-path}/ajax_indicator_small.gif"); + } +} + +.smiley-statistics { + margin: 0; + padding: 0; + + dt { + clear: left; + float: left; + padding-right: 0.5em; + + &::after { + content: ':'; + } + } + dd { + font-weight: 700; + margin: 0; + text-align: right; + } +} + +.smiley-picker { + width: 420px; + .smileys { + text-align: center; + width: 100%; + padding: 5px 0; + } + + .smiley, .empty { + display: inline-block; + height: 80px; + vertical-align: middle; + width: 80px; + } + .smiley { + box-sizing: border-box; + cursor: pointer; + max-height: 80px; + max-width: 80px; + text-align: center; + + // see http://css-tricks.com/centering-in-the-unknown/ + &:before { + content: ''; + display: inline-block; + height: 100%; + vertical-align: middle; + margin-right: -0.25em; + } + img { + vertical-align: middle; + max-height: 76px; + max-width: 72px; + margin-right: 0.25em; + } + + &:hover { + background-color: fade(@active-color, 10%); + border-radius: 20px; + box-shadow:0 -1px 4px fade(@active-color, 10%), 0 2px 5px #888; + } + } + + .navigation { + border-collapse: collapse; + width: 100%; + img { vertical-align: text-top; } + td { padding: 2px; } + &.top td { border-bottom: 1px solid @brand-color-dark; } + &.bottom td { border-top: 1px solid @brand-color-dark; } + .active a { + color: @active-color; + font-weight: bold; + } + } +} +.smiley-picker-dialog { + .ui-dialog-content { + padding: 0; + } +}
\ No newline at end of file diff --git a/resources/assets/stylesheets/less/start.less b/resources/assets/stylesheets/less/start.less new file mode 100644 index 0000000..32ad49b --- /dev/null +++ b/resources/assets/stylesheets/less/start.less @@ -0,0 +1,218 @@ +#sort1, #sort0, #sort3 { + padding : 0 0px; +} +#admin_widget_container { + padding-top: 1em; + width: 100%; +} + +#main, #choices { + width: 99%; + border: 1px solid @dark-gray-color-60; +} + +.studip-widget-wrapper { + margin: 0 0 20px; + padding: 0; +} + +.studip-widget { + border: 1px solid @base-color-20; + transition: border-color 300ms ease-in-out; + + .widget-header { + box-sizing: border-box; + + background-color: @content-color-20; + color: @brand-color-dark; + + font-size: 1.1em; + font-weight: bold; + line-height: 2em; + padding: 0 1ex; + text-align: left; + + overflow: hidden; + text-overflow: ellipsis; + } + + .header-options { + float: right; + white-space: nowrap; + } + + section.contentbox { + border: none; + > header { + display: none; + } + } + + > div > article.studip { + border: none; + > header { + display: none; + } + } +} + +.studip-widget:hover { + border: 1px solid @brand-color-darker; + transition: border-color 300ms ease-in-out; + +} +#widget_choices{ + border-right: 1px dashed @brand-color-darker; + margin-right: 25px; +} +.start-widgetcontainer { + padding: 0; + margin-top: 0; + width: 100%; +} + +/* some improvements for ui_widget elements */ +.ui-widget_start { + font-family: Arial, Helvetica, sans-serif; + font-size: 1.0em; + padding: 0; +} + +.ui-widget_columnl { + float: left; + width: 100%; +} +.ui-widgetContainer { + color: white; + background-image: none; +} + +.ui-widget_columnr { + float: right; +} + +.ui-widget_head { + line-height: 30px; + + text-align: center; + color: white; + font-size: 1.3em; + background-color: @content-color; +} + +.ui-widget_head:hover { + cursor:move; + +} +.ui-widget_head h1 { + line-height: 100px; + text-align: center; + color: black; +} + +.addclip-widgets { + color: #000; + list-style: none; + margin: 0; + padding: 0; + + a:link, a:visited { + color: #000; + } + a:hover, a:active { + color: @active-color; + } + + li { + border-top: 1px solid @content-color; + padding: 4px 0; + + &:first-child { + border-top: 0; + } + } + p { + margin-left: 25px; + } +} + + +// Wirklich wichtiger Code +div.start-widgetcontainer { + display: flex; + justify-content: space-between; + align-items: stretch; + + > ul { + box-sizing: border-box; + display: inline-block; + + list-style-type: none; + margin: 0; + padding: 0; + vertical-align: top; + &:first-child { + flex-grow: 2; + max-width: 65%; + min-width: 65%; + } + &:last-child { + flex-grow: 1; + margin-left: 20px; + max-width: 33%; + min-width: 33%; + } + &.empty { + display: none; + } + &.move { + border: @base-color-80 dashed 1px; + } + } +} + +div.edit-widgetcontainer { + .start-widgetcontainer { + min-height: 60px; + margin-bottom: 2em; + } +} +div.available-widgets { + ul { + box-sizing: border-box; + display: inline-block; + + list-style-type: none; + margin: 0; + padding: 0; + + min-height: 60px; + width: 100%; + + li { + float: left; + margin-right: 5px; + } + + &.move { + border: @base-color-80 dashed 1px; + } + } + + .studip-widget { + width: 250px; + display: inline-block; + } +} + +@media screen and (max-width: 1024px) { + div.start-widgetcontainer { + display: block; + ul.portal-widget-list { + display: block; + margin-left: 0; + min-width: 100%; + max-width: 100%; + } + } +} diff --git a/resources/assets/stylesheets/less/statusgroups.less b/resources/assets/stylesheets/less/statusgroups.less new file mode 100644 index 0000000..278f632 --- /dev/null +++ b/resources/assets/stylesheets/less/statusgroups.less @@ -0,0 +1,88 @@ +section.course-statusgroups { + article { + header { + h1 { + a { + display: inline; + + &.no-contentbox-link::before { + background-image: none; + width: 0; + } + + img { + vertical-align: bottom; + } + } + + } + } + + section { + border-left: 1px solid @content-color-20; + border-right: 1px solid @content-color-20; + + table { + td.memberactions { + text-align: right; + } + + thead { + tr th { + background-color: @content-color-20; + } + } + + tbody { + tr td { + span.member-invisible { + font-style: italic; + color: @light-gray-color; + } + } + } + + tfoot { + tr td { + background-color: @content-color-20; + padding-left: 5px; + padding-right: 0; + } + } + } + + div.statusgroup-no-members { + font-style: italic; + margin: 15px; + } + } + + &.draggable.open { + background-color: @white; + } + + } + + footer { + background-color: @content-color-20; + border-top: 1px solid @black; + font-size: medium; + padding: 5px; + padding-left: 18px; + text-align: left; + } + + &.ui-sortable { + article.ui-sortable-placeholder { + border-style: dotted; + } + + .sg-sortable-handle { + cursor: move; + background-image: url("@{image-path}/anfasser_24.png"); + background-position: 3px center; + background-repeat: no-repeat; + width: 12px; + } + } +} diff --git a/resources/assets/stylesheets/less/studip-overlay.less b/resources/assets/stylesheets/less/studip-overlay.less new file mode 100644 index 0000000..e36c11d --- /dev/null +++ b/resources/assets/stylesheets/less/studip-overlay.less @@ -0,0 +1,87 @@ +.modal-overlay { + .ui-widget-overlay; + + position: fixed; + left: 0; + top: 0; + right: 0; + bottom: 0; + + &-local { + background-color: fadeout(@light-gray-color, 50%); + position: absolute; + } + &-ajax { + // Fallback to gif for browsers that don't support svg. Fortunately, + // the support for multiple background images and svg covers the same + // browsers (except for some old android versions that we can neglect). + // Thus said, if the loading animation looks ugly - update your + // browser ffs! + background-image: url("@{image-path}/ajax_indicator_small.gif"); + background-image: none, url("@{image-path}/ajax-indicator-white.svg"); + background-position: center; + background-repeat: no-repeat; + } + &-ajax.modal-overlay-dark { + background-image: none, url("@{image-path}/ajax-indicator-black.svg"); + } + + // Progress + &.ui-front { + .center() { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } + + cursor: wait; + + h1 { + .center(); + + margin-bottom: 0; + padding-bottom: 100px; + + color: #fff; + border-bottom: 0; + } + progress { + .center(); + + margin-top: 45px; + width: 80%; + height: 20px; + + appearance: none; + + background-size: auto; + + border: none; + border-radius: 2px; + box-shadow: 0 0 3px @light-gray-color-20; + + background-color: @light-gray-color; + + &::-moz-progress-bar, &::-webkit-progress-value { + background-color: @yellow-40; + transition: all 300ms; + } + } + ul.overlay-progress-log { + position: absolute; + top: 50%; + left: 10%; + right: 10%; + + list-style: none; + margin: 60px 0 0; + padding: 0; + text-align: center; + color: white; + max-height: 120px; + overflow: hidden; + } + } + +} diff --git a/resources/assets/stylesheets/less/studip-selection.less b/resources/assets/stylesheets/less/studip-selection.less new file mode 100644 index 0000000..24044cc --- /dev/null +++ b/resources/assets/stylesheets/less/studip-selection.less @@ -0,0 +1,105 @@ +.studip-selection { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + > * { + flex: 1 0 100%; + } + > legend { + margin-bottom: 0 !important; + } + + // General list item styles and placeholders + ul, li { + list-style: none; + margin: 0; + padding: 0; + } + li { + display: inline-block; + vertical-align: top; + } + + li:not(.empty-placeholder) { + color: @base-color; + cursor: pointer; + margin: 1px 0; + padding-right: 0.5em; + } + + li.empty-placeholder { + color: fadeout(@text-color, 30%); + &:not(:only-child) { + display: none; + } + } + + .studip-selection-selectable li.empty-placeholder { + color: @text-color; + padding-left: 20px; + position: relative; + + .icon('before', 'info-circle', 'info'); + &::before { + position: absolute; + left: 0; + top: 3px; + } + } + + // Selected and selectable lists + .studip-selection-image img { + transition: opacity 300ms; + } + + .studip-selection-selected .studip-selection-label { + .icon('before', 'radiobutton-checked', 'clickable'); + } + .studip-selection-selectable .studip-selection-label { + .icon('before', 'radiobutton-unchecked', 'clickable'); + } + + .studip-selection-image + .studip-selection-label { + &::before { + display: none; + } + } + + .studip-selection-selected li:hover { + .studip-selection-image, + .studip-selection-label::before { + .background-icon('remove-circle-full', 'clickable'); + } + } + .studip-selection-selectable li:hover { + .studip-selection-image, + .studip-selection-label::before { + .background-icon('add-circle-full', 'clickable'); + } + } + + .studip-selection-selected, + .studip-selection-selectable { + flex: 1 1 300px; + padding-top: 0; + + h2 { + margin-top: 0; + } + + li:hover { + .studip-selection-image { + background-position: center; + background-repeat: no-repeat; + background-size: contain; + img { + opacity: 0; + } + } + .studip-selection-label { + color: @active-color; + } + } + } +} diff --git a/resources/assets/stylesheets/less/study-area-selection.less b/resources/assets/stylesheets/less/study-area-selection.less new file mode 100644 index 0000000..9af5c02 --- /dev/null +++ b/resources/assets/stylesheets/less/study-area-selection.less @@ -0,0 +1,44 @@ +/* --- Studienbereichsauswahl ----------------------------------------------- */ +#study_area_selection { + padding: 1em; + + h3 { margin-top: 1em; } + .odd { background-color: #f3f5f8; } + .even { background-color: @content-color-20; } +} + +#study_area_selection_none, +#study_area_selection_at_least_one { + font-style: italic; +} + +#study_area_selection_chosen { + float: left; + margin: 0; + padding: 0; + width: 49%; +} + +#study_area_selection_selectables { + margin: 0 0 0 50%; + padding: 0; + width: 49%; +} + +#study_area_selection_chosen, +#study_area_selection_selectables { + ul, li { + list-style: none; + margin: 0; + padding: 0; + } + li { padding-left: 1em; } +} + +#admin_seminare_assi { + #study_area_selection { + font-size: 0.8em; + + h3 { font-weight: normal; } + } +}
\ No newline at end of file diff --git a/resources/assets/stylesheets/less/studygroup.less b/resources/assets/stylesheets/less/studygroup.less new file mode 100644 index 0000000..84fe438 --- /dev/null +++ b/resources/assets/stylesheets/less/studygroup.less @@ -0,0 +1,49 @@ +ul.studygroup-gallery { + list-style: none; + padding-left: 15px; + li { + box-sizing: border-box; + vertical-align: top; + text-align: center; + margin: 5px; + width: 120px; + height: 150px; + max-height: 150px; + overflow: hidden; + display: inline-blocK; + section { + margin: 0 auto; + + } + } +} + +.studygroupmemberlist { + .member-avatar { + position: relative; + img { + margin-right: 5px; + } + } + .new-member .member-avatar { + .icon('after', 'star', 'new', 12); + &::after { + position: absolute; + margin: -1px 0 0 -14px; + } + } + tr > .actions { + text-align: right; + white-space: nowrap; + } +} + +.studygroup-browse { + td.studygroup-title { + a { + display: block; + max-width: 40em; + min-width: 15em; + } + } +} diff --git a/resources/assets/stylesheets/less/tables.less b/resources/assets/stylesheets/less/tables.less new file mode 100644 index 0000000..0f2f778 --- /dev/null +++ b/resources/assets/stylesheets/less/tables.less @@ -0,0 +1,807 @@ +/* --- Tabellen ------------------------------------------------------------- */ +table.header, .table_header { //normale Tabellenheader + background-color: @table-header-color; + border-bottom: 1px solid #575757; + color: #000; + padding: 4px; + +} + +.table_header_bold { //formerly known as topic(-header) + background-color: @brand-color-lighter; + border-color: @brand-color-darker; + border-style: solid; + border-width: 0 0 1px 0; + color: @white; + font-size: 12pt; + padding: 3px 5px; + + img, svg { vertical-align: middle; } // for the topic-icons +} + + +table.links1 { background-color: @white; } +table.logintable { + background-image: url("@{image-path}/login.jpg"); + background-size: 750px 350px; + h1 { + border-bottom: 0; + font-size: 2.5em; + } +} + + +.gradient-bar(@flow-content: true) { + #gradient > .vertical-three-colors(#cdd9ed, #e3eaf6, 40%, #e3eaf6); + border-top: 1px solid @brand-color-lighter; + line-height: 17pt; + height: 25px; +} +.gradient-bar(@flow-content: true) when (@flow-content) { + &:last-child { padding-right: 5px; } + + img,svg { + padding: 0 2px; + vertical-align: text-bottom; + } +} + +table.toolbar { + .gradient-bar(false); +} +td.toolbar, td.printhead { + .gradient-bar(); +} + +td { + &.aufklapp { background-color: #fffaee; } + &.angemeldet { border: 1px solid #000; } + &.nix { background-color: transparent; } + &.quote { + border: 1px solid #000; + font-size: 8px; + } + &.rahmen_steel { + background-color: #f3f5f8; + border: 1px solid #000; + } + &.rahmen_table_row_odd { + background-color: @content-color-20; + border: 1px solid #000; + } + &.rahmen_white { + background: #fff; + border: 1px solid #000; + } + &.table_header_bold_red { + border: none; + background-color: #fdc6c6; + border-bottom: 1px solid #990000; + color: #000; + height: 20px; + } +} + +table.blank, td.blank, td.onlineinfo, td.blanksmall { + background-color: #fff; +} + +td.tree-indent { + img, svg { + vertical-align: bottom; + } +} + +td.tree-elbow-line { + background: url("@{image-path}/datatree_1.gif") repeat-y; + vertical-align: bottom; + width: 5px; +} + +td.tree-elbow-end { + vertical-align: top; + white-space: nowrap; + width: 5px; +} + +td.tree-elbow-line, td.tree-elbow-end { + img, svg { + display: block; + } +} + +/* --- table.collapsable ---------------------------------------------------- */ +// TODO: This is pretty hard to understand and should be replaced with an easier, +// better structured solution +.collapsable { + .header-row > td { + border-bottom: 0; + padding-left: 0; + } + + .toggle-indicator { + color: #000; + font-weight: bold; + + a { + background: left center no-repeat; + .background-icon('arr_1down', 'clickable'); + color: #000; + cursor: pointer; + display: block; + } + + } + .empty .toggle-indicator a { + .background-icon('arr_1right', 'inactive'); + } + .collapsed .toggle-indicator a { + .background-icon('arr_1right', 'clickable'); + } + td.label-cell, .toggle-indicator a, .empty .toggle-indicator { + padding-left: 20px; + } + > .collapsed { + tr:not(.header-row) { + display: none; + } + .toggle-indicator ~ *:not(.dont-hide) > * { + opacity: 0; + pointer-events: none; + } + } +} +* + html .collapsable .collapsed .header-row { display: inline-block !important; } // IE-Hack + +/* --- Table details -------------------------------------------------------- */ +.loaded-details { + > td { padding: 0 0 5px 20px !important; } + table { + border-top: 0; + } +} + +/* --- Sonstige ------------------------------------------------------------- */ +.gruppe0 { background-color: @group-color-0 !important; } +.gruppe1 { background-color: @group-color-1 !important; } +.gruppe2 { background-color: @group-color-2 !important; } +.gruppe3 { background-color: @group-color-3 !important; } +.gruppe4 { background-color: @group-color-4 !important; } +.gruppe5 { background-color: @group-color-5 !important; } +.gruppe6 { background-color: @group-color-6 !important; } +.gruppe7 { background-color: @group-color-7 !important; } +.gruppe8 { background-color: @group-color-8 !important; } + +#my_seminars, #settings-notifications { + .gruppe0, .gruppe1, .gruppe2, .gruppe3, .gruppe4, + .gruppe5, .gruppe6, .gruppe7, .gruppe9 { + width: 1px; + } + .mycourse_elements > img { + display: none; + } + .special_nav { + float: right; + } +} + +.grey { background: #bbb none; } +.white { background: #fff none; } + +.red_gradient { + #gradient > .vertical-three-colors(#e3969a, #e8b6b9, 60%, #e8b6b9); + border-top: 2px solid #b35357; +} + +/* --- Styles fuer printhead und printcontent ------------------------------- */ +table { + td.printcontent { + background-color: @dark-gray-color-5; + text-align: left; + } + td.printcontent:hover { + background-color: @dark-gray-color-5; + } + td.printhead2 { + background-image: url("@{image-path}/content_object_arr-right.png"); + border-top: 1px solid @brand-color-lighter; + padding: 0; + } + td.printhead3 { + background-image: url("@{image-path}/content_object_arr-down.png"); + border-top: 1px solid @brand-color-lighter; + padding: 0; + } +} + +/* classes for sortable table headers --------------------------------------- */ +tr.sortable { + th.sortasc, + th.sortdesc { + .tablesorter-header-inner { + display: inline-block; + } + } + + th.sortasc { + .icon('after', 'arr_1up'); + } + th.sortdesc { + .icon('after', 'arr_1down'); + } +} + +.tablesorter .filtered { + display: none; +} + +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; +} + +/* styles for settings tables */ +.settings { + border-collapse: collapse; + margin-bottom: 2em; + width: 100%; + + thead th, tbody th { + .table_header_bold; + text-align: center; + } + td, th { + padding: 8px; + vertical-align: top; + } + tbody { + &.maxed { + input[type=email], input[type=password], input[type=tel], input[type=text], input[type=url], select, textarea { + &:first-child { + box-sizing: border-box; + width: 100%; + } + } + td[colspan]:first-child { + input[type=email], input[type=password], input[type=tel], input[type=text], input[type=url], select, textarea { + width: 200px; + } + } + } + &.privacy td:first-child ~ td { + font-style: italic; + text-align: center; + } + td:first-child label { + display: block; + font-weight: bold; + } + } + td:first-child[colspan], .divider > th, .divider > td { + background-color: lighten(@brand-color-lighter, 20%); + border-bottom: 1px solid #444; + border-top: 1px solid #444; + color: #000; + font-weight: bold; + text-align: center; + } + + dfn, small { + display: block; + font-weight: normal; + } + dfn { + font-size: 0.8em; + font-style: italic; + padding-top: 0.5em; + } + tfoot { + td { + background: @table-footer-color; + text-align: center; + } + tr:first-child td { + border-top: 1px solid #575757; + } + } + label.required:after { + color: red; + content:'*'; + font-size: 1.5em; + padding-left: 5px; + vertical-align: middle; + } + &.notification tbody td { + text-align: center; + &:first-child:not([colspan]) { padding-left: 0; padding-right: 0; font-size: small; } + &:nth-child(-n+2) { text-align: left; } + } + .bordered { + &.left { border-left: 1px solid @brand-color-lighter; } + &.right { border-right: 1px solid @brand-color-lighter; } + } +} + +table.tree { + .header > td { + .gradient-bar(); + + a.link { + padding-left: 5px; + &.open { + .background-icon('arr_1down', 'clickable'); + background-position: left center; + background-repeat: no-repeat; + padding-left: 20px; + } + &.closed { + .background-icon('arr_1right', 'clickable'); + background-position: left center; + background-repeat: no-repeat; + padding-left: 20px; + } + } + } + td.blank { + background: #fff; + border: 0; + } + td.in-between { + background: #fff url("@{image-path}/tree-line.gif") center repeat-y; + border: 0; + } + td.leaf { + background: #fff url("@{image-path}/tree-leaf.gif") center no-repeat; + border: 0; + } + td.end { + background: #fff url("@{image-path}/tree-end.gif") center no-repeat; + border: 0; + } + .centered { + text-align: center; + table { margin: auto; text-align: left; } + } +} + +.table_footer, .table_footer td { + background-color: #e9e9e9; + border-top: 1px solid #c8c8c8; +} + +// New table definition +table.default { + border-collapse: collapse; + margin-bottom: 1em; + width: 100%; + + .wrap-content { + word-break: break-all; + } + + .font-size-adjusted{ + font-size: 1.1em; + } + + th, td, caption { + &.nowrap { + white-space: nowrap; + } + padding: 5px; + text-align: left; + } + > caption { + background-color: transparent; + padding-top: 0px; + color: @headings-color; + font-size: 1.4em; + text-align: left; + + header { + > h2 { + border: 0; + font-size: inherit; + font-weight: normal; + margin: 0; + padding: 0; + } + > p { + font-size: 0.7em; + font-weight: normal; + margin: 0; + padding: 0; + } + } + } + > thead { + > tr > th { + background-color: @content-color-20; + border-bottom: 1px solid fadeout(@brand-color-lighter, 80%); + border-top: 1px solid @brand-color-darker; + font-size: 1.0em; + } + } + > tbody { + > tr { + > th { + background-color: @content-color-20; + border-top: 1px solid @brand-color-darker; + border-bottom: 1px solid fadeout(@brand-color-lighter, 80%); + text-align: left; + } + > td { + border-bottom: 1px solid @table-header-color; + transition: background-color 0.3s; + } + > td.dragHandle { + background: #ffffff url("@{image-path}/anfasser_24.png") center no-repeat; + cursor: move; + } + &.dragover > td { + background-color: @yellow-20; + } + } + } + > tbody > tr.new > td { + font-weight: bold; + &:first-child { + position: relative; + &::before { + display: block; + content: ''; + position: absolute; + + top: 0; + bottom: 0; + left: 0; + width: 2px; + background-color: @red; + } + } + .action-menu { + font-weight: normal; + } + } + > tbody:last-of-type > tr:last-child > td { + > table > tbody > tr > td { + border-bottom: 1px solid @table-header-color; + } + border-bottom: 1px solid @brand-color-darker; + } + // Hover effect + &:not(.nohover) > tbody:not(.nohover) > tr:not(.nohover):hover > td:not(.nohover) { + background-color: fadeout(@light-gray-color, 80%); + } + > tfoot { + > tr > td { + background-color: @content-color-20; + border-top: 1px solid @brand-color-darker; + padding-left: 10px; + padding-right: 10px; + } + } + td.avatar, th.avatar { + padding: 5px; + } + .actions { + float: right; + text-align: right; + white-space: nowrap; + img, svg, input[type="image"] { + vertical-align: middle; + } + } + > caption .actions { + font-size: @font-size-base; + border-left: 1px solid @brand-color-darker; + margin-bottom: -5px; + min-height: 26px; + padding-bottom: 3px; + padding-left: 0.5em; + padding-top: 4px; + } + td.actions, th.actions { + float: none; + } + + > caption { + .caption-container { + display: flex; + align-items: stretch; + justify-content: space-between; + margin-bottom: -5px; + } + .caption-content { + flex-grow: 1; + border-right: 1px solid @brand-color-darker; + padding-bottom: 5px; + padding-right: 0.5em; + margin-right: 0.5em; + } + .caption-actions { + align-self: flex-end; + } + } + + > tbody.toggleable { + &.toggled { + .toggle-switch { + .background-icon('arr_1right', 'clickable'); + } + tr:not(:first-child) { + display: none; + } + } + .toggle-switch { + .hide-text(); + .background-icon('arr_1down', 'clickable'); + display: inline-block; + height: 16px; + text-align: center; + vertical-align: top; + width: 16px; + } + } + + dfn, small { + display: block; + font-weight: normal; + } + dfn { + font-size: 0.8em; + font-style: italic; + padding-top: 0.5em; + } + label.required:after { + color: red; + content:'*'; + font-size: 1.5em; + padding-left: 5px; + vertical-align: middle; + } + + &.has-form { + input[type=text], textarea { + box-sizing: border-box; + min-width: 200px; + width: 100%; + } + textarea { + min-height: 100px; + } + } + + tfoot { + // Fix button and select alignment + select { + vertical-align: middle; + } + // Adjust button margins + .button { + margin-bottom: 0; + margin-top: 0; + } + } + + colgroup { + col.checkbox { + width: 24px; + } + } +} + + +// Remove trailing border and margin in content boxes if table is last element +article.studip > section > table.default:last-child { + margin-bottom: 0; + + > tbody:last-child > tr:last-child > td { + border-bottom: 0; + } +} + +table.withdetails { + > tbody > tr:not(.details) > td:first-child { + .background-icon('arr_1right', 'clickable'); + background-repeat: no-repeat; + background-position: 2px center; + padding-left: 20px; + > a { + margin-left: -20px; + padding-left: 20px; + } + } + > tbody > tr.open > td { + background-color: fadeout(@light-gray-color, 80%); + } + > tbody > tr.open > td:first-child { + .background-icon('arr_1down', 'clickable'); + } + tr.details { + display: none; + max-height: 0px; + overflow: hidden; + transition: max-height 0.8s; + } + tr.open + tr.details { + display: table-row; + max-height: 200px; + overflow: hidden; + transition: max-height 0.8s; + background-color: transparent !important; + > td { + padding-top: 0px; + padding-bottom: 10px; + > .detailscontainer { + padding: 5px; + border: 1px solid @table-header-color; + margin-top: -1px; + border-top-color: white; + } + } + } +} +.no-js table.withdetails tr.details { + display: table-row; +} + +.sortable-dreieck(@direction) { + &::after { + background-repeat: no-repeat; + content: ' '; + display: inline-block; + height: 16px; + margin-left: 0; + .retina-background-image('dreieck_@{direction}.png', 'dreieck_@{direction}@2x.png', 16px, 16px); + vertical-align: text-top; + width: 16px; + } +} + +.sortable-table { + .header, .tablesorter-header:not(.sorter-false) { + white-space: nowrap; + + color: @base-color; + &:hover { + color: @active-color; + cursor: pointer; + } + } + .headerSortUp, .tablesorter-headerDesc .tablesorter-header-inner { + .sortable-dreieck('down'); + &::after { + vertical-align: inherit; + } + } + .headerSortDown, .tablesorter-headerAsc .tablesorter-header-inner { + .sortable-dreieck('up'); + &::after { + vertical-align: inherit; + } + } + .tablesorter-headerUnSorted:not(.sorter-false) .tablesorter-header-inner { + margin-right: 15px; + } +} + +// Schedule table +table#schedule_data { + width: 100%; + table-layout: fixed; + thead { + tr { + td { + text-align: center; + vertical-align: top; + background-color: @content-color-20; + padding-right: 2px; + padding: 0px; + &:first-child { + width: 40px; + } + } + &:first-child { + td:first-child { + background-color: inherit; + } + } + } + } + tbody { + td:first-child { + text-align: right; + vertical-align: top; + background-color: @content-color-20; + padding-right: 2px; + padding: 0px; + } + } +} + +// Responsive helper +.table-scrollbox-horizontal { + .scrollbox-horizontal(); +} + +//New table form for Course Search +table.course-search{ + @max-width-s: 8em; + @max-width-m: 48em; + @max-width-l: 100%; + border: 1px solid @content-color-40; + padding: 0px; + border-top: 0; + caption.legend { + box-sizing: border-box; + background-color: @fieldset-header; + border: 1px solid @content-color-40; + border-bottom: 0; + color: @brand-color-dark; + font-size: 12pt; + font-weight: bold; + line-height: 2em; + padding: 0; + text-align: left; + text-indent: 15px; + } +} + +//Show Tree Table +table.show-tree { + width:100%; + padding: 0px 10px 10px 10px; + td.b-top-va-center { + border-top: 1px solid @content-color-40; + padding-top: 10px; + vertical-align:middle; + } + img[role=root-icon]{ + position: relative; + top: 1px; + } + div.sem-root-icon{ + display: inline-block; + vertical-align: top; + } + div.sem-path{ + display: inline-block; + padding-left: 5px; + div.sem-path-dir{ + // padding-left: 5px; + } + div.sem_path_info{ + // margin-left:30px; + padding-top:10px; + div.sem_path_title{ + font-weight: bold; + font-size: 1.4em; + margin: 3px 0px 5px 0px; + } + div.sem_path_text{ + padding-top:2px; + } + } + } + table.show-tree-kids{ + width: 100%; + td.kids-tree-row{ + width: 50%; + } + ul.semtree{ + padding-left: 0px !important; + a{ + padding-top: 5px !important; + padding-bottom: 3px !important; + padding-left: 14px !important; + margin-left: -4px !important; + display: block; + } + a:hover{ + background-color: @fieldset-header !important; + color: @base-color !important; + } + } + + } +} diff --git a/resources/assets/stylesheets/less/tabs.less b/resources/assets/stylesheets/less/tabs.less new file mode 100644 index 0000000..448aeaa --- /dev/null +++ b/resources/assets/stylesheets/less/tabs.less @@ -0,0 +1,142 @@ +div.clear +{ + clear: both; + visibility: hidden; +} + +// Common styles for both tab sets +#tabs { + line-height: 20px; + float: none; + flex: 0 1 auto; + margin: 0; + + + ul, li { + list-style: none; + margin: 0; + padding: 0; + } + li { float: left; } + a { + color: #000; + } +} + +// Main tab set with the tabs sitting on top of the main content +#tabs { + padding-left: 0px; + + span { padding: 0; } + .quiet img { opacity: 0.25; } + + li { + background-color: @dark-gray-color-10; + + &:last-child { + border-right: none; + } + &.current { + color: @base-color; + } + &:hover { + //background-color: #fff; + border-bottom: 3px solid @dark-gray-color-30; + color: @base-color; + padding-top: 0px; + padding-bottom: 1px; + } + + &.current { + border-bottom: 3px solid @dark-gray-color-80; + padding-top: 0px; + padding-bottom: 1px; + line-height: 25px; + a, span.quiet { + color: @base-color; + } + } + } + a, span.quiet { + color: #000; + float: left; + display: block; + + padding: 3px 8px 3px; + white-space: nowrap; + height: 23px; + } +} + +.tab-icon { + float: left; + margin: 4px 5px 0 -0.5em; + .size(16px, 16px); + display: none; +} + +.tab-subnav { + float: right; + + .action-menu-icon { + position: relative; + top: -5px; + height: 14px; + + img { + vertical-align: middle; + filter: hue-rotate(350deg) saturate(8.7%) brightness(177.3%); + } + } + + + .action-menu-content { + z-index: 1000; + position: absolute; + top: inherit; + right: inherit; + padding: 0px 0px 10px 0px; + margin-top: 10px; + background: @content-color-20; + box-shadow: 1px 1px 1px @dark-gray-color-60; + text-align: left; + white-space: nowrap; + + ul { + display: flex; + flex-direction: column; + } + + a:hover { + color: @red !important; + } + + } + + .action-menu-content:before, + .action-menu-content:after { + bottom: 100%; + left: 11px; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + } + + .action-menu-content:before { + border-color: rgba(194, 225, 245, 0); + border-bottom-color: @dark-gray-color-60; + border-width: 9px; + margin-left: -8px; + } + .action-menu-content:after { + border-color: rgba(194, 225, 245, 0); + border-bottom-color: @content-color-20; + border-width: 8px; + margin-left: -8px; + } +} + + diff --git a/resources/assets/stylesheets/less/tfa.less b/resources/assets/stylesheets/less/tfa.less new file mode 100644 index 0000000..b5b38f5 --- /dev/null +++ b/resources/assets/stylesheets/less/tfa.less @@ -0,0 +1,63 @@ +.tfa-app-code { + code.qr { + display: none; + } + .qrcode img { + margin: auto; + width: 40%; + max-width: 50vw; + } +} + +form.default { + .tfa-code-input { + text-align: center; + .tfa-code-wrapper { + border: 1px solid @base-gray; + display: inline-block; + font-size: 2em; + line-height: 2em; + margin: 0.5em 0; + } + + input[type="number"] { + background: @dark-gray-color-10; + border: 0; + box-sizing: unset; + font-family: monospace; + height: 1em; + min-width: 0; + width: 1.5ex; + margin: 0; + padding: 0.5em 0.25em; + text-align: center; + vertical-align: top; + + color: @base-gray; + &:focus { + background-color: @activity-color-20; + color: #000; + outline: 0; + } + &:invalid { + box-shadow: none; + color: #888; + outline: 0; + } + + &:nth-child(3) { + margin-right: 0.5em; + } + + // Hide spinner elements + -moz-appearance: textfield; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ + } + } + } +} diff --git a/resources/assets/stylesheets/less/tour.less b/resources/assets/stylesheets/less/tour.less new file mode 100644 index 0000000..e0068cd --- /dev/null +++ b/resources/assets/stylesheets/less/tour.less @@ -0,0 +1,102 @@ +/* --- tour --------------------------------------------------- */ +#tour_controls { + button { + vertical-align:middle; + } + table { + text-align: center; + width: 100%; + td { + text-align: center; + } + } + div { + padding-top:5px; + } + position: fixed; + bottom: 20px; + right: 20px; + z-index:20001; + border: solid 1px #28497c; + background-color: #ffffff; + padding: 10px 10px; + font-family: @font-family-base; + overflow-y: auto; + box-shadow: 0px 0px 8px rgba(0,0,0,0.5); +} + +#tour_title { + font-style: italic; +} + +.tour_focus_box { + border: 2px dashed #ffbd33; +} + +#tour_tip { + padding:10px 20px; + position: absolute; + z-index:20000; + max-width: 300px; + font-family: @font-family-base; + font-size: 16px; + box-sizing: border-box; + background-color: #28497c; + color: #ffffff; border: + solid 1px #aaaaaa; + box-shadow: 0px 0px 8px rgba(0,0,0,0.5); + a.link-extern { + .icon('before', 'link-extern', 'info_alt', 16, 2px); + } + a.link-intern { + .icon('before', 'link-intern', 'info_alt', 16, 2px); + } + a, a:link, a:visited { + color: #FFFFFF; + text-decoration: none; + } + a:hover, a:active, a:hover.index, a:active.index, a:hover.tree { + color: #FFFFFF; + text-decoration: underline; + } +} + +#tour_tip_interactive { + padding:10px 20px; + position: absolute; + z-index:20000; + max-width: 300px; + font-family: @font-family-base; + font-size: 16px; + box-sizing: border-box; + background-color: #ffbd33; + color: #000000; + border: solid 1px #aaaaaa; + box-shadow: 0px 0px 8px rgba(0,0,0,0.5); +} + +#tour_tip_title { + font-weight: bold; +} + +.tourArrow { + position: absolute; + display: block; + width: 0; + height: 0; +} + +#tour_overlay { + background-color: #ffffff; + opacity: 0.4; + position: fixed; + z-index: 10000; + width: 100%; + height: 100%; +} + +#tour_selector_overlay { + background-color: #000000; + opacity: 0.5; + position: absolute; +} diff --git a/resources/assets/stylesheets/less/typography.less b/resources/assets/stylesheets/less/typography.less new file mode 100644 index 0000000..c74b885 --- /dev/null +++ b/resources/assets/stylesheets/less/typography.less @@ -0,0 +1,90 @@ +// Body reset + +html { + font-size: 62.5%; + -webkit-tap-highlight-color: rgba(0,0,0,0); +} + +body { + font-family: @font-family-base; + font-size: @font-size-base; + line-height: @line-height-base; + color: @text-color; +} + +// Reset fonts for relevant elements + +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +// +// Typography +// -------------------------------------------------- + + +// Headings +// ------------------------- + +h1, h2, h3, h4, h5, h6 { + font-family: @headings-font-family; + font-weight: @headings-font-weight; + line-height: @headings-line-height; + color: @headings-color; +} + +h1, +h2, +h3 { + margin-top: @line-height-computed; + margin-bottom: (@line-height-computed / 2); +} + +h4, +h5, +h6, { + margin-top: (@line-height-computed / 2); + margin-bottom: (@line-height-computed / 2); +} + +h1 { font-size: @font-size-h1; } +h2 { font-size: @font-size-h2; } +h3 { font-size: @font-size-h3; } +h4 { font-size: @font-size-h4; } +h5 { font-size: @font-size-h5; } +h6 { font-size: @font-size-h6; } + + +// Headings with borders +// ------------------------- + +h1, h2 { + font-size: 1.3em; +} + + +h1 { + margin-top: 0px; + span { + display: block; + font-size: (@font-size-h1 * 0.75); + font-weight: (@headings-font-weight / 7); + padding: (@line-height-computed / 4) 0; + } +} + +// Body text +// ------------------------- + +p { + margin: 0 0 (@line-height-computed / 2); +} + +.text-center { + text-align: center; +} diff --git a/resources/assets/stylesheets/less/variables.less b/resources/assets/stylesheets/less/variables.less new file mode 100644 index 0000000..7fe4d2c --- /dev/null +++ b/resources/assets/stylesheets/less/variables.less @@ -0,0 +1,33 @@ +@text-color: #000; + +@font-family-base: "Lato", sans-serif; + +@font-size-base: 14px; +@font-size-large: ceil((@font-size-base * 1.25)); // ~18px +@font-size-small: ceil((@font-size-base * 0.85)); // ~12px + +@font-size-h1: floor((@font-size-base * 1.4)); // ~36px +@font-size-h2: floor((@font-size-base * 1.2)); // ~30px +@font-size-h3: ceil((@font-size-base * 1.1)); // ~24px +@font-size-h4: @font-size-base; // ~18px +@font-size-h5: @font-size-base; +@font-size-h6: ceil((@font-size-base * 0.85)); // ~12px + +//** Unit-less `line-height` for use in components like buttons. +@line-height-base: 1.428571429; // 20/14 +//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc. +@line-height-computed: floor((@font-size-base * @line-height-base)); // ~20px + +//** By default, this inherits from the `<body>`. +@headings-font-family: inherit; +@headings-font-weight: 550; +@headings-line-height: 1.1; +@headings-color: @base-gray; + +// Design specific +@bar-bottom-container-height: 40px; +@header-height: 70px; + +// Drag & Drop specific: +@drag_and_drop_z_index: 1000; +@drag_and_drop_border: 1px solid @base-color; diff --git a/resources/assets/stylesheets/less/visibility.less b/resources/assets/stylesheets/less/visibility.less new file mode 100644 index 0000000..f1d713b --- /dev/null +++ b/resources/assets/stylesheets/less/visibility.less @@ -0,0 +1,61 @@ +.media-breakpoint-large-down(@rules) { + @rules(); +} + +.media-breakpoint-medium-down(@rules) { + @media (max-width: (@major-breakpoint-large - 1px)) { @rules(); } +} + +.media-breakpoint-small-down(@rules) { + @media (max-width: (@major-breakpoint-medium - 1px)) { @rules(); } +} + +.media-breakpoint-tiny-down(@rules) { + @media (max-width: (@major-breakpoint-small - 1px)) { @rules(); } +} + + +.media-breakpoint-large-up(@rules) { + @media (min-width: (@major-breakpoint-large)) { @rules(); } +} + +.media-breakpoint-medium-up(@rules) { + @media (min-width: (@major-breakpoint-medium)) { @rules(); } +} + +.media-breakpoint-small-up(@rules) { + @media (min-width: (@major-breakpoint-small)) { @rules(); } +} + +.media-breakpoint-tiny-up(@rules) { + @rules(); +} + + +.hidden-large-down { + .media-breakpoint-large-down({ display: none !important; }) +} +.hidden-large-up { + .media-breakpoint-large-up({ display: none !important; }); +} + +.hidden-medium-down { + .media-breakpoint-medium-down({ display: none !important; }) +} +.hidden-medium-up { + .media-breakpoint-medium-up({ display: none !important; }); +} + +.hidden-small-down { + .media-breakpoint-small-down({ display: none !important; }) +} +.hidden-small-up { + .media-breakpoint-small-up({ display: none !important; }); +} + +.hidden-tiny-down { + .media-breakpoint-tiny-down({ display: none !important; }) +} +.hidden-tiny-up { + .media-breakpoint-tiny-up({ display: none !important; }); +} diff --git a/resources/assets/stylesheets/mixins.less b/resources/assets/stylesheets/mixins.less new file mode 100644 index 0000000..9646596 --- /dev/null +++ b/resources/assets/stylesheets/mixins.less @@ -0,0 +1,8 @@ +@image-path: "../images"; +@icon-path: "@{image-path}/icons/16"; + +@import (reference) "mixins/colors.less"; +@import (reference) "mixins/twitter-mixins.less"; +@import (reference) "mixins/studip.less"; +@import (reference) "mixins/arrow.less"; +@import (reference) "mixins/flex.less"; diff --git a/resources/assets/stylesheets/mixins.scss b/resources/assets/stylesheets/mixins.scss new file mode 100644 index 0000000..5cf90a7 --- /dev/null +++ b/resources/assets/stylesheets/mixins.scss @@ -0,0 +1,7 @@ +$image-path: "../images"; +$icon-path: "#{$image-path}/icons"; + +@import "mixins/misc"; +@import "mixins/colors"; +@import "mixins/studip"; +@import "mixins/arrow"; diff --git a/resources/assets/stylesheets/mixins/arrow.less b/resources/assets/stylesheets/mixins/arrow.less new file mode 100644 index 0000000..8b910aa --- /dev/null +++ b/resources/assets/stylesheets/mixins/arrow.less @@ -0,0 +1,124 @@ +/* + * arrow.less - CSS arrows mixin + * + * 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 + * @since 2.4 + */ + +#arrow { + .init() { + position: relative; + } + + .pseudo(@width, @color) { + border: (@width) solid fadeout(@color, 100%); + content: ""; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + } + + // Top + .top-pseudo(@width, @color) { + #arrow > .pseudo(@width, @color); + border-bottom-color: @color; + bottom: 100%; + left: 50%; + margin-left: (-@width); + } + .top(@width, @color, @margin) { + #arrow > .init(); + &:before { #arrow > .top-pseudo(@width, @color); } + margin-top: (@margin); + } + .top(@width, @color) { + #arrow > .top(@width, @color, @width); + } + .top-border(@width, @color, @border-width, @border-color, @margin) { + #arrow > .top(@width, @border-color, @margin); + &:after { #arrow > .top-pseudo(@width - @border-width, @color); } + } + .top-border(@width, @color, @border-width, @border-color) { + #arrow > .top-border(@width, @color, @border-width, @border-color, @width); + } + + // Right + .right-pseudo(@width, @color) { + #arrow > .pseudo(@width, @color); + border-left-color: @color; + left: 100%; + top: 50%; + margin-top: (-@width); + } + .right(@width, @color, @margin) { + #arrow > .init(); + &:before { #arrow > .right-pseudo(@width, @color); } + margin-right: (@margin); + } + .right(@width, @color) { + #arrow > .right(@width, @color, @width); + } + .right-border(@width, @color, @border-width, @border-color, @margin) { + #arrow > .right(@width, @border-color, @margin); + &:after { #arrow > .right-pseudo(@width - @border-width, @color); } + } + .right-border(@width, @color, @border-width, @border-color) { + #arrow > .right-border(@width, @color, @border-width, @border-color, @width); + } + + // Bottom + .bottom-pseudo(@width, @color) { + #arrow > .pseudo(@width, @color); + border-top-color: @color; + top: 100%; + left: 50%; + margin-left: (-@width); + } + .bottom(@width, @color, @margin) { + #arrow > .init(); + &:before { #arrow > .bottom-pseudo(@width, @color); } + margin-bottom: (@margin); + } + .bottom(@width, @color) { + #arrow > .bottom(@width, @color, @width); + } + .bottom-border(@width, @color, @border-width, @border-color, @margin) { + #arrow > .bottom(@width, @border-color, @margin); + &:after { #arrow > .bottom-pseudo(@width - @border-width, @color); } + } + .bottom-border(@width, @color, @border-width, @border-color) { + #arrow > .bottom-border(@width, @color, @border-width, @border-color, @width); + } + + // Left + .left-pseudo(@width, @color) { + #arrow > .pseudo(@width, @color); + border-right-color: @color; + right: 100%; + top: 50%; + margin-top: -(@width); + } + .left(@width, @color, @margin) { + #arrow > .init(); + &:before { #arrow > .left-pseudo(@width, @color); } + margin-left: (@margin); + } + .left(@width, @color) { + #arrow > .left(@width, @color, @width); + } + .left-border(@width, @color, @border-width, @border-color, @margin) { + #arrow > .left(@width, @border-color, @margin); + &:after { #arrow > .left-pseudo(@width - @border-width, @color); } + } + .left-border(@width, @color, @border-width, @border-color) { + #arrow > .left-border(@width, @color, @border-width, @border-color, @width); + } +} diff --git a/resources/assets/stylesheets/mixins/arrow.scss b/resources/assets/stylesheets/mixins/arrow.scss new file mode 100644 index 0000000..83242b1 --- /dev/null +++ b/resources/assets/stylesheets/mixins/arrow.scss @@ -0,0 +1,127 @@ +/* + * arrow.less - CSS arrows mixin + * + * 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 + * @since 4.4 + */ + +%base { + position: relative; +} + +@mixin arrow-pseudo($width, $color) { + border: $width solid fade-out($color, 1); + content: ""; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} + +// TOP +@mixin arrow-top-pseudo($width, $color) { + @include arrow-pseudo($width, $color); + border-bottom-color: $color; + bottom: 100%; + right: 50%; + margin-right: -$width; + +} +@mixin arrow-top($width, $color, $margin: $width) { + @extend %base; + margin-top: $margin; + + &::before { + @include arrow-top-pseudo($width, $color); + } +} + +@mixin arrow-top-border($width, $color, $border-width, $border-color, $margin: $width) { + @include arrow-top($width, $border-color, $margin); + &::after { + @include arrow-top-pseudo($width - $border-width, $color); + } +} + +// RIGHT +@mixin arrow-right-pseudo($width, $color) { + @include arrow-pseudo($width, $color); + border-left-color: $color; + left: 100%; + top: 50%; + margin-top: -$width; +} + +@mixin arrow-right($width, $color, $margin: $width) { + @extend %base; + margin-right: $margin; + + &::before { + @include arrow-right-pseudo($width, $color); + } +} + +@mixin arrow-right-border($width, $color, $border-width, $border-color, $margin: $width) { + @include arrow-right($width, $border-color, $margin); + &::after { + @include arrow-right-pseudo($width - $border-width, $color); + } +} + +// BOTTOM +@mixin arrow-bottom-pseudo($width, $color) { + @include arrow-pseudo($width, $color); + border-top-color: $color; + top: 100%; + left: 50%; + margin-left: -$width; +} + +@mixin arrow-bottom($width, $color, $margin: $width) { + @extend %base; + margin-bottom: $margin; + + &::before { + @include arrow-bottom-pseudo($width, $color); + } +} + +@mixin arrow-bottom-border($width, $color, $border-width, $border-color, $margin: $width) { + @include arrow-bottom($width, $border-color, $margin); + &::after { + @include arrow-bottom-pseudo($width - $border-width, $color); + } +} + +// LEFT +@mixin arrow-left-pseudo($width, $color) { + @include arrow-pseudo($width, $color); + border-right-color: $color; + right: 100%; + top: 50%; + margin-top: -$width; +} + +// +@mixin arrow-left($width, $color, $margin: $width) { + @extend %base; + margin-left: $margin; + + &::before { + @include arrow-left-pseudo($width, $color); + } +} + +@mixin arrow-left-border($width, $color, $border-width, $border-color, $margin: $width) { + @include arrow-left($width, $border-color, $margin); + &::after { + @include arrow-left-pseudo($width - $border-width, $color); + } +} diff --git a/resources/assets/stylesheets/mixins/colors.less b/resources/assets/stylesheets/mixins/colors.less new file mode 100644 index 0000000..4d4e4e9 --- /dev/null +++ b/resources/assets/stylesheets/mixins/colors.less @@ -0,0 +1,216 @@ +//if you like, change this (your brand color) +@base-color: #28497c; // #28497c + + +//PLEASE, no changes from here +//@base-gray: #3c454e; // #3c454e +//calculated base gray +@base-gray: hsv(hsvhue(@base-color), + hsvsaturation(#3c454e), + hsvvalue(#3c454e)); + +@brand-color-dark: @base-color; +@brand-color-darker: hsv((hsvhue(@base-color) - 0.2), + (hsvsaturation(@base-color) + 4.5%), + (hsvvalue(@base-color) - 4.7)); // #1f3f70; + +@brand-color-light: hsv(hsvhue(@base-color), + (hsvsaturation(@base-color) - 5.5%), + (hsvvalue(@base-color) + 7.5)); // #36598f; + +@brand-color-lighter: hsv((hsvhue(@base-color) + 2.3), + (hsvsaturation(@base-color) - 41.8%), + (hsvvalue(@base-color) + 23.9)); // #899ab9; + +@public-course-bgcolor: @red; + +@table-header-color: @dark-gray-color-15; +@table-footer-color: @dark-gray-color-15; + +@active-color: @red; +/* This code calculates another activity color in case you dont wanna stick with red + +@active-color: hsv((hsvhue(@red) - hsvhue(@base-color) + hsvhue(@origin-base-color)), +hsvsaturation(@red), +hsvvalue(@red)); +*/ + +@black: #000000; +@white: #fff; + +// Default studip base color +@origin-base-color: #28497c; // #28497c + +@base-color-80: mix(@base-color, #fff, 80%); // #536d96 +@base-color-60: mix(@base-color, #fff, 60%); // #7e92b0 +@base-color-40: mix(@base-color, #fff, 40%); // #a9b6cb +@base-color-20: mix(@base-color, #fff, 20%); // #d4dbe5 + +@content-color: @brand-color-lighter; +@content-color-80: mix(@content-color, #fff, 80%); // #a1aec7 +@content-color-60: mix(@content-color, #fff, 60%); // #b8c2d5 +@content-color-40: mix(@content-color, #fff, 40%); // #d0d7e3 +@content-color-20: mix(@content-color, #fff, 20%); // #e7ebf1 +@content-color-10: mix(@content-color, #fff, 10%); // #e7ebf1 + +@light-gray-color: @dark-gray-color-75; +@light-gray-color-80: @dark-gray-color-60; +@light-gray-color-60: @dark-gray-color-45; +@light-gray-color-40: @dark-gray-color-30; +@light-gray-color-20: @dark-gray-color-15; + +@dark-gray-color: @base-gray; +@dark-gray-color-80: mix(@dark-gray-color, #fff, 80%); // #636a71 +@dark-gray-color-75: mix(@dark-gray-color, #fff, 75%); // #6c737a +@dark-gray-color-60: mix(@dark-gray-color, #fff, 60%); // #898f94 +@dark-gray-color-45: mix(@dark-gray-color, #fff, 45%); // #a7abaf +@dark-gray-color-40: mix(@dark-gray-color, #fff, 40%); // #b1b5b8 +@dark-gray-color-30: mix(@dark-gray-color, #fff, 30%); // #c4c7c9 +@dark-gray-color-20: mix(@dark-gray-color, #fff, 20%); // #d8dadc +@dark-gray-color-15: mix(@dark-gray-color, #fff, 15%); // #e1e3e4 +@dark-gray-color-10: mix(@dark-gray-color, #fff, 10%); // #ebeced +@dark-gray-color-5: mix(@dark-gray-color, #fff, 5%); // #f5f5f6 + + +@activity-color: @yellow; +@activity-color-80: mix(@activity-color, #fff, 80%); // #ffca5c +@activity-color-60: mix(@activity-color, #fff, 60%); // #ffd785 +@activity-color-40: mix(@activity-color, #fff, 40%); // #ffe4ad +@activity-color-20: mix(@activity-color, #fff, 20%); // #fff2d6 + +//colors. a lot of. + +@yellow: #ffbd33; +@yellow-80: mix(@yellow, #fff, 80%); // #ffca5c +@yellow-60: mix(@yellow, #fff, 60%); // #ffd785 +@yellow-40: mix(@yellow, #fff, 40%); // #ffe4ad +@yellow-20: mix(@yellow, #fff, 20%); // #fff2d6 + +@orange: #f26e00; +@orange-80: mix(@orange, #fff, 80%); // #f58b33 +@orange-60: mix(@orange, #fff, 60%); // #f7a866 +@orange-40: mix(@orange, #fff, 40%); // #fac599 +@orange-20: mix(@orange, #fff, 20%); // #fce2cc + +@red: #d60000; +@red-80: mix(@red, #fff, 80%); // #de3333 +@red-60: mix(@red, #fff, 60%); // #e76666 +@red-40: mix(@red, #fff, 40%); // #ef9999 +@red-20: mix(@red, #fff, 20%); // #f7cccc + +@violet: #b02e7c; +@violet-80: mix(@violet, #fff, 80%); // #c05896 +@violet-60: mix(@violet, #fff, 60%); // #d082b0 +@violet-40: mix(@violet, #fff, 40%); // #dfabcb +@violet-20: mix(@violet, #fff, 20%); // #efd5e5 + +@dark-violet: #682c8b; +@dark-violet-80: mix(@dark-violet, #fff, 80%); // #8656a2 +@dark-violet-60: mix(@dark-violet, #fff, 60%); // #a480b9 +@dark-violet-40: mix(@dark-violet, #fff, 40%); // #c2aad0 +@dark-violet-20: mix(@dark-violet, #fff, 20%); // #e0d4e7 + +@green: #6ead10; +@green-80: mix(@green, #fff, 80%); // #8bbd40 +@green-60: mix(@green, #fff, 60%); // #a8ce70 +@green-40: mix(@green, #fff, 40%); // #c5dea0 +@green-20: mix(@green, #fff, 20%); // #e2efcf + +@dark-green: #008512; +@dark-green-80: mix(@dark-green, #fff, 80%); // #339d41 +@dark-green-60: mix(@dark-green, #fff, 60%); // #66b570 +@dark-green-40: mix(@dark-green, #fff, 40%); // #99cea0 +@dark-green-20: mix(@dark-green, #fff, 20%); // #cce6cf + +@petrol: #129c94; +@petrol-80: mix(@petrol, #fff, 80%); // #41afaa +@petrol-60: mix(@petrol, #fff, 60%); // #70c3bf +@petrol-40: mix(@petrol, #fff, 40%); // #a0d7d4 +@petrol-20: mix(@petrol, #fff, 20%); // #cfebe9 + +@brown: #a85d45; +@brown-80: mix(@brown, #fff, 80%); // #b97d6a +@brown-60: mix(@brown, #fff, 60%); // #ca9eaf +@brown-40: mix(@brown, #fff, 40%); // #dcbeb4 +@brown-20: mix(@brown, #fff, 20%); // #edded9 + +@fieldset-header: @content-color-20; +@fieldset-border: @base-color-20; + +// contrast colors +@contrast-content-white: contrast(@content-color, #ffffff, #000000, 67%); +@contrast-content-gray: contrast(@content-color, @dark-gray-color, #000000 , 67%); +@contrast-content-hovergray: contrast(@content-color, @dark-gray-color-10, @dark-gray-color); + +// Group colors (for my courses grouping) +@group-color-0: @dark-violet; +@group-color-1: @violet; +@group-color-2: @red; +@group-color-3: @orange; +@group-color-4: @yellow; +@group-color-5: @green; +@group-color-6: @dark-green; +@group-color-7: @petrol; +@group-color-8: @brown; + +// Calender color mapping +@calendar-day-event: @brand-color-dark; +@calendar-day-event-aux: @base-color-60; + +@calendar-category-1: @dark-violet; +@calendar-category-1-aux: @dark-violet-60; + +@calendar-category-2: @violet; +@calendar-category-2-aux: @violet-60; + +@calendar-category-3: @red; +@calendar-category-3-aux: @red-60; + +@calendar-category-4: @orange; +@calendar-category-4-aux: @orange-60; + +@calendar-category-5: @yellow; +@calendar-category-5-aux: @yellow-60; + +@calendar-category-6: @green; +@calendar-category-6-aux: @green-60; + +@calendar-category-7: @dark-green; +@calendar-category-7-aux: @dark-green-60; + +@calendar-category-8: @petrol; +@calendar-category-8-aux: @petrol-60; + +@calendar-category-9: @brown; +@calendar-category-9-aux: @brown-60; + +@calendar-category-10: @dark-violet-60; +@calendar-category-10-aux: @dark-violet-20; + +@calendar-category-11: @violet-60; +@calendar-category-11-aux: @violet-20; + +@calendar-category-12: @red-60; +@calendar-category-12-aux: @red-20; + +@calendar-category-13: @orange-60; +@calendar-category-13-aux: @orange-20; + +@calendar-category-14: @yellow-60; +@calendar-category-14-aux: @yellow-20; + +@calendar-category-15: @green-60; +@calendar-category-15-aux: @green-20; + +@calendar-category-16: @dark-green-60; +@calendar-category-16-aux: @dark-green-20; + +@calendar-category-17: @petrol-60; +@calendar-category-17-aux: @petrol-20; + +@calendar-category-18: @brown-60; +@calendar-category-18-aux: @brown-20; + +/* used for course category 255 refer to (resources/assets/stylesheets/less/calendar.less) */ +@calendar-category-255: @light-gray-color-60; +@calendar-category-255-aux: @light-gray-color-20; diff --git a/resources/assets/stylesheets/mixins/colors.scss b/resources/assets/stylesheets/mixins/colors.scss new file mode 100644 index 0000000..99ba8de --- /dev/null +++ b/resources/assets/stylesheets/mixins/colors.scss @@ -0,0 +1,214 @@ +//if you like, change this (your brand color) +$base-color: #28497c; // #28497c + + +//PLEASE, no changes from here +//$base-gray: #3c454e; // #3c454e +//calculated base gray +$base-gray: hsl(hue($base-color), + saturation(#3c454e), + lightness(#3c454e)); + +$brand-color-dark: $base-color; +$brand-color-darker: hsl(hue($base-color), + (saturation($base-color) + 5.4%), + (lightness($base-color) - 4%)); // #1f3f70; + +$brand-color-light: hsl(hue($base-color), + (saturation($base-color) - 6%), + (lightness($base-color) + 6.5%)); // #36598f; + +$brand-color-lighter: hsl((hue($base-color) + 2.5), + (saturation($base-color) - 25.5%), + (lightness($base-color) + 31%)); // #899ab9; +/* This code calculates another activity color in case you dont wanna stick with red + +$active-color: hsv((hsvhue($red) - hsvhue($base-color) + hsvhue($origin-base-color)), +hsvsaturation($red), +hsvvalue($red)); +*/ + +$black: #000000; +$white: #fff; + +// Default studip base color +$origin-base-color: #28497c; // #28497c + +$base-color-80: mix($base-color, #fff, 80%); // #536d96 +$base-color-60: mix($base-color, #fff, 60%); // #7e92b0 +$base-color-40: mix($base-color, #fff, 40%); // #a9b6cb +$base-color-20: mix($base-color, #fff, 20%); // #d4dbe5 + +$content-color: $brand-color-lighter; +$content-color-80: mix($content-color, #fff, 80%); // #a1aec7 +$content-color-60: mix($content-color, #fff, 60%); // #b8c2d5 +$content-color-40: mix($content-color, #fff, 40%); // #d0d7e3 +$content-color-20: mix($content-color, #fff, 20%); // #e7ebf1 +$content-color-10: mix($content-color, #fff, 10%); // #e7ebf1 + +$dark-gray-color: $base-gray; +$dark-gray-color-80: mix($dark-gray-color, #fff, 80%); // #636a71 +$dark-gray-color-75: mix($dark-gray-color, #fff, 75%); // #6c737a +$dark-gray-color-60: mix($dark-gray-color, #fff, 60%); // #898f94 +$dark-gray-color-45: mix($dark-gray-color, #fff, 45%); // #a7abaf +$dark-gray-color-40: mix($dark-gray-color, #fff, 40%); // #b1b5b8 +$dark-gray-color-30: mix($dark-gray-color, #fff, 30%); // #c4c7c9 +$dark-gray-color-20: mix($dark-gray-color, #fff, 20%); // #d8dadc +$dark-gray-color-15: mix($dark-gray-color, #fff, 15%); // #e1e3e4 +$dark-gray-color-10: mix($dark-gray-color, #fff, 10%); // #ebeced +$dark-gray-color-5: mix($dark-gray-color, #fff, 5%); // #f5f5f6 + +$light-gray-color: $dark-gray-color-75; +$light-gray-color-80: $dark-gray-color-60; +$light-gray-color-60: $dark-gray-color-45; +$light-gray-color-40: $dark-gray-color-30; +$light-gray-color-20: $dark-gray-color-15; + +//colors. a lot of. + +$yellow: #ffbd33; +$yellow-80: mix($yellow, #fff, 80%); // #ffca5c +$yellow-60: mix($yellow, #fff, 60%); // #ffd785 +$yellow-40: mix($yellow, #fff, 40%); // #ffe4ad +$yellow-20: mix($yellow, #fff, 20%); // #fff2d6 + +$orange: #f26e00; +$orange-80: mix($orange, #fff, 80%); // #f58b33 +$orange-60: mix($orange, #fff, 60%); // #f7a866 +$orange-40: mix($orange, #fff, 40%); // #fac599 +$orange-20: mix($orange, #fff, 20%); // #fce2cc + +$red: #d60000; +$red-80: mix($red, #fff, 80%); // #de3333 +$red-60: mix($red, #fff, 60%); // #e76666 +$red-40: mix($red, #fff, 40%); // #ef9999 +$red-20: mix($red, #fff, 20%); // #f7cccc + +$violet: #b02e7c; +$violet-80: mix($violet, #fff, 80%); // #c05896 +$violet-60: mix($violet, #fff, 60%); // #d082b0 +$violet-40: mix($violet, #fff, 40%); // #dfabcb +$violet-20: mix($violet, #fff, 20%); // #efd5e5 + +$dark-violet: #682c8b; +$dark-violet-80: mix($dark-violet, #fff, 80%); // #8656a2 +$dark-violet-60: mix($dark-violet, #fff, 60%); // #a480b9 +$dark-violet-40: mix($dark-violet, #fff, 40%); // #c2aad0 +$dark-violet-20: mix($dark-violet, #fff, 20%); // #e0d4e7 + +$green: #6ead10; +$green-80: mix($green, #fff, 80%); // #8bbd40 +$green-60: mix($green, #fff, 60%); // #a8ce70 +$green-40: mix($green, #fff, 40%); // #c5dea0 +$green-20: mix($green, #fff, 20%); // #e2efcf + +$dark-green: #008512; +$dark-green-80: mix($dark-green, #fff, 80%); // #339d41 +$dark-green-60: mix($dark-green, #fff, 60%); // #66b570 +$dark-green-40: mix($dark-green, #fff, 40%); // #99cea0 +$dark-green-20: mix($dark-green, #fff, 20%); // #cce6cf + +$petrol: #129c94; +$petrol-80: mix($petrol, #fff, 80%); // #41afaa +$petrol-60: mix($petrol, #fff, 60%); // #70c3bf +$petrol-40: mix($petrol, #fff, 40%); // #a0d7d4 +$petrol-20: mix($petrol, #fff, 20%); // #cfebe9 + +$brown: #a85d45; +$brown-80: mix($brown, #fff, 80%); // #b97d6a +$brown-60: mix($brown, #fff, 60%); // #ca9eaf +$brown-40: mix($brown, #fff, 40%); // #dcbeb4 +$brown-20: mix($brown, #fff, 20%); // #edded9 + +$fieldset-header: $content-color-20; +$fieldset-border: $base-color-20; + +// contrast colors +$contrast-content-white: contrast($content-color, #ffffff, #000000, 67%); +$contrast-content-gray: contrast($content-color, $dark-gray-color, #000000 , 67%); +$contrast-content-hovergray: contrast($content-color, $dark-gray-color-10, $dark-gray-color); + +$public-course-bgcolor: $red; + +$table-header-color: $dark-gray-color-15; +$table-footer-color: $dark-gray-color-15; + +$active-color: $red; + +$activity-color: $yellow; +$activity-color-80: mix($activity-color, #fff, 80%); // #ffca5c +$activity-color-60: mix($activity-color, #fff, 60%); // #ffd785 +$activity-color-40: mix($activity-color, #fff, 40%); // #ffe4ad +$activity-color-20: mix($activity-color, #fff, 20%); // #fff2d6 + +// Group colors (for my courses grouping) +$group-color-0: $dark-violet; +$group-color-1: $violet; +$group-color-2: $red; +$group-color-3: $orange; +$group-color-4: $yellow; +$group-color-5: $green; +$group-color-6: $dark-green; +$group-color-7: $petrol; +$group-color-8: $brown; + +// Calender color mapping +$calendar-day-event: $brand-color-dark; +$calendar-day-event-aux: $base-color-60; + +$calendar-category-1: $dark-violet; +$calendar-category-1-aux: $dark-violet-60; + +$calendar-category-2: $violet; +$calendar-category-2-aux: $violet-60; + +$calendar-category-3: $red; +$calendar-category-3-aux: $red-60; + +$calendar-category-4: $orange; +$calendar-category-4-aux: $orange-60; + +$calendar-category-5: $yellow; +$calendar-category-5-aux: $yellow-60; + +$calendar-category-6: $green; +$calendar-category-6-aux: $green-60; + +$calendar-category-7: $dark-green; +$calendar-category-7-aux: $dark-green-60; + +$calendar-category-8: $petrol; +$calendar-category-8-aux: $petrol-60; + +$calendar-category-9: $brown; +$calendar-category-9-aux: $brown-60; + +$calendar-category-10: $dark-violet-60; +$calendar-category-10-aux: $dark-violet-20; + +$calendar-category-11: $violet-60; +$calendar-category-11-aux: $violet-20; + +$calendar-category-12: $red-60; +$calendar-category-12-aux: $red-20; + +$calendar-category-13: $orange-60; +$calendar-category-13-aux: $orange-20; + +$calendar-category-14: $yellow-60; +$calendar-category-14-aux: $yellow-20; + +$calendar-category-15: $green-60; +$calendar-category-15-aux: $green-20; + +$calendar-category-16: $dark-green-60; +$calendar-category-16-aux: $dark-green-20; + +$calendar-category-17: $petrol-60; +$calendar-category-17-aux: $petrol-20; + +$calendar-category-18: $brown-60; +$calendar-category-18-aux: $brown-20; + +$calendar-category-255: $light-gray-color-60; +$calendar-category-255-aux: $light-gray-color-20; diff --git a/resources/assets/stylesheets/mixins/flex.less b/resources/assets/stylesheets/mixins/flex.less new file mode 100644 index 0000000..8b8586f --- /dev/null +++ b/resources/assets/stylesheets/mixins/flex.less @@ -0,0 +1,51 @@ +.flex() { + display: flex; +} + +.flex-direction-row() { + flex-direction: row; +} +.flex-direction-row-reverse() { + flex-direction: row-reverse; +} + +.flex-direction-column() { + flex-direction: column; +} +.flex-direction-column-reverse() { + flex-direction: column-reverse; +} + +.flex-wrap(@wrap) { + flex-wrap: @wrap; +} + +.flex-grow(@growth) { + flex-grow: @growth; +} + +.flex-align-content(@stretch) { + align-content: @stretch; +} + +.flex-align-items(@stretch) { + align-items: @stretch; +} + +.flex-justify-content(@justification) { + justify-content: @justification; +} + +.flex-justify-content() { + justify-content: flex-start; +} + +.flex-align-self(@alignment) { + align-self: @alignment; +} +.flex-item(@growth) { + flex: @growth; +} +.flex-item(@growth, @shrink, @basis: auto) { + flex: @growth @shrink @basis; +} diff --git a/resources/assets/stylesheets/mixins/misc.scss b/resources/assets/stylesheets/mixins/misc.scss new file mode 100644 index 0000000..f8f7a3b --- /dev/null +++ b/resources/assets/stylesheets/mixins/misc.scss @@ -0,0 +1,17 @@ +// Clearfix +// -------- +// For clearing floats like a boss h5bp.com/q +@mixin clearfix { + *zoom: 1; + &::before, + &::after { + display: table; + content: ""; + // Fixes Opera/contenteditable bug: + // http://nicolasgallagher.com/micro-clearfix-hack/#comment-36952 + line-height: 0; + } + &::after { + clear: both; + } +} diff --git a/resources/assets/stylesheets/mixins/studip.less b/resources/assets/stylesheets/mixins/studip.less new file mode 100644 index 0000000..178db45 --- /dev/null +++ b/resources/assets/stylesheets/mixins/studip.less @@ -0,0 +1,235 @@ +/* + * studip-mixins.less + * + * This file contains all mixins created specifically for Stud.IP + * while mixins.less should contain a copy of the mixins from + * twitter's bootstrap. + * + * 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 + * @since 2.4 + */ + +// Drop shadows +.box-shadow(@shadow1, @shadow2) { + -webkit-box-shadow: @shadow1, @shadow2; + -moz-box-shadow: @shadow1, @shadow2; + box-shadow: @shadow1, @shadow2; +} + +// Double transition +.transition(@transition1, @transition2) { + .transition(~"@{transition1}, @{transition2}"); +} +// Double transform +.transform(@transformation1, @transformation2) { + -webkit-transform: @transformation1 @transformation2; + -moz-transform: @transformation1 @transformation2; + -ms-transform: @transformation1 @transformation2; + -o-transform: @transformation1 @transformation2; + transform: @transformation1 @transformation2; +} + +// Disable text selection by user +.disable-select() { + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + supported by Chrome and Opera */ +} + +// Retina background icons +.retina-background-image(@image0, @image1, @width: 100%, @height: @width) { + background-image: url("@{image-path}/@{image0}"); + @media (-webkit-min-device-pixel-ratio: 2), + (min-resolution: 192dpi) + { + background-image: url("@{image-path}/@{image1}"); + .background-size(@width @height); + } +} + +// Role to color mapping +.role2color(@role) when (@role = 'info') { + @color: 'black'; +} + +.role2color(@role) when (@role = 'clickable'), (@role = 'link') { + @color: 'blue'; +} + +.role2color(@role) when (@role = 'accept'), (@role = 'status-green') { + @color: 'green'; +} + +.role2color(@role) when (@role = 'inactive') { + @color: 'grey'; +} + +.role2color(@role) when (@role = 'navigation') { + @color: 'lightblue'; +} + +.role2color(@role) when (@role = 'new'), (@role = 'attention'), (@role = 'status-red') { + @color: 'red'; +} + +.role2color(@role) when (@role = 'info_alt') { + @color: 'white'; +} + +.role2color(@role) when (@role = 'sort'), (@role = 'status-yellow') { + @color: 'yellow'; +} + +.background-icon(@icon, @role: 'clickable', @size: 16, @append: 0) { + .role2color(@role); + + & when (ispercentage(@size)) { + @bgsize: @size; + } + & when not (ispercentage(@size)) { + @bgsize: unit(@size, px); + } + + @temp-icon: replace("@{color}/@{icon}", "\.(png|svg)$", '', g); + @svg: "@{image-path}/icons/@{temp-icon}.svg"; + + + & when (@append = 0) { + background-image: url("@{svg}"); + & when (ispercentage(@size)) { + background-size: @size; + } + & when not (ispercentage(@size)) { + background-size: unit(@size, px); + } + } + & when (@append = 1) { + background-image+: url("@{svg}"); + & when (ispercentage(@size)) { + background-size+: @size; + } + & when not (ispercentage(@size)) { + background-size+: unit(@size, px); + } + } +} + +.background-icons(@icon0, @role0, @size0: 16, @icon1, @role1: @role0, @size1: @size0) { + & { + .background-icon(@icon0, @role0, @size0, 1); + } + & { + .background-icon(@icon1, @role1, @size1, 1); + } +} + +.icon(@position, @icon, @role: "clickable", @size: 16, @padding: 0) when (@position = "before") { + &::before { + background-repeat: no-repeat; + content: ' '; + display: inline-block; + height: unit(@size, px); + margin-right: @padding; + .background-icon(@icon, @role, @size); + vertical-align: text-top; + width: unit(@size, px); + } +} + +.icon(@position, @icon, @role: "clickable", @size: 16, @padding: 0) when (@position = "after") { + &::after { + background-repeat: no-repeat; + content: ' '; + display: inline-block; + height: unit(@size, px); + margin-left: @padding; + .background-icon(@icon, @role, @size); + vertical-align: text-top; + width: unit(@size, px); + } +} + +// Scrollboxes +// From http://lea.verou.me/2012/04/background-attachment-local/ +// and http://dabblet.com/gist/6134408 +.scrollbox-vertical { + overflow: auto; + + background: + /* Shadow covers */ + linear-gradient(white 30%, rgba(255,255,255,0)), + linear-gradient(rgba(255,255,255,0), white 70%) 0 100%, + + /* Shadows */ + radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.2), rgba(0,0,0,0)), + radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.2), rgba(0,0,0,0)) 0 100%; + background: + /* Shadow covers */ + linear-gradient(white 30%, rgba(255,255,255,0)), + linear-gradient(rgba(255,255,255,0), white 70%) 0 100%, + + /* Shadows */ + radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.2), rgba(0,0,0,0)), + radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.2), rgba(0,0,0,0)) 0 100%; + background-repeat: no-repeat; + background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; + + /* Opera doesn't support this in the shorthand */ + background-attachment: local, local, scroll, scroll; +} + +.scrollbox-horizontal { + overflow: auto; + + background: + /* Shadow covers */ + linear-gradient(90deg, white 30%, rgba(255,255,255,0)), + linear-gradient(90deg, rgba(255,255,255,0), white 70%) 100% 0, + + /* Shadows */ + radial-gradient(farthest-side at 0 50%, rgba(0,0,0,.2), rgba(0,0,0,0)), + radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,.2), rgba(0,0,0,0)) 100% 0; + background: + /* Shadow covers */ + linear-gradient(90deg, white 30%, rgba(255,255,255,0)), + linear-gradient(90deg, rgba(255,255,255,0), white 70%) 100% 0, + + /* Shadows */ + radial-gradient(farthest-side at 0 50%, rgba(0,0,0,.2), rgba(0,0,0,0)), + radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,.2), rgba(0,0,0,0)) 100% 0; + background-repeat: no-repeat; + background-size: 40px 100%, 40px 100%, 14px 100%, 14px 100%; + + /* Opera doesn't support this in the shorthand */ + background-attachment: local, local, scroll, scroll; +} + +// Define action icons for widgets +/******************** + ** Widget actions ** + ********************/ +.widget-action(@action, @icon: @action, @role: 'clickable', @rules: {}) { + .widget-action[data-action="@{action}"] { + .hide-text(); + .square(16px); + .background-icon(@icon, @role, 16px); + + background-repeat: no-repeat; + cursor: pointer; + display: block; + // vertical-align: middle; + + @rules(); + } +} diff --git a/resources/assets/stylesheets/mixins/studip.scss b/resources/assets/stylesheets/mixins/studip.scss new file mode 100644 index 0000000..3fc8926 --- /dev/null +++ b/resources/assets/stylesheets/mixins/studip.scss @@ -0,0 +1,182 @@ +/** + * studip-mixins.less + * + * This file contains all mixins created specifically for Stud.IP + * while mixins.less should contain a copy of the mixins from + * twitter's bootstrap. + * + * 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 + * @since 4.4 + */ + +// Clearfix +// -------- +// For clearing floats like a boss h5bp.com/q +@mixin clearfix { + &::before, + &::after { + display: table; + content: ''; + // Fixes Opera/contenteditable bug: + // http://nicolasgallagher.com/micro-clearfix-hack/#comment-36952 + line-height: 0; + } + &::after { + clear: both; + } +} + +// CSS image replacement +// ------------------------- +// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 +@mixin hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + + + +@mixin background-icon($icon, $role: clickable, $size: 16) { + $icon: unquote($icon); + $role: unquote($role); + + $color: 'blue'; + @if $role == info { + $color: 'black'; + } @else if $role == accept or $role == status-green { + $color: 'green'; + } @else if $role == inactive { + $color: 'grey'; + } @else if $role == navigation { + $color: 'lightblue'; + } @else if $role == new or $role == attention or $role == status-red { + $color: 'red'; + } @else if $role == info_alt or $role == info-alt { + $color: 'white'; + } @else if $role == sort or $role == status-yellow { + $color: 'yellow'; + } + + @if $size { + @if unitless($size) { + $size: $size * 1px; + } + background-size: $size; + } + + $svg: "#{$icon-path}/#{$color}/#{$icon}.svg"; + + background-image: url("#{$svg}"); + background-size: $size; +} + +@mixin icon($position, $icon, $role, $size: 16px, $padding: 0) { + $position: unquote($position); + + @if unitless($size) { + @warn "Assuming icon size to be in pixels"; + $size: $size * 1px; + } + + @if $position == before or $position == after { + &::#{$position} { + @include background-icon($icon, $role, $size); + background-repeat: no-repeat; + content: ' '; + display: inline-block; + height: $size; + vertical-align: text-top; + width: $size; + + @if position == before { + margin-right: $padding; + } @else { + margin-left: $padding; + } + } + } +} + +// Scrollboxes +// From http://lea.verou.me/2012/04/background-attachment-local/ +// and http://dabblet.com/gist/6134408 +%scrollbox-vertical { + overflow: auto; + + background: + /* Shadow covers */ + linear-gradient(white 30%, rgba(255,255,255,0)), + linear-gradient(rgba(255,255,255,0), white 70%) 0 100%, + + /* Shadows */ + radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.2), rgba(0,0,0,0)), + radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.2), rgba(0,0,0,0)) 0 100%; + background: + /* Shadow covers */ + linear-gradient(white 30%, rgba(255,255,255,0)), + linear-gradient(rgba(255,255,255,0), white 70%) 0 100%, + + /* Shadows */ + radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.2), rgba(0,0,0,0)), + radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.2), rgba(0,0,0,0)) 0 100%; + background-repeat: no-repeat; + background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; + + /* Opera doesn't support this in the shorthand */ + background-attachment: local, local, scroll, scroll; +} + +%scrollbox-horizontal { + overflow: auto; + + background: + /* Shadow covers */ + linear-gradient(90deg, white 30%, rgba(255,255,255,0)), + linear-gradient(90deg, rgba(255,255,255,0), white 70%) 100% 0, + + /* Shadows */ + radial-gradient(farthest-side at 0 50%, rgba(0,0,0,.2), rgba(0,0,0,0)), + radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,.2), rgba(0,0,0,0)) 100% 0; + background: + /* Shadow covers */ + linear-gradient(90deg, white 30%, rgba(255,255,255,0)), + linear-gradient(90deg, rgba(255,255,255,0), white 70%) 100% 0, + + /* Shadows */ + radial-gradient(farthest-side at 0 50%, rgba(0,0,0,.2), rgba(0,0,0,0)), + radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,.2), rgba(0,0,0,0)) 100% 0; + background-repeat: no-repeat; + background-size: 40px 100%, 40px 100%, 14px 100%, 14px 100%; + + /* Opera doesn't support this in the shorthand */ + background-attachment: local, local, scroll, scroll; +} + +// Define action icons for widgets +@mixin widget-action($action, $icon: $action, $role: clickable) { + .widget-action[data-action="#{$action}"] { + @include hide-text(); + + width: 16px; + height: 16px; + + @include background-icon($icon, $role, 16px); + + background-repeat: no-repeat; + cursor: pointer; + display: block; + // vertical-align: middle; + + @content; + } +} diff --git a/resources/assets/stylesheets/mixins/twitter-mixins.less b/resources/assets/stylesheets/mixins/twitter-mixins.less new file mode 100644 index 0000000..53b3201 --- /dev/null +++ b/resources/assets/stylesheets/mixins/twitter-mixins.less @@ -0,0 +1,607 @@ +// Taken from Twitter's bootstrap toolkit, https://github.com/twitter/bootstrap +// Source: https://github.com/twitter/bootstrap/blob/master/less/mixins.less + +// +// Mixins +// -------------------------------------------------- + + +// UTILITY MIXINS +// -------------------------------------------------- + +// Clearfix +// -------- +// For clearing floats like a boss h5bp.com/q +.clearfix { + &:before, + &:after { + display: table; + content: ""; + // Fixes Opera/contenteditable bug: + // http://nicolasgallagher.com/micro-clearfix-hack/#comment-36952 + line-height: 0; + } + &:after { + clear: both; + } +} + +// Webkit-style focus +// ------------------ +.tab-focus() { + // Default + outline: thin dotted #333; + // Webkit + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +// Center-align a block level element +// ---------------------------------- +.center-block() { + display: block; + margin-left: auto; + margin-right: auto; +} + +// IE7 inline-block +// ---------------- +.ie7-inline-block() { + *display: inline; /* IE7 inline-block hack */ + *zoom: 1; +} + +// IE7 likes to collapse whitespace on either side of the inline-block elements. +// Ems because we're attempting to match the width of a space character. Left +// version is for form buttons, which typically come after other elements, and +// right version is for icons, which come before. Applying both is ok, but it will +// mean that space between those elements will be .6em (~2 space characters) in IE7, +// instead of the 1 space in other browsers. +.ie7-restore-left-whitespace() { + *margin-left: .3em; + + &:first-child { + *margin-left: 0; + } +} + +.ie7-restore-right-whitespace() { + *margin-right: .3em; +} + +// Sizing shortcuts +// ------------------------- +.size(@height, @width) { + width: @width; + height: @height; +} +.square(@size) { + .size(@size, @size); +} + +// Placeholder text +// ------------------------- +.placeholder(@color: @placeholderText) { + &:-moz-placeholder { + color: @color; + } + &:-ms-input-placeholder { + color: @color; + } + &::-webkit-input-placeholder { + color: @color; + } +} + +// Text overflow +// ------------------------- +// Requires inline-block or block for proper styling +.text-overflow() { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +// CSS image replacement +// ------------------------- +// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + + +// FONTS +// -------------------------------------------------- + +#font { + #family { + .serif() { + font-family: @serifFontFamily; + } + .sans-serif() { + font-family: @sansFontFamily; + } + .monospace() { + font-family: @monoFontFamily; + } + } + .shorthand(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) { + font-size: @size; + font-weight: @weight; + line-height: @lineHeight; + } + .serif(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) { + #font > #family > .serif; + #font > .shorthand(@size, @weight, @lineHeight); + } + .sans-serif(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) { + #font > #family > .sans-serif; + #font > .shorthand(@size, @weight, @lineHeight); + } + .monospace(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) { + #font > #family > .monospace; + #font > .shorthand(@size, @weight, @lineHeight); + } +} + + +// FORMS +// -------------------------------------------------- + +// Block level inputs +.input-block-level { + display: block; + width: 100%; + min-height: 30px; // Make inputs at least the height of their button counterpart + .box-sizing(border-box); // Makes inputs behave like true block-level elements +} + + + +// Mixin for form field states +.formFieldState(@textColor: #555, @borderColor: #ccc, @backgroundColor: #f5f5f5) { + // Set the text color + > label, + .help-block, + .help-inline { + color: @textColor; + } + // Style inputs accordingly + .checkbox, + .radio, + input, + select, + textarea { + color: @textColor; + } + input, + select, + textarea { + border-color: @borderColor; + .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work + &:focus { + border-color: darken(@borderColor, 10%); + .box-shadow(inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@borderColor, 20%)); + } + } + // Give a small background color for input-prepend/-append + .input-prepend .add-on, + .input-append .add-on { + color: @textColor; + background-color: @backgroundColor; + border-color: @textColor; + } +} + + + +// CSS3 PROPERTIES +// -------------------------------------------------- + +// Border Radius +.border-radius(@radius) { + border-radius: @radius; +} + +// Single Corner Border Radius +.border-top-left-radius(@radius) { + border-top-left-radius: @radius; +} +.border-top-right-radius(@radius) { + border-top-right-radius: @radius; +} +.border-bottom-right-radius(@radius) { + border-bottom-right-radius: @radius; +} +.border-bottom-left-radius(@radius) { + border-bottom-left-radius: @radius; +} + +// Single Side Border Radius +.border-top-radius(@radius) { + .border-top-right-radius(@radius); + .border-top-left-radius(@radius); +} +.border-right-radius(@radius) { + .border-top-right-radius(@radius); + .border-bottom-right-radius(@radius); +} +.border-bottom-radius(@radius) { + .border-bottom-right-radius(@radius); + .border-bottom-left-radius(@radius); +} +.border-left-radius(@radius) { + .border-top-left-radius(@radius); + .border-bottom-left-radius(@radius); +} + +// Drop shadows +.box-shadow(@shadow) { + box-shadow: @shadow; +} + +// Transitions +.transition(@transition) { + transition: @transition; +} +.transition-delay(@transition-delay) { + transition-delay: @transition-delay; +} + +// Transformations +.rotate(@degrees) { + transform: rotate(@degrees); +} +.scale(@ratio) { + transform: scale(@ratio); +} +.translate(@x, @y) { + transform: translate(@x, @y); +} +.skew(@x, @y) { + transform: skew(@x, @y); +} +.translate3d(@x, @y, @z) { + transform: translate3d(@x, @y, @z); +} + +// Backface visibility +// Prevent browsers from flickering when using CSS 3D transforms. +// Default value is `visible`, but can be changed to `hidden +// See git pull https://github.com/dannykeane/bootstrap.git backface-visibility for examples +.backface-visibility(@visibility){ + backface-visibility: @visibility; +} + +// Background clipping +// Heads up: FF 3.6 and under need "padding" instead of "padding-box" +.background-clip(@clip) { + background-clip: @clip; +} + +// Background sizing +.background-size(@size){ + background-size: @size; +} + + +// Box sizing +.box-sizing(@boxmodel) { + box-sizing: @boxmodel; +} + +// User select +// For selecting text on the page +.user-select(@select) { + user-select: @select; +} + +// Resize anything +.resizable(@direction) { + resize: @direction; // Options: horizontal, vertical, both + overflow: auto; // Safari fix +} + +// CSS3 Content Columns +.content-columns(@columnCount, @columnGap: @gridGutterWidth) { + column-count: @columnCount; + column-gap: @columnGap; +} + +// Optional hyphenation +.hyphens(@mode: auto) { + word-wrap: break-word; + hyphens: @mode; +} + +// Opacity +.opacity(@opacity) { + opacity: (@opacity / 100); +} + + + +// BACKGROUNDS +// -------------------------------------------------- + +// Add an alphatransparency value to any background or border color (via Elyse Holladay) +#translucent { + .background(@color: @white, @alpha: 1) { + background-color: hsla(hue(@color), saturation(@color), lightness(@color), @alpha); + } + .border(@color: @white, @alpha: 1) { + border-color: hsla(hue(@color), saturation(@color), lightness(@color), @alpha); + .background-clip(padding-box); + } +} + +// Gradient Bar Colors for buttons and alerts +.gradientBar(@primaryColor, @secondaryColor, @textColor: #fff, @textShadow: 0 -1px 0 rgba(0,0,0,.25)) { + color: @textColor; + text-shadow: @textShadow; + #gradient > .vertical(@primaryColor, @secondaryColor); + border-color: @secondaryColor @secondaryColor darken(@secondaryColor, 15%); + border-color: rgba(0,0,0,.1) rgba(0,0,0,.1) fadein(rgba(0,0,0,.1), 15%); +} + +// Gradients +#gradient { + .horizontal(@startColor: #555, @endColor: #333) { + background-color: @endColor; + background-image: -moz-linear-gradient(left, @startColor, @endColor); // FF 3.6+ + background-image: -webkit-gradient(linear, 0 0, 100% 0, from(@startColor), to(@endColor)); // Safari 4+, Chrome 2+ + background-image: -webkit-linear-gradient(left, @startColor, @endColor); // Safari 5.1+, Chrome 10+ + background-image: -o-linear-gradient(left, @startColor, @endColor); // Opera 11.10 + background-image: linear-gradient(to right, @startColor, @endColor); // Standard, IE10 + background-repeat: repeat-x; + } + .vertical(@startColor: #555, @endColor: #333) { + background-color: mix(@startColor, @endColor, 60%); + background-image: -moz-linear-gradient(top, @startColor, @endColor); // FF 3.6+ + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), to(@endColor)); // Safari 4+, Chrome 2+ + background-image: -webkit-linear-gradient(top, @startColor, @endColor); // Safari 5.1+, Chrome 10+ + background-image: -o-linear-gradient(top, @startColor, @endColor); // Opera 11.10 + background-image: linear-gradient(to bottom, @startColor, @endColor); // Standard, IE10 + background-repeat: repeat-x; + } + .directional(@startColor: #555, @endColor: #333, @deg: 45deg) { + background-color: @endColor; + background-repeat: repeat-x; + background-image: -moz-linear-gradient(@deg, @startColor, @endColor); // FF 3.6+ + background-image: -webkit-linear-gradient(@deg, @startColor, @endColor); // Safari 5.1+, Chrome 10+ + background-image: -o-linear-gradient(@deg, @startColor, @endColor); // Opera 11.10 + background-image: linear-gradient(@deg, @startColor, @endColor); // Standard, IE10 + } + .vertical-three-colors(@startColor: #00b3ee, @midColor: #7a43b6, @colorStop: 50%, @endColor: #c3325f) { + background-color: mix(@midColor, @endColor, 80%); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), color-stop(@colorStop, @midColor), to(@endColor)); + background-image: -webkit-linear-gradient(@startColor, @midColor @colorStop, @endColor); + background-image: -moz-linear-gradient(top, @startColor, @midColor @colorStop, @endColor); + background-image: -o-linear-gradient(@startColor, @midColor @colorStop, @endColor); + background-image: linear-gradient(@startColor, @midColor @colorStop, @endColor); + background-repeat: no-repeat; + } + .radial(@innerColor: #555, @outerColor: #333) { + background-color: @outerColor; + background-image: -webkit-gradient(radial, center center, 0, center center, 460, from(@innerColor), to(@outerColor)); + background-image: -webkit-radial-gradient(circle, @innerColor, @outerColor); + background-image: -moz-radial-gradient(circle, @innerColor, @outerColor); + background-image: -o-radial-gradient(circle, @innerColor, @outerColor); + background-repeat: no-repeat; + } + .striped(@color: #555, @angle: 45deg) { + background-color: @color; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(.25, rgba(255,255,255,.15)), color-stop(.25, transparent), color-stop(.5, transparent), color-stop(.5, rgba(255,255,255,.15)), color-stop(.75, rgba(255,255,255,.15)), color-stop(.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); + } +} + + +// COMPONENT MIXINS +// -------------------------------------------------- + +// Horizontal dividers +// ------------------------- +// Dividers (basically an hr) within dropdowns and nav lists +.nav-divider(@top: #e5e5e5, @bottom: @white) { + // IE7 needs a set width since we gave a height. Restricting just + // to IE7 to keep the 1px left/right space in other browsers. + // It is unclear where IE is getting the extra space that we need + // to negative-margin away, but so it goes. + *width: 100%; + height: 1px; + margin: ((@baseLineHeight / 2) - 1) 1px; // 8px 1px + *margin: -5px 0 5px; + overflow: hidden; + background-color: @top; + border-bottom: 1px solid @bottom; +} + +// Button backgrounds +// ------------------ +.buttonBackground(@startColor, @endColor, @textColor: #fff, @textShadow: 0 -1px 0 rgba(0,0,0,.25)) { + // gradientBar will set the background to a pleasing blend of these, to support IE<=9 + .gradientBar(@startColor, @endColor, @textColor, @textShadow); + background-color: @endColor; /* Darken IE7 buttons by default so they stand out more given they won't have borders */ + + // in these cases the gradient won't cover the background, so we override + &:hover, &:active, &.active, &.disabled, &[disabled] { + color: @textColor; + background-color: @endColor; + *background-color: darken(@endColor, 5%); + } +} + +// Navbar vertical align +// ------------------------- +// Vertically center elements in the navbar. +// Example: an element has a height of 30px, so write out `.navbarVerticalAlign(30px);` to calculate the appropriate top margin. +.navbarVerticalAlign(@elementHeight) { + margin-top: (@navbarHeight - @elementHeight) / 2; +} + + + +// Grid System +// ----------- + +// Centered container element +.container-fixed() { + margin-right: auto; + margin-left: auto; + .clearfix(); +} + +// Table columns +.tableColumns(@columnSpan: 1) { + float: none; // undo default grid column styles + width: ((@gridColumnWidth) * @columnSpan) + (@gridGutterWidth * (@columnSpan - 1)) - 16; // 16 is total padding on left and right of table cells + margin-left: 0; // undo default grid column styles +} + +// Make a Grid +// Use .makeRow and .makeColumn to assign semantic layouts grid system behavior +.makeRow() { + margin-left: @gridGutterWidth * -1; + .clearfix(); +} +.makeColumn(@columns: 1, @offset: 0) { + float: left; + margin-left: (@gridColumnWidth * @offset) + (@gridGutterWidth * (@offset - 1)) + (@gridGutterWidth * 2); + width: (@gridColumnWidth * @columns) + (@gridGutterWidth * (@columns - 1)); +} + +// The Grid +#grid { + + .core (@gridColumnWidth, @gridGutterWidth) { + + .spanX (@index) when (@index > 0) { + .span@{index} { .span(@index); } + .spanX(@index - 1); + } + .spanX (0) {} + + .offsetX (@index) when (@index > 0) { + .offset@{index} { .offset(@index); } + .offsetX(@index - 1); + } + .offsetX (0) {} + + .offset (@columns) { + margin-left: (@gridColumnWidth * @columns) + (@gridGutterWidth * (@columns + 1)); + } + + .span (@columns) { + width: (@gridColumnWidth * @columns) + (@gridGutterWidth * (@columns - 1)); + } + + .row { + margin-left: @gridGutterWidth * -1; + .clearfix(); + } + + [class*="span"] { + float: left; + min-height: 1px; // prevent collapsing columns + margin-left: @gridGutterWidth; + } + + // Set the container width, and override it for fixed navbars in media queries + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { .span(@gridColumns); } + + // generate .spanX and .offsetX + .spanX (@gridColumns); + .offsetX (@gridColumns); + + } + + .fluid (@fluidGridColumnWidth, @fluidGridGutterWidth) { + + .spanX (@index) when (@index > 0) { + .span@{index} { .span(@index); } + .spanX(@index - 1); + } + .spanX (0) {} + + .offsetX (@index) when (@index > 0) { + .offset@{index} { .offset(@index); } + .offset@{index}:first-child { .offsetFirstChild(@index); } + .offsetX(@index - 1); + } + .offsetX (0) {} + + .offset (@columns) { + margin-left: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) + (@fluidGridGutterWidth*2); + *margin-left: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) - (.5 / @gridRowWidth * 100 * 1%) + (@fluidGridGutterWidth*2) - (.5 / @gridRowWidth * 100 * 1%); + } + + .offsetFirstChild (@columns) { + margin-left: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) + (@fluidGridGutterWidth); + *margin-left: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) - (.5 / @gridRowWidth * 100 * 1%) + @fluidGridGutterWidth - (.5 / @gridRowWidth * 100 * 1%); + } + + .span (@columns) { + width: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)); + *width: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) - (.5 / @gridRowWidth * 100 * 1%); + } + + .row-fluid { + width: 100%; + .clearfix(); + [class*="span"] { + .input-block-level(); + float: left; + margin-left: @fluidGridGutterWidth; + *margin-left: @fluidGridGutterWidth - (.5 / @gridRowWidth * 100 * 1%); + } + [class*="span"]:first-child { + margin-left: 0; + } + + // generate .spanX and .offsetX + .spanX (@gridColumns); + .offsetX (@gridColumns); + } + + } + + .input(@gridColumnWidth, @gridGutterWidth) { + + .spanX (@index) when (@index > 0) { + input.span@{index}, textarea.span@{index}, .uneditable-input.span@{index} { .span(@index); } + .spanX(@index - 1); + } + .spanX (0) {} + + .span(@columns) { + width: ((@gridColumnWidth) * @columns) + (@gridGutterWidth * (@columns - 1)) - 14; + } + + input, + textarea, + .uneditable-input { + margin-left: 0; // override margin-left from core grid system + } + + // Space grid-sized controls properly if multiple per line + .controls-row [class*="span"] + [class*="span"] { + margin-left: @gridGutterWidth; + } + + // generate .spanX + .spanX (@gridColumns); + + } + +} diff --git a/resources/assets/stylesheets/print.less b/resources/assets/stylesheets/print.less new file mode 100644 index 0000000..01aea62 --- /dev/null +++ b/resources/assets/stylesheets/print.less @@ -0,0 +1,299 @@ +@import "mixins.less"; +@import "less/breakpoints.less"; +@import "less/visibility.less"; +@import "less/fullcalendar-print.less"; +@import "less/resources-print.less"; +@import "less/qrcode-print.less"; +@import (reference) "less/schedule.less"; + + +/******************************************************************************* + Druck-Stylesheet für Stud.IP + - nur pt Größenangaben verwenden + - auf background-colors verzichten (werden nicht gedruckt) +*******************************************************************************/ + +@page { + margin-top: 15mm; + margin-bottom: 15mm; + margin-left: 15mm; + margin-right: 15mm; +} + +html, body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + background: none; + height: auto; +} + +body, input, textarea, td, th, blockquote, p, form, ul, h4 { + font-size: 10pt; +} + +#layout_wrapper { + display: block; +} +#layout_content { + margin-right: 0; +} + +#header *, #barTopMenu, #barTopTools, #barTopStudip, #barBottomLeft, +#barBottommiddle, #barBottomright, #barBottomshadow, #tabs, #tabs2, +#layout_infobox, td.infoboxrahmen, td.infobox, td.infobox-img, +#schedule_icons, #edit_inst_entry, #layout_sidebar, #barBottomContainer, +.messagebox_buttons a.close, a.button, button.button, +#layout-sidebar, .helpbar-container, .helpbar, +#skip_link_navigation, .skip_target, nav.action-menu, +#barTopFont { + display: none !important; +} + +img { + border: 0; +} + +a, a:link, a:visited { + color: #000000; +} + +a:visited, a:link, a:hover, a:active { + text-decoration: none; +} + +h1, .topic { + font-size: 15pt; + margin: 3pt 0 2pt 0; +} + +h1 span { + display: block; + font-size: 14.25px; + font-weight: 100; + padding: 5px 0; +} + +section header h1 { + font-size: 12pt; + margin-top: 8pt; +} + +h2 { + font-size: 13pt; +} + +h3 { + font-size: 12pt; +} + +td.angemeldet { + border: 1pt solid #000000; +} + +td.rahmen_white { + border: 1pt solid #000000; + background: #FFFFFF none; +} + +td.rahmen_steel { + border: 1pt solid #000000; + background-color: #f3f5f8; +} + +td.rahmen_table_row_odd { + border: 1pt solid #000000; + background-color: #ebebeb; +} + +.hidden { + display: none; +} + +/* --- stud.ip-title oben -------------------------------------------------- */ +#barTopFont { + font-weight: normal; + display: block !important; + font-size: 18pt; + padding: 0 0 2pt 0; + margin: 0 0 5pt 0; + border-bottom: 1pt solid #000000; + +} + +/* --- studip-logo im footer ------------------------------------------------ */ +#layout_footer * { + display: none; +} + +#layout_footer { + width: 100%; + text-align: center; + padding: 2pt 0 0 0; + margin: 5pt 0 0 0; + border-top: 1pt solid #000000; + + &:after { + content: url('../images/logos/logo2b.png'); + } +} + +/* --- print-style for calendar api------------------------------------------ */ +#schedule { + width: 100%; + height: 100%; + + #schedule_headings { + margin-left: 41pt; + background: none; + } + + #schedule_data { + width: 100%; + table-layout: fixed; + + thead td { + text-align: center; + } + + th:first-child, td:first-child { + width: 40px; + } + + td { + vertical-align: top; + } + } + + div.schedule_entry { + position: absolute; + margin: 0; + padding: 0; + font-size: 11pt; + color: #000000; + + dl { + height: 100%; + margin: 0; + color: black ! important; + background-color: @white; + border: 1px solid @light-gray-color-60; + overflow: hidden; + + dd { + margin: 0; + overflow: hidden; + word-wrap: break-word; + } + + dt { + background-color: transparent ! important; + } + + a:hover { + text-decoration: underline; + } + } + } + + div.schedule_day { + border-left: 1pt solid black; + position: relative; + } + + div.schedule_hours { + border-top: 1px solid #ddd; + padding-bottom: 1px; + } + + div.snatch { + position: absolute; + bottom: 4pt; + text-align: center; + width: 100%; + cursor: ns-resize; + padding-bottom: 2pt; + } + + #new_entry { + position: absolute; + border: 2pt solid #E0E0F0; + width: 400pt; + height: 230pt; + background-color: #E8EEF7; + font-size: 12pt; + } + + div.new_entry { + position: absolute; + left: 50%; + top: 180pt; + margin-left: -25%; + height: 320pt; + width: 50%; + font-size: 12pt; + background-color: #E8EEF7; + border: 2pt solid #E0E0F0; + } + + div.schedule_marker { + border-bottom: 1px dotted #ddd; + border-top: 1px solid #ddd; + padding: 0; + } +} + +/* aus style.css */ +h1.content, h2.content, h3.content { color:#24437c; } +hr.content { + margin:0 1em; + background-color:#555555; + border-radius:5px; + height:1px; + border:none; +} + +table.content { + border-collapse:collapse; + + td { + border:thin solid #666666; + font-size:smaller; + padding:3px; + } +} + +div.content { + background-color:#f3f5f8; + clear:both; + margin:0; + overflow:hidden; + padding:2px; +} + +.quote { + background:#eeeeee none; + border:1px solid black; + margin-left:20px; + margin-right:20px; + padding:3px; +} + +td.quote { + border:1px solid #000000; + font-size:8px; +} + +a.link-intern { + padding-left:18px; + .background-icon('link-intern', 'clickable'); + background-repeat: no-repeat; +} + +a.link-extern { + padding-left:18px; + .background-icon('link-extern', 'clickable'); + background-repeat: no-repeat; +} + +.formatted-content { + display: inline; +} diff --git a/resources/assets/stylesheets/scss/actionmenu.scss b/resources/assets/stylesheets/scss/actionmenu.scss new file mode 100644 index 0000000..f62a515 --- /dev/null +++ b/resources/assets/stylesheets/scss/actionmenu.scss @@ -0,0 +1,252 @@ +$action-menu-icon-size: 20px; +$action-menu-shadow: 1px 1px 1px $dark-gray-color-60; + +.action-menu { + display: inline-block; + position: relative; + text-align: right; + vertical-align: middle; + + &:not(.is-open) .action-menu-content { + display: none; + } +} + +.action-menu-wrapper { + position: absolute; + + &:not(.is-open) { + display: none; + } +} + +.action-menu, +.action-menu-wrapper { + z-index: 2; + + .action-menu-content { + position: absolute; + top: -4px; + right: -4px; + + padding: 4px 8px; + + background: white; + border: thin solid $dark-gray-color-45; + box-shadow: $action-menu-shadow; + font-weight: normal; + text-align: left; + white-space: nowrap; + } + + .action-menu-icon { + z-index: 1; + + position: relative; + cursor: pointer; + display: inline-block; + padding: 0; + width: 20px; + height: 20px; + + // Create animated icon that changes to close icon on activation/hover + div { + width: ($action-menu-icon-size / 4); + height: ($action-menu-icon-size / 4); + transform: translate((-($action-menu-icon-size / 8)), 0); + transition: all .25s ease-in-out; + + display: block; + position: absolute; + background: $base-color; + border-radius: 50%; + opacity: 1; + left: 50%; + + &:nth-child(1) { + top: 0px; + } + + &:nth-child(2) { + top: ($action-menu-icon-size / 2); + transform: translate((-($action-menu-icon-size / 8)), (-($action-menu-icon-size / 8))); + } + + &:nth-child(3) { + bottom: 0; + } + } + } + + .action-menu-title { + font-weight: bold; + margin: 0.2em 0 0.3em; + } + + .action-menu-list { + list-style: none; + margin: 0; + padding: 0; + } + + .action-menu-item { + line-height: 1; + padding: 0; + + > a, + > label { + margin: 0; + padding: 3px 0; + display: block; + } + + &.action-menu-item-disabled { + > a, + > label { + &, + &:hover { + color: $dark-gray-color-80; + cursor: default; + } + } + } + + a img, + a svg, + .action-menu-no-icon, + input[type="image"] { + display: inline-block; + margin: 0 0.25em; + vertical-align: middle; + + width: $action-menu-icon-size; + height: $action-menu-icon-size; + } + + > button { + background: transparent; + border: 0; + line-height: 20px; + margin: 0; + padding: 3px 0; + } + + > label, + > button { + color: $base-color; + cursor: pointer; + &:hover { + color: $active-color; + } + } + } + + &.is-open { + z-index: 3; + .action-menu-icon { + div { + border-radius: 0; + + &:nth-child(1) { + left: 0; + transform: rotate(45deg) translate((($action-menu-icon-size / 4) + 0.5), (($action-menu-icon-size / 4) + 0.5)); + width: 100%; + } + + &:nth-child(2) { + opacity: 0; + } + + &:nth-child(3) { + left: 0; + transform: rotate(-45deg) translate(($action-menu-icon-size / 4), (-($action-menu-icon-size / 4))); + width: 100%; + } + } + } + } + + &.is-reversed { + .action-menu-content { + top: auto; + bottom: -4px; + + .action-menu-list .action-menu-item:last-of-type { + padding-right: 20px; + } + } + } +} + + +/* copied from copyable-links.less and modified */ +.js-action-confirm-animation { + $animation-name: js-action-confirm-confirmation; + $animation-duration: 2s; + + // Position confirmation message above the link + position: relative; + + div { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + text-align: center; + + @include icon(before, check-circle, status-green, 16px, 5px); + } + + // Flip the link and confirmation message along the x axis + a, + div { + backface-visibility: hidden; + pointer-events: none; + } + + a { + @keyframes #{$animation-name}-front { + 0% { + opacity: 1; + transform: rotateX(0); + } + 33% { + opacity: 0; + transform: rotateX(180deg); + } + 66% { + opacity: 0; + transform: rotateX(180deg); + } + to { + opacity: 1; + transform: rotateX(0); + } + } + animation: #{$animation-name}-front $animation-duration linear; + } + + div { + @keyframes #{$animation-name}-back { + 0% { + opacity: 0; + transform: rotateX(180deg); + } + 33% { + opacity: 1; + transform: rotateX(0); + } + 66% { + opacity: 1; + transform: rotateX(0); + } + to { + opacity: 0; + transform: rotateX(180deg); + } + } + animation: #{$animation-name}-back $animation-duration linear; + } +} diff --git a/resources/assets/stylesheets/scss/admin-courses.scss b/resources/assets/stylesheets/scss/admin-courses.scss new file mode 100644 index 0000000..bbc06b0 --- /dev/null +++ b/resources/assets/stylesheets/scss/admin-courses.scss @@ -0,0 +1,30 @@ +.button.has-notice, +.button.has-no-notice { + &::before { + display: inline-block; + height: 16px; + vertical-align: sub; + width: 16px; + } + &::before { + margin-right: 0.5ex; + } +} + +.button.has-notice { + &::before { + content: url("#{$image-path}/icons/blue/file-text.svg"); + } + &:hover::before { + content: url("#{$image-path}/icons/white/file-text.svg"); + } + +} +.button.has-no-notice { + &::before { + content: url("#{$image-path}/icons/blue/file.svg"); + } + &:hover::before { + content: url("#{$image-path}/icons/white/file.svg"); + } +} diff --git a/resources/assets/stylesheets/scss/admission.scss b/resources/assets/stylesheets/scss/admission.scss new file mode 100644 index 0000000..3a3ed05 --- /dev/null +++ b/resources/assets/stylesheets/scss/admission.scss @@ -0,0 +1,44 @@ +#rulelist div.admissionrule { + display: list-item; + list-style-type: disc; + margin-left: 25px; +} + +.hover_box { + div { + display: inline; + } + .action_icons { + display: inline; + margin-left: 15px; + } +} + +.condition { + margin-left: 20px; +} + +.check_actions { + font-weight: normal; + + a { + cursor: pointer; + } +} + +#userlists { + div { + margin-bottom: 10px; + + a { + &.userlist-action { + margin-left: 2px; + margin-right: 2px; + } + + img { + vertical-align: bottom; + } + } + } +} diff --git a/resources/assets/stylesheets/scss/blubber.scss b/resources/assets/stylesheets/scss/blubber.scss new file mode 100644 index 0000000..dc577b9 --- /dev/null +++ b/resources/assets/stylesheets/scss/blubber.scss @@ -0,0 +1,731 @@ +.blubber_panel { + display: flex; + align-items: stretch; + height: calc(100vh - 174px); + transition: opacity 100ms, filter 100ms; + &.waiting { + filter: blur(1px); + opacity: 0.5; + } + [v-if], + [v-for], + [v-show] { + display: none; + } + .context_info { + .followunfollow { + &.loading { + pointer-events: none; + } + > .follow { + display: none; + } + &.unfollowed { + text-decoration: line-through; + } + &.unfollowed > .follow { + display: inline-block; + } + &.unfollowed > .unfollow { + display: none; + } + } + } +} + +.blubber_thread { + border: 1px solid $content-color-40; + background-color: $dark-gray-color-5; + + width: 100%; + max-width: 100%; + + margin-right: 12px; + + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: stretch; + align-content: stretch; + position: relative; + + [v-if], + [v-for], + [v-show] { + display: none; + } + + .scrollable_area { + max-height: calc(100vh - 240px); + overflow: auto; + &.scrolled::before { + //the shadow! + content: ''; + left: 0px; + right: 0px; + height: 20px; + display: block; + position: absolute; + background: linear-gradient(to bottom, rgba(0,0,0,0.08), rgba(0,0,0,0)); + z-index: 10; + } + } + + &.dragover { + background-color: $yellow-40; + .writer > textarea { + background-color: $yellow-40; + } + ol.comments > li.mine > .content::after, + ol.comments > li.theirs > .content::after { + background-color: $yellow-40; + } + } + + .context_info { + border-bottom: 1px solid $content-color-40; + text-align: center; + } + .writer { + border-top: 1px solid $content-color-40; + } + + + ol.comments { + list-style-type: none; + margin: 0px; + padding: 0px; + + > li { + display: none; + &.new { + animation: blubber-scaling 300ms ease-out; + } + align-items: flex-end; + justify-content: flex-start; + + margin-top: 20px; + padding-right: 10px; + padding-left: 10px; + + &:last-child { + margin-bottom: 10px; + } + + > .content { + max-width: 60%; + margin-left: 5px; + margin-right: 5px; + padding: 5px; + + > .html { + max-width: 100%; + overflow: hidden; + img { + max-width: 100%; + max-height: 95vh; + } + } + + > .edit { + display: none; + } + + &.editing { + > .html { + display: none; + } + > .edit { + display: block; + width: 300px; + height: 20px; + } + } + } + + &.mine { + display: flex; + flex-direction: row-reverse; + > .content { + background-color: $base-color; + color: $white; + + .opengraph { + background-color: $base-color-80; + border-color: $base-color-60; + } + + a.link-extern { + @include icon(before, link-extern, info-alt); + + &::before { + opacity: 0.8; + transition: opacity 200ms; + } + } + a.link-intern { + @include icon(before, link-intern, info-alt); + + &::before { + opacity: 0.8; + transition: opacity 200ms; + } + } + + a, + a:link, + a:visited { + color: $white; + opacity: 0.8; + transition: opacity 200ms; + } + a:hover, + a:active, + a:hover.index, + a:active.index, + a:hover.tree { + color: $white; + opacity: 1; + transition: opacity 200ms; + } + a.link-extern:hover::before, + a.link-intern:hover::before { + opacity: 1; + transition: opacity 200ms; + } + + //Now the small triangular: + @include arrow-right(10px, $base-color); + &::before { + top: 100%; + } + &::after { + content: ''; + height: 10px; + width: 10px; + background-color: $dark-gray-color-5; + position: absolute; + pointer-events: none; + left: 100%; + top: 100%; + } + > .name { + display: none; + } + blockquote { + background-color: rgba(255, 255, 255, 0.1); + } + } + > .avatar { + display: none; + } + .answer_comment { + display: none; + } + } + &.theirs { + display: flex; + > .content { + background-color: $content-color-20; + @include arrow-left(10px, $content-color-20); + &::before { + top: 100%; + } + &::after { + content: ''; + height: 10px; + width: 10px; + background-color: $dark-gray-color-5; + position: absolute; + pointer-events: none; + left: -10px; + top: 100%; + } + > .name { + color: $light-gray-color-80; + font-size: 0.8em; + display: block; + } + } + > .avatar { + min-width: 40px; + min-height: 40px; + width: 40px; + height: 40px; + background-repeat: no-repeat; + background-size: 40px auto; + background-position: center center; + margin-right: 10px; + } + .answer_comment > img { + vertical-align: text-bottom; + transform: rotate(180deg); + } + } + &.more { + display: flex; + justify-content: center; + } + > .time { + font-size: 0.8em; + color: $light-gray-color; + time { + @media screen and (max-width: $major-breakpoint-small) { + display: none; + } + } + } + } + + } + + .writer { + background-color: $white; + background-image: linear-gradient(to left, $content-color-60, $content-color-60); + background-size: 0% 100%; + background-repeat: no-repeat; + padding: 5px; + + display: flex; + justify-content: space-around; + align-items: center; + + > textarea { + border: 1px solid $content-color-40; + background-color: $white; + width: calc(100% - 140px); + height: 34px; + resize: none; + padding: 5px; + max-height: 40vh; + overflow: auto !important; + } + .send { + display: none; + cursor: pointer; + } + label { + cursor: pointer; + } + &.filled { + .send { + display: block; + } + label { + display: none; + } + } + } + + .thread_posting { + border-bottom: 1px solid $content-color-40; + background-color: $white; + + .contextinfo { + background-color: $content-color-20; + border-bottom: 1px solid $content-color-40; + color: $dark-gray-color-60; + font-size: 0.8em; + padding: 7px 5px 5px 75px; + position: relative; + a { + color: $dark-gray-color-60; + } + time { + float: right; + margin-left: 0.5em; + } + .avatar { + position: absolute; + left: 10px; + top: 10px; + + height: 40px; + width: 40px; + background-repeat: no-repeat; + background-position: center center; + background-size: 100% auto; + } + } + + + .content { + padding: 10px 10px 10px 75px; + img { + max-width: 100%; + max-height: 95vh; + } + } + } + + .empty_blubber_background { + padding-top: 100px; + background-image: url('#{$icon-path}/lightblue/blubber.svg'), url('#{$icon-path}/blue/blubber.svg'), url('#{$icon-path}/lightblue/blubber.svg'); + background-repeat: no-repeat; + background-size: 40% 40%, 30% 30%, 70% 70%; + background-position: 70% 50%, 30% 45%, center 0%; + background-blend-mode: normal, normal, overlay; + background-color: mix($dark-gray-color-5, rgba(255, 255, 255, 0), 70%); + text-align: center; + height: 40vh; + color: $content-color; + font-size: 1.6em; + > :first-child { + position: relative; + top: 170px; + } + } +} + +#blubber_stream_container { + display: flex; + align-items: stretch; + width: calc(100% - 270px); + @media screen and (max-width: $major-breakpoint-medium) { + width: 100%; + } + @media screen and (min-width: $major-breakpoint-large) { + max-width: calc(#{$major-breakpoint-large} - 100px); + } +} + + + +.blubber_sideinfo { + width: 270px; + max-width: 270px; + + margin-left: 5px; + border: 1px solid $content-color-40; + box-sizing: border-box; + + max-height: calc(100vh - 140px); + overflow: auto; + + .indented { + padding: 10px; + } + .new_section { + border-top: 1px solid $content-color-40; + } + + .members { + margin-bottom: 10px; + li { + padding-top: 10px; + padding-bottom: 10px; + border-bottom: 1px solid $content-color-40; + &:first-child { + border-top: 1px solid $content-color-40; + } + } + &.topless li:first-child { + padding-top: 0px; + border-top: none; + } + &.bottomless li { + border-bottom: none; + padding-bottom: 0px; + } + } + + .headline { + display: flex; + margin-bottom: 10px; + &:last-child { + margin-bottom: 0px; + } + .side { + display: flex; + flex-direction: column; + justify-content: center; + .icons { + margin-top: 5px; + } + } + + .avatar { + min-width: 50px; + min-height: 50px; + max-width: 50px; + max-height: 50px; + display: block; + background-size: 100% 100%; + background-position: center; + margin-right: 10px; + } + } + + .context_info { + border-bottom: 1px solid $content-color-40; + + .blubber_course_info { + + } + .blubber_private_info { + .icon { + text-align: center; + } + + .avatar { + min-width: 50px; + min-height: 50px; + max-width: 50px; + max-height: 50px; + display: block; + background-size: 100% 100%; + background-position: center; + margin-right: 10px; + } + } + } + +} + +.lowprio_info { + color: $light-gray-color; +} + +.studip-dialog { + .blubber_panel { + height: inherit; + } + #blubber_stream_container { + width: 100%; + } + .blubber_thread { + width: 100%; + max-width: 100%; + } +} + +#blubber-index { + @media screen and (max-width: $major-breakpoint-small) { + #page_title_container, + .secondary-navigation { + display: none; + } + } +} + + +.blubber_threads_widget { + .sidebar-widget-header { + .actions { + float: right; + } + } + + .sidebar-widget-content { + padding: 0px; + max-height: calc(100vh - 359px); + overflow: auto; + + .scrollable_area.scrolled::before { + content: ''; + width: 100%; + max-width: 540px; + height: 20px; + display: block; + position: absolute; + background: linear-gradient(to bottom, rgba(0,0,0,0.08), rgba(0,0,0,0)); + z-index: 10; + } + + .scrollable_area.scrolled ol li.active { + &::before { + display: none; + } + &::after { + display: none; + } + } + + ol { + list-style-type: none; + padding-left: 0px; + + li { + border-bottom: thin solid $content-color-40; + + height: 50px; + max-height: 50px; + overflow: hidden; + padding: 10px; + cursor: pointer; + color: $base-color; + font-weight: bold; + &:last-child { + border-bottom: none; + } + + &.unseen { + border-left: 3px solid $active-color; + padding-left: 7px; + } + + &[v-if], + &[v-for], + &[v-show] { + display: none; + } + + &.more { + display: flex; + justify-content: center; + } + + &.active { + background-color: $yellow-40; + + &::before { + content: ''; + position: absolute; + height: 0px; + width: 0px; + border-top: 35px transparent solid; + border-bottom: 35px transparent solid; + border-left: 10px $content-color-40 solid; + right: -10px; + margin-top: -10px; + } + &::after { + content: ''; + position: absolute; + height: 0px; + width: 0px; + border-top: 35px transparent solid; + border-bottom: 35px transparent solid; + border-left: 10px $yellow-40 solid; + right: -9px; + margin-top: -70px; + } + } + + a { + display: flex; + .avatar { + min-width: 50px; + max-width: 50px; + min-height: 50px; + max-height: 50px; + margin-right: 10px; + background-repeat: no-repeat; + background-size: 50px 50px; + background-position: center center; + } + .info { + display: flex; + flex-direction: column; + height: 60px; + max-height: 60px; + overflow: hidden; + .name { + max-height: 40px; + overflow: hidden; + } + time { + font-size: 0.8em; + font-weight: normal; + color: $light-gray-color; + } + } + + } + } + } + } +} + +.center { + display: flex; + justify-content: center; +} + + +.blubber-edit-icons { + margin-top: 10px; + + > * { + margin: 10px; + } +} + + +form.default { + .blubber_composer_select_container { + input, select, .container { + width: calc(100% - 50px); + display: inline-block; + } + } +} + +.float_right { + float: right; +} + +ol.tagcloud { + list-style-type: none; + padding: 0px; + margin: 0px; + > li { + display: inline-block; + margin-right: 10px; + &.size10 { + font-size: 1.6em; + } + &.size9 { + font-size: 1.5em; + } + &.size8 { + font-size: 1.4em; + } + &.size7 { + font-size: 1.3em; + } + &.size6 { + font-size: 1.2em; + } + &.size5 { + font-size: 1.1em; + } + &.size4 { + font-size: 1em; + } + &.size3 { + font-size: 0.9em; + } + &.size2 { + font-size: 0.8em; + } + &.size1 { + font-size: 0.7em; + } + } +} + +@keyframes blubber-scaling { + from { + opacity: 0.8; + transform: scale(0.8,0.8); + } + to { + opacity: 1; + transform: scale(1,1); + } +} + +//Animationen des Widgets: +.blubberthreadwidget-list-move, .blubberthreadwidget-list-enter-active, .blubberthreadwidget-list-leave-active { + transition: transform 0.5s; +} +.blubberthreadwidget-list-enter, .blubberthreadwidget-list-leave-to { + transform: translateY(-70px); +} + +.responsive-display { + .blubber_thread { + margin-right: 0; + } +} diff --git a/resources/assets/stylesheets/scss/breakpoints.scss b/resources/assets/stylesheets/scss/breakpoints.scss new file mode 100644 index 0000000..18681fc --- /dev/null +++ b/resources/assets/stylesheets/scss/breakpoints.scss @@ -0,0 +1,5 @@ +//** Major Breakpoints +$major-breakpoint-tiny: 0; +$major-breakpoint-small: 576px; +$major-breakpoint-medium: 768px; +$major-breakpoint-large: 1200px; diff --git a/resources/assets/stylesheets/scss/buttons.scss b/resources/assets/stylesheets/scss/buttons.scss new file mode 100644 index 0000000..5d86edd --- /dev/null +++ b/resources/assets/stylesheets/scss/buttons.scss @@ -0,0 +1,131 @@ +@import '../mixins'; + +@mixin button() { + background: white; + border: 1px solid $base-color; + border-radius: 0; + box-sizing: border-box; + color: $base-color; + cursor: pointer; + display: inline-block; + font-family: $font-family-base; + font-size: 14px; + line-height: 130%; + margin: 0.8em 0.6em 0.8em 0; + min-width: 100px; + overflow: visible; + padding: 5px 15px; + position: relative; + text-align: center; + text-decoration: none; + vertical-align: middle; + white-space: nowrap; + width: auto; + + &:hover, + &:active { + background: $base-color; + color: white; + outline: 0; + } + &:focus { + outline: dotted 1px #000; + } + &::-moz-focus-inner { + border: 0; + } + + &.disabled, + &[disabled] { + box-shadow: none; + background: $light-gray-color-20; + cursor: default; + opacity: 0.65; + + &:hover { + color: $base-color; + } + } + + transition: none; +} + +a.button, +button.button { + @include button(); +} + +.button-with-empty-icon { + white-space: nowrap; + + &::before { + background-repeat: no-repeat; + content: " "; + float: left; + height: 16px; + margin: 1px 5px 0 -8px; + width: 16px; + } +} + +@mixin button-with-icon($icon, $role, $roleOnHover) { + @extend .button-with-empty-icon; + &::before { + @include background-icon($icon, $role); + } + + &:hover::before { + @include background-icon($icon, $roleOnHover); + } + + &.disabled, + &[disabled] { + &:hover::before { + @include background-icon($icon, $role); + } + } +} + +.button.accept { + @include button-with-icon(accept, clickable, info_alt); +} +.button.cancel { + @include button-with-icon(decline, clickable, info_alt); +} +.button.edit { + @include button-with-icon(edit, clickable, info_alt); +} +.button.move-up { + @include button-with-icon(arr_1up, clickable, info_alt); +} +.button.move-down { + @include button-with-icon(arr_1down, clickable, info_alt); +} +.button.add { + @include button-with-icon(add, clickable, info_alt); +} +.button.trash { + @include button-with-icon(trash, clickable, info_alt); +} +.button.download { + @include button-with-icon(download, clickable, info_alt); +} +.button.search { + @include button-with-icon(search, clickable, info_alt); +} + +/* Grouped Buttons */ +.button-group { + display: inline-block; + list-style: none; + margin: 0 0.8em 0 0; + padding: 0; + vertical-align: middle; + + button, + .button { + float: left; + margin-left: 5px; + margin-right: 0; + } +} diff --git a/resources/assets/stylesheets/scss/contentbar.scss b/resources/assets/stylesheets/scss/contentbar.scss new file mode 100644 index 0000000..4604008 --- /dev/null +++ b/resources/assets/stylesheets/scss/contentbar.scss @@ -0,0 +1,116 @@ +.contentbar { + background-color: $dark-gray-color-5; + border: 1px solid $content-color-40; + gap: 10px; + margin-bottom: 15px; + min-height: 58px; + padding: 0px 9px 0px 9px; + display: flex; + justify-content: space-between; + flex-wrap: nowrap; + + .contentbar_title { + align-items: center; + display: flex; + font-size: 1.3em; + max-width: 60%; + + ul.breadcrumb { + list-style: none; + padding-left: 10px; + width: 100%; + display: inline-block; + + div { + float: left; + max-width: 100%; + } + + li { + display: inline; + } + + li+li:before { + padding: 5px; + color: $black; + content: "/"; + } + + } + } + + .textblock { + display: inline-block; + margin-left: 10px; + margin-right: 10px; + } + + .contentbar_info { + align-items: center; + text-align: right; + display: flex; + flex-wrap: nowrap; + gap: 5px; + margin: 5px; + + .contentbar-icons { + display: flex; + + label, .consuming_mode_trigger, nav { + align-self: center; + } + + .consuming_mode_trigger { + @media (max-width: 767px) { + display: none !important; + } + + @-moz-document url-prefix() { + position: relative; + top: -3px; + } + } + + .consuming_mode_trigger:not(body.consuming_mode .consuming_mode_trigger) { + display: inline-block; + width: 24px; + height: 24px; + @include icon(before, fullscreen-on, clickable, 24px, 0px); + margin-left: 5px; + } + + nav { + position: relative; + top: 2px; + } + } + } +} + +body.consuming_mode { + #barBottomContainer, #flex-header, .secondary-navigation, #barTopStudip, #page_title_container { + max-height: 0px; + opacity: 0; + overflow: hidden; + } + + #layout_wrapper { + padding-top: 0px; + } + + #layout-sidebar { + margin-left: -280px; + opacity: 0; + } + + .contentbar { + margin-left: 10px; + margin-right: -0; + padding-left: 20px; + padding-right: 20px; + } + + .consuming_mode_trigger { + @include icon(before, fullscreen-off, clickable, 25px, 5px); + } +} diff --git a/resources/assets/stylesheets/scss/contents.scss b/resources/assets/stylesheets/scss/contents.scss new file mode 100644 index 0000000..252ab78 --- /dev/null +++ b/resources/assets/stylesheets/scss/contents.scss @@ -0,0 +1,90 @@ +.contents-widget { + margin: 10px; + .content-items { + grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr)); + grid-gap: 5px; + max-width: none; + width: 100%; + + .content-item { + height: 100px; + + .content-item-link { + height: 90px; + padding: 5px; + + .content-item-img-wrapper { + margin: 5px; + margin-right: 10px; + margin-top: 0; + width: 32px; + } + + .content-item-text { + .content-item-title { + font-size: 1.8rem; + margin-bottom: 5px; + } + + .content-item-description { + font-size: small; + } + } + } + } + } +} + +.content-items { + display: grid; + grid-template-columns: repeat(auto-fit, 270px); + grid-gap: 15px; + list-style: none; + max-width: 1170px; + padding: 0; + + .content-item { + background-color: $dark-gray-color-5; + border: solid thin $light-gray-color-40; + height: 150px; + + .content-item-link { + color: unset; + display: flex; + height: 130px; + padding: 10px; + transition: 0.5s; + + .content-item-img-wrapper { + margin: 15px; + margin-top: 0; + width: 64px; + } + + .content-item-text { + .content-item-title { + color: $base-color; + font-size: 2rem; + } + } + + &:hover { + background-color: $white; + color: unset; + + .content-item-text { + .content-item-title { + color: $red; + } + } + + } + } + } +} + +@media (max-width: 820px) { + .content-items { + grid-template-columns: 100%; + } +} diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss new file mode 100755 index 0000000..08199bb --- /dev/null +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -0,0 +1,4065 @@ +/* definitions */ + +$companion-types: ( + default: basic, + unsure: unsure, + special: special, + alert: alert, + sad: sad, + happy: happy, + pointing: pointing-right +); + +$tile-colors: ( + black: #000, + charcoal: #3c454e, + royal-purple: #8656a2, + iguana-green: #66b570, + queen-blue: #536d96, + verdigris: #41afaa, + mulberry: #bf5796, + pumpkin: #f26e00, + sunglow: #ffca5c, + apple-green: #8bbd40, + studip-blue: #28497c, + studip-lightblue: #e7ebf1, + studip-red: #d60000, + studip-green: #008512, + studip-yellow: #ffbd33, + studip-gray: #636a71, +); + +$icon-colors:( + icon-red: #cb1800, + icon-blue: #24437c, + icon-green: #00962d, + icon-gray: #6e6e6e, + icon-yellow: #ffad00 +); + +$blockadder-items: ( + before-after: block-comparison, + canvas: block-canvas, + gallery: block-gallery, + image-map: block-imagemap, + audio: audio, + chart: vote, + code: computer, + confirm: accept, + date: date, + dialog-cards: dialog-cards, + document: file-text, + download: download, + embed: code, + folder: folder-full, + headline: block-eyecatcher, + iframe: door-enter, + key-point: exclaim-circle, + link: link-extern, + table-of-contents: table-of-contents, + text: edit, + typewriter: wizard, + video: video2, + accordion: block-accordion, + list: view-list, + tabs: block-tabs +); + +$media-buttons: ( + play: play, + stop: stop, + pause: pause, + prev: arr_eol-left, + next: arr_eol-right +); + +/* * * * * * * * +c o n t e n t s +* * * * * * * * */ +.cw-contents-overview-teaser { + max-width: 782px; + background-color: $content-color-20; + background-image: url("#{$image-path}/courseware-keyvisual-negative.svg"); + background-repeat: no-repeat; + background-size: 196px; + background-position-y: 50%; + background-position-x: 24px; + padding: 24px; + margin-bottom: 10px; + + .cw-contents-overview-teaser-content { + padding-left: 220px; + + header{ + font-size: 1.5em; + margin-bottom: 0.5em; + } + } +} +.responsive-display { + .cw-contents-overview-teaser { + max-width: 782px; + background-size: 60%; + background-position-y: 24px; + background-position-x: 50%; + padding: 24px; + margin-bottom: 10px; + + .cw-contents-overview-teaser-content { + padding-top: 28%; + padding-left: 0; + text-align: justify; + + header{ + font-size: 1.5em; + margin: 1em 0 0.5em 0; + text-align: center; + } + } + } +} + +/* * * * * * * * * * * +c o n t e n t s e n d +* * * * * * * * * * */ + +/* * * * * * +r i b b o n +* * * * * */ +$consum_ribbon_width: calc(100% - 58px); +#course-courseware-index, +#contents-courseware-courseware { + #layout_container { + overflow-x: hidden; + position: relative; + #layout_content { + overflow: hidden; + } + } +} + +.cw-ribbon-wrapper-consume { + position: fixed; + padding: 1em; + background-color: $white; + width: $consum_ribbon_width; + height: 46px; + z-index: 42; +} +.cw-ribbon-consume-bottom { + position: fixed; + top: 74px; + height: 8px; + left: 0; + width: calc(100% - 1em); + background-color: $white; + z-index: 40; +} + +.cw-ribbon-sticky-top { + position: fixed; + top: 40px; + height: 20px; + width: calc(100% - 314px); + background-color: $white; + z-index: 40; +} +.cw-ribbon-sticky-bottom { + position: fixed; + top: 112px; + height: 19px; + width: calc(100% - 314px); + background-color: $white; + z-index: 39; +} +.cw-ribbon-sticky-spacer { + height: 80px; +} +.cw-ribbon { + display: flex; + flex-wrap: wrap; + height: auto; + min-height: 30px; + border: solid thin $dark-gray-color-30; + margin-bottom: 15px; + padding: 1em; + justify-content: space-between; + background-color: $dark-gray-color-5; + + &.cw-ribbon-sticky { + position: fixed; + top: 56px; + width: calc(100% - 344px); + z-index: 40; + } + + &.cw-ribbon-consume { + width: $consum_ribbon_width; + position: fixed; + margin-bottom: 0; + } + + .cw-ribbon-wrapper-left { + display: flex; + max-width: calc(100% - 106px); + + .cw-ribbon-nav { + min-width: 75px; + } + + .cw-ribbon-breadcrumb { + font-size: 1.25em; + line-height: 1.5em; + + ul { + list-style: none; + padding-left: 0; + + li+li:before { + padding: 0 0.25em; + content: '/'; + background-repeat: no-repeat; + background-position: center; + } + + .cw-ribbon-breadcrumb-item { + display: inline-flex; + + span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 12em; + } + + a { + color: $base-color; + text-decoration: none; + &:hover { + color: $active-color; + } + } + + &.cw-ribbon-breadcrumb-item-current { + cursor: default; + } + } + } + } + } + + .cw-ribbon-wrapper-right { + display: flex; + } + + .cw-ribbon-button { + height: 24px; + width: 24px; + margin: 0 0.5em; + border: none; + background-color: transparent; + background-repeat: no-repeat; + background-position: center; + background-size: 24px; + cursor: pointer; + outline: none; + + &.cw-ribbon-button-menu { + @include background-icon(table-of-contents, clickable, 24); + } + + &.cw-ribbon-button-zoom { + @include background-icon(fullscreen-on, clickable, 24); + } + + &.cw-ribbon-button-zoom-out { + @include background-icon(fullscreen-off, clickable, 24); + } + + &.cw-ribbon-button-prev { + @include background-icon(arr_1left, clickable, 24); + margin: 0 0.5em 0 0; + } + + &.cw-ribbon-button-next { + @include background-icon(arr_1right, clickable, 24); + margin: 0 1em 0 0; + } + + &.cw-ribbon-button-prev-disabled { + @include background-icon(arr_1left, inactive, 24); + margin: 0 0.5em 0 0; + cursor: default; + } + + &.cw-ribbon-button-next-disabled { + @include background-icon(arr_1right, inactive, 24); + margin: 0 1em 0 0; + cursor: default; + } + } + + .cw-ribbon-action-menu { + vertical-align: text-top; + margin: 2px 0 0 2px; + &.is-open { + z-index: 32; + } + } +} + +.cw-ribbon-tools { + background-color: $white; + border: solid thin $content-color-40; + box-shadow: 2px 2px #ccc; + position: absolute; + right: -570px; + top: 15px; + height: calc(100% - 32px); + max-height: 760px; + max-width: calc(100% - 28px); + display: flex; + flex-flow: row; + transition: right 0.8s; + z-index: 42; + + &.unfold { + right: 12px; + } + + &.cw-ribbon-tools-consume { + position: fixed; + } + + &.cw-ribbon-tools-sticky { + position: fixed; + top: 56px; + } + + .cw-ribbon-tool-content { + height: 100%; + width: 540px; + background-color:$white; + padding: 0; + overflow: hidden; + + + .cw-ribbon-tool-content-nav { + position: sticky; + top: 0; + background-color: $white; + margin: 0; + padding: 10px 0 0 0; + color: $base-color; + display: flex; + border-bottom: solid thin $content-color-40; + z-index: 43; + + .cw-tools-hide-button { + border: none; + height: 36px; + width: 24px; + min-width: 24px; + margin-right: 1em; + padding: 0 4px; + float: right; + + @include background-icon(decline, clickable, 24); + background-repeat: no-repeat; + background-size: 24px; + background-position: center right; + background-color: #fff; + outline: none; + cursor: pointer; + } + + ul { + list-style-type: none; + width: 100%; + display: flex; + flex-wrap: wrap; + margin: 0; + padding: 0; + } + li { + padding: 0 8px; + margin-top: 8px; + text-align: center; + flex-grow: 0.5; + cursor: pointer; + + &:after { + display: block; + content: ''; + border-bottom: solid 3px $dark-gray-color-75; + transform: scaleX(0); + transition: transform 300ms ease-in-out; + margin: 17px 0 0 0; + } + + &.active, + &:hover { + color: $black; + &:after { + transform: scaleX(1); + } + } + } + + &:hover li { + &.active::after { + transform: scaleX(0); + } + &:hover::after { + transform: scaleX(1); + } + } + } + + .cw-ribbon-tool { + padding: 14px 8px 6px 8px; + height: calc(100% - 64px); + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: thin; + scrollbar-color: $base-color $white; + } + } +} + +.cw-structural-element-consumemode { + .cw-ribbon-tools { + top: 14px; + } +} + +.responsive-display { + .cw-ribbon-sticky-top, + .cw-ribbon-sticky-bottom, + .cw-ribbon-wrapper-consume, + .cw-ribbon-consume-bottom { + width: 100%; + } + .cw-ribbon { + &.cw-ribbon-sticky { + width: calc(100% - 62px); + } + } + .cw-ribbon-sticky-spacer { + height: 75px; + } +} + +/* * * * * * +ribbon end +* * * * * */ + + +/* * * * * * * * * + structual element +* * * * * * * * * */ + +.cw-structural-element { + + &.cw-structural-element-consumemode { + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; + padding: 0; + background-color: $white; + z-index: 1004; + overflow-y: scroll; + overflow-x: hidden; + scrollbar-width: thin; + scrollbar-color: $base-color #f5f5f5; + } + + .cw-wellcome-screen { + .cw-wellcome-screen-keyvisual { + margin: 14px 0 42px 0; + width: 100%; + height: 400px; + background-image: url('../../assets/images/courseware-keyvisual.svg'); + background-repeat: no-repeat; + background-position: center center; + } + header { + padding: 0.5em 0; + text-align: center; + font-size: 2.25em; + } + .cw-wellcome-screen-actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + } + + } + + .cw-container-wrapper { + max-width: 1115px; + margin: 0; + padding: 0; + display: flex; + flex-wrap: wrap; + align-items: stretch; + + &.cw-container-wrapper-consume { + margin: 0 auto; + padding: 6em 1em 1em 1em; + } + } + + .cw-structural-element-description { + width: 400px; + height: 200px; + overflow-y: auto; + resize: none; + } + .cw-structural-element-color { + color: $white; + &.black { + background-color: map-get($tile-colors, "black"); + } + &.charcoal { + background-color: map-get($tile-colors, "charcoal"); + } + &.royal-purple { + background-color: map-get($tile-colors, "royal-purple"); + } + &.iguana-green { + background-color: map-get($tile-colors, "iguana-green"); + } + &.queen-blue { + background-color: map-get($tile-colors, "queen-blue"); + } + &.verdigris { + background-color: map-get($tile-colors, "verdigris"); + } + &.mulberry { + background-color: map-get($tile-colors, "mulberry"); + } + &.pumpkin { + background-color: map-get($tile-colors, "pumpkin"); + } + &.sunglow { + background-color: map-get($tile-colors, "sunglow"); + } + &.apple-green { + background-color: map-get($tile-colors, "apple-green"); + } + + &.studip-blue { + background-color: map-get($tile-colors, "studip-blue"); + } + &.studip-lightblue { + background-color: map-get($tile-colors, "studip-lightblue"); + } + &.studip-red { + background-color: map-get($tile-colors, "studip-red"); + } + &.studip-green { + background-color: map-get($tile-colors, "studip-green"); + } + &.studip-yellow { + background-color: map-get($tile-colors, "studip-yellow"); + } + &.studip-gray { + background-color: map-get($tile-colors, "studip-gray"); + } + } + + .cw-structural-element-info { + width: 600px; + tr:first-child { + width: 12em; + vertical-align: top; + } + } +} + +.cw-structural-element-dialog { + input[type=text] { + width: 20em; + } +} + +.cw-structural-element-image-preview { + display: block; + max-width: 300px; + max-height: 256px; + width: auto; + height: auto; + margin: 0 auto; +} + +.cw-structural-element-image-preview-placeholder { + width: 400px; + height: 225px; + background-color: $black; +} + +.cw-element-permissions { + label { + display: block; + padding: 6px 0; + } + table.default { + > caption { + padding: 0; + font-size: 1.25em; + } + } +} + +.cw-element-export { + label { + input { + vertical-align: middle; + } + + span { + vertical-align: middle; + } + } +} + +/* * * * * * * * * * * * + structual element end +* * * * * * * * * * * */ + +/* * * * * + container +* * * * */ +.cw-container { + margin-bottom: 1em; + + &.cw-container-colspan-full { + max-width: 1095px; + width: 100%; + } + &.cw-container-colspan-half { + max-width: 540px; + width: 100%; + margin-right: 15px; + } + &.cw-container-colspan-half-center { + width: 1095px; + .cw-container-content { + width: 540px; + margin: auto; + } + } + + .cw-container-header { + background-color: $content-color-20; + max-height: 30px; + padding: 4px 10px; + + span { + color: $base-color; + font-weight: 700; + line-height: 2em; + font-size: 1.1em; + } + + .cw-container-actions { + position: relative; + float: right; + margin-top: 4px; + // z-index: 31; + .is-open { + z-index: 31; + } + } + } + + &.cw-container-active { + .cw-container-content { + border: solid thin $content-color-40; + } + } + + + .cw-block-wrapper { + padding: 0; + margin: 0; + list-style: none; + + &.cw-block-wrapper-active { + padding: 14px 10px; + } + + .cw-block-item { + padding: 0; + margin: 0 0 1em 0; + } + } + + .cw-container-list-block-list { + padding: 0; + list-style: none; + } + + .cw-container-tabs-block-list, + .cw-container-accordion-block-list { + list-style: none; + padding: 0 1em; + } +} + +.cw-container-section-delete { + img { + cursor: pointer; + } +} + +form.cw-container-dialog-edit-form { + display: flex; + flex-wrap: wrap; + width: 640px; + + fieldset { + margin-right: 4px; + } +} + +/* * * * * * * + container end +* * * * * * */ + +/* * * + block +* * */ + +.cw-default-block { + display: flex; + flex-flow: row; + .cw-default-block-invisible-info { + img { + vertical-align: text-bottom; + } + } + +} +.cw-content-wrapper { + display: flex; + flex-flow: column; + width: 100%; + .cw-block-content { + overflow: auto; + } +} +.cw-content-wrapper-active { + border: solid thin $content-color-40; + .cw-block-content { + padding: 0.5em; + } +} +.cw-block-header { + background-color: $content-color-20; + max-height: 30px; + padding: 4px 10px; + + span { + color: $base-color; + font-weight: 700; + line-height: 2em; + font-size: 1.1em; + } + + .cw-block-actions { + position: relative; + float: right; + margin-top: 4px; + .is-open{ + z-index: 30; + } + } +} + +.cw-block-features { + + header{ + background-color: $content-color-20; + color: $base-color; + font-weight: 600; + padding: 0.5em; + } + + .cw-block-features-content{ + margin: 1em; + } +} + +.cw-button-feature-close { + float: right; + border: none; + height: 24px; + width: 24px; + @include background-icon(decline, clickable); + background-repeat: no-repeat; + background-color: transparent; + cursor: pointer; + outline: none; +} + +.cw-keypoint-content { + display: flex; + min-height: 52px; + padding:1.5em 2.5em 1.5em 2.5em; + border-style: solid; + border-width: 2px; + align-items: center; +} +.cw-keypoint-content > img { + flex-shrink: 0; +} +.cw-keypoint-red { + border-color: map-get($icon-colors, 'icon-red'); +} +.cw-keypoint-blue { + border-color: map-get($icon-colors, 'icon-blue'); +} +.cw-keypoint-green { + border-color: map-get($icon-colors, 'icon-green'); +} +.cw-keypoint-gray { + border-color: map-get($icon-colors, 'icon-gray'); +} +.cw-keypoint-yellow { + border-color: map-get($icon-colors, 'icon-yellow'); +} +.cw-keypoint-sentence { + display: inline; + font-size: 1.25em; + padding-left: 1.5em; + margin-top: 10px; +} +.cw-keypoint-label-color { + width: 32px !important; + min-width: 32px !important; + height: 32px; + padding: 5px !important; + + > .cw-keypoint-input-color { + visibility: hidden; + position: absolute; + } + + > .cw-keypoint-input-color + div { + cursor: pointer; + border: 2px solid transparent; + height: 32px; + width: 32px; + } + + > .cw-keypoint-input-color:checked + div { /* (RADIO CHECKED) IMAGE STYLES */ + @include background-icon(accept, info_alt, 24); + background-repeat: no-repeat; + background-position: center; + } +} +label[for="cw-keypoint-color"] { + vertical-align: top; +} +.cw-keypoint-label-color { + display: inline-block !important; +} +.cw-keypoint-icons { + background-color: $white; +} +.cw-keypoint-bg-red { + background-color: map-get($icon-colors, 'icon-red'); +} +.cw-keypoint-bg-blue { + background-color: map-get($icon-colors, 'icon-blue'); +} +.cw-keypoint-bg-green { + background-color: map-get($icon-colors, 'icon-green'); +} +.cw-keypoint-bg-gray { + background-color: map-get($icon-colors, 'icon-gray'); +} +.cw-keypoint-bg-yellow { + background-color: map-get($icon-colors, 'icon-yellow');; +} + + +.cw-static-content-iframe { + width: 100%; +} +.cw-static-content-preview { + span { + display: block; + } + img { + width: 50%; + } +} + +.cw-block-edit textarea { + width: -moz-available; + height: -moz-available; + min-height: 8em; + border: solid thin $content-color-40; + resize: none; +} + +.cw-typewriter-content { + + .vue-typer { + + &.font-typewriter{ + font-family: "Lucida Sans Typewriter", "Lucida Console", Monaco, "Bitstream Vera Sans Mono", monospace; + } + &.font-trebuchet { + font-family: "Trebuchet MS", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", sans-serif; + } + &.font-tahoma { + font-family: Tahoma, Verdana, Segoe, sans-serif; + } + &.font-georgia { + font-family: Georgia, Times, "Times New Roman", serif; + } + &.font-narrow { + font-family: "Arial Narrow", Arial, "Helvetica Condensed", Helvetica, sans-serif; + } + + &.size-tall { + font-size: 1.25em; + line-height: 1.25em; + } + &.size-grande { + font-size: 1.5em; + line-height: 1.25em; + } + &.size-huge { + font-size: 2em; + line-height: 1.25em; + } + } +} + +.cw-block-actions { + padding-left: 14px; +} +.cw-button-box { + float: right; +} + +/* * * * * * + block end +* * * * * */ + +/* * * * * + t r e e + * * * * */ + +.cw-tree { + ul { + list-style: none; + padding-left: 1.25em; + margin-bottom: 20px; + + &.cw-tree-chapter-list, + &.cw-tree-root-list { + padding-left: 0; + } + + .cw-tree-item-is-root, + .cw-tree-item-first-level { + font-size: 16px; + border-bottom: solid thin $content-color-40; + .cw-tree-item-link { + padding-left: 3px; + } + } + .cw-tree-item-is-root{ + display: block; + font-size: 18px; + .cw-tree-item-link { + padding-left: 24px; + + @include background-icon(courseware, clickable, 18); + background-repeat: no-repeat; + background-position: 3px 3px; + &:hover { + @include background-icon(courseware, attention, 18); + } + &.cw-tree-item-link-current { + @include background-icon(courseware, info, 18); + } + } + } + .cw-tree-item-first-level { + margin: 28px 0 12px 0; + &:hover { + background-color: $content-color-20; + } + } + + .cw-tree-item-link { + display: inline-block; + width: calc(100% - 14px); + text-align: justify; + + &:hover { + background-color: $light-gray-color-20; + color: $active-color; + } + &::before { + content: '\2022'; + color: $base-color; + font-weight: 700; + width: 1em; + margin-left: -1em; + margin-right: 4px; + vertical-align: top; + } + + &.cw-tree-item-link-current { + color: $black; + font-weight: 600; + &::before { + color: $black; + } + } + } + + } + + .cw-tree-item-first-level, + .cw-tree-item-is-root { + .cw-tree-item-link::before{ + content: ''; + width: 0; + margin: 0; + } + } + + .cw-tree-item { + margin-bottom: 5px; + div { + display: inline; + &.cw-tree-item-is-root, + &.cw-tree-item-first-level{ + display: block; + } + } + } +} + +/* * * * * * * +t r e e e n d +* * * * * * * */ + +/* * * * * * * * * * * * * * * +c o l l a p s i b l e b o x +* * * * * * * * * * * * * * */ +.cw-collapsible { + + border: solid thin $content-color-40; + margin-bottom: -1px; + height: 34px; + + &.cw-collapsible-open { + height: unset; + } + + .cw-collapsible-title { + @include background-icon(arr_1right, clickable); + background-position: 6px center; + background-repeat: no-repeat; + background-color: $content-color-20; + padding: 0.5em 2em; + margin: 0; + font-weight: 600; + color: $base-color; + cursor: pointer; + + &.cw-collapsible-open { + @include background-icon(arr_1down, clickable); + } + + img { + vertical-align: middle; + } + } + + .cw-collapsible-content { + padding: 0; + visibility: hidden; + height: 0; + &.cw-collapsible-content-open { + visibility: visible; + height: unset; + padding: 10px; + } + } +} + +/* * * * * * * * * * * * * * * * * * +c o l l a p s i b l e b o x e n d +* * * * * * * * * * * * * * * * * */ + +/* * * * +t a b s +* * * */ +$icons: ( + accept, + add, + add-circle, + admin, + aladdin, + arr_1down, + arr_1left, + arr_1right, + arr_1up, + arr_2down, + arr_2left, + arr_2right, + arr_2up, + audio, + audio3, + billboard, + block-canvas, + block-comparison, + block-eyecatcher, + block-gallery, + block-gallery2, + block-imagemap, + brainstorm, + campusnavi, + chat2, + code, + community2, + computer, + consultation, + content, + courseware, + crown, + date-single, + decline, + decline-circle, + doctoral_cap, + download, + dropbox, + edit, + exclaim, + exclaim-circle, + export, + favorite, + filter, + globe, + graph, + group2, + group3, + group4, + home2, + info, + info-circle, + install, + institute, + key, + knife, + learnmodule, + lightbulb, + lightbulb2, + link2, + link3, + link-extern, + link-intern, + literature, + lock-locked, + lock-unlocked, + mail2, + medal, + metro, + microphone, + module, + network, + notification, + notification2, + opencast, + outer-space, + permalink, + person, + phone, + picture, + place, + plugin, + question, + question-circle, + ranking, + remove, + remove-circle, + resources, + roles, + schedule2, + search, + settings, + span-empty, + span-1quarter, + span-2quarter, + span-3quarter, + span-full, + spiral, + sport, + staple, + star, + star-empty, + star-halffull, + test, + tools, + topic, + ufo, + video2, + visibility-visible, + wizard +); + +.cw-tabs-nav { + display: flex; + flex-wrap: wrap; + list-style: none; + padding: 0; + margin: 0; + border: solid thin $content-color-40; + border-bottom: none; + + li { + background-color: $white; + padding: 1em 0 4px 0; + margin: 0 7px 0 21px; + color: $base-color; + cursor: pointer; + text-align: center; + flex-grow: 1; + max-width: max-content; + + &::after { + display: block; + margin-top: 4px; + margin-bottom: -5px; + margin-left: -14px; + width: calc(100% + 28px); + content: ''; + border-bottom: solid 3px $dark-gray-color-75; + transform: scaleX(0); + transition: transform 300ms ease-in-out; + } + + &.is-active, + &:hover { + color: $black; + &:after { + transform: scaleX(1); + } + } + + @each $icon in $icons { + &.cw-tabs-nav-icon-text-#{$icon} { + &::before { + @include background-icon($icon); + background-repeat: no-repeat; + background-position: left bottom; + + display: inline-block; + height: 16px; + width: 16px; + margin-bottom: -2px; + padding-left: 4px; + content: ''; + } + + } + &.is-active.cw-tabs-nav-icon-text-#{$icon}, + &.cw-tabs-nav-icon-text-#{$icon}:hover { + &::before { + @include background-icon($icon, info); + } + } + }; + @each $icon in $icons { + &.cw-tabs-nav-icon-solo-#{$icon} { + &::before { + display: inline-block; + height: 24px; + width: 24px; + content: ''; + + @include background-icon($icon, clickable, 24); + background-repeat: no-repeat; + background-position: center; + } + } + &.is-active.cw-tabs-nav-icon-solo-#{$icon}, + &.cw-tabs-nav-icon-solo-#{$icon}:hover { + &::before { + @include background-icon($icon, info); + } + } + }; + + } + + &:hover li { + &.is-active::after { + transform: scaleX(0); + } + &:hover::after { + transform: scaleX(1); + } + } +} +.cw-tabs-content { + border: solid thin $content-color-40; + padding: 4px; +} +.cw-block-wrapper-active { + .cw-tabs-content { + padding: 14px 0; + } +} + +.studip-dialog-with-tab { + .studip-dialog-body .studip-dialog-content { + padding: 0 4px; + .cw-tab-in-dialog { + .cw-tabs-nav { + border: none; + border-bottom: solid thin $content-color-40; + margin-bottom: 4px; + } + .cw-tabs-content { + border: none; + min-width: 500px; + min-height: 400px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: $base-color #f5f5f5; + } + } + } +} + +.cw-tabs { + .cw-tab { + visibility: hidden; + height: 0; + + &.cw-tab-active { + visibility: visible; + height: unset; + } + } + &.cw-course-manager-tabs { + .cw-tab { + display: none; + + &.cw-tab-active { + display: block; + } + } + } +} + +/* * * * * * * * +t a b s e n d +* * * * * * * */ + +/* * * * * * * * * * +b l o c k a d d e r +* * * * * * * * * */ + +.cw-tools-element-adder-tabs { + display: flex; + list-style: none; + padding: 0; + margin: 0; + border: solid thin $content-color-40; + border-bottom: none; + + .cw-tools-element-adder-tab { + background-color: $white; + padding: 1em 1.5em 0 1.5em; + margin: 0 1em; + color: $base-color; + cursor: pointer; + text-align: center; + flex-grow: 1; + + &:after { + display: block; + margin-top: 4px; + content: ''; + border-bottom: solid 3px $dark-gray-color-75; + transform: scaleX(0); + transition: transform 300ms ease-in-out; + } + + &.active, + &:hover { + color: $black; + &:after { + transform: scaleX(1); + } + } + + } + + &:hover .cw-tools-element-adder-tab{ + &.active::after { + transform: scaleX(0); + } + &:hover::after { + transform: scaleX(1); + } + } + +} + +.cw-tools-element-adder { + padding-bottom: 4em; +} + +.cw-blockadder-item { + margin-bottom: 4px; + padding: 1em 1em 1em 6em; + @include background-icon(unit-test, clickable, 48); + background-position: 12px center; + background-repeat: no-repeat; + border: solid thin $content-color-40; + cursor: pointer; + + &:hover { + border-color: $base-color; + } + + @each $item, $icon in $blockadder-items { + &.cw-blockadder-item-#{$item} { + @include background-icon($icon, clickable, 48); + } + } + + .cw-blockadder-item-title { + font-weight: 600; + } +} + +.cw-block-adder-area { + @include background-icon(add, clickable); + background-repeat: no-repeat; + background-position: calc(50% - 5em) calc(50% - 1px); + border: solid thin $content-color-40; + padding: 1em 0; + color: $base-color; + text-align: center; + width: 100%; + font-weight: 600; + cursor: pointer; + + &.cw-block-adder-active { + border: solid thin $base-color; + background-color: $base-color; + @include background-icon(add, info-alt); + color: $white; + } + &.cw-block-adder-disabled { + @include background-icon(add, inactive); + color: $dark-gray-color-80; + cursor: default; + } +} +.cw-block-helper-buttons { + display: inline-block; + width: 100%; + + .cw-block-helper-reset { + float: right; + } + + .button.cw-block-helper-reset::before { + content: ''; + @include background-icon(refresh); + background-repeat: no-repeat; + float: left; + height: 16px; + width: 16px; + margin: 1px 5px 0 -8px; + } +} + +.cw-block-helper-results { + margin-top: 5px; +} + +.cw-element-adder-favs-wrapper { + display: flex; + .cw-element-adder-all-blocks { + &.fav-edit-active { + .cw-blockadder-item { + height: 5em; + } + } + + } + .cw-element-adder-favs { + + .cw-block-fav-item { + @include background-icon(star-empty, clickable, 48); + background-position: center; + background-repeat: no-repeat; + height: 5em; + width: 5em; + padding: 1em; + margin: 0 0 4px 4px; + border: solid thin $content-color-40; + cursor: pointer; + + &:hover { + border-color: $base-color; + } + + &.cw-block-fav-item-active { + @include background-icon(star, clickable, 48); + } + } + } +} + + + +/* * * * * * * * * * * * * +b l o c k a d d e r e n d +* * * * * * * * * * * * */ + +/* * * * * * * * * * * * * * * * +c o m p a n i o n o v e r l a y +* * * * * * * * * * * * * * * * */ + +.cw-companion-overlay { + position: fixed; + bottom: 46px; + right: 0; + width: 360px; + max-width: calc(100% - 140px); + height: 120px; + z-index: 42000; + border: solid thin $content-color-40; + background-color: $white; + background-repeat: no-repeat; + background-position: 1em center; + background-size: 100px; + box-shadow: 5px 5px #ececed; + padding-left: 120px; + transform: translateX(100%); + transition: transform .5s ease; + + @each $type, $image in $companion-types { + &.#{$type} { + background-image: url("#{$image-path}/companion/Tin_#{$image}.svg"); + } + } + + &.cw-companion-overlay-in { + transform: translateX(0); + right: 12px; + } + + .cw-companion-overlay-content { + display: inline-block; + position: relative; + top: 25%; + padding: 0 1em; + } + + .cw-compantion-overlay-close { + @include background-icon(decline); + background-color: $white; + background-repeat: no-repeat; + + position: absolute; + top: 7px; + right: 7px; + height: 16px; + width: 16px; + border: none; + cursor: pointer; + } +} + +.cw-companion-box { + display: flex; + height: 120px; + border: solid thin $content-color-40; + background-color: $white; + background-repeat: no-repeat; + background-position: 1em center; + background-size: 100px; + padding-left: 120px; + align-items: center; + margin-bottom: 1em; + + @each $type, $image in $companion-types { + &.#{$type} { + background-image: url("#{$image-path}/companion/Tin_#{$image}.svg"); + } + } +} + +.cw-container-wrapper { + .cw-companion-box-wrapper { + width: 100%; + } +} + +/* * * * * * * * * * * * * * * * * * +c o m p a n i o n s m a l l e n d +* * * * * * * * * * * * * * * * * */ + +/* * * * * * * * * * +v i e w w i d g e t +* * * * * * * * * * */ +.cw-action-widget, +.cw-view-widget { + li { + color: $base-color; + &:hover { + color: $active-color; + cursor: pointer; + } + &.active { + padding-left: 10px !important; + } + } +} +.cw-action-widget { + .cw-action-widget-edit{ + @include background-icon(edit, clickable); + } + .cw-action-widget-add{ + @include background-icon(add, clickable); + } + .cw-action-widget-info{ + @include background-icon(info, clickable); + } + .cw-action-widget-star{ + @include background-icon(star, clickable); + } + .cw-action-widget-trash{ + @include background-icon(trash, clickable); + } + .cw-action-widget-export{ + @include background-icon(export, clickable); + } + .cw-action-widget-oer{ + @include background-icon(service, clickable); + } +} + +/* * * * * * * * * * * * * * +v i e w w i d g e t e n d +* * * * * * * * * * * * * */ + +/* * * * * * * * * * * * * * * * * * +c o m m e n t s & f e e d b a c k +* * * * * * * * * * * * * * * * * * */ + +.cw-block-feedback-items, +.cw-block-comments-items { + min-height: 1em; + max-height: 225px; + overflow-y: auto; + overflow-x: hidden; + margin: -1em -1em 0em -0.5em; + scroll-behavior: smooth; +} + +.cw-talk-bubble { + margin: 10px 20px; + position: relative; + width: 80%; + height: auto; + background-color: $dark-gray-color-10; + border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + float: left; + + .cw-talk-bubble-talktext { + padding: 1em; + text-align: left; + line-height: 1.5em; + + .cw-talk-bubble-talktext-time { + color: $dark-gray-color-45; + text-align: right; + font-size: 0.8em; + margin-bottom: -0.5em; + } + } + + &.cw-talk-bubble-own-post { + float: right; + } + + &:after{ + content: ' '; + position: absolute; + width: 0; + height: 0; + top: 0px; + bottom: auto; + border: 22px solid; + border-color: $dark-gray-color-10 transparent transparent transparent; + border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + left: -20px; + right: auto; + } + + &.cw-talk-bubble-own-post:after{ + left: auto; + right: -20px; + } + + .cw-talk-bubble-user { + padding: 1em 1em 0 1em; + + .cw-talk-bubble-avatar{ + display: inline-block; + } + + span { + padding-left: 4px; + color: $dark-gray-color-45; + font-weight: 600; + vertical-align: top; + } + } +} + +.cw-block-feedback-create, +.cw-block-comment-create { + border-top: solid thin $content-color-40; + padding-top: 1em; + textarea { + width: calc(100% - 6px); + resize: none; + } +} + +/* * * * * * * * * * * * * * * * * * * * * * +c o m m e n t s & f e e d b a c k e n d +* * * * * * * * * * * * * * * * * * * * * * */ + +/* * * * * * * +w y s i w y g +* * * * * * */ +textarea.studip-wysiwyg { + width: 100% +} + +/* * * * * * * * * * * +w y s i w y g e n d +* * * * * * * * * * */ + +/* * * * * * +d i a l o g +* * * * * */ + +.studip-dialog-backdrop { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: fade-out($base-color, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 3001; +} +.studip-dialog-body { + position: absolute; + background: $white; + box-shadow: 0 0 8px fade-out($black, 0.5); + overflow-x: auto; + display: flex; + flex-direction: column; + padding: 0.2em; + max-height: 98vh; + + .studip-dialog-header, + .studip-dialog-footer { + padding: 7px; + display: flex; + } + .studip-dialog-header { + background: $base-color none repeat scroll 0 0; + border-bottom: 1px solid $dark-gray-color-10; + color: $white; + justify-content: space-between; + font-size: 1.3em; + padding: 0.5em 1em; + cursor: grab; + + &.drag-active { + cursor: grabbing; + } + } + .studip-dialog-close-button { + @include background-icon(decline, info-alt, 16); + background-repeat: no-repeat; + background-position-y: center; + + width: 22px; + height: 22px; + margin-right: -10px; + margin-left: 2em; + cursor: pointer; + outline: none; + } + .studip-dialog-content { + color: $black; + position: relative; + padding: 15px; + overflow-y: auto; + min-width: 100%; + // resize: both; + box-sizing: border-box; + } + .studip-dialog-footer { + border-top: 1px solid $dark-gray-color-10; + justify-content: center; + } + + &.studip-dialog-warning, + &.studip-dialog-alert { + .studip-dialog-content { + padding: 15px 15px 15px 62px; + background-position: 12px center; + background-repeat: no-repeat; + box-sizing: border-box; + display: flex; + align-items: center; + } + } + + &.studip-dialog-alert { + .studip-dialog-header { + background: $active-color none repeat scroll 0 0; + } + .studip-dialog-content { + @include background-icon(question-circle-full, attention, 32); + } + } + &.studip-dialog-warning { + .studip-dialog-header { + color: $black; + background: $activity-color none repeat scroll 0 0; + } + .studip-dialog-close-button { + @include background-icon(decline, info); + } + .studip-dialog-content { + @include background-icon(question-circle-full, status-yellow, 32); + } + } + +} +/* * * * * * * * * +d i a l o g e n d +* * * * * * * * */ + +/* * * * * * * * * +d a s h b o a r d +* * * * * * * * */ + +.cw-dashboard { + display: flex; + // TODO: Fixed width? + width: 1112px; + flex-wrap: wrap; + + .cw-dashboard-box { + margin-bottom: 1em; + margin-right: 1em; + + &.cw-dashboard-box-full { + // TODO: Fixed width? + width: 1095px; + } + &.cw-dashboard-box-half { + // TODO: Fixed width? + width: 540px; + } + } + .cw-dashboard-overview { + display: flex; + justify-content: space-evenly; + .cw-oblong { + margin-right: 1em; + } + } + .cw-dashboard-progress { + + .cw-dashboard-progress-breadcrumb { + span { + color: $base-color; + cursor: pointer; + + &:hover { + color: $active-color; + } + } + } + + .cw-dashboard-progress-chapter { + text-align: center; + + h1 { + border: none; + margin: 0; + padding: 0; + } + + .cw-progress-circle { + font-size: 18px; + margin: 1em auto; + + &.cw-dashboard-progress-current { + font-size: 12px; + margin: -4.5em 0 2em 260px; + } + } + } + + .cw-dashboard-progress-subchapter-list { + border-top: solid thin $content-color-40; + margin: -0.5em; + height: 300px; + overflow-y: scroll; + overflow-x: hidden; + padding: 1em; + scrollbar-width: thin; + scrollbar-color: $base-color $dark-gray-color-5; + } + } +} + +.cw-dashboard-progress-item { + border-bottom: solid thin $content-color-40; + width: 492px; + cursor: pointer; + + &:hover{ + background-color: hsla(217,6%,45%,.2); + } + + &:last-child { + border: none; + } + + .cw-dashboard-progress-item-value, + .cw-dashboard-progress-item-description { + display: inline-block; + height: 70px; + vertical-align: top; + line-height: 70px; + } + + .cw-dashboard-progress-item-value { + width: 70px; + color: $base-color; + font-size: xx-large; + + .cw-progress-circle { + font-size: 12px; + margin: 4px; + } + } + .cw-dashboard-progress-item-description { + width: 404px; + color: $base-color; + padding-left: 14px; + } +} + +.cw-dashboard-activities { + max-height: 476px; + list-style: none; + padding: 0; + margin: -0.5em; + scrollbar-width: thin; + scrollbar-color:$base-color #f5f5f5; + overflow-y: auto; + overflow-x: hidden; + + .cw-activity-item { + border-bottom: solid thin $content-color-40; + padding: 0.5em; + + &:last-child { + border: none; + } + + p { + margin: 0 0 4px 0; + img { + padding-right: 0.5em; + vertical-align: text-bottom; + } + &.cw-activity-item-text { + padding-left: 23px; + } + } + + a{ + + } + } +} + +/* * * * * * * * * * * * +d a s h b o a r d e n d +* * * * * * * * * * * */ + +/* * * * * * +o b l o n g +* * * * * */ + +.cw-oblong-large { + border: solid thin $base-color; + width: 520px; + + .cw-oblong-value, + .cw-oblong-description { + display: inline-block; + height: 90px; + vertical-align: top; + line-height: 90px; + text-align: center; + } + + .cw-oblong-value { + width: 90px; + color: $base-color; + font-size: xx-large; + } + .cw-oblong-description { + width: 426px; + background-color: $base-color; + color: $white; + img { + vertical-align: middle; + margin-right: 4px; + } + + } +} + +.cw-oblong-small { + border: solid thin $base-color; + width: 271px; + + .cw-oblong-value, + .cw-oblong-description { + display: inline-block; + height: 60px; + vertical-align: top; + line-height: 60px; + text-align: center; + } + + .cw-oblong-value { + width: 60px; + background-color: $base-color; + color: $white; + font-size: x-large; + } + .cw-oblong-description { + width: calc(100% - 64px); + background-color: $white; + color: $base-color; + img { + vertical-align: middle; + margin-right: 8px; + } + + } +} + +/* * * * * * * * * * +o b l o n g e n d +* * * * * * * * * */ + +/* * * * * * * * * * * * * * * +p r o g r e s s c i r c l e +* * * * * * * * * * * * * * */ + +.cw-progress-circle { + font-size: 14px; + margin: 10px; + position: relative; + padding: 0; + width: 5em; + height: 5em; + background-color: $dark-gray-color-10; + border-radius: 50%; + line-height: 5em; + + &:after{ + border: none; + position: absolute; + top: 0.35em; + left: 0.35em; + text-align: center; + display: block; + border-radius: 50%; + width: 4.3em; + height: 4.3em; + background-color: white; + content: " "; + } + + span { + position: absolute; + line-height: 5em; + width: 5em; + text-align: center; + display: block; + color: $base-color; + z-index: 2; + } + + .left-half-clipper { + border-radius: 50%; + width: 5em; + height: 5em; + position: absolute; + clip: rect(0, 5em, 5em, 2.5em); + } + + &.over50 .left-half-clipper { + clip: rect(auto,auto,auto,auto); + } + + .value-bar { + position: absolute; + clip: rect(0, 2.5em, 5em, 0); + width: 5em; + height: 5em; + border-radius: 50%; + border: 0.45em solid $base-color; + box-sizing: border-box; + } + + &.over50 .first50-bar { + position: absolute; + clip: rect(0, 5em, 5em, 2.5em); + background-color: $base-color; + border-radius: 50%; + width: 5em; + height: 5em; + } + + &:not(.over50) .first50-bar { + display: none; + } + + &.p0 .value-bar { display: none; } + + @for $i from 1 through 100 { + &.p#{$i} .value-bar { + transform: rotate(360 * $i / 100 + 0deg); + } + } +} + +/* * * * * * * * * * * * * * * * * * +p r o g r e s s c i r c l e e n d +* * * * * * * * * * * * * * * * * */ + +/* * * * * * * +m a n a g e r +* * * * * * */ + +.cw-course-manager { + display: flex; + flex-wrap: wrap; + max-width: 1120px; + + .cw-course-manager-tabs { + max-width: 560px; + width: calc(50% - 10px); + margin-right: 20px; + + &:last-child{ + margin: 0; + } + + .cw-tabs-content { + min-height: 400px; + padding: 10px; + resize: vertical; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: $base-color $white; + } + } +} + +.cw-manager-element { + + .cw-sort-ease-move { + transition: all 0.4s ease-in-out; + } + + .cw-manager-element-title { + + img { + vertical-align: text-bottom; + } + + .cw-manager-element-breadcrumb { + display: inline; + .cw-manager-element-breadcrumb-home, + .cw-manager-element-breadcrumb-item { + cursor: pointer; + color: $base-color; + + &::after { + content: ' / '; + } + &:hover { + color: $active-color; + } + } + } + .cw-manager-element-actions { + position: relative; + display: inline; + float: right; + cursor: pointer; + z-index: 32; + } + header { + padding: 0.25em 0 0.5em 0; + font-size: 1.6em; + font-weight: 700; + } + } + .cw-manager-element-containers { + margin-bottom: 8px; + } + .cw-manager-container { + margin-bottom: 10px; + border: solid thin $content-color-40; + + &:last-child { + margin-bottom: 0; + } + + .cw-manager-container-title { + font-weight: 700; + padding: 4px 4px 4px 8px; + color: $base-color; + background-color: $content-color-20; + + &.cw-manager-container-clickable-title { + cursor: pointer; + } + + img { + vertical-align: middle; + } + } + .cw-manager-container-blocks { + margin: 4px; + } + .cw-manager-block { + border: solid thin $content-color-40; + padding: 1em; + margin-bottom: 4px; + background-color: $white; + + &.cw-manager-block-clickable { + cursor: pointer; + &:hover { + background-color: $base-color; + color: $white; + } + } + + img { + vertical-align: middle; + } + + &:last-child { + margin-bottom: 0; + } + } + + } + .cw-manager-element-subchapters { + + } + .cw-manager-element-item { + border: solid thin $content-color-40; + padding: 1em; + margin-bottom: 4px; + text-align: center; + vertical-align: middle; + background-color: $white; + color: $base-color; + cursor: pointer; + + img { + vertical-align: middle; + } + &:last-child { + margin-bottom: 0; + } + &:hover { + color: $white; + background-color: $base-color; + } + + &.cw-manager-element-item-sorting:hover{ + background-color: $white; + color: $base-color; + } + } + + .cw-manager-filing { + border: solid thin $content-color-40; + margin-top: 4px; + @include background-icon(arr_eol-down, clickable, 24); + background-color: $white; + background-position: calc(50% - 10em) calc(50% - 1px); + background-repeat: no-repeat; + padding: 1em 0; + color: $base-color; + text-align: center; + width: 100%; + font-weight: 600; + cursor: pointer; + + &.cw-manager-filing-active { + @include background-icon(arr_eol-down, info-alt, 24); + background-color: $activity-color; + border: solid thin $activity-color; + color: $white; + } + &.cw-manager-filing-disabled { + @include background-icon(arr_eol-down, inactive, 24); + background-color: $white; + color: $dark-gray-color-80; + } + } + + .cw-manager-block-buttons, + .cw-manager-container-buttons, + .cw-manager-element-item-buttons { + display: inline; + float: right; + img { + cursor: pointer; + transition: opacity 0.4s ease-in-out; + + &.cw-manager-icon-disabled { + opacity: 0; + cursor: unset; + } + } + } + + .cw-collapsible-content { + display: none; + &.cw-collapsible-content-open { + display: block; + } + } +} + +/* * * * * * * * * * * +m a n a g e r e n d +* * * * * * * * * * */ + +/* * * * * * +b l o c k s +* * * * * */ + +.cw-block-title { + padding: 4px; + background-color: $content-color-20; + color: $base-color; + font-weight: 700; + text-align: center; + border: solid thin $content-color-40; + border-bottom: none; +} +/* * * * * * * * * +b l o c k s e n d +* * * * * * * * */ + +/* * * * * * * * * * +a u d i o b l o c k +* * * * * * * * * * */ +.cw-block-audio { + .cw-audio-container { + border: solid thin $content-color-40; + padding-top: 1em; + } + .cw-audio-controls { + text-align: right; + padding: 0 0.5em; + } + .cw-audio-range { + margin: 0 5px 10px 0; + &::-moz-focus-outer { + border: 0; + } + &.ui-widget-content { + background-color: $base-color; + } + .ui-widget-header { + background-color: $dark-gray-color-5; + } + .ui-slider-handle { + border-radius: 20px; + width: 1em; + height: 1.7em; + top: -0.5em; + background-color: $dark-gray-color-20; + border-color: $content-color-40; + cursor: pointer; + margin-left: -2px; + } + } + .cw-audio-button { + border: solid thin $content-color-40; + background-color: $white; + background-repeat: no-repeat; + background-position: center center; + background-size: 24px; + min-height: 27px; + line-height: 130%; + padding: 5px 15px 5px 30px; + cursor: pointer; + font-size: 14px; + box-sizing: border-box; + text-align: center; + text-decoration: none; + vertical-align: bottom; + white-space: nowrap; + min-width: unset; + margin: 5px; + height: 46px; + width: 46px; + display: inline-block; + outline: none; + + &:hover { + background-color: $base-color; + } + + @each $button, $icon in $media-buttons { + &.cw-audio-#{$button}button { + @include background-icon($icon, clickable, 24); + &:hover { + @include background-icon($icon, info-alt, 24); + } + } + }; + } + + + .cw-audio-time { + position: relative; + top: -1em; + color: $base-gray; + } + + .cw-audio-range { + display: block; + margin: 0 auto 1.5em; + -webkit-appearance: none; + position: relative; + overflow: hidden; + height: 18px; + width: 100%; + cursor: pointer; + border-radius: 0; + } + + .cw-audio-range::-webkit-slider-runnable-track { + background: $dark-gray-color-20; + } + + .cw-audio-range::-webkit-slider-thumb { + -webkit-appearance: none; + width: 9px; /* 1 */ + height: 18px; + background: $white; + box-shadow: -100vw 0 0 100vw $base-color; + border: solid thin $content-color-40; + } + + .cw-audio-range::-moz-range-track { + height: 18px; + background: $dark-gray-color-10; + } + + .cw-audio-range::-moz-range-thumb { + background: $white; + height: 18px; + width: 9px; + border: solid thin $content-color-40; + border-radius: 0 !important; + box-shadow: -100vw 0 0 100vw $base-color; + box-sizing: border-box; + } + + .cw-audio-range::-ms-fill-lower { + background: $base-color; + } + + .cw-audio-range::-ms-thumb { + background: $white; + border: solid thin $content-color-40; + height: 18px; + width: 9px; + box-sizing: border-box; + } + + .cw-audio-range::-ms-ticks-after { + display: none; + } + + .cw-audio-range::-ms-ticks-before { + display: none; + } + + .cw-audio-range::-ms-track { + background: $dark-gray-color-20; + color: transparent; + height: 18px; + border: none; + } + + .cw-audio-range::-ms-tooltip { + display: none; + } + .cw-audio-playlist-wrapper { + margin-top: -1em; + padding-top: 1em; + border: solid thin $content-color-40; + border-top: none; + + .cw-audio-playlist { + padding-left: 0; + list-style: none; + cursor: pointer; + + .cw-playlist-item { + @include background-icon(file-audio2, clickable, 24); + background-repeat: no-repeat; + background-position: 1em center; + + margin: 1em; + padding: 1em; + padding-left: 4em; + color: $base-color; + &:hover { + color: $active-color; + } + &.current-item { + @include background-icon(play, clickable, 24); + font-weight: 700; + &.is-playing { + @include background-icon(pause, clickable, 24); + } + } + &:not(:last-child) { + border-bottom: solid thin $dark-gray-color-30; + } + } + } + .cw-audio-playlist-recorder { + border-top: solid thin $content-color-40; + padding: 1em; + } + } + + + .cw-audio-current-track { + @include background-icon(file-audio2, info, 96); + background-position: top center; + background-repeat: no-repeat; + width: 100%; + min-height: 140px; + margin: 1em 0 2em 0; + p { + text-align: center; + padding-top: 106px; + } + } + .cw-audio-empty { + @include background-icon(file, info, 96); + border: solid thin $content-color-40; + background-position: center 1em; + background-repeat: no-repeat; + min-height: 140px; + padding: 1em; + p { + text-align: center; + padding-top: 106px; + } + } +} +/* * * * * * * * * * * * * * +a u d i o b l o c k e n d +* * * * * * * * * * * * * */ + +/* * * * * * * * * * +v i d e o b l o c k +* * * * * * * * * * */ +.cw-block-video { + video { + width: 100%; + } +} +/* * * * * * * * * * * * * * +v i d e o b l o c k e n d +* * * * * * * * * * * * * */ + +/* * * * * * * * * * * * * * * * * +b e f o r e a f t e r b l o c k +* * * * * * * * * * * * * * * * */ +.cw-block-before-after { + .twentytwenty-container { + width: 100% !important; + z-index: 19; + .twentytwenty-handle { + z-index: 18; + } + .twentytwenty-overlay { + z-index: 17; + } + img { + z-index: 16; + } + } +} +/* * * * * * * * * * * * * * * * * * * * +b e f o r e a f t e r b l o c k e n d +* * * * * * * * * * * * * * * * * * * */ + +/* * * * * * * * * * +c h a r t b l o c k +* * * * * * * * * */ + +.cw-block-chart { + .cw-block-chart-item-remove { + float: right; + margin-right: 5px; + cursor: pointer; + img { + vertical-align: text-top; + } + } +} + +/* * * * * * * * * * * * * * +c h a r t b l o c k e n d +* * * * * * * * * * * * * */ + +/* * * * * * * * * +c o d e b l o c k +* * * * * * * * */ +.cw-block-code { + pre { + margin-bottom: 0; + } + + .hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: $dark-gray-color-5; + color: black; + border: solid thin $content-color-40; + } + + .hljs-comment, + .hljs-quote, + .hljs-variable { + color: $dark-green; + } + + .hljs-keyword, + .hljs-selector-tag, + .hljs-selector-class, + .hljs-built_in, + .hljs-name, + .hljs-tag { + color: $base-color; + font-weight: 600; + } + + .hljs-string, + .hljs-title, + .hljs-section, + .hljs-attribute, + .hljs-literal, + .hljs-template-tag, + .hljs-template-variable, + .hljs-type, + .hljs-addition { + color: $orange-80; + font-weight: 400; + } + + .hljs-deletion, + .hljs-selector-attr, + .hljs-selector-pseudo, + .hljs-meta { + color: $petrol; + font-weight: 400; + } + + .hljs-doctag { + color: $dark-gray-color-75; + font-weight: 400; + } + + .hljs-attr { + color: $active-color; + font-weight: 400; + } + + .hljs-symbol, + .hljs-bullet, + .hljs-link { + color: $petrol; + font-weight: 400; + } + + .hljs-emphasis { + font-style: italic; + font-weight: 400; + } + + .hljs-strong { + font-weight: 600; + } + + .code-lang { + background: $dark-gray-color-5; + border: solid thin $content-color-40; + border-top: none; + padding: 5px 10px; + text-align: right; + color: $dark-gray-color-45; + font-family: monospace; + text-transform: full-width; + } + +} +/* * * * * * * * * * * * * +c o d e b l o c k e n d +* * * * * * * * * * * * */ + +/* * * * * * * * * * * * * +c o n f i r m b l o c k +* * * * * * * * * * * * */ +.cw-block-confirm { + .cw-block-confirm-content{ + border: solid thin $content-color-40; + padding: 1em; + display: flex; + .cw-block-confirm-checkbox img{ + margin-right: 2em; + vertical-align: middle; + height: 100%; + } + .cw-block-confirm-text { + margin: 0; + } + } +} +/* * * * * * * * * * * * * * * * +c o n f i r m b l o c k e n d +* * * * * * * * * * * * * * * */ + +/* * * * * * * * * * +d a t e b l o c k +* * * * * * * * * */ +.cw-container-colspan-half { + .cw-block-date { + .cw-block-content { + font-size: 9px; + } + } +} + +.cw-block-date { + + .cw-date-countdown, + .cw-date-date { + margin: 0 auto; + width: max-content; + } + + .cw-date-countdown { + .cw-date-countdown-digit { + display: inline-block; + margin-right: 4px; + + .cw-date-countdown-number { + font-size: 6em; + line-height: 1.25em; + height: 1.25em; + padding: 3px 29px; + background: rgba(245,245,245,1); + font-weight: 300; + } + + .cw-date-countdown-label-sg, + .cw-date-countdown-label-pl { + padding: 5px; + font-size: 1.25em; + text-align: left; + text-transform: uppercase; + } + } + } + + .cw-date-date { + .cw-date-date-space { + display: inline-block; + width: 2em; + + } + + .cw-date-date-digits{ + display: inline-block; + + .cw-date-date-number { + font-size: 5em; + line-height: 1.25em; + height: 1.25em; + padding: 0.25em; + background: rgba(245,245,245,1); + font-weight: 300; + } + } + } +} + +/* * * * * * * * * * * * * +d a t e b l o c k e n d +* * * * * * * * * * * * */ + +/* * * * * * * * * * * * +c a n v a s b l o c k +* * * * * * * * * * * */ +.cw-block-canvas { + .cw-canvasblock-canvas { + max-width: 100%; + border: solid thin $content-color-40; + } + + .cw-canvasblock-upload-message{ + display: none; + } + + .cw-canvasblock-original-img { + display: none; + } + + .cw-canvasblock-tool-selected-text { + cursor: text; + } + + h1.cw-canvasblock-description { + border-bottom: none; + } + + .cw-canvasblock-toolbar { + border: solid thin $content-color-40; + border-bottom: none; + } + + .cw-canvasblock-buttonset { + display: inline-block; + padding: 5px; + margin-right: 0.5em; + } + + .cw-canvasblock-tool-selected-text { + cursor: text; + } + + button { + cursor: pointer; + user-select: none; + border: solid thin $content-color-40; + height: 32px; + width: 32px; + background-color: white; + background-position: center; + background-repeat: no-repeat; + background-size: 24px 24px; + + &:focus { + outline: none; + } + + &.cw-canvasblock-color { + $colors: ( + white: #ffffff, + blue: #3498db, + green: #2ecc71, + purple: #9b59b6, + red: #e74c3c, + yellow: #fed330, + orange: #f39c12, + grey: #95a5a6, + darkgrey: #34495e, + black: #000000, + ); + + @each $name, $color in $colors { + &.#{"" + $name} { + background-color: $color; + } + } + + &.selected-color { + border: solid 2px $black; + } + } + + &.cw-canvasblock-reset { + @include background-icon(refresh, clickable, 24); + } + + &.cw-canvasblock-size { + @include background-icon(stop, clickable); + + &.cw-canvasblock-size-small { + background-size: 8px 7px; + } + &.cw-canvasblock-size-normal { + background-size: 16px 14px; + } + &.cw-canvasblock-size-large { + background-size: 22px 20px; + } + &.cw-canvasblock-size-huge { + background-size: 26px 24px; + } + &.selected-size { + border: solid 2px $black; + } + } + + &.cw-canvasblock-tool { + &.cw-canvasblock-tool-pen { + @include background-icon(comment, clickable); + } + + &.cw-canvasblock-tool-text { + vertical-align: top; + font-size: 22px; + color: $base-color; + font-weight: 600; + } + + &.selected-tool { + border: solid 2px $black; + } + } + + &.cw-canvasblock-undo { + @include background-icon(arr_2left, clickable, 24); + } + + &.cw-canvasblock-download { + @include background-icon(download, clickable, 24); + } + &.cw-canvasblock-store { + @include background-icon(upload, clickable, 24); + } + &.cw-canvasblock-show-all { + @include background-icon(group2, clickable, 24); + &.selected-view { + border: solid 2px $black; + } + } + &.cw-canvasblock-show-own { + @include background-icon(person, clickable, 24); + &.selected-view { + border: solid 2px $black; + } + } + } +} +/* * * * * * * * * * * * * * * +c a n v a s b l o c k e n d +* * * * * * * * * * * * * * */ + +/* * * * * * * * * * * * * +d o c u m e n t b l o c k +* * * * * * * * * * * * */ +.cw-block-document { + .cw-pdf-header { + position: relative; + + .cw-pdf-button-prev, + .cw-pdf-button-next { + position: absolute; + border: none; + background-repeat: no-repeat; + background-color: transparent; + height: 24px; + width: 24px; + margin: 2px 12px; + cursor: pointer; + outline: none; + } + + .cw-pdf-button-prev { + left: 0; + @include background-icon(arr_1left, clickable, 18); + &.inactive { + @include background-icon(arr_1left, navigation, 18); + } + } + + .cw-pdf-button-next { + right: 0; + @include background-icon(arr_1right, clickable, 18); + &.inactive { + @include background-icon(arr_1right, navigation, 18); + } + } + + .cw-pdf-download { + display: inline-block; + width: 18px; + height: 18px; + margin: 0 0.25em; + border: none; + cursor: pointer; + vertical-align: sub; + + background: no-repeat scroll 0 0; + @include background-icon(download, clickable, 18); + } + } + .cw-pdf-canvas { + border: solid thin $content-color-40; + width: calc(100% - 2px); + } + .cw-pdf-downloadbox { + border: solid thin $content-color-40; + padding: 0.5em 1em; + + .cw-pdf-file-info { + @include background-icon(file, clickable, 24); + display: inline-block; + background-repeat: no-repeat; + padding-left: 26px; + margin: 1em; + line-height: 24px; + color: $base-color; + &.cw-pdf-fileicon-pdf { + @include background-icon(file-pdf, clickable, 24); + } + } + .cw-pdf-download-icon { + float: right; + @include background-icon(download, clickable, 24); + height: 24px; + width: 24px; + background-repeat: no-repeat; + margin: 1em; + } + } +} +/* * * * * * * * * * * * * * * * * * +d o c u m e n t b l o c k e n d +* * * * * * * * * * * * * * * * * */ + +/* * * * * * * * * * * +e m b e d b l o c k +* * * * * * * * * * */ + +.cw-block-embed { + .cw-block-content { + .cw-block-embed-iframe-wrapper { + overflow-y: hidden; + iframe{ + height: 100% !important; + width: 100% !important; + } + } + .cw-block-embed-info { + margin-top: 0.5em; + } + } +} + +/* * * * * * * * * * * * * * +e m b e d b l o c k e n d +* * * * * * * * * * * * * */ + +/* * * * * * * * * * * +i f r a m e b l o c k +* * * * * * * * * * */ + +.cw-block-iframe { + .cw-block-content { + iframe { + border: solid thin $content-color-40; + width: calc(100% - 2px); + } + + .cw-block-iframe-cc-data { + border: solid thin $content-color-40; + border-top: none; + margin-top: -6px; + padding-top: 10px; + height: 75px; + + .cw-block-iframe-cc { + width: 120px; + height: 50px; + margin-left: 4px; + display: inline-block; + background-repeat: no-repeat; + &.cw-block-iframe-cc-by { + background-image: url("#{$image-path}/cc/by.svg"); + } + &.cw-block-iframe-cc-by-nc { + background-image: url("#{$image-path}/cc/by-nc.eu.svg"); + } + &.cw-block-iframe-cc-by-nc-nd { + background-image: url("#{$image-path}/cc/by-nc-nd.eu.svg"); + } + &.cw-block-iframe-cc-by-nc-sa { + background-image: url("#{$image-path}/cc/by-nc-sa.svg"); + } + &.cw-block-iframe-cc-by-nd { + background-image: url("#{$image-path}/cc/by-nd.svg"); + } + &.cw-block-iframe-cc-by-sa { + background-image: url("#{$image-path}/cc/by-sa.svg"); + } + } + .cw-block-iframe-cc-infos{ + display: inline-block; + vertical-align: top; + padding-left: 1em; + p { + margin: 0; + } + } + } + + + + } +} + +/* * * * * * * * * * * * * * +i f r a m e b l o c k e n d +* * * * * * * * * * * * * */ + + +/* * * * * * * * * * * * +f o l d e r b l o c k +* * * * * * * * * * * */ +.cw-block-folder-list { + border: solid thin #ccc; + padding: 4px; + list-style: none; + + .cw-block-folder-file-item { + list-style: none; + + &:not(:last-child) { + border-bottom: solid thin #ccc; + } + a { + display: block; + } + &:hover { + background-color: hsla(217,6%,45%,.2); + } + } + .cw-block-folder-download-icon { + @include background-icon(download, clickable, 24); + background-repeat: no-repeat; + + float: right; + height: 24px; + width: 24px; + margin: 1em; + } +} + // for folder and download block +.cw-block-file-info { + @include background-icon(file, clickable, 24); + background-repeat: no-repeat; + + display: inline-block; + padding-left: 26px; + margin: 1em; + line-height: 24px; + color: $base-color; + + &.cw-block-file-icon-empty { + color: $black; + @include background-icon(folder-empty, info, 24); + } + &.cw-block-file-icon-none { + color: $black; + @include background-icon(file, info, 24); + } + &.cw-block-file-icon-audio { + @include background-icon(file-audio, clickable, 24); + } + &.cw-block-file-icon-pic { + @include background-icon(file-pic, clickable, 24); + } + &.cw-block-file-icon-video { + @include background-icon(file-video, clickable, 24); + } + &.cw-block-file-icon-pdf { + @include background-icon(file-pdf, clickable, 24); + } + &.cw-block-file-icon-word { + @include background-icon(file-word, clickable, 24); + } + &.cw-block-file-icon-spreadsheet { + @include background-icon(file-excel, clickable, 24); + } + &.cw-block-file-icon-text { + @include background-icon(file-text, clickable, 24); + } + &.cw-block-file-icon-ppt { + @include background-icon(file-ppt, clickable, 24); + } + &.cw-block-file-icon-archive { + @include background-icon(file-archive, clickable, 24); + } + &.cw-block-file-icon-file { + @include background-icon(file, clickable, 24); + } +} +/* * * * * * * * * * * * * * * +f o l d e r b l o c k e n d +* * * * * * * * * * * * * * */ + + +/* * * * * * * * * * * * * * +d o w n l o a d b l o c k +* * * * * * * * * * * * * */ +.cw-block-download { + .cw-block-download-content { + border: solid thin $content-color-40; + padding: 4px; + .cw-block-download-file-item { + a { + display: block; + } + &:hover { + background-color: fade-out($dark-gray-color-75, 0.8); + } + .cw-block-download-download-icon { + @include background-icon(download, clickable, 24); + background-repeat: no-repeat; + + float: right; + height: 24px; + width: 24px; + margin: 1em; + } + } + } + +} + +/* * * * * * * * * * * * * * * * * +d o w n l o a d b l o c k e n d +* * * * * * * * * * * * * * * * */ + +/* * * * * * * * * * * * * +g a l l e r y b l o c k +* * * * * * * * * * * * */ + +.cw-block-gallery { + .cw-block-content { + overflow: hidden; + } +} + +.cw-block-gallery-content { + position: relative; + margin: auto; +} + +.cw-block-gallery-slides { + display: none; + img { + display: block; + max-width: 100%; + margin-left: auto; + margin-right: auto; + } +} + +.cw-block-gallery-prev, +.cw-block-gallery-next { + cursor: pointer; + position: absolute; + background-color: fade-out($white, 0.6); + top: 50%; + height: 36px; + width: 36px; + background-repeat: no-repeat; + background-position: center; + margin-top: -22px; + transition: 200ms ease; + user-select: none; + + &:hover { + background-color: $base-color; + } +} + +.cw-block-gallery-prev { + @include background-icon(arr_1left, clickable, 24); + &:hover{ + @include background-icon(arr_1left, info-alt, 24); + } +} +.cw-block-gallery-next { + right: 0; + @include background-icon(arr_1right, clickable, 24); + &:hover { + @include background-icon(arr_1right, info-alt, 24); + } +} + +.cw-block-gallery-file-name { + color: $white; + font-size: 15px; + padding: 8px 12px; + position: absolute; + bottom: 8px; + width: 100%; + text-align: center; + span { + background-color: fade-out($black, 0.6); + padding: 0.5em; + } +} + +.cw-block-gallery-number-text { + color: $white; + font-size: 12px; + padding: 8px 12px; + position: absolute; + top: 0; + background-color: fade-out($black, 0.6); +} + + .cw-block-gallery-fade { + -webkit-animation-name: fade; + -webkit-animation-duration: 1.5s; + animation-name: fade; + animation-duration: 1.5s; +} + +@-webkit-keyframes fade { + from {opacity: .4} + to {opacity: 1} +} + +@keyframes fade { + from {opacity: .4} + to {opacity: 1} +} + +/* * * * * * * * * * * * * * * * * +g a l l e r y b l o c k e n d +* * * * * * * * * * * * * * * * */ + + +/* * * * * * * * * * * * * * * * * +i m a g e m a p b l o c k +* * * * * * * * * * * * * * * * */ +.cw-block-image-map { + .cw-image-map-canvas, + .cw-image-map-original-img { + display: none; + } +} + +/* * * * * * * * * * * * * * * * * +i m a g e m a p b l o c k e n d +* * * * * * * * * * * * * * * * */ + +/* * * * * * * * * * +l i n k b l o c k +* * * * * * * * * */ + +.cw-block-link { + a { + text-decoration: none; + } + .cw-link { + border: solid thin $content-color-40; + color: $base-color; + height: 20px; + padding: 1em; + + .cw-link-title { + margin-left: 3em; + } + + &:hover { + background-color: $base-color; + border: solid thin $base-color; + color: $white; + } + + &.internal { + @include background-icon(link-intern, clickable, 28); + background-position: 1em 50%; + background-repeat: no-repeat; + + &:hover { + @include background-icon(link-intern, info-alt, 28); + } + } + + &.external { + @include background-icon(link-extern, clickable, 28); + background-position: 1em 50%; + background-repeat: no-repeat; + &:hover { + @include background-icon(link-extern, info-alt, 28); + } + + .cw-link-og-image { + display: inline-block; + max-width: 40%; + vertical-align: top; + margin-right: 2em; + } + .cw-link-og-textwrapper { + display: inline-block; + max-width: 50%; + p { + margin: 0; + } + .cw-link-og-site { + font-size: 1.25em; + } + .cw-link-og-title { + font-weight: 600; + } + .cw-link-og-description { + color: $base-color-80; + text-align: justify; + } + } + } + } +} + +/* * * * * * * * * * * * * +l i n k b l o c k e n d +* * * * * * * * * * * * */ + +/* +dialog cards block +*/ +.cw-block-dialog-cards-content { + display: flex; + .cw-dialogcards { + flex: auto; + .scene { + margin: 0 auto; + width: 440px; + height: 600px; + perspective: 880px; + display: none; + &.active { + display: block; + animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + perspective: 1000px; + } + } + + .card { + width: 100%; + height: 78%; + transition: transform 1s; + transform-style: preserve-3d; + cursor: pointer; + position: relative; + top: 11%; + } + + .card.is-flipped { + transform: rotateY(180deg); + } + + .card__face { + position: absolute; + width: 100%; + height: 100%; + color: $black; + text-align: center; + font-weight: bold; + font-size: 1.2em; + backface-visibility: hidden; + box-shadow: 0 2px 15px fade-out($black, 0.7); + + img { + max-width: 380px; + max-height: 220px; + margin-top: 1em; + } + + .cw-dialogcards-front-no-image { + @include background-icon(question, navigation, 150); + } + + .cw-dialogcards-back-no-image { + @include background-icon(exclaim, navigation, 150); + } + + .cw-dialogcards-front-no-image, + .cw-dialogcards-back-no-image { + width: 100%; + height: 180px; + margin-top: 2em; + background-repeat: no-repeat; + background-position-x: center; + } + + p { + margin: 1em 3em 1em 4em; + padding-right: 1em; + overflow-y: auto; + max-height: 12em; + text-align: justify; + } + } + + .card__face--front { + @include background-icon(arr_1right, clickable); + background-color: $white; + background-repeat: no-repeat; + background-position: 95% 95%; + } + + .card__face--back { + @include background-icon(arr_1left, clickable); + background-color: $white; + background-repeat: no-repeat; + background-position: 5% 95%; + transform: rotateY(180deg); + } + } + + .cw-dialogcards-navbutton { + color: transparent; + width: 35px; + height: 35px; + background-color: #bbb; + border-radius: 2px; + background-position: 50%; + background-repeat: no-repeat; + background-color: $base-color; + border: none; + outline: none; + display: block; + z-index: 4; + margin: auto; + padding: 0; + cursor: pointer; + + &.cw-dialogcards-prev { + @include background-icon(arr_1left, info-alt, 24); + + } + + &.cw-dialogcards-next { + @include background-icon(arr_1right, info-alt, 24); + right: 0; + } + + &.cw-dialogcards-prev-disabled, + &.cw-dialogcards-next-disabled { + background-color: $light-gray-color-40; + } + } + + @keyframes shake { + 10%, 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, 50%, 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, 60% { + transform: translate3d(4px, 0, 0); + } + } +} + + +/* +dialog cards block end +*/ + +/* +headline block +*/ + +.cw-block-headline { + .cw-block-headline-content { + min-height: 600px; + background-position: center; + background-size: 1095px; + + &.half { + min-height: 300px; + } + + &.heavy { + padding: 2em; + h1, h2 { + display: inline; + border: none; + padding: 0.25em 0; + } + h1 { + font-size: 10em; + line-height: 1.6em; + } + h2 { + font-size: 2em; + line-height: 1.6em; + } + } + &.bigicon_top { + .icon-layer { + display: flex; + align-items: center; + background-repeat: no-repeat; + background-position: center calc(50% - 8em); + background-size: 196px; + min-height: 600px; + + @each $icon in $icons { + &.icon-black-#{$icon} { + @include background-icon($icon, info, 196); + } + &.icon-white-#{$icon} { + @include background-icon($icon, info-alt, 196); + } + &.icon-studip-blue-#{$icon} { + @include background-icon($icon, clickable, 196); + } + &.icon-studip-red-#{$icon} { + @include background-icon($icon, status-red, 196); + } + &.icon-studip-yellow-#{$icon} { + @include background-icon($icon, status-yellow, 196); + } + &.icon-studip-green-#{$icon} { + @include background-icon($icon, status-green, 196); + } + }; + + &.half { + min-height: 300px; + background-size: 144px; + background-position: center calc(50% - 4em); + } + } + + + .cw-block-headline-textbox { + width: 100%; + .cw-block-headline-title { + h1 { + margin-top: 1.5em; + border: none; + font-size: 5em; + text-align: center; + } + } + + .cw-block-headline-subtitle { + h2 { + border: none; + font-size: 18px; + text-align: center; + margin-top: 10px; + } + } + } + } + &.bigicon_before { + .icon-layer { + display: flex; + align-items: center; + background-repeat: no-repeat; + background-position: 4em center; + min-height: 600px; + + &.half { + min-height: 300px; + } + @each $icon in $icons { + &.icon-black-#{$icon} { + @include background-icon($icon, info, 196); + } + &.icon-white-#{$icon} { + @include background-icon($icon, info-alt, 196); + } + &.icon-studip-blue-#{$icon} { + @include background-icon($icon, clickable, 196); + } + &.icon-studip-red-#{$icon} { + @include background-icon($icon, status-red, 196); + } + &.icon-studip-yellow-#{$icon} { + @include background-icon($icon, status-yellow, 196); + } + &.icon-studip-green-#{$icon} { + @include background-icon($icon, status-green, 196); + } + }; + } + + .cw-block-headline-textbox { + width: 100%; + .cw-block-headline-title { + + h1 { + border: none; + font-size: 5em; + text-align: left; + margin-left: 4.25em; + } + } + + .cw-block-headline-subtitle { + display: none; + } + } + } + + &.ribbon { + .icon-layer { + display: flex; + align-items: center; + min-height: 600px; + + &.half { + min-height: 300px; + } + + .cw-block-headline-textbox { + width: 100%; + padding: 1em 0; + background-color: fade-out($black, 0.5); + .cw-block-headline-title { + + h1 { + border: none; + font-size: 5em; + text-align: center; + } + } + + .cw-block-headline-subtitle { + h2 { + border: none; + font-size: 18px; + text-align: center; + margin-top: 10px; + } + } + } + } + } + } +} + +/* +headline block end +*/ + +/* +toc block +*/ +.cw-block-table-of-contents-list { + padding: 0; + list-style: none; + border: solid thin $content-color-40; + li { + &:not(:last-child) { + border-bottom: solid thin $dark-gray-color-30; + } + a { + display: block; + padding: 1em; + } + &:hover { + background-color: fade-out($dark-gray-color-75, 0.8); + } + } +} + +.cw-block-table-of-contents-list-details { + padding: 0; + list-style: none; + border: solid thin $content-color-40; + li { + &:not(:last-child) { + border-bottom: solid thin $dark-gray-color-30; + } + + &:hover { + background-color: fade-out($dark-gray-color-75, 0.8); + } + a { + display: block; + padding: 1em; + } + } +} + +.cw-block-table-of-contents-title-box { + min-height: 3em; + padding-left: 1em; + border-left-width: 10px; + border-left-style: solid; + + @each $name, $color in $tile-colors { + &.#{"" + $name} { + border-color: $color; + } + }; + + p, p:hover { + color: $black; + } +} + +.cw-block-table-of-contents-tiles.cw-tiles { + &.cw-tiles-space-between { + justify-content: space-between; + } + .tile { + margin: 0 0 5px 0; + &.cw-tile-margin { + margin: 0 5px 5px 0; + } + } +} + +.cw-container-colspan-half { + .cw-block-table-of-contents-tiles.cw-tiles { + justify-content: space-between; + .tile { + margin: 0 0 5px 0; + width: 267px; + } + } +} + +/* +toc block end +*/ + +/* +text block +*/ +.cw-block-text { + .cktoolbar { + width: 100% !important; + max-width: 100% !important; + position: relative !important; + top: 0 !important; + } + .cke { + width: 100% !important; + } + .ckplaceholder { + height: 0 !important; + } +} +/* +text block end +*/ + +/* +cw tiles +*/ + +.cw-tiles { + list-style: none; + display: flex; + flex-wrap: wrap; + padding-left: 0; + + .tile { + height: 420px; + width: 270px; + margin: 0 5px 5px 0; + background-color: $base-color; + cursor: pointer; + &:last-child { + margin-right: 0; + } + + @each $name, $color in $tile-colors { + &.#{"" + $name} { + background-color: $color; + } + }; + } + .preview-image { + height: 180px; + width: 100%; + background-size: auto 180px; + background-repeat: no-repeat; + background-color: $black; + background-position: center; + } + .description { + height: 220px; + padding: 10px 14px; + color: $white; + position: relative; + + header { + font-size: 1.25em; + color: $white; + border: none; + margin-bottom: 0.75em; + width: 240px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .description-text-wrapper { + overflow: hidden; + height: 10em; + display: -webkit-box; + margin-bottom: 1em; + -webkit-line-clamp: 7; + -webkit-box-orient: vertical; + p { + text-align: justify; + } + } + + footer{ + width: 242px; + text-align: right; + color: $white; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + img { + vertical-align: text-bottom; + } + } + } +} + +/* +cw tiles end +*/ + +/* +vSelect +*/ +.cw-vs-select { + max-width: 48em; + + .vs__dropdown-toggle { + border: solid thin $content-color-40; + border-radius: 0; + } + .vs__option-with-icon{ + padding-left: 8px; + } + .vs__option-color { + border: solid thin $content-color-40; + padding-left: 20px; + height: 20px; + margin-right: 4px; + } +} + +/* +vSelect end +*/ + +/* cw manager copy */ + +.cw-manager-copy-selector { + ul { + padding: 0; + margin: 0; + list-style: none; + } + button { + width: 100%; + border: solid thin $content-color-40; + background-color: $white; + padding: 1em; + margin-bottom: 4px; + color: $base-color; + cursor: pointer; + outline: none; + &:hover { + color:$white; + background-color: $base-color; + } + } +} + +/* cw manager copy end*/ diff --git a/resources/assets/stylesheets/scss/dates.scss b/resources/assets/stylesheets/scss/dates.scss new file mode 100644 index 0000000..30e9978 --- /dev/null +++ b/resources/assets/stylesheets/scss/dates.scss @@ -0,0 +1,42 @@ +table.dates { + width: calc(100% - 4px); + .themen_list > * { + background-color: transparent; + } + tr.ausfall { + transition: opacity 300ms; + opacity: 0.5; + + &:hover { + opacity: 1; + } + } + .nextdate { + background-color: $content-color-40; + } + + .topic-droppable { + &.active { + background-color: $activity-color-40; + } + &.hovered { + background-color: $activity-color-80; + } + } + .draggable-topic-handle { + cursor: move; + background: transparent url("#{$image-path}/anfasser_24.png") 3px center no-repeat; + padding-left: 12px; + } + .ui-draggable-dragging { + img.icon-shape-trash { + display: none; + } + } +} + +.themen-list { + > .list-placeholder:not(:only-child) { + display: none; + } +} diff --git a/resources/assets/stylesheets/scss/files.scss b/resources/assets/stylesheets/scss/files.scss new file mode 100644 index 0000000..7896319 --- /dev/null +++ b/resources/assets/stylesheets/scss/files.scss @@ -0,0 +1,3 @@ +table.documents tfoot td div.pagination-wrapper { + float: right; +} diff --git a/resources/assets/stylesheets/scss/fullscreen.scss b/resources/assets/stylesheets/scss/fullscreen.scss new file mode 100644 index 0000000..8ff20eb --- /dev/null +++ b/resources/assets/stylesheets/scss/fullscreen.scss @@ -0,0 +1,79 @@ +$transition-duration: 300ms; + +.fullscreen-toggle { + background: none; + border: 0px; + width: 28px; + height: 28px; + + @include background-icon(zoom-in2, clickable, 20); + background-position: center; + background-repeat: no-repeat; + + text-indent: 200%; + overflow: hidden; + + clear: both; + float: right; + + position: relative; + top: 1px; + right: 12px; + + cursor: pointer; + + z-index: 100; +} + +#barBottomContainer, +#flex-header, +.secondary-navigation { + top: 0px; + margin-bottom: 0px; +} +#layout_footer { + max-height: 40px; + overflow: hidden; +} + +html:not(.is-fullscreen-immediately) { + #barBottomContainer, + #flex-header, + .secondary-navigation { + transition: top $transition-duration, margin-bottom $transition-duration, opacity $transition-duration; + } + #layout-sidebar { + transition: left $transition-duration, margin-right $transition-duration, opacity $transition-duration; + } + #layout_footer { + transition: opacity $transition-duration, max-height $transition-duration, padding $transition-duration; + } +} + +html.is-fullscreen { + #barBottomContainer, + #flex-header, + .secondary-navigation { + margin-bottom: -70px; + opacity: 0; + top: -142px; + } + + #layout-sidebar { + left: -300px; + margin-right: -270px; + opacity: 0; + } + + #layout_footer { + opacity: 0; + max-height: 0px; + padding: 0px; + } + + .fullscreen-toggle { + @include background-icon(zoom-out2, clickable, 20); + margin-bottom: 16px; + right: 0px; + } +} diff --git a/resources/assets/stylesheets/scss/grid.scss b/resources/assets/stylesheets/scss/grid.scss new file mode 100644 index 0000000..67cf047 --- /dev/null +++ b/resources/assets/stylesheets/scss/grid.scss @@ -0,0 +1,30 @@ +$grid-gap: 15px; // 10px would lead to fitting 4 columns on a default 1440px wide screen +$grid-element-width: 270px; + +.studip-grid { + // $header-padding: 2px; + // $padding: 5px; + // $header-size: 80px; + // $element-height: (100px + $header-size); + + display: grid; + grid-template-columns: repeat(auto-fill, $grid-element-width); + grid-template-rows: repeat(auto-fit, max-content); + grid-gap: $grid-gap; +} + +.studip-grid-element { + border: 1px solid $light-gray-color; +} + +// Responsive displays +@include media-breakpoint-small-down() { + .studip-grid { + grid-template-columns: 1fr 1fr; + } +} +@include media-breakpoint-tiny-down() { + .studip-grid { + grid-template-columns: 100%; + } +} diff --git a/resources/assets/stylesheets/scss/installer.scss b/resources/assets/stylesheets/scss/installer.scss new file mode 100644 index 0000000..d7a3100 --- /dev/null +++ b/resources/assets/stylesheets/scss/installer.scss @@ -0,0 +1,267 @@ +@import "../mixins/colors.scss"; + +.stage { + $image-path: '../images/'; + $icon-path: '#{$image-path}icons/'; + + background: #fff; + margin: 0 auto; + margin-top: 1em; + padding: 1em; + width: 800px; + + &.ui-dialog { + position: relative; + top: auto; + left: auto; + + &.ui-widget.ui-widget-content .ui-dialog-titlebar { + background-image: url('#{$image-path}logos/studip-logo.svg'); + background-position: right 10px top 10px; + background-repeat: no-repeat; + background-size: 120px; + + div:first-of-type { + font-size: 20px; + font-weight: lighter; + line-height: 1; + } + } + + .ui-dialog-content.ui-widget-content { + min-height: 15em; + padding: 0.5em 1em; + + display: flex; + align-items: start; + flex-direction: row; + flex-wrap: wrap; + + > * { + box-sizing: border-box; + flex: 0 0 100%; + } + > .half-sized { + flex: 1; + } + } + } + + footer { + border-top: 1px solid #444; + text-align: center; + + ul { + list-style: none; + margin: 0; + padding: 0; + + li { + display: inline-block; + + &:not(:first-child)::before { + content: ' | '; + } + } + } + } + + dl { + dt { + box-sizing: border-box; + clear: left; + float: left; + min-width: 200px; + padding-right: 0.5em; + } + dd { + box-sizing: border-box; + margin-left: 200px; + width: calc(100% - 200px); + word-break: break-all; + + &.failed, + &.success, + &.notice { + &::before { + display: inline-block; + height: 16px; + width: 16px; + + vertical-align: text-top; + } + } + + &.failed { + &::before { + content: url('#{$icon-path}red/decline.svg') ' '; + } + color: $red; + } + &.success { + &::before { + content: url('#{$icon-path}green/accept.svg') ' '; + } + color: $green; + } + &.notice { + &::before { + content: url('#{$icon-path}blue/info-circle.svg') ' '; + } + color: black; + } + + code { + font-weight: bold; + white-space: nowrap; + } + textarea { + width: 100%; + height: 40em; + } + } + + &::after { + clear: both; + display: block; + content: ''; + height: 0px; + visibility: hidden; + } + + &.requests { + dt:not(.succeeded):not(.failed):not(.requesting) { + background: url('#{$icon-path}/black/date.svg') no-repeat right center; + background-size: 16px; + } + dt.requesting { + background: url('#{$image-path}/ajax-indicator-black.svg') no-repeat top 3px right; + background-size: 16px; + } + + dd.success, + dd.failed, + progress { + display: none; + } + + dt.succeeded + dd.success, + dt.failed + dd.success + dd.failed { + display: block; + } + dt.event-sourced + dd.success + dd.failed + progress { + display: inline-block; + } + + progress { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + border: 1px solid #000; + color: $base-color-60; + margin-left: 1em; + width: 550px; + height: 20px; + + &::-moz-progress-bar, + &::-webkit-progress-bar { + background-color: $base-color-60; + } + + + div { + position: absolute; + width: 550px; + height: 22px; + + &::before { + position: absolute; + left: 2px; + top: 2px; + content: attr(data-file); + } + + &::after { + position: absolute; + right: 2px; + top: 2px; + content: attr(data-percent) '%'; + } + } + } + } + } + + p { + text-align: justify; + } + + code { + background-color: $dark-gray-color; + color: $white; + padding: 2px 4px; + } + + div.type-text { + &.required { + label::after { + color: $red; + content: '*'; + } + } + } + label { + &:not(.plain) { + display: block; + float: left; + padding: 2px; + width: 200px; + } + + input { + display: block; + margin: 1px; + margin-left: 100px; + } + + &.vertical { + float: none; + width: auto; + + + input { + margin-left: 0; + width: 100%; + } + } + } + .studip-checkbox + label { + float: none; + width: auto; + } + + .messagebox { + margin-bottom: 2em; + } + + strong.required::after { + color: $red; + content: '*'; + } + + #progress { + box-sizing: border-box; + + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + color: $base-color-60; + margin: 0 1em -4px; + width: calc(100% - 2em); + height: 4px; + + &::-moz-progress-bar, + &::-webkit-progress-bar { + background-color: $base-color-60; + } + } +} diff --git a/resources/assets/stylesheets/scss/mvv.scss b/resources/assets/stylesheets/scss/mvv.scss new file mode 100644 index 0000000..0642a2e --- /dev/null +++ b/resources/assets/stylesheets/scss/mvv.scss @@ -0,0 +1,33 @@ +#index_filter { + label.mvv-name-search { + display: block; + input[type="text"] { + box-sizing: border-box; + border: 1px solid $base-color-60; + border-right-width: 30px; + float: left; + height: 22px; + width: 100%; + } + + input[type="submit"] { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; + + // Activate when twitter-mixins is included as scss and remove the above rules + // @include hide-text(); + + width: 29px; + height: 20px; + @include background-icon(search, info_alt); + float: left; + background-position: center 3px; + background-repeat: no-repeat; + vertical-align: top; + margin-left: -30px; + } + } +} diff --git a/resources/assets/stylesheets/scss/my_courses.scss b/resources/assets/stylesheets/scss/my_courses.scss new file mode 100644 index 0000000..7666f2d --- /dev/null +++ b/resources/assets/stylesheets/scss/my_courses.scss @@ -0,0 +1,10 @@ +.missing_course { + .content { + font-weight: bold; + } + border: 2px solid $red; + display: inline-block; + padding: 5px; + margin: 5px 0; + background: $white; +} diff --git a/resources/assets/stylesheets/scss/oer.scss b/resources/assets/stylesheets/scss/oer.scss new file mode 100755 index 0000000..38d1cb2 --- /dev/null +++ b/resources/assets/stylesheets/scss/oer.scss @@ -0,0 +1,485 @@ +.oer_material_overview { + list-style-type: none; + text-align: center; + + display: flex; + flex-wrap: wrap; + justify-content: left; + align-items: stretch; + + padding-left: 0px; + margin-top: 20px; +} + +.lernmarktplatz.structure { + list-style-type: none; + padding-left: 0px; + padding-right: 5px; + li { + padding: 5px; + border-top: thin solid $table-header-color; + padding-right: 0px; + } + li.folder { + padding-bottom: 0px; + } + ol { + margin-top: 6px; + padding-left: 40px; + list-style-type: none; + } + > li:last-child { + border-bottom: thin solid $table-header-color; + } +} + + +.author_information { + > li { + display: flex; + } + .avatar { + background-position: center center; + background-repeat: no-repeat; + background-size: 100% 100%; + width: 100px; + min-width: 100px; + height: 100px; + margin-right: 10px; + } + .author_name { + font-weight: bold; + display: inline; + } + .author_host { + font-size: 0.8em; + display: inline; + color: $dark-gray-color-80; + } + .description { + margin-top: 5px; + } +} + +ul.reviews, ol.reviews { + list-style-type: none; + padding: 0px; + margin: 0px; + > li.review { + margin-bottom: 10px; + border: thin solid $base-color-60; + padding: 10px; + display: flex; + > .avatar { + width: 50px; + height: 50px; + } + > .content { + margin-left: 10px; + width: 100%; + .review_text { + margin-top: 5px; + margin-bottom: 5px; + } + .origin { + color: $dark-gray-color-80; + font-size: 0.8em; + } + .timestamp { + float: right; + color: $dark-gray-color-80; + font-size: 0.8em; + } + } + } +} + +.oer_mymaterial { + .inlineform { + display: inline; + } +} + +.maininfo { + border: thin solid $brand-color-light; + padding: 10px; +} + +.lernmarktplatz_player { + display: block; + margin-left: auto; + margin-right: auto; + width: 100%; + max-width: 1000px; + height: calc((100vw - 270px) * 2 / 3); + max-height: 666px; + border: 1px solid $content-color-40; + background: black; + &.image { + background-repeat: no-repeat; + background-position: center center; + background-size: contain; + border: none; + background-color: transparent; + } +} + +#audioplayer { + width: 100%; + max-width: 1000px; +} + +.oercampus_editmaterial { + .drag-and-drop { + width: 260px; + margin-left: 0px; + height: 60px; + background-position: center 40px; + padding-top: 100px; + } + + .autoren { + &.multiple label { + cursor: pointer; + } + input[type=checkbox] { + display: none; + } + input[type=checkbox]:checked + div { + text-decoration: line-through; + } + .avatar { + display: inline-block; + background-position: center center; + background-repeat: no-repeat; + background-size: 100% 100%; + width: 20px; + min-width: 20px; + height: 20px; + margin-right: 5px; + position: relative; + top: 5px; + } + } + .oer_tags_container { + margin-top: 10px; + } + +} + +.oercampus_editmaterial, .oer_material_overview { + article.contentbox { + display: inline-block; + margin-right: 15px; + margin-bottom: 15px; + margin-top: 0px; + margin-left: 0px; + + width: 270px; + max-width: 270px; + box-sizing: border-box; + border: solid 1px $base-color-60; + transition: all 300ms ease 0s; + position: relative; + + header { + display: flex; + align-items: center; + padding-left: 5px; + + width: 100%; + background-color: $content-color-20; + color: $brand-color-dark; + font-size: 12pt; + font-weight: bold; + text-align: left; + line-height: 2em; + height: 40px; + max-height: 40px; + overflow: hidden; + } + + h1 { + padding: 5px; + margin: 0px; + color: $base-color; + border-bottom: medium none; + font-size: medium; + display: flex; + align-items: center; + img { + margin-right: 10px; + } + .title { + max-height: 34px; + } + } + + overflow: hidden; + + .image { + display: block; + margin: 0px; + height: 180px; + background-position: center center; + background-size: cover; + background-repeat: no-repeat; + background-color: white; + } + } +} + +.oer_add_to_course { + margin-bottom: 20px; +} + +.oer_search { + .searchform { + max-width: 840px; + box-sizing: border-box; + } + + .oneliner { + display: flex; + + .frame { + border: thin solid $content-color-40; + display: flex; + justify-content: space-between; + align-items: stretch; + width: 100%; + height: 35px; + + .activefilter { + display: flex; + align-items: center; + justify-content: space-between; + border: solid thin black; + background-color: $content-color-20; + margin: 3px; + padding: 5px; + } + .niveau { + min-width: 115px; + } + + .erasefilter { + margin-left: 5px; + } + + button { + border-right: none; + border-bottom: none; + border-top: none; + height: 35px; + &.active { + background-color: $base-color; + } + &.erase { + background-color: white; + border-left: none; + } + } + + input { + padding-left: 10px; + border: none; + width: 100%; + } + } + + button { + border: thin solid $content-color-40; + background-color: $content-color-20; + display: flex; + align-items: center; + justify-content: center; + width: 35px; + } + + > button { + margin-left: 10px; + } + } + + .filterpanel { + position: absolute; + z-index: 1; + background-color: white; + padding: 10px; + width: 819px; + max-width: calc(100% - 50px); + animation: oer-filter-panel-appears 200ms ease-out; + border: thin solid $content-color-40; + margin: 0px; + margin-top: 46px; + height: 183px; + display: flex; + justify-content: space-around; + align-items: top; + font-size: 1.2em; + + > * { + width: 50%; + } + + @include arrow-top-border(9px, white, 1px, $content-color-40, 46px); + + &::before, &::after { + right: 50px; + } + h3 { + margin-top: 10px; + font-weight: normal; + } + + .level_filter { + width: 300px; + .level_labels { + display: flex; + justify-content: space-between; + font-size: 0.8em; + color: grey; + margin-top: 20px; + } + .level_numbers { + display: flex; + justify-content: space-between; + } + #difficulty_slider { + margin-top: 5px; + width: 93%; + margin-left: auto; + margin-right: auto; + } + } + } + .filterpanel_shadow { + position: absolute; + z-index: 0; + background-color: $dark-gray-color-45; + padding: 10px; + width: 819px; + max-width: calc(100% - 50px); + animation: oer-filter-panel-appears 200ms ease-out; + margin: 0px; + margin-top: 49px; + margin-left: 3px; + height: 183px; + } + + + + [v-if], [v-for] { + display: none !important; + } + + article.contentbox { + animation: oer-material-appears 200ms ease-out; + } + + .browser { + margin-top: 15px; + padding: 10px; + background-color: $content-color-20; + width: 840px; + max-width: 100%; + box-sizing: border-box; + height: 200px; + max-height: 200px; + overflow: hidden; + .intro { + display: flex; + justify-content: space-around; + align-items: center; + > * { + margin-left: 25px; + margin-right: 25px; + max-height: 200px; + } + .illustration { + max-width: 30%; + max-height: 180px; + } + } + h3 { + margin-top: 10px; + } + .back-button { + float: left; + position: relative; + top: 20px; + } + .tags { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + max-width: 100%; + > li { + margin-right: 30px; + animation: oer-tag-appears 400ms ease-out; + text-transform: capitalize; + a.button { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + } +} + +.oer_columns { + display: flex; + > aside { + min-width: 270px; + max-width: 270px; + } + > div { + padding-left: 20px; + } +} + +@keyframes oer-filter-panel-appears { + from { + max-height: 0px; + border-bottom-width: 0px; + overflow: hidden; + } + 99% { + max-height: 183px; + overflow: hidden; + border-bottom-width: 10px; + } + to { + max-height: 183px; + overflow: hidden; + border-bottom-width: 10px; + } +} + +@keyframes oer-material-appears { + from { + opacity: 0; + max-width: 0px; + overflow: hidden; + } + to { + overflow: hidden; + max-width: 270px; + opacity: 1; + } +} + +@keyframes oer-tag-appears { + from { + opacity: 0; + transform: scale(0.5); + filter: blur(4px); + } + to { + transform: scale(1); + opacity: 1; + filter: blur(0px); + } +} diff --git a/resources/assets/stylesheets/scss/report.scss b/resources/assets/stylesheets/scss/report.scss new file mode 100644 index 0000000..d221b71 --- /dev/null +++ b/resources/assets/stylesheets/scss/report.scss @@ -0,0 +1,94 @@ +// Alert dialog (like createQuestion) +.ui-dialog.ui-widget.ui-widget-content.report { + &-info, + &-success, + &-warning, + &-error { + min-width: 30em; + + .ui-dialog-titlebar { + font-weight: bold; + text-align: left; + } + + .ui-dialog-content { + background-position: 12px 8px; + background-repeat: no-repeat; + background-size: 32px; + box-sizing: border-box; + max-height: 60vh; + padding: 15px 15px 15px 55px; + } + + .ui-dialog-buttonpane { + text-align: center; + + .ui-dialog-buttonset { + float: none; + > * { + display: inline-block; + } + } + } + } + + &-info { + .ui-dialog-titlebar { + background-color: $base-color; + color: white; + } + .ui-dialog-content { + background-image: url("#{$image-path}/messagebox/info.png"); + } + } + + &-success { + .ui-dialog-titlebar { + background-color: $dark-green; + color: white; + } + + .ui-dialog-content { + background-image: url("#{$image-path}/messagebox/success.png"); + } + } + + &-warning { + .ui-dialog-titlebar { + background-color: $yellow; + color: black; + } + + .ui-button-icon { + .ui-icon { + .ui-icon-closethick { + @include background-icon(decline, clickable); + } + } + } + + .ui-dialog-content { + background-image: url("#{$image-path}/messagebox/advice.png"); + } + + .ui-dialog-titlebar-close { + background: transparent; + border: 0; + + .ui-icon, .ui-icon:hover { + @include background-icon(decline, info); + background-position: 0; + } + } + } + + &-error { + .ui-dialog-titlebar { + background-color: $red; + color: white; + } + .ui-dialog-content { + background-image: url("#{$image-path}/messagebox/error.png"); + } + } +} diff --git a/resources/assets/stylesheets/scss/resources.scss b/resources/assets/stylesheets/scss/resources.scss new file mode 100644 index 0000000..41d87ba --- /dev/null +++ b/resources/assets/stylesheets/scss/resources.scss @@ -0,0 +1,546 @@ +.resource-object { + width: 30em; + float: left; + margin: 1em; + + .resource-details { + padding: 0.5em; + + .resource-description { + height: 10em; + + .resource-picture { + height: 10em; + width: 10em; + float: left; + } + } + + .small-resource-description { + height: 5em; + + .resource-picture { + height: 5em; + width: 5em; + float: left; + } + } + } +} + +tr.resource-planning-selected-request { + td { + background: $yellow-40; + } +} + +.resource-picture { + height: 10em; + width: 10em; +} + + +/* resource category selection */ +.resource-category-select-icon-label > .resource-category-select-radio { + visibility: hidden; + position: absolute; +} + +#layout-sidebar .room-search-tree-widget { + max-height: unset !important; +} + +ul.resource-tree { + list-style-type: none; + padding-left: 0; + + & > li { + padding-left: 18px; + text-indent: -19px; + + & > ul.resource-tree { + padding-left: 3px; + } + } +} + +.resource-tree { + .selected-resource { + background-color: $origin-base-color; + color: $white; + padding: 2px; + width: calc(100% - 21px); + } + + a { + img:not(.resource-tree-node) { + margin-left: 1px; + } + + &.selected-resource { + display: inline-block; + padding-left: 18px; + text-indent: -19px; + + img { + margin-left: 4px; + } + } + } + + img.resource-tree-node { + padding-top: 2px; + vertical-align: top; + } +} + +/* temporary permission list */ + +#resource-temporary-permissions { + fieldset.bulk-datetime { + display: none; + } + + input.bulk-datetime-enable:checked ~ fieldset.bulk-datetime { + display: block; + } +} + +/* desktop view */ + +@media all and (min-width: 800px) { + form.resource-search { + display: flex; + flex-wrap: wrap; + } + + fieldset.resource-search { + flex-grow: 1; + } +} + +.resource-action-tile { + margin-bottom: 1em; + + article { + border: none; + } +} + +@media all and (min-width: 800px) { + .overview-action-tile-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + } + + .overview-action-tile { + width: 20em; + max-width: 45%; + flex-grow: 1; + margin-right: 10px; + height: 9em; + } +} + +@media all and (max-width: 799px) { + .overview-action-tile { + width: 95%; + margin-bottom: 1em; + } +} + +.room-search-form { + ul.criteria-list { + list-style: none; + margin: 0; + padding: 0; + + li { + margin-bottom: 0.5em; + + img.remove-icon { + margin-right: 5px; + } + + .special-item-switch { + vertical-align: text-bottom; + } + + & > label { + input, select { + width: calc(100% - 2em); + height: 30px; + + &.has-date-picker { + width: calc(100% - 2em - 84px); + } + + &[type=number], &[data-time=yes] { + width: 4em; + } + + &[type=date] { + width: 10em; + max-width: 10em; + } + } + + .select2-wrapper { + select, span.selection, span.select2 { + height: 30px; + width: calc(100% - 2em) !important; + } + } + } + } + } +} + +.room-search-widget_criteria-list { + list-style-type: none; + padding-left: 0.5em; + + .item { + margin-bottom: 0.75em; + + > label { + display: inline-block; + width: calc(100% - 2em); + } + + > label.range-search-label { + > .range-input-container { + margin-left: 1em; + + > input[type="number"] { + max-width: 5em; + } + } + } + } +} + + +.room-clipboard-special-actions { + margin-top: 0.25em; + margin-left: 0.25em; +} + +.room-clipboard-group-action { + display: block; + cursor: pointer; +} + +.resource-request { + .overlapping-requests { + color: $yellow-60; + } + + .overlapping-bookings { + color: $red-60; + } + + .resource-available { + color: $green-60; + } +} + +.booking-view-button-container { + width: 100%; + text-align: center; +} + +.create-booking-form { + .fieldset-row { + &.inner-row { + padding-top: 0; + display: flow-root; + } + + .time-option-container { + padding-top: 1ex; + } + + fieldset { + min-width: 340px; + padding-top: 1ex; + } + + #begin_date-weekdays, + #end_date-weekdays { + span, input { + max-width: 7.7em; + } + } + + #RepeatIntervalSelectField-Daily, #RepeatIntervalSelectField-Weekly { + margin-left: 2em; + margin-bottom: 2em; + } + } + + .singledates { + .booking-list-interval-date { + &.not-taking-place { + text-decoration: line-through; + color: $light-gray-color; + } + + margin-right: 1.2em; + } + } + + .booking-list-interval-actions { + img { + cursor: pointer; + } + } + + label.assigned-user-label div.assigned-user-search-wrapper { + display: flex; + flex-direction: row; + + .delete-assigned-user-icon { + margin-top: 0.5em; + margin-left: 0.5em; + } + } +} + +@media screen and (min-width: 1024px) { + /* individual booking plan print view */ + .sidebar .colour-selectors { + display: flex; + flex-direction: column; + margin: 1em; + text-align: center; + + .colour-selector { + width: calc(100% - 1em); + height: 4em; + margin: 0.5em; + + input[type="color"] { + display: none; + } + } + + .print-action { + margin-top: 2em; + width: 100%; + height: 6em; + } + } +} + +.dragged-colour { + width: 10%; + height: 10%; +} + +@media screen { + section.room-schedule { + margin-bottom: 2em; + } +} + +@media print { + section.room-schedule { + width: 100%; + height: 95%; + page-break-after: always; + } +} + + +/* Rules for the map keys on a booking plan page: */ +.map-key-list { + list-style-type: none; + padding-left: 1em; + padding-top: 1em; + + .map-key { + white-space: nowrap; + display: inline; + margin-right: 2em; + vertical-align: middle; + + span { + width: 2em; + display: inline-block; + height: 1em; + + } + } +} + +article.room-list-item { + header > nav.action-menu > a.action-menu-icon { + border-right: none; + margin-right: 0; + } + + section > ul.property-list { + list-style: none; + padding-left: 0; + flex-grow: 1; + } +} + +.fc-time, .fc-widget-header { + background-color: $content-color-20; +} + +.request-list { + a.request-marking-icon { + background-repeat: no-repeat; + display: block; + width: 16px; + height: 16px; + @include background-icon(radiobutton-unchecked); + + &[data-marked="1"] { + @include background-icon(radiobutton-checked, status-red); + } + + &[data-marked="2"] { + @include background-icon(radiobutton-checked, status-yellow); + } + + &[data-marked="3"] { + @include background-icon(radiobutton-checked, status-green); + } + } +} + +#booking-plan-jmpdate-button { + width: 100px; + height: 31.5px; + margin: 0.5em 0.2em; + padding: 0.4em; +} + +#booking-plan-jmpdate { + width: 100px; + height: 19px; + margin: 0.5em 0.2em; + padding: 0.4em; +} + +form#resolve-request, form#decline-request { + dl { + dt, dd { + &:not(:last-child) { + margin-bottom: 5px; + } + } + dt { + grid-column: 1; + } + + dd { + grid-column: 2; + } + + margin: 0; + display: grid; + grid-template-columns: 40% auto; + + } +} + +@media all and (min-width: 1600px) { + form#resolve-request { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + article.assign-dates { + div { + overflow-x: auto; + max-height: 250px; + } + } + article.assign-dates, div[data-dialog-button] { + header { + margin: 0; + } + + table { + > tbody:last-of-type { + > tr:last-child { + > td { + border-bottom: none; + } + } + } + &.default { + > thead { + > tr { + > th { + &:first-child { + z-index: 2; + background-color: $content-color-20; + min-width: 180px; + left: 0; + } + position: sticky; + top: 0; + z-index: 1; + border-top: none; + border-bottom: none !important; + box-shadow: inset 0 1px 0 $brand-color-darker; + } + } + } + > tbody { + > tr { + > td { + &:first-child { + position: sticky; + left: 0; + z-index: 1; + background: $white; + } + } + } + } + } + } + + margin: 0; + height: 100%; + overflow-y: auto; + width: 100%; + padding: 0; + } + + article { + &.left-part, &.right-part { + + flex-grow: 1; + margin-bottom: 10px; + } + + &.left-part { + width: 50%; + } + &.right-part { + width: 40%; + padding-left: 1em; + } + + section { + padding-top: 0; + } + } + } +} + + +@media all and (max-width: 1599px) { + form#resolve-request article.right-part { + padding-bottom: 10px; + } +}
\ No newline at end of file diff --git a/resources/assets/stylesheets/scss/sidebar.scss b/resources/assets/stylesheets/scss/sidebar.scss new file mode 100644 index 0000000..d6e5897 --- /dev/null +++ b/resources/assets/stylesheets/scss/sidebar.scss @@ -0,0 +1,349 @@ +$sidebar-width: 270px; + +#layout-sidebar { + background: $white; + flex: 0 1 auto; + position: relative; + left: 0px; + margin-right: 5px; + margin-left: 15px; + text-align: left; + min-width: 270px; +} +.sidebar { + padding-bottom: 7px; + width: $sidebar-width; + z-index: 2; + + border-left: 0; + display: inline-block; + flex: 0 0 auto; + margin-bottom: 1em; + position: relative; + + .sidebar-image { + width: $sidebar-width; + height: 60px; + max-height: 60px; + + background-image: url("#{$image-path}/sidebar/noicon-sidebar.png"); + background-size: cover; + + position: relative; + &-with-context { + margin-bottom: 0px; + } + + display: flex; + align-items: flex-end; + } + + .sidebar-context { + flex: 0; + + background-color: rgba(255, 255, 255, 1); + border: 0px solid $base-color-20; + padding: 0px; + max-height: 60px; + } + + .sidebar-title { + flex: 1; + + box-sizing: border-box; + max-height: 60px; + padding: 12px 15px 0; + border-bottom: 12px solid transparent; + + color: $white; + font-size: 1.2em; + overflow: hidden; + word-break: break-word; + position: relative; + line-height: 1.1; + text-align: left; + text-overflow: ellipsis; + /* it may happen that some browser does not support the following, then (...) won't appear */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + + .course-avatar-medium, + .stream-avatar-medium, + .avatar-medium { + max-width: 60px; + height: 60px; + } + .sidebar-widget, + .sidebar-widget-placeholder { + background: $white; + border: 1px solid $content-color-40; + margin: 15px 0px 0; + } + .sidebar-widget-header { + @include clearfix(); + background: $content-color-20; + color: $base-color; + font-weight: bold; + padding: 4px; + } + .sidebar-widget-options { + float: right; + opacity: 0; + transition: all 0.5s; + } + .sidebar-widget:hover .sidebar-widget-options { + opacity: 1; + } + + // Links inside the sidebar + a.link-intern { + @include icon(before, link-intern, clickable, 16px, 2px); + } + a.link-extern { + @include icon(before, link-extern, clickable, 16px, 2px); + } + + // Prevent selects from growing too large + select { + max-width: 100%; + } +} + +ul.widget-list { + list-style: none; + margin: 0; + padding: 0; + > li { + background-repeat: no-repeat; + background-position: 0 1px; + background-size: 16px 16px; + padding-left: 20px; + word-wrap: break-word; + } +} +div#sidebar-navigation { + div.sidebar-widget-header { + display: none; + } + div.sidebar-widget-content { + border-top: 0px; + } +} +.widget-links { + margin: 5px; + > li img { + vertical-align: text-top; + } + a { + display: block; + } + .widget-content a:only-child { + box-sizing: border-box; + line-height: 16px; + } + span[disabled] { + color: $dark-gray-color-80; + cursor: not-allowed; + font-weight: lighter; + } + &.sidebar-navigation > li.active { + background-color: $base-color; + margin-left: -4px; + //#arrow > .right-border(14px, $content-color-20, 1px, $content-color-40, -5px); + @include arrow-right-border(14px, $base-color, 1px, $base-color, -5px); + a { + color: $white; + padding-left: 4px; + } + } + &.sidebar-views > li.active { + background: $activity-color-40; + margin-left: -5px; + box-shadow: inset 0 0 0 1px $activity-color; + @include arrow-right-border(14px, $activity-color-40, 1px, $activity-color, -5px); + a { + color: $base-color; + padding-left: 4px; + } + } + &.sidebar-navigation > li, + &.sidebar-views > li { + padding-left: 5px; + + &.active { + + line-height: 2em; + &:before, &:after { + margin-left: -1px; + } + display: block; + + // Obtuse angle looks kinda ugly with borders + &:before { + border-left-width: floor((14px * 2 / 3)); + } + &:after { + border-left-width: floor((14px * 2 / 3 - 1)); + } + } + } + + .link-form { + display: inline-block; + + button { + background: transparent; + border: 0; + color: $base-color; + margin: 0; + padding: 0; + + &:hover { + color: $active-color; + cursor: pointer; + } + } + } +} + +.sidebar-widget-cloud { + margin: 0px; + padding: 0px; + max-width: 100%; + overflow: hidden; + > li { display: inline-block; } + a.weigh-1 { font-size: 0.7em; } + a.weigh-2 { font-size: 0.8em; } + a.weigh-3 { font-size: 0.9em; } + a.weigh-4 { font-size: 1.0em; } + a.weigh-5 { font-size: 1.1em; } + a.weigh-6 { font-size: 1.3em; } + a.weigh-7 { font-size: 1.5em; } + a.weigh-8 { font-size: 1.7em; } + a.weigh-9 { font-size: 1.9em; } + a.weigh-10 { font-size: 2.1em; } +} + +.sidebar-widget { + background: $white; + @include clearfix(); + + .widget-options { + list-style: none; + margin: 0; + padding: 0; + + > li { + line-height: 1.5em; + margin-left: 0; + padding-left: 0; + } + + .options-checkbox { + background-repeat: no-repeat; + background-position: left 2px; + display: block; + padding-left: 20px; + + &.options-checked { + @include background-icon(checkbox-checked); + } + &.options-unchecked { + @include background-icon(checkbox-unchecked); + } + } + + .options-radio { + background-repeat: no-repeat; + background-position: left 2px; + display: block; + padding-left: 20px; + + &.options-checked { + @include background-icon(radiobutton-checked); + } + &.options-unchecked { + @include background-icon(radiobutton-unchecked); + } + } + } +} +.sidebar-widget-content { + border-top: 1px solid $content-color-40; + overflow-wrap: break-word; + padding: 4px; + transition: all 0.5s; +} + +.sidebar-widget-header { + @include clearfix(); + .sidebar-widget-extra { + float: right; + } +} + +// TODO: These two should be combined into one widget +select.sidebar-selectlist { + overflow-y: auto; + width: 100%; +} +.selector-widget select { + cursor: pointer; + padding: 0; + + option { + padding: 0 0.5em; + } +} + +.sidebar-search { + .needles input[type=text] { + box-sizing: border-box; + border: 1px solid $base-color-60; + border-right-width: 30px; + float: left; + height: 22px; + width: 100%; + } + input[type=submit] { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; + + // Activate when twitter-mixins is included as scss and remove the above rules + // @include hide-text(); + + width: 29px; + height: 20px; + @include background-icon(search, info_alt); + float: left; + background-position: center 3px; + background-repeat: no-repeat; + vertical-align: top; + margin-left: -30px; + } + ul.needles { + list-style: none; + margin: 0; + padding: 0; + li { + @include clearfix(); + margin-bottom: 0.5em; + &:last-child { + margin-bottom: 0; + } + } + } + ul.filters { + list-style: none; + margin: 0; + padding: 0; + li { + display: inline-block; + } + } +} diff --git a/resources/assets/stylesheets/scss/table_of_contents.scss b/resources/assets/stylesheets/scss/table_of_contents.scss new file mode 100644 index 0000000..6b78c37 --- /dev/null +++ b/resources/assets/stylesheets/scss/table_of_contents.scss @@ -0,0 +1,261 @@ +$base-gray-color-5: mix($base-gray, #fff, 5%); + +ul.numberedchapters { + counter-reset: section; + list-style-type: none; + + a:before { + counter-increment: section; + content: counters(section,".") " "; + } +} + +label { + cursor: pointer; + margin-top: 7px; +} + +label[for=cb-toc], label[for=cb-fullscreen] { +} + + +#cb-toc, #cb-toc-close { + visibility: hidden; + display: none; +} + +#cb-toc:checked + .check-box + #cb-toc-close + article.toc_overview, button#toc-button:hover article.toc_overview { + visibility: visible; + width: 540px; + overflow: hidden; +} + +#cb-toc-close:checked article.toc_overview { + visibility: hidden; + width: 0; +} + +.toc_overview { + visibility: hidden; + width: 0%; + z-index: 100; + position: absolute; + right: 2px; + top: 176px; + background-color: $white; + min-height: 10%; + max-height: 100%; + border: 1px solid #d0d7e3; + margin-bottom: 10px; + -webkit-box-shadow: 2px 2px #ccc; + box-shadow: 2px 2px #ccc; + + + > section { + max-width: 100%; + overflow-y: scroll; + height: 580px; + margin-top: 7px; + } +} + +#toc { + margin: 10px; + text-align: left; +} + +#toc_header { + height: 58px; + overflow: hidden; + background-color: $white; + color: $black !important; + margin-bottom: -0.5em; + border-bottom: thin solid #d0d7e3; + display: flex; + justify-content: space-between; + align-items: center; + + label { + margin-right: 15px; + } +} + +#toc_h1 { + color: $black; + font-weight: 500; + margin-left: 10px; + margin-bottom: unset; +} +.toc_transform { + transition: all 0.8s ease!important; +} + +#main_content { + opacity: 1; + +} + +/* Table of contents */ +#toc_nav { + height: 40px; + position: fixed; + background-color: $brand-color-darker; +} + +#toc_icon { + float: right; +} + +section > .toc { + > li { + margin-bottom: 1.4em; + font-size: 1.2em; + } +} + +.toc { + list-style: none; + padding: 0; + margin-left: 2%; + + #chapter0 { + margin-top: 5px; + margin-bottom: 5px; + } + + > li { + font-size: 16px; + padding-top: 20px; + + img, svg { + vertical-align: bottom; + } + } + + li#chap1 { + margin-bottom: 1.8em; + font-size: 1em; + + > div { + border-bottom: 1px solid #E9E9E9; + margin-bottom: 5px; + } + } + + > li a { + display: inline-flex; + width: 100%; + } + + li div { + padding-left: 5px; + padding-right: 5px; + } + + li div:hover { + background-color: $light-gray-color-20; + color: $base-color; + + } + + li.active > div { + color: $black; + } + + li.active > div:hover { + color: $white; + } + + li.active > div a { + color: $black; + } + + .toc > li { + font-size: 14px; + padding-top: 3px; + } + + .toc .toc { + list-style: disc; + margin-left: 4%; + } + + .toc >li .selected { + font-weight: 700; + background-color: $light-gray-color-20; + } + +} + +#wikifooter { + background-color: $content-color-20; + border-top: 1px solid $brand-color-darker; + clear: both; + margin-left: 0; + padding: 0; + height: 58px; +} + +#toc_bc_nav { + position: absolute; + right: 20px; +} + +@media (max-width: 767px) { + + #main_content header { + width:375px; + } + + #toc { + max-width: 94%!important; + } + + ul.breadcrumb { + list-style: none; + font-size: 18px; + padding-left: 10px; + width: 70%; + } + + #toc_header { + width: 90%; + } + + .consuming_mode .toc_overview { + top: 51px!important; + } +} + +.wiki { + border: unset!important; +} + +.action-menu { + +} + +#bc_username, #bc_version { + display: inline-block; +} + +.consuming_mode .toc_overview { + top: 6px; +} + + +#toc-button { + background-image: url(../images/icons/blue/table-of-contents.svg); + background-size: 24px; + + height: 24px; + width: 24px; + margin: 0 .5em; + border: none; + background-color: transparent; + background-repeat: no-repeat; + background-position: 50%; + background-size: 24px; + cursor: pointer; + outline: none; +} diff --git a/resources/assets/stylesheets/scss/tables.scss b/resources/assets/stylesheets/scss/tables.scss new file mode 100644 index 0000000..85d1d98 --- /dev/null +++ b/resources/assets/stylesheets/scss/tables.scss @@ -0,0 +1,7 @@ +table.default { + colgroup { + col.checkbox { + width: 30px; + } + } +} diff --git a/resources/assets/stylesheets/scss/tooltip.scss b/resources/assets/stylesheets/scss/tooltip.scss new file mode 100644 index 0000000..a2f07e3 --- /dev/null +++ b/resources/assets/stylesheets/scss/tooltip.scss @@ -0,0 +1,48 @@ +/* Tooltips for Stud.IP with CSS3 only -------------------------------------- */ +%tooltip { + @include arrow-bottom-border(9px, $dark-gray-color-5, 1px, $dark-gray-color-30, 5px); + + background-color: $dark-gray-color-5; + border: 1px solid $dark-gray-color-30; + box-shadow: 0 1px 0 fade-out(#fff, 0.5) inset; + font-size: $font-size-base; + margin-bottom: 8px; + max-width: 230px; + padding: 10px; + position: absolute; + text-align: left; + text-shadow: 0 1px 0 fade-out(#fff, 0.5); + white-space: normal; + z-index: 10000; + + word-wrap: break-word; + hyphens: auto; +} + +.studip-tooltip { + @extend %tooltip; +} + +.tooltip { + display: inline-block; + position: relative; + + &.tooltip-icon { + @include icon(before, info-circle, inactive); + } + &.tooltip-important { + @include icon(before, info-circle, attention); + } + + .tooltip-content { + @extend %tooltip; + display: none; + } + &:hover .tooltip-content { + bottom: 100%; + display: inline-block; + left: 50%; + margin-left: -129px; + width: 230px; + } +} diff --git a/resources/assets/stylesheets/scss/variables.scss b/resources/assets/stylesheets/scss/variables.scss new file mode 100644 index 0000000..a91078b --- /dev/null +++ b/resources/assets/stylesheets/scss/variables.scss @@ -0,0 +1,141 @@ +@import '../mixins/colors.scss'; + +$text-color: #000; + +$font-family-base: "Lato", sans-serif; + +$font-size-base: 14px; +$font-size-large: ceil($font-size-base * 1.25); // ~18px +$font-size-small: ceil($font-size-base * 0.85); // ~12px + +$font-size-h1: floor($font-size-base * 1.4); // ~36px +$font-size-h2: floor($font-size-base * 1.2); // ~30px +$font-size-h3: ceil($font-size-base * 1.1); // ~24px +$font-size-h4: $font-size-base; // ~18px +$font-size-h5: $font-size-base; +$font-size-h6: ceil($font-size-base * 0.85); // ~12px + +//** Unit-less `line-height` for use in components like buttons. +$line-height-base: 1.428571429; // 20/14 +//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc. +$line-height-computed: floor($font-size-base * $line-height-base); // ~20px + +//** By default, this inherits from the `<body>`. +$headings-font-family: inherit; +$headings-font-weight: 700; +$headings-line-height: 1.1; +$headings-color: #444444; + +// Design specific +$bar-bottom-container-height: 40px; +$header-height: 80px; + +:root { + // The special handling for -- as #{"--"} seems to be neccessary to make + // scss evaluate the color variables + #{"--"}active-color: $active-color; + #{"--"}active-color: $active-color; + #{"--"}activity-color: $activity-color; + #{"--"}activity-color-20: $activity-color-20; + #{"--"}activity-color-40: $activity-color-40; + #{"--"}activity-color-60: $activity-color-60; + #{"--"}activity-color-80: $activity-color-80; + #{"--"}base-color: $base-color; + #{"--"}base-color-20: $base-color-20; + #{"--"}base-color-40: $base-color-40; + #{"--"}base-color-60: $base-color-60; + #{"--"}base-color-80: $base-color-80; + #{"--"}base-gray: $base-gray; + #{"--"}black: $black; + #{"--"}brand-color-dark: $brand-color-dark; + #{"--"}brand-color-darker: $brand-color-darker; + #{"--"}brand-color-light: $brand-color-light; + #{"--"}brand-color-lighter: $brand-color-lighter; + #{"--"}brown: $brown; + #{"--"}brown-20: $brown-20; + #{"--"}brown-40: $brown-40; + #{"--"}brown-60: $brown-60; + #{"--"}brown-80: $brown-80; + #{"--"}content-color: $content-color; + #{"--"}content-color-10: $content-color-10; + #{"--"}content-color-20: $content-color-20; + #{"--"}content-color-40: $content-color-40; + #{"--"}content-color-60: $content-color-60; + #{"--"}content-color-80: $content-color-80; + #{"--"}contrast-content-gray: $contrast-content-gray; + #{"--"}contrast-content-hovergray: $contrast-content-hovergray; + #{"--"}contrast-content-white: $contrast-content-white; + #{"--"}dark-gray-color: $dark-gray-color; + #{"--"}dark-gray-color-10: $dark-gray-color-10; + #{"--"}dark-gray-color-15: $dark-gray-color-15; + #{"--"}dark-gray-color-20: $dark-gray-color-20; + #{"--"}dark-gray-color-30: $dark-gray-color-30; + #{"--"}dark-gray-color-40: $dark-gray-color-40; + #{"--"}dark-gray-color-45: $dark-gray-color-45; + #{"--"}dark-gray-color-5: $dark-gray-color-5; + #{"--"}dark-gray-color-60: $dark-gray-color-60; + #{"--"}dark-gray-color-75: $dark-gray-color-75; + #{"--"}dark-gray-color-80: $dark-gray-color-80; + #{"--"}dark-green: $dark-green; + #{"--"}dark-green-20: $dark-green-20; + #{"--"}dark-green-40: $dark-green-40; + #{"--"}dark-green-60: $dark-green-60; + #{"--"}dark-green-80: $dark-green-80; + #{"--"}dark-violet: $dark-violet; + #{"--"}dark-violet-20: $dark-violet-20; + #{"--"}dark-violet-40: $dark-violet-40; + #{"--"}dark-violet-60: $dark-violet-60; + #{"--"}dark-violet-80: $dark-violet-80; + #{"--"}fieldset-border: $fieldset-border; + #{"--"}fieldset-header: $fieldset-header; + #{"--"}green: $green; + #{"--"}green-20: $green-20; + #{"--"}green-40: $green-40; + #{"--"}green-60: $green-60; + #{"--"}green-80: $green-80; + #{"--"}light-gray-color: $light-gray-color; + #{"--"}light-gray-color-20: $light-gray-color-20; + #{"--"}light-gray-color-40: $light-gray-color-40; + #{"--"}light-gray-color-60: $light-gray-color-60; + #{"--"}light-gray-color-80: $light-gray-color-80; + #{"--"}orange: $orange; + #{"--"}orange-20: $orange-20; + #{"--"}orange-40: $orange-40; + #{"--"}orange-60: $orange-60; + #{"--"}orange-80: $orange-80; + #{"--"}origin-base-color: $origin-base-color; + #{"--"}petrol: $petrol; + #{"--"}petrol-20: $petrol-20; + #{"--"}petrol-40: $petrol-40; + #{"--"}petrol-60: $petrol-60; + #{"--"}petrol-80: $petrol-80; + #{"--"}public-course-bgcolor: $public-course-bgcolor; + #{"--"}red: $red; + #{"--"}red-20: $red-20; + #{"--"}red-40: $red-40; + #{"--"}red-60: $red-60; + #{"--"}red-80: $red-80; + #{"--"}table-footer-color: $table-footer-color; + #{"--"}table-header-color: $table-header-color; + #{"--"}violet: $violet; + #{"--"}violet-20: $violet-20; + #{"--"}violet-40: $violet-40; + #{"--"}violet-60: $violet-60; + #{"--"}violet-80: $violet-80; + #{"--"}white: $white; + #{"--"}yellow: $yellow; + #{"--"}yellow-20: $yellow-20; + #{"--"}yellow-40: $yellow-40; + #{"--"}yellow-60: $yellow-60; + #{"--"}yellow-80: $yellow-80; + + #{"--"}group-color-0: $dark-violet; + #{"--"}group-color-1: $violet; + #{"--"}group-color-2: $red; + #{"--"}group-color-3: $orange; + #{"--"}group-color-4: $yellow; + #{"--"}group-color-5: $green; + #{"--"}group-color-6: $dark-green; + #{"--"}group-color-7: $petrol; + #{"--"}group-color-8: $brown; +} diff --git a/resources/assets/stylesheets/scss/visibility.scss b/resources/assets/stylesheets/scss/visibility.scss new file mode 100644 index 0000000..130d783 --- /dev/null +++ b/resources/assets/stylesheets/scss/visibility.scss @@ -0,0 +1,82 @@ +@mixin media-breakpoint-large-down() { + @content; +} +@mixin media-breakpoint-medium-down() { + @media (max-width: ($major-breakpoint-large - 1px)) { + @content; + } +} +@mixin media-breakpoint-small-down() { + @media (max-width: ($major-breakpoint-medium - 1px)) { + @content; + } +} +@mixin media-breakpoint-tiny-down() { + @media (max-width: ($major-breakpoint-small - 1px)) { + @content; + } +} + +@mixin media-breakpoint-large-up() { + @media (min-width: ($major-breakpoint-large)) { + @content; + } +} +@mixin media-breakpoint-medium-up() { + @media (min-width: ($major-breakpoint-medium)) { + @content; + } +} +@mixin media-breakpoint-small-up() { + @media (min-width: ($major-breakpoint-small)) { + @content; + } +} +@mixin media-breakpoint-tiny-up() { + @content; +} + + +.hidden-large-down { + @include media-breakpoint-large-down() { + display: none !important; + } +} +.hidden-large-up { + @include media-breakpoint-large-up() { + display: none !important; + } +} + +.hidden-medium-down { + @include media-breakpoint-medium-down() { + display: none !important; + } +} +.hidden-medium-up { + @include media-breakpoint-medium-up() { + display: none !important; + } +} + +.hidden-small-down { + @include media-breakpoint-small-down() { + display: none !important; + } +} +.hidden-small-up { + @include media-breakpoint-small-up() { + display: none !important; + } +} + +.hidden-tiny-down { + @include media-breakpoint-tiny-down() { + display: none !important; + } +} +.hidden-tiny-up { + @include media-breakpoint-tiny-up() { + display: none !important; + } +} diff --git a/resources/assets/stylesheets/scss/wiki.scss b/resources/assets/stylesheets/scss/wiki.scss new file mode 100644 index 0000000..d4faea4 --- /dev/null +++ b/resources/assets/stylesheets/scss/wiki.scss @@ -0,0 +1,146 @@ +div.wikitoc { + font-size: 1em; + margin-bottom: 5px; + + h1, h2, h3, h4 { + color: black; + font-size: 1em; + margin: 0 0 0 -10px; + } + + ul { + list-style: none; + margin-bottom: 0; + margin-top: 0; + padding-left: 0; + + ul { padding-left: 10px; } + ul ul { padding-left: 20px; } + ul ul ul { padding-left: 30px; } + } +} + +div.wikitoc_editlink { + font-size: 1em; + margin-bottom: -10px; + padding-top: 5px; +} +span.wikitoc_editlink { + font-size: 75%; +} + +span.wikitoc_toggler { + font-size: 0.8em; +} +textarea.wiki-editor { + display: block; + height: 250px; + width: 98%; +} + +body#wiki #main_content { + a:not([href]) { + scroll-margin-top: ceil($bar-bottom-container-height + 10); + } + td.printcontent:last-child:not(:first-child) { + padding-right: 22px; + } +} + +.no-js #wiki button[name="submit-and-edit"] { + display: none; +} + +a.wiki-restricted { + $icon-size: 12px; + @include background-icon(lock-locked, info, $icon-size); + background-position: left center; + background-repeat: no-repeat; + + padding-left: $icon-size; +} + +.wiki-background { + @include background-icon(wiki, navigation, 260); + background-repeat: no-repeat; + background-position: center; + background-color: hsla(0,0%,100%,0.70); + background-blend-mode: overlay; +} + +.flex { + display: flex; + justify-content: center; +} + +.image1 { + height: 140px; + width: 160px; + margin-top: 90px; + margin-left: 10px; +} + +.image2 { + height: 180px; + width: 200px; + margin-top: 120px; + margin-left: 10px; +} + +.wiki-teaser { + font-size: 24px; +} + +.wiki-info-aside { + float: left; + width: 35%; + margin-right: 5%; +} + +.wiki-backlinks { + max-width: 60%; +} + +.wiki-index { + padding-left: 12px; + overflow: auto; +} + +$authors: ( + 0: $dark-gray-color-20, + 1: $red-20, + 2: $green-20, + 3: $brown-20, + 4: $dark-violet-20, + 5: $orange-20, + 6: $dark-green-20, + 7: $violet-20, + 8: $yellow-20, + 9: $petrol-20, + 10: $dark-gray-color-40, + 11: $red-40, + 12: $green-40, + 13: $brown-40, + 14: $dark-violet-40, + 15: $orange-40, + 16: $dark-green-40, + 17: $violet-40, + 18: $yellow-40, + 19: $petrol-40, + 20: $dark-gray-color-60, + 21: $red-60, + 22: $green-60, + 23: $brown-60, + 24: $dark-violet-60, + 25: $orange-60, + 26: $dark-green-60, + 27: $violet-60, + 28: $yellow-60, + 29: $petrol-60 +); + +@each $index, $bgcolor in $authors { + .wiki-author#{$index} { + background-color: $bgcolor; + } +} diff --git a/resources/assets/stylesheets/statusgroups.less b/resources/assets/stylesheets/statusgroups.less new file mode 100644 index 0000000..5ae6c78 --- /dev/null +++ b/resources/assets/stylesheets/statusgroups.less @@ -0,0 +1,15 @@ +@import "mixins.less"; +@import (less) "vendor/jquery-nestable.css"; + +.tree-seperator { + list-style-type: none; +} +table.movable { + margin-bottom: 2em; +} +.ordering { + display: none; +} +.js .ordering { + display: block; +} diff --git a/resources/assets/stylesheets/studip-jquery-ui.less b/resources/assets/stylesheets/studip-jquery-ui.less new file mode 100644 index 0000000..c9fb6de --- /dev/null +++ b/resources/assets/stylesheets/studip-jquery-ui.less @@ -0,0 +1,178 @@ +@z-index: 1001; + +@import "mixins.less"; + +@import (less) "jquery-ui.structure.css"; +@import "less/jquery-ui/custom.less"; +@import "less/jquery-ui/studip.less"; +@import "~jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.css"; +@import "~multiselect/css/multi-select.css"; + +// Tweaks/adjustments for multi-select +.ms-container { + @avatar-size: 32px; + @avatar-border: 2px; + @element-padding: 2px; + @icon-size: 16px; + + background: none; + width: 100%; + + .ms-selectable, + .ms-selection { + color: @dark-gray-color; + width: 47%; + + li.ms-elem-selectable, + li.ms-elem-selection { + background: #fff; + border-bottom-color: @content-color-20; + color: @dark-gray-color; + padding: @element-padding; + } + li { + display: flex; + align-items: center; + span { + flex: 10 0 auto; + } + + &.ms-hover, &:hover { + background: @brand-color-dark; + color: #fff; + } + &.disabled { + background-color: @content-color-20; + color: @dark-gray-color; + cursor: not-allowed; + } + &[style*="background-image"] { + min-height: (2 * @element-padding + @avatar-size + 2 * @avatar-border); + + background-repeat: no-repeat; + background-size: @avatar-size; + background-position: (@element-padding + @avatar-border) center; + padding-left: (@element-padding + @avatar-size + 2 * @avatar-border); + + &.ms-elem-selection { + background-position: (@element-padding + @avatar-border + @icon-size) center; + } + } + } + } + + .ms-selectable li:not(.disabled) { + .icon('after', "arr_1right", 'info_alt', @icon-size); + &::after { + flex: 0 1 auto; + visibility: hidden; + } + &:hover { + background-color: @brand-color-dark; + &::after { + visibility: visible; + } + } + } + + .ms-selection li { + &[style*="background-image"] { + padding-left: (@element-padding + @icon-size + @element-padding + @avatar-size + 2 * @avatar-border) + } + + position: relative; + + .icon('before', 'arr_1left', 'info_alt', @icon-size); + &::before { + flex: 0 1 auto; + position: absolute; + left: @element-padding; + top: 50%; + transform: translate(0, -50%); + visibility: hidden; + } + &:hover { + background-color: @brand-color-dark; + &::before { + visibility: visible; + } + } + } + + .ms-list { + border-radius: 0; + border-color: @light-gray-color-40; + position: relative; + } + + .ms-optgroup-label { + color: @dark-gray-color-60; + } + + // Default multi select with STUDIP.MultiSelect.create() + &.studip-multi-select { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + + form.default & { + max-width: 48em; + } + + .ms-selectable, + .ms-selection { + flex: 1; + width: auto; + } + + .header { + display: flex; + flex-direction: row; + flex-wrap:nowrap; + align-items: center; + justify-content: space-between; + + background: @dark-gray-color-10; + border: 1px solid @dark-gray-color-30; + border-bottom: 0; + + padding-left: 0.5em; + } + + .button { + font-size: smaller; + } + + .ms-focus { + border-color: @brand-color-dark; + box-shadow: none; + } + + .ms-selectable { + order: 2; + + .header { + justify-content: flex-end; + } + + li::after { + display: none; + } + li { + .icon('before', 'arr_2left', 'info_alt', @icon-size); + } + } + + .ms-selection { + order: 1; + li { + padding-left: 20px; + .icon('before', 'arr_2right', 'info_alt', @icon-size); + } + } + } +} + +.ui-menu .ui-menu-item { + list-style: none; +} diff --git a/resources/assets/stylesheets/studip.less b/resources/assets/stylesheets/studip.less new file mode 100644 index 0000000..b3515ac --- /dev/null +++ b/resources/assets/stylesheets/studip.less @@ -0,0 +1,686 @@ +/******************************************************************************* + Standard-Stylesheet für Stud.IP im Safire-Design + - use http://www.colorzilla.com/gradient-editor/ for gradients +*******************************************************************************/ +@import "mixins.less"; + +@import "less/font-face-lato.less"; +@import "less/variables.less"; +@import "less/breakpoints.less"; +@import "less/typography.less"; +@import "less/visibility.less"; +@import "less/responsive.less"; + +@import "less/links.less"; +@import "less/tables.less"; +@import "less/forms.less"; +@import "less/content.less"; +@import "less/css_tree.less"; + +@import "less/layouts.less"; +@import "less/header.less"; +@import "less/personal-notifications.less"; +@import "less/navigation.less"; + +@import "less/clipboard.less"; +@import "less/helpbar.less"; +@import "less/content_box.less"; +@import "less/badges.less"; +@import "less/studip-selection.less"; +@import "less/article.less"; +@import "less/comments.less"; + +@import "less/ajax.less"; +@import "less/autocomplete.less"; +@import "less/avatar.less"; +@import "less/buttons.less"; +@import "less/messagebox.less"; +@import "less/messages.less"; +@import "less/quicksearch.less"; +@import "less/search.less"; +@import "less/skiplinks.less"; +@import "less/tabs.less"; +@import "less/questionnaire.less"; +@import "less/qrcode.less"; +@import "less/copyable-links.less"; + +@import "less/admin.less"; +@import "less/calendar.less"; +@import "less/contacts.less"; +@import "less/cronjobs.less"; +@import "less/dashboard.less"; +@import "less/documents.less"; +@import "less/files.less"; +@import "less/evaluation.less"; +@import "less/index.less"; +@import "less/news.less"; +@import "less/schedule.less"; +@import "less/study-area-selection.less"; +@import "less/tour.less"; +@import "less/ilias-interface.less"; +@import "less/studygroup.less"; +@import "less/raumzeit.less"; +@import "less/opengraph.less"; +@import "less/statusgroups.less"; +@import "less/start.less"; +@import "less/profile.less"; +@import "less/consultation.less"; + +@import "less/mobile.less"; +@import "less/pagination.less"; +@import "less/enrolment.less"; +@import "less/dialog.less"; +@import "less/studip-overlay.less"; +@import "less/lists.less"; +@import "less/selects.less"; +@import "less/plus.less"; +@import "less/coursewizard.less"; +@import "less/smileys.less"; +@import "less/big-image-handler.less"; +@import "less/i18n.less"; +@import "less/tfa.less"; +@import "less/scroll-to-top.less"; + +@import "less/globalsearch.less"; +@import "less/gradebook.less"; + +@import "less/navigation-hoverborder.less"; + +@import "less/deprecated.less"; + +@import "less/mvv.less"; +@import "less/overlapping.less"; +@import "less/fullcalendar.less"; + +@import "less/feedback.less"; + +// Class for DOM elements that should only be visible to Screen readers +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} + +/* --- Standardvorgaben ----------------------------------------------------- */ +//TODO: the Body-Background color should be similar to A:link.toolbar and A:visited.toolbar for best effect!! +html, body { + height: 100%; +} +body { + background-color: #d8dadc; + background-repeat: repeat-x; + margin: 0; +} + +ul + br, table + br { + display: none; +} + +blockquote { + border-left: 3px solid @brand-color-lighter; + padding: 5px; + margin: 8px; + padding-left: 8px; + margin-left: 0px; + background-color: rgba(0,0,0,0.05); + > .author { + background-color: @brand-color-lighter; + padding: 4px; + margin-left: -10px; + padding-left: 15px; + margin-right: -5px; + color: white; + margin-top: -5px; + margin-bottom: 5px; + } +} + +dd { margin-left: 1.5em; } + +input.placeholder { opacity: 0.7; } +::placeholder { + color: rgba(0, 0, 0, 0.7); +} + +table.content { + border-collapse: collapse; + + td { + border: thin solid #666; + padding: 3px; + } +} + +div.indent { margin-left: 2em; } + +h1.topic, h2.topic, h3.topic { + font-weight: bold; + line-height: 1em; + margin-bottom: 0.1em; + margin-top: 0.1em; + padding: 0.1em; +} +h1.topic { font-size: 1.6em; } +h2.topic, h3.topic { font-size: 1.2em; } + +ul.clean, ol.clean { + list-style-type: none; + padding: 0px; + margin: 0px; + > li { + margin-top: 2px; + margin-bottom: 2px; + padding: 0px; + } +} + +.hidden { + display: none; +} + +/* for defining flex rows quickly: */ +.flex-row { + display: flex; + flex-direction: row; +} + +/* --- media preview -------------------------------------------------------- */ +.preview { + img, audio, video { + max-height: 500px; + max-width: 750px; + } +} + +.mainmenu { + margin-top: 7px; + text-align: left; + font-size: 16px; + padding: 5px; +} + + + +.minor { + color: gray; + font-size: 0.75em; +} +.quiet { color: gray; } + +.middle { vertical-align: middle; } +.text-bottom { vertical-align: text-bottom; } +.text-top { vertical-align: text-top !important; } +.center { text-align: center; } +.nodisplay { display: none; } + +.bordered { + border: 1px solid @content-color-40; + padding: 10px; +} +.bordered + .bordered { + border-top: none; +} + +/* --- index.php anpassungen an den boxen zur vereinheitlichung ------------- */ +table.index_box { + border-collapse: collapse; + margin-bottom: 1em; + width: 100%; +} + +td.index_box_cell { + background-color: @content-color-20; + padding: 4px; +} + +/* overdiv */ +div.overdiv { + background-color: @content-color-20; + margin: 0; + padding: 0; + position: absolute; + width: 600px; + z-index: 2; + + .title { margin: 0; } + a.title { + padding: 2px; + float: right; + } + div.title { + background: @brand-color-lighter; + height: 1.4em; + padding: 0; + } + h4.title { + color: #fff; + float: left; + font-size: 1em; + overflow: hidden; + padding: 2px; + width: 90%; + } + + div.content { + background-color: @content-color-20; + clear: both; + margin: 0; + overflow: hidden; + padding: 2px; + } +} + +/* --- Editor Toolbar ------------------------------------------------------- */ +.add_toolbar { + box-sizing: border-box; +} +.editor_toolbar { + display: inline-block; + + .buttons { + font-size: 0.75em; + + .clearfix(); + margin: 0 !important; // Locked since .buttons is pretty generic + padding: 0 !important; // and other styles could easily interfere + + border-spacing: 0; // Chrome needs this + + .left { float: left; } + .right { float: right; } + + .ui-button { + background: @dark-gray-color-15; + display: inline-block; + height: 1.4em; + line-height: 1.4; + padding: 0.4em 1em; + + &:hover { + background-color: @base-color-60; + color: @contrast-content-white; + } + } + } +} + +/* --- Plugin Administration ------------------------------------------------ */ +.plugin_image { + text-align: center; + width: 88px; + vertical-align: top; +} + +.plugin_score { white-space: nowrap; } +.plugin_install { text-align: center; } + +.plugin_description { + a.read_more_link { + display: none; + } +} + +.plugin_description.short { + div { + max-height: 15em; + overflow: hidden; + position: relative; + + p.read_more { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 4em; + margin: 0; + background-image: linear-gradient(to bottom, fadeout(@white, 100%), @white) + } + } + + .read_more_link { + .icon('before', 'add', 'clickable'); + + span { + vertical-align: middle; + padding-top: 3px; + } + } + + a { + display: inline-block; + } +} + + + +img.plugin_preview { + height: 60px; + width: 80px; +} + +/* --- User Administration -------------------------------------------------- */ + +.user_form { width: 250px; } + +.times-rooms-grid { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + margin: 0 -0.5em; + section { + flex: 1; + min-width: 23em; + max-width: 100%; + padding: 0 0.5em; + + &:empty { + height: 0; + } + + section { + padding: 0; + } + } +} + + +.resources-grid { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + margin: 0 -0.5em; + > section, div { + flex: 1; + display:block; + margin: 0 0.5em 1.5ex; + } +} +/* --- Veranstaltungsverwaltung --------------------------------------------- */ +.boxed-grid { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + + margin: 0 -0.5em; + + &, li { + list-style: none; + margin: 0; + padding: 0; + } + + li { + flex: 1 1 23em; + display: block; + min-width: 23em; + max-width: 100%; + padding: 0 0.5em; + + &:empty { + height: 0; + } + } + + a { + box-sizing: border-box; + background-color: @content-color-20; + border: 1px solid #D0D0D0; + display: block; + height: 11em; + margin: 0 0 1em; + overflow: hidden; + padding: 1em; + position: relative; + + &:hover { + background-color: #F2F2F2; + border-color: #A4A4A4; + + p { color: #000; } + } + } + img { + height: calc(100% - 20px); + position: absolute; + top: 10px; + right: 10px; + bottom: 10px; + opacity: 0.1; + } + + + h3 { + color: inherit; + font-size: 2em; + font-weight: normal; + margin: 0; + padding: 0; + } + p { + color: #666; + } +} + +/* --- general style classes ------------------------------------------------ */ +.arrow_down { + background: transparent top left no-repeat !important; + .background-icon('arr_1down', 'clickable') !important; +} +.arrow_right { + background: transparent top left no-repeat !important; + .background-icon('arr_1right', 'clickable') !important; +} +h1:hover, h2:hover, h3:hover, h4:hover { + .arrow_down { + .background-icon('arr_1down', 'attention'); + } + .arrow_right { + .background-icon('arr_1right', 'attention'); + } +} + +.invalid { border: 2px dotted red; } // an invalid form entry +.invalid_message { + display: none; + font-weight: bold; + color: red; +} +.invisible { display: none; } +.no-break { white-space: nowrap; } + +/* classes for the news modules in Stud.IP ---------------------------------- */ +.news_item { margin: 3px; } + +/* error message */ +.error { + background-color: #fcc; + border: 1px solid #fcc; + color: #000; + display: none; + font-size: 11px; + padding: 4px 10px; + + p { margin: 0; } + div.arrow { + border: 10px solid; + border-color: transparent transparent #FCC transparent; + height: 0; + left: 60px; + position: absolute; + top: -18px; + width: 0; + } +} + +.setting_info { + font-size: 0.9em; + font-style: italic; + text-align: right; + color: #444; +} + +pre.usercode { + padding: 5px; + background-color: rgba(255, 255, 255, 0.5); + border: hsla(0, 0%, 0%, 0.1) 5px solid; +} + + +.semtree li { + font-weight: bold; + list-style: none; + padding-bottom: 5px; +} + +/* descriptional texts */ +p.info { + padding: 10px; + margin: 0; +} + +.draggable { margin-top: 4px; } +.draggable_folder { margin-bottom: 3px; } + +/* --- institute administration ------------------------------------------- */ +.admin-institute { + input[type=text], input[type=tel], input[type=url], input[type=email], select:first-child { + box-sizing: border-box; + width: 98%; + } +} + +/* --- info text neu lecture --------------------------------------------- */ +div.info { padding-left: 1%; } + +/* --- rating --- */ +.printhead .rating img { padding: 0; } + +/* --- online list --- */ +.online-list { + display: flex; + flex-wrap: wrap; + > div { + flex: 1; + flex-basis: 300px; + margin-left: 10px; + &:first-child { + margin-left: 0px; + } + } +} + +/* Simple Content Module */ +.scm { + // Workaround for :last-child which is not supported by IE8 + .content_title { + td { + text-align: right; + white-space: nowrap; + &:first-child { + text-align: left; + white-space: normal; + } + } + input[type=text] { + width: 200px; + } + } + .content_body { + td { + padding: 22px; + } + textarea { + height: 200px; + width: 100%; + resize: vertical; + } + } + .table_footer td { + text-align: center; + } +} + +.no-js .hidden-no-js{ + display:none; +} +.js .hidden-js{ + display: none; +} +.no-js #enrollment ul{ + cursor: auto; +} + +.svg-input { + input { + display: none; + } + svg, img { + cursor: pointer; + } +} + +// course members +a.new-member { + .icon('after', 'star', 'attention', 8px); +} + +// calculate difference in images and apply filter +.recolor() { + filter: + hue-rotate(unit((hsvhue(@base-color) - hsvhue(#28497c)), deg)) + saturate((100 + hsvsaturation(@base-color)-hsvsaturation(#28497c))) + brightness((100 + hsvvalue(@base-color)-hsvvalue(#28497c))); +} + +.recolor-reset() { + filter: + hue-rotate(unit(hsvhue(#28497c)-hsvhue(@base-color), deg)) + saturate(100% - hsvsaturation(@base-color)+hsvsaturation(#28497c)) + brightness(100% - hsvvalue(@base-color)+hsvvalue(#28497c)); +} + +#barTopMenu li a img, +#barTopMenu li a canvas, +#layout-sidebar .sidebar-image > img { + .recolor; +} + +.recolor { + .recolor(); +} + +/** + * Style the details tag according to stud.ip + */ +details.studip { + summary { + .icon('before', 'arr_1right', 'clickable'); + cursor: pointer; + + &::before { + vertical-align: text-bottom; + } + + // Hide default icon + list-style: none; + &::-webkit-details-marker { + display: none; + } + } + + &[open] { + summary { + .icon('before', 'arr_1down', 'clickable'); + } + } + +} + +mark { + background-color: @activity-color-60; +} diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss new file mode 100644 index 0000000..d003669 --- /dev/null +++ b/resources/assets/stylesheets/studip.scss @@ -0,0 +1,30 @@ +/******************************************************************************* + Standard-Stylesheet für Stud.IP im Safire-Design + - use http://www.colorzilla.com/gradient-editor/ for gradients +*******************************************************************************/ +@import "mixins"; + +@import "scss/variables"; +@import "scss/breakpoints"; +@import "scss/visibility"; + +@import "scss/actionmenu"; +@import "scss/admin-courses"; +@import "scss/admission"; +@import "scss/blubber"; +@import "scss/contentbar"; +@import "scss/contents"; +@import "scss/courseware"; +@import "scss/dates"; +@import "scss/files"; +@import "scss/fullscreen"; +@import "scss/my_courses"; +@import "scss/oer"; +@import "scss/report"; +@import "scss/resources"; +@import "scss/sidebar"; +@import "scss/tooltip"; +@import "scss/table_of_contents"; +@import "scss/wiki"; + +@import "scss/grid"; diff --git a/resources/assets/stylesheets/vendor/jquery-nestable.css b/resources/assets/stylesheets/vendor/jquery-nestable.css new file mode 100644 index 0000000..246ec13 --- /dev/null +++ b/resources/assets/stylesheets/vendor/jquery-nestable.css @@ -0,0 +1,45 @@ +/* TODO This certainly needs to be cleaned up */ +.dd { position: relative; display: block; margin: 0; padding: 0; max-width: 600px; list-style: none; font-size: 13px; line-height: 20px; } + +.dd-list { display: block; position: relative; margin: 0; padding: 0; list-style: none; } +.dd-list .dd-list { padding-left: 30px; } +.dd-collapsed .dd-list { display: none; } + +.dd-item, +.dd-empty, +.dd-placeholder { display: block; position: relative; margin: 0; padding: 0; min-height: 20px; font-size: 13px; line-height: 20px; } + +.dd-handle { display: block; height: 30px; margin: 5px 0; padding: 5px 10px; color: #333; text-decoration: none; font-weight: bold; border: 1px solid #ccc; + background: #fafafa; + background: -webkit-linear-gradient(top, #fafafa 0%, #eee 100%); + background: -moz-linear-gradient(top, #fafafa 0%, #eee 100%); + background: linear-gradient(to bottom, #fafafa 0%, #eee 100%); + -webkit-border-radius: 3px; + border-radius: 3px; + box-sizing: border-box; -moz-box-sizing: border-box; +} +.dd-handle:hover { color: #2ea8e5; background: #fff; } + +.dd-item > button { display: block; position: relative; cursor: pointer; float: left; width: 25px; height: 20px; margin: 5px 0; padding: 0; text-indent: 100%; white-space: nowrap; overflow: hidden; border: 0; background: transparent; font-size: 12px; line-height: 1; text-align: center; font-weight: bold; } +.dd-item > button:before { content: '+'; display: block; position: absolute; width: 100%; text-align: center; text-indent: 0; } +.dd-item > button[data-action="collapse"]:before { display: block; content: '-'; } + +.dd-placeholder, +.dd-empty { margin: 5px 0; padding: 0; min-height: 30px; background: #f2fbff; border: 1px dashed #b6bcbf; box-sizing: border-box; -moz-box-sizing: border-box; } +.dd-empty { border: 1px dashed #bbb; min-height: 100px; background-color: #e5e5e5; + background-image: -webkit-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), + -webkit-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff); + background-image: -moz-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), + -moz-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff); + background-image: linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), + linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff); + background-size: 60px 60px; + background-position: 0 0, 30px 30px; +} + +.dd-dragel { position: absolute; pointer-events: none; z-index: 9999; } +.dd-dragel > .dd-item .dd-handle { margin-top: 0; } +.dd-dragel .dd-handle { + -webkit-box-shadow: 2px 4px 6px 0 rgba(0,0,0,.1); + box-shadow: 2px 4px 6px 0 rgba(0,0,0,.1); +} diff --git a/resources/assets/stylesheets/webservices.scss b/resources/assets/stylesheets/webservices.scss new file mode 100644 index 0000000..07ced78 --- /dev/null +++ b/resources/assets/stylesheets/webservices.scss @@ -0,0 +1,485 @@ +body { + font-family: sans-serif; + color: #000000; + background-color: #ffffff; +} + +h1 { + font-family: serif; + color: #000000; + font-size: 150%; + text-align: left; +} + +h2 { + font-family: serif; + color: #000000; + font-size: 120%; + text-align: left; + border-width: 1pt 0pt 0pt 0pt; + border-style: solid; + border-color: #8888dd; +} + +h3 { + font-family: serif; + font-size: 110% +} + +table tr td { + vertical-align: top; +} + +table tr th { + vertical-align: top; +} + +table.headerlinks { + width: 100%; + border-collapse: collapse; +} + +table.headerlinks tr td { + padding: 2pt; + margin: 0pt; + font-variant: small-caps; + background-color: #ddddff; + border-width: 0pt 0pt 1pt 0pt; + border-style: solid; + border-color: #8888dd; +} + +table.headerlinks tr td.prevnext { + text-align: right; +} + +.footer { + margin-top: 16pt; + border-width: 1pt 0pt 0pt 0pt; + border-style: solid; + border-color: #8888dd; + font-size: 70%; + font-style: italic; + text-align: right; +} + +table.metadata { + left: 0pt; + margin: 8pt 0pt 0pt 0pt; + border-collapse: collapse; +} + +table.metadata tr td.key { + font-weight: bold; + padding: 0pt 4pt 0pt 0pt; +} + +table.metadata tr td.value { + padding: 0pt; +} + +table.parameters { + border: none; + background-color: #ddddff; + border-collapse: collapse; + table-layout: fixed; + table-layout: auto; + width: 100%; +} + +table.parameters tr th { + color: #ffffff; + background-color: #8888dd; + border: solid 1px #8888dd; + padding: 2pt; + font-weight: bold; +} + +table.parameters tr td { + border: solid 1px #8888dd; + padding: 2pt; +} + +table.parameters tr td.value { + font-family: monospace; + font-size: 90%; +} + +table.parameters tr td.name { + font-family: monospace; + font-size: 90%; +} + +table.parameters tr td.required { + /* font-variant: small-caps; */ +} + +table.inputparameters { + border: none; + background-color: #ddddff; + border-collapse: collapse; + table-layout: auto; + width: 95%; +} + +table.inputparameters tr th { + color: #ffffff; + background-color: #8888dd; + border: solid 1px #8888dd; + padding: 2pt; + font-weight: bold; +} + +table.inputparameters tr td { + border: solid 1px #8888dd; + padding: 2pt; +} + +table.inputparameters tr td.value { + font-family: monospace; + font-size: 90%; +} + +table.inputparameters tr td.name { + font-family: monospace; + font-size: 90%; +} + +table.inputparameters tr td.required { + /* font-variant: small-caps; */ +} + +table.outputparameters { + border: none; + background-color: #ddddff; + border-collapse: collapse; + table-layout: auto; + width: 95%; +} + +table.outputparameters tr th { + color: #ffffff; + background-color: #8888dd; + border: solid 1px #8888dd; + padding: 2pt; + font-weight: bold; +} + +table.outputparameters tr td { + border: solid 1px #8888dd; + padding: 2pt; +} + +table.outputparameters tr td.value { + font-family: monospace; + font-size: 90%; +} + +table.outputparameters tr td.name { + font-family: monospace; + font-size: 90%; +} + +table.outputparameters tr td.required { + /* font-variant: small-caps; */ +} + +table.resultcodes { + border: none; + background-color: #ddddff; + border-collapse: collapse; + table-layout: auto; + width: 95%; +} + +table.resultcodes tr th { + color: #ffffff; + background-color: #7777dd; + border: solid 1px #8888dd; + padding: 2pt; + font-weight: bold; +} + +table.resultcodes tr td { + border: solid 1px #8888dd; + padding: 2pt; +} + +table.resultcodes tr td.value { + font-family: monospace; + font-size: 90%; +} + +table.resultcodes tr.default td.value { + font-style: italic; +} + +table.resultcodes tr td.name { + font-family: monospace; + font-size: 90%; +} + +table.resultcodes tr td.required { + /* font-variant: small-caps; */ +} + +table.element_details { + width: 100%; + border: none; + background-color: #ddddff; + border-collapse: collapse; + table-layout: auto; + width: 95%; +} + +table.element_details tr th { + color: #ffffff; + background-color: #8888dd; + border: solid 1px #8888dd; + padding: 2pt; + font-weight: bold; + text-align: left; + width: 160px; +} + +table.element_details tr td { + border: solid 1px #8888dd; + padding: 2pt; +} + +table.example { + border: none; + border-collapse: collapse; + table-layout: auto; + width: 95%; +} + +table.example tr th { + background-color: #ddddff; + text-align: left; + border: solid 1px #8888dd; + padding: 2pt; +} + +table.example tr td { + background-color: #ddddff; + text-align: left; + border: solid 1px #8888dd; + padding: 2pt; +} + +table.example tr td.header { + text-align: left; + border: none; + padding: 2pt; + background-color: #ffffff; +} + +.xml { + font-family: monospace; + font-size: 90%; + white-space: pre; +} + +.xml .decl { + font-weight: bold; + color: #008800; +} + +.xml .decl .elem .name { + font-weight: bold; + color: #008800; +} + +.xml .elem .name { + font-weight: bold; + color: #000088; +} + +.xml .pcdata { + font-style: italic; +} + +.xml .elem .attr .name { + font-weight: normal; + color: #000088; +} + +.url { + font-family: monospace; + font-size: 90%; + white-space: pre; +} + +.url .functionparam .name { + color: #880000; + font-weight: bold +} + +.url .functionparam .value { + color: #880000; + font-weight: bold +} + +.url .param .name { + color: #000088; + font-weight: bold +} + +.url .param .value { + color: #008800; + font-weight: bold +} + +blockquote { + margin-top: 10pt; + margin-bottom: 10pt; +} + +pre { + font-family: monospace; + font-size: 90%; +} + +a:hover { + background-color: #ddddff; + text-decoration: underline; +} + +a { + color: #0000bb; + text-decoration: none; +} + +a:visited { + color: #0000bb; + text-decoration: none; +} + +a img { + border-style: none; +} + +.disabled { + color: #888888; +} + +.active { + color: #888888; + font-weight: bold; +} + +table.apilist { + border: none; + background-color: #ddddff; + border-collapse: collapse; + table-layout: fixed; + table-layout: auto; + width: 100%; +} + +table.apilist tr th { + color: #ffffff; + background-color: #8888dd; + border: solid 1px #8888dd; + padding: 2pt; + font-weight: bold; + text-align: left; +} + +table.apilist tr td { + border: solid 1px #8888dd; + padding: 2pt; +} + +table.functionlist { + border: none; + background-color: #ddddff; + border-collapse: collapse; + table-layout: fixed; + table-layout: auto; + width: 100%; +} + +table.functionlist tr th { + color: #ffffff; + background-color: #8888dd; + border: solid 1px #8888dd; + padding: 2pt; + font-weight: bold; + text-align: left; +} + +table.functionlist tr td { + border: solid 1px #8888dd; + padding: 2pt; +} + +table.typelist { + border: none; + background-color: #ddddff; + border-collapse: collapse; + table-layout: fixed; + table-layout: auto; + width: 100%; +} + +table.typelist tr th { + color: #ffffff; + background-color: #8888dd; + border: solid 1px #8888dd; + padding: 2pt; + font-weight: bold; + text-align: left; +} + +table.typelist tr td { + border: solid 1px #8888dd; + padding: 2pt; +} + +input { + border: 1px #8888dd solid; + font-family: sans-serif; + font-size: 9pt; + background-color: #ffffff; +} + +input.required { + border: 2px #8888dd solid; +} + +input[name=\'submit\'] { + margin-top: 5px; + text-align: center; + font-weight: bold; + cursor: hand; + background-color: #eeeeff; +} + +select { + border: 1px #8888dd solid; + font-family: sans-serif; + font-size: 9pt; + background-color: #ffffff; +} + +select.required { + border: 2px #8888dd solid; +} + +td.status { + font-variant: small-caps; +} + +.broken_freeze { + color: #ff4444; + font-weight: bold; +} + +div.broken_freeze { + border: dashed 1px #ff4444; + margin-top: 1em; + margin-bottom: 1.5em; + padding: 1em; +} diff --git a/resources/assets/stylesheets/wysiwyg.less b/resources/assets/stylesheets/wysiwyg.less new file mode 100644 index 0000000..221f3e2 --- /dev/null +++ b/resources/assets/stylesheets/wysiwyg.less @@ -0,0 +1,185 @@ +/** + * styles.css - CSS styles for WYSIWYG editor. + * + * 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 "mixins.less"; + +/* configure look and feel of CKEditor's content area and toolbar */ +.animated-height-change { /* content area in HTML source view mode */ + transition: height 0.2s; +} + +/* override inherited font-weight of label */ +.cke { + text-indent: 0; +} + +.cktoolbar { /* toolbar */ + padding: 0; + margin: 0; + text-indent: 0; + z-index: 1; +} +.cke_editable { + min-height: 6em; +} + +/* CKEDITOR dialogs */ +select.cke_dialog_ui_input_select { + background-color: #ffffff !important; +} + +/* CKEDITOR image dialog */ +.cke_dialog .ImagePreviewBox { + border: 1px ridge black !important; +} + +/* CKEDITOR mathjax Plugin (TeX) Link color */ +a.cke_mathjax_doc[href] { + color: #28497c !important; + text-decoration: none !important; +} + +/* Override 'Select Style Menu' CSS */ +.cke_combo_off a.cke_combo_button:hover, +.cke_combo_off a.cke_combo_button:focus, +.cke_combo_on a.cke_combo_button:hover, +.cke_combo_on a.cke_combo_button:focus { + box-shadow: none !important; +} +.cke_combo_off a.cke_combo_button:active, +.cke_combo_on a.cke_combo_button:active, +.cke_combo_on a.cke_combo_button { + box-shadow: none !important; +} + +/* The CSS arrow, which belongs to the toolbar collapser. */ +.cke_toolbox_collapser .cke_arrow { + display: inline-block; + + /* Pure CSS Arrow */ + + width: 5px !important; + height: 5px !important; + font-size: 12px !important; + margin-top: 4px !important; + border-left: 0 !important; + border-bottom: 0 !important; + border-right: 2px solid black !important; + border-top: 2px solid black !important; + transform: rotate(315deg) !important; + /*margin-right: 0.5em;*/ + + /*Hide the text.*/ + text-indent: 100%; + white-space: nowrap; + overflow: hidden; +} + +.cke_toolbox_collapser.cke_toolbox_collapser_min .cke_arrow { + margin-top: 1px !important; + transform: rotate(135deg) !important; +} + +.cke_toolbox_collapser { + border: none !important; + border-bottom-color: transparent !important; + + border-radius: 0 !important; + + box-shadow: none !important; + + background-image: linear-gradient(to bottom, #ffffff, #ffffff) !important; + filter: none !important; +} + +.cke_toolbox_collapser:hover { + background: #E3EAF6 !important; + filter: none !important; +} +/* drop zone effects */ +.drag .dropzone { + display: block; +} +.dropzone { + pointer-events: none; + display: none; + position:absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + + border: 2px dashed lightgrey; + background-color: white; + z-index: 998; + opacity: 0.7; + + text-align: center; + vertical-align: middle; + font-size: 20px; + font-weight: bold; +} + +/* other settings */ + +.wiki-link { + background-image: url(../javascripts/ckeditor/plugins/studip-wiki/icons/wikilink-grey.png); + background-repeat: no-repeat; + background-position: right top; + background-size: 8px; + padding-right: 9px; +} + + +/* from plugins/emojione/styles/emojione.css */ + +.emojione, .cke_hand[data-shortcode] { + /* Emoji Sizing */ + font-size: inherit; + height: 30px !important; + width: 30px !important; + + /* Inline alignment adjust the margins */ + display: inline-block; + margin: -.2ex .15em .2ex; + line-height: normal; + vertical-align: middle; +} + +/* Margin for lists */ +.cke_editable ul, +.cke_editable ol { + margin: 0.5em 0; +} +.cke_editable ul ul, +.cke_editable ol ul, +.cke_editable ul ol, +.cke_editable ol ol { + margin-top: 0; + margin-bottom: 0; +} + +.cke { + .cke_inner { + border-color: @light-gray-color-40; + } + + &.cke_chrome:focus, + &.cke_chrome:active, + &.cke_chrome_focused { + outline: 0; + + .cke_inner { + border-color: @brand-color-dark; + } + } +} diff --git a/resources/locales/de.json b/resources/locales/de.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/resources/locales/de.json @@ -0,0 +1 @@ +{} diff --git a/resources/locales/en.json b/resources/locales/en.json new file mode 100644 index 0000000..94def68 --- /dev/null +++ b/resources/locales/en.json @@ -0,0 +1 @@ +{" Dieser Filter enth\u00e4lt keine (neuen) Personen.":" Dieser Filter enth\u00e4lt keine (neuen) Personen.","(Seite %{pageNum} von %{pageCount})":"(Seite %{pageNum} von %{pageCount})","[versteckt]":"[hidden]","[Vertretung]":"[Substitute]","<%= count %> ausgew\u00e4hlt":"<%= count %> ausgew\u00e4hlt","Abbrechen":"Cancel","abgeschlossene Seiten":"abgeschlossene Seiten","Abschicken":"Submit","AbschlussKategorie suchen":"AbschlussKategorie suchen","Abschnitt":"Section","Abschnitt bearbeiten":"Edit section","Abschnitt l\u00f6schen":"Delete section","Abschnitt unwiderruflich l\u00f6schen":"Abschnitt unwiderruflich l\u00f6schen","Abschnitt wurde erfolgreich eingef\u00fcgt.":"Abschnitt wurde erfolgreich eingef\u00fcgt.","Abschnitte":"Abschnitte","Abschnitte sortieren":"Abschnitte sortieren","Aktionen":"Actions","Aktionsmen\u00fc":"Action menu","Aktivit\u00e4ten":"Activities","Alle Bl\u00f6cke":"Alle Bl\u00f6cke","Alle entfernen":"Alle entfernen","Alle hinzuf\u00fcgen":"Alle hinzuf\u00fcgen","Alle R\u00e4ume anzeigen":"Alle R\u00e4ume anzeigen","Alle Teilnehmenden haben Leserechte":"Alle Teilnehmenden haben Leserechte","Alle Teilnehmenden haben Schreibrechte":"Alle Teilnehmenden haben Schreibrechte","Alles exportieren":"Alles exportieren","Alles importieren":"Alles importieren","Allgemeine Einstellungen":"General settings","an dieser Stelle einf\u00fcgen":"an dieser Stelle einf\u00fcgen","angefangene Seiten":"angefangene Seiten","Anmelderegel konfigurieren":"Configure admission setting","Antwort":"Reply","Apr":"Apr","April":"April","Art der Kapitelabfolge":"Art der Kapitelabfolge","Art des Links":"Art des Links","Audio":"Audio","Audio Aufnahmen zulassen":"Audio Aufnahmen zulassen","Aufgaben & Interaktion":"Aufgaben & Interaktion","Aufnahme aktivieren":"Aufnahme aktivieren","Aufnahme beenden":"Aufnahme beenden","Aufnahme l\u00e4uft":"Aufnahme l\u00e4uft","Aufnahme l\u00f6schen":"Aufnahme l\u00f6schen","Aufnahme speichern":"Aufnahme speichern","Aufnahme starten":"Aufnahme starten","Aufnahme wiederholen":"Aufnahme wiederholen","Aufnahme wurde erfolgreich im Dateibereich abgelegt.":"Aufnahme wurde erfolgreich im Dateibereich abgelegt.","Aug":"Aug","August":"August","Aus der Veranstaltung austragen":"Aus der Veranstaltung austragen","Aus meine Inhalte kopieren":"Aus meine Inhalte kopieren","Aus Veranstaltung kopieren":"Aus Veranstaltung kopieren","ausgew\u00e4hlt":"ausgew\u00e4hlt","Author":"Author","Autoplay":"Autoplay","Autoplay Timer in Sekunden":"Autoplay Timer in Sekunden","Autor":"Author","Autor\/-in":"Author","Balkendiagramm":"Balkendiagramm","Band":"Band","Bearbeiten":"Edit","Bearbeiten.":"Bearbeiten.","Bedingung konfigurieren":"Bedingung konfigurieren","beliebig":"any","Benachrichtigungen aktiviert":"Notifications activated","Benachrichtigungen f\u00fcr diese Konversation abstellen.":"Unsubscribe from notifications for this conversation.","Beschreibung":"Description","Beschriftung":"Beschriftung","bevorstehende Seiten":"bevorstehende Seiten","Bezeichnung":"Notation","Bild":"Picture","Bild %u von %u":"Bild %u von %u","Bild hochladen":"Bild hochladen","Bild im Dateibereich speichern":"Bild im Dateibereich speichern","Bild l\u00f6schen":"Delete picture","Bild R\u00fcckseite":"Bild R\u00fcckseite","Bild Vorderseite":"Bild Vorderseite","Bild wurde erfolgreich im Dateibereich abgelegt.":"Bild wurde erfolgreich im Dateibereich abgelegt.","Bilddatei":"Bilddatei","bis":"until","Bitte %u Zeichen mehr eingeben":"Bitte %u Zeichen mehr eingeben","Bitte %u Zeichen weniger eingeben":"Bitte %u Zeichen weniger eingeben","Bitte best\u00e4tigen Sie die Aktion":"Please confirm action","Bitte geben Sie ein Datum an":"Bitte geben Sie ein Datum an","Bitte geben Sie eine Uhrzeit an":"Bitte geben Sie eine Uhrzeit an","Bitte geben Sie Ihren tats\u00e4chlichen Nachnamen an.":"Please enter your real last name.","Bitte geben Sie Ihren tats\u00e4chlichen Vornamen an.":"Bitte geben Sie Ihren tats\u00e4chlichen Vornamen an.","Bitte laden Sie die Seite neu, um fortzufahren":"Bitte laden Sie die Seite neu, um fortzufahren","Bitte w\u00e4hlen Sie ein Nachherbilder aus.":"Bitte w\u00e4hlen Sie ein Nachherbilder aus.","Bitte w\u00e4hlen Sie ein Video aus":"Bitte w\u00e4hlen Sie ein Video aus","Bitte w\u00e4hlen Sie ein Vorherbilder aus.":"Bitte w\u00e4hlen Sie ein Vorherbilder aus.","Bitte w\u00e4hlen Sie eine Datei aus":"Bitte w\u00e4hlen Sie eine Datei aus","Bitte w\u00e4hlen Sie eine Seite als Ziel aus":"Bitte w\u00e4hlen Sie eine Seite als Ziel aus","Bitte w\u00e4hlen Sie einen g\u00fcltigen Wert aus!":"Bitte w\u00e4hlen Sie einen g\u00fcltigen Wert aus!","Bitte w\u00e4hlen Sie einen Ort aus, an dem der Block eingef\u00fcgt werden soll.":"Bitte w\u00e4hlen Sie einen Ort aus, an dem der Block eingef\u00fcgt werden soll.","blau":"blau","Blau":"Blau","Blenden Sie die restlichen Termine aus":"Blenden Sie die restlichen Termine aus","Blenden Sie die restlichen Termine ein":"Show the remaining dates","Block":"Block","Block bearbeiten":"Block bearbeiten","Block hinzuf\u00fcgen":"Block hinzuf\u00fcgen","Block l\u00f6schen":"Block l\u00f6schen","Block unwiderruflich l\u00f6schen":"Block unwiderruflich l\u00f6schen","Block wurde erfolgreich eingef\u00fcgt.":"Block wurde erfolgreich eingef\u00fcgt.","Block wurde erstellt am":"Block wurde erstellt am","Block wurde erstellt von":"Block wurde erstellt von","Blockassistent":"Blockassistent","Blockbeschreibung":"Blockbeschreibung","Bl\u00f6cke":"Bl\u00f6cke","Bl\u00f6cke sortieren":"Bl\u00f6cke sortieren","Breite":"Width","Cachetyp":"Cachetyp","Cachetyp ausw\u00e4hlen":"Cachetyp ausw\u00e4hlen","Contextmen\u00fc":"Contextmen\u00fc","Countdown":"Countdown","Creative Commons Angaben":"Creative Commons Angaben","Das Aktivieren des WYSIWYG Editors ist fehlgeschlagen.":"Das Aktivieren des WYSIWYG Editors ist fehlgeschlagen.","Das Herunterladen dieser Datei ist nur eingeschr\u00e4nkt m\u00f6glich.":"The download of this file is restricted.","Das Lesezeichen wurde gesetzt":"Das Lesezeichen wurde gesetzt","Das Passwort ist zu kurz. Es sollte mindestens 8 Zeichen lang sein.":"The password is too short. It should have at least 8 characters.","Das Passwort stimmt nicht mit dem Best\u00e4tigungspasswort \u00fcberein!":"Das Passwort stimmt nicht mit dem Best\u00e4tigungspasswort \u00fcberein!","Datei":"Datei","Datei hochladen":"Upload file","Datei ist nicht verf\u00fcgbar":"Datei ist nicht verf\u00fcgbar","Datei ist zu gro\u00df oder hat eine nicht erlaubte Endung.":"Datei ist zu gro\u00df oder hat eine nicht erlaubte Endung.","Dateibereich":"File area","Dateibereich Datei":"Dateibereich Datei","Dateibereich der Veranstaltung":"Dateibereich der Veranstaltung","Dateibereich dieser Veranstaltung":"Dateibereich dieser Veranstaltung","Dateibereich Ordner":"Dateibereich Ordner","Dateien":"Files","Dateinamen anzeigen":"Dateinamen anzeigen","Dateipfad":"Dateipfad","Dateityp":"File type","Datenfeld in Original-Sprache nicht verf\u00fcgbar.":"Datenfeld in Original-Sprache nicht verf\u00fcgbar.","Datensatz":"Datensatz","Datensatz entfernen":"Datensatz entfernen","Datensatz hinzuf\u00fcgen":"Datensatz hinzuf\u00fcgen","Datum":"Date","deaktiviert":"deaktiviert","Der Benutzername enth\u00e4lt unzul\u00e4ssige Zeichen, er darf keine Sonderzeichen oder Leerzeichen enthalten.":"Der Benutzername enth\u00e4lt unzul\u00e4ssige Zeichen, er darf keine Sonderzeichen oder Leerzeichen enthalten.","Der Benutzername ist zu kurz, er sollte mindestens 4 Zeichen lang sein.":"Der Benutzername ist zu kurz, er sollte mindestens 4 Zeichen lang sein.","Detailanzeige umschalten":"Switch detailed view","Detaillierte Veranstaltungsliste":"Detaillierte Veranstaltungsliste","Dez":"Dez","Dezember":"December","Di":"Tue.","Dialog wird geladen...":"Dialog wird geladen...","Die angeforderte Seite ist nicht Teil dieser Courseware.":"Die angeforderte Seite ist nicht Teil dieser Courseware.","Die beiden Werte \"$1\" und \"$2\" stimmen nicht \u00fcberein. ":"Die beiden Werte \"$1\" und \"$2\" stimmen nicht \u00fcberein. ","Die E-Mail-Adresse ist nicht korrekt!":"Die E-Mail-Adresse ist nicht korrekt!","Die Person ist bereits eingetragen.":"Die Person ist bereits eingetragen.","Die Seite muss danach neu geladen werden, um den WYSIWYG Editor zu laden.":"Die Seite muss danach neu geladen werden, um den WYSIWYG Editor zu laden.","Die Teilnahme ist bindend. Bitte wenden Sie sich an die Lehrenden.":"Participation is binding. Please contact the lecturers.","Dienstag":"Tuesday","Diese Courseware":"Diese Courseware","Diese Datei ist kein Bild. Bitte w\u00e4hlen Sie ein Bild aus.":"Diese Datei ist kein Bild. Bitte w\u00e4hlen Sie ein Bild aus.","Diese Datei ist zu gro\u00df. Bitte w\u00e4hlen Sie eine kleinere Datei.":"Diese Datei ist zu gro\u00df. Bitte w\u00e4hlen Sie eine kleinere Datei.","diese Seite":"diese Seite","diese Seite inkl. darunter liegende Seiten":"diese Seite inkl. darunter liegende Seiten","Diese Seite steht Ihnen leider nicht zur Verf\u00fcgung":"Diese Seite steht Ihnen leider nicht zur Verf\u00fcgung","Diesen Dialog schlie\u00dfen":"Diesen Dialog schlie\u00dfen","Dieser Abschnitt enth\u00e4lt keine Bl\u00f6cke.":"Dieser Abschnitt enth\u00e4lt keine Bl\u00f6cke.","Dieser Block wird bereits bearbeitet.":"Dieser Block wird bereits bearbeitet.","Dieser Ordner ist leer":"This folder is empty","Dieses Bild wird verkleinert dargestellt. Klicken Sie f\u00fcr eine gr\u00f6\u00dfere Darstellung.":"Dieses Bild wird verkleinert dargestellt. Klicken Sie f\u00fcr eine gr\u00f6\u00dfere Darstellung.","Dieses Element enth\u00e4lt keine Abschnitte.":"Dieses Element enth\u00e4lt keine Abschnitte.","Dieses Element enth\u00e4lt keine Seiten.":"Dieses Element enth\u00e4lt keine Seiten.","Dieses Seite enth\u00e4lt keine darunter liegenden Seiten":"Dieses Seite enth\u00e4lt keine darunter liegenden Seiten","Do":"Thu.","Dokument hinzuf\u00fcgen":"Add document","Dokument suchen":"Search document","Donnerstag":"Thursday","Download-Icon anzeigen":"Download-Icon anzeigen","Downloads":"Downloads","Dunkelgrau":"Dunkelgrau","Editierberechtigung f\u00fcr Tutor\/-innen":"Editierberechtigung f\u00fcr Tutor\/-innen","eigener Dateibereich":"eigener Dateibereich","Eigener Dateibereich":"Eigener Dateibereich","Einen Abschnitt ausw\u00e4hlen":"Einen Abschnitt ausw\u00e4hlen","Einen Abschnitt hinzuf\u00fcgen":"Einen Abschnitt hinzuf\u00fcgen","Einstellungen":"Settings","Einstellungen wurden \u00fcbernommen":"Einstellungen wurden \u00fcbernommen","Elemente f\u00fcr Ihren ersten Inhalt wurden angelegt":"Elemente f\u00fcr Ihren ersten Inhalt wurden angelegt","Elemente hinzuf\u00fcgen":"Elemente hinzuf\u00fcgen","Endpunkt w\u00e4hlen":"Endpunkt w\u00e4hlen","Enth\u00e4lt der Inhalt eine oder mehrere Dateien?":"Enth\u00e4lt der Inhalt eine oder mehrere Dateien?","Entwurf":"Draft","ePortfolio":"ePortfolio","Erlauben":"Allow","Erstellen":"Create","erstellt von":"erstellt von","Ersten Inhalt erstellen":"Ersten Inhalt erstellen","erstes Element":"erstes Element","Es gab einen Fehler beim Hochladen der Datei(en):":"Es gab einen Fehler beim Hochladen der Datei(en):","Es ist ein Fehler aufgetretten! Das Bild konnte nicht gespeichert werden.":"Es ist ein Fehler aufgetretten! Das Bild konnte nicht gespeichert werden.","Es ist ein Fehler aufgetretten! Die Aufnahme konnte nicht gespeichert werden.":"Es ist ein Fehler aufgetretten! Die Aufnahme konnte nicht gespeichert werden.","Es ist keine Audio-Datei verf\u00fcgbar":"Es ist keine Audio-Datei verf\u00fcgbar","Es steht keine Auswahl zur Verf\u00fcgung":"Es steht keine Auswahl zur Verf\u00fcgung","Es wurden bisher noch keine Inhalte eingepflegt.":"Es wurden bisher noch keine Inhalte eingepflegt.","Es wurden keine neuen Ergebnisse f\u00fcr \"<%= needle %>\" gefunden.":"Es wurden keine neuen Ergebnisse f\u00fcr \"<%= needle %>\" gefunden.","Es wurden keine Veranstaltungen gefunden.":"No course found.","Export":"Export","Export l\u00e4uft, bitte haben sie einen Moment Geduld...":"Export l\u00e4uft, bitte haben sie einen Moment Geduld...","Export l\u00e4uft...":"Export l\u00e4uft...","Export Options":"Export Options","Exportieren":"Export","Extern":"External","Externe Inhalte":"Externe Inhalte","Externer Link":"Externer Link","Fach hinzuf\u00fcgen":"Fach hinzuf\u00fcgen","Fach l\u00f6schen":"Delete field of study","Fachsemester ausw\u00e4hlen (optional)":"Fachsemester ausw\u00e4hlen (optional)","FAQ":"FAQ","Farbgruppierung \u00e4ndern":"Change colour grouping","Favoriten":"Favourites","Favoriten bearbeiten":"Favoriten bearbeiten","Favoriten bearbeiten schlie\u00dfen":"Favoriten bearbeiten schlie\u00dfen","Feb":"Feb","Februar":"February","Feedback":"Feedback","Feedback anzeigen":"Feedback anzeigen","Fehler":"Error","Fehler beim Aufruf des News-Controllers":"Fehler beim Aufruf des News-Controllers","Fehler beim Aufruf des Tour-Controllers":"Fehler beim Aufruf des Tour-Controllers","Fehler beim Hochladen der Datei.":"Fehler beim Hochladen der Datei.","Form entfernen":"Form entfernen","Fortschritt":"Progress","Fortschritt erst beim Herunterladen":"Fortschritt erst beim Herunterladen","Fr":"Fri.","Frei":"Frei","Freitag":"Friday","F\u00fcgt einen Standard-Abschnitt mit einem Text-Block hinzu":"F\u00fcgt einen Standard-Abschnitt mit einem Text-Block hinzu","F\u00fcllen Sie noch die rot markierten Stellen korrekt aus.":"F\u00fcllen Sie noch die rot markierten Stellen korrekt aus.","f\u00fcr alle":"f\u00fcr alle","F\u00fcr diesen Cachetyp ist keine Konfiguration erforderlich.":"F\u00fcr diesen Cachetyp ist keine Konfiguration erforderlich.","gelb":"gelb","Gelb":"Yellow","Gesch\u00e4tzter zeitlicher Aufwand":"Gesch\u00e4tzter zeitlicher Aufwand","Geschwindigkeit":"Geschwindigkeit","Gestaltung":"Gestaltung","grau":"grau","Grau":"Grau","gro\u00df":"large","Gr\u00f6\u00dfe":"Size","Gro\u00dfe Schrift":"Gro\u00dfe Schrift","Gro\u00dfes Icon davor":"Gro\u00dfes Icon davor","Gro\u00dfes Icon oben":"Gro\u00dfes Icon oben","gr\u00fcn":"gr\u00fcn","Gr\u00fcn":"Green","Grunddaten":"Basic details","Gruppe":"Group","Gruppen":"Groups","Halb":"Halb","Halbe Breite":"Halbe Breite","Halbe Breite (zentriert)":"Halbe Breite (zentriert)","Handelt es sich bei dem Inhalt haupts\u00e4chlich um Text?":"Handelt es sich bei dem Inhalt haupts\u00e4chlich um Text?","Haupttitel":"Haupttitel","Hellblau":"Hellblau","hellgrau":"hellgrau","HH:mm":"HH:mm","Hierauf antworten.":"Hierauf antworten.","Hiermit exportieren Sie die Seite \"{{ currentElement.attributes.title }}\" als ZIP-Datei.":"Hiermit exportieren Sie die Seite \"{{ currentElement.attributes.title }}\" als ZIP-Datei.","Hintergrundbild":"Hintergrundbild","Hintergrundfarbe":"Hintergrundfarbe","Hintergrundtyp":"Hintergrundtyp","hinzuf\u00fcgen":"add","H\u00f6he":"Height","Holzkohle":"Holzkohle","Hostname":"Hostname","Ich helfe Ihnen bei der Auswahl des richtigen Blocks. Beantworten Sie mir einfach ein paar Fragen. Meine Vorschl\u00e4ge werden dann hier anzeigen.":"Ich helfe Ihnen bei der Auswahl des richtigen Blocks. Beantworten Sie mir einfach ein paar Fragen. Meine Vorschl\u00e4ge werden dann hier anzeigen.","Icon":"Icon","Icon-Farbe":"Icon-Farbe","Ihre Eingaben wurden bislang noch nicht gespeichert.":"Ihre Eingaben wurden bislang noch nicht gespeichert.","Import erfolgreich!":"Import erfolgreich!","Import l\u00e4uft":"Import l\u00e4uft","Importieren":"Import","In dem ausgew\u00e4hlten <strong>Semester<\/strong> wurden keine Veranstaltungen belegt.\n <br>\n W\u00e4hlen Sie links im <strong>Semesterfilter<\/strong> ein anderes Semester aus!\n <\/br>":"In dem ausgew\u00e4hlten <strong>Semester<\/strong> wurden keine Veranstaltungen belegt.\n <br>\n W\u00e4hlen Sie links im <strong>Semesterfilter<\/strong> ein anderes Semester aus!\n <\/br>","Infobox nach Download":"Infobox nach Download","Infobox vor Download":"Infobox vor Download","Information":"Information","Informationen":"Information","Informationen anzeigen":"Informationen anzeigen","Informationen zum Audio-Block":"Informationen zum Audio-Block","Informationen zum Best\u00e4tigungs-Block":"Informationen zum Best\u00e4tigungs-Block","Informationen zum Bildvergleich-Block":"Informationen zum Bildvergleich-Block","Informationen zum Blickfang-Block":"Informationen zum Blickfang-Block","Informationen zum Block":"Informationen zum Block","Informationen zum Chart-Block":"Informationen zum Chart-Block","Informationen zum Date-Block":"Informationen zum Date-Block","Informationen zum Dateiordner-Block":"Informationen zum Dateiordner-Block","Informationen zum DialogCards-Block":"Informationen zum DialogCards-Block","Informationen zum Document-Block":"Informationen zum Document-Block","Informationen zum Download-Block":"Informationen zum Download-Block","Informationen zum Embed-Block":"Informationen zum Embed-Block","Informationen zum Galerie-Block":"Informationen zum Galerie-Block","Informationen zum IFrame-Block":"Informationen zum IFrame-Block","Informationen zum Inhaltsverzeichnis-Block":"Informationen zum Inhaltsverzeichnis-Block","Informationen zum Leinwand-Block":"Informationen zum Leinwand-Block","Informationen zum Link-Block":"Informationen zum Link-Block","Informationen zum Merksatz-Block":"Informationen zum Merksatz-Block","Informationen zum Quelltext-Block":"Informationen zum Quelltext-Block","Informationen zum Schreibmaschinen-Block":"Informationen zum Schreibmaschinen-Block","Informationen zum Text-Block":"Informationen zum Text-Block","Informationen zum Verweissensitive-Grafik-Block":"Informationen zum Verweissensitive-Grafik-Block","Informationen zum Video-Block":"Informationen zum Video-Block","Informationen zur Seite":"Informationen zur Seite","Inhalt":"Content","Inhalte werden geladen":"Inhalte werden geladen","Intern":"Internal","Interner Link":"Interner Link","Ja":"Yes","Jan":"Jan","Januar":"January","Jetzt":"Jetzt","Jul":"Jul","Juli":"July","Jun":"Jun","Juni":"June","Kachelansicht":"Kachelansicht","Kacheln":"Kacheln","Karte":"Karte","Karte entfernen":"Karte entfernen","Karte hinzuf\u00fcgen":"Karte hinzuf\u00fcgen","Karte umdrehen":"Karte umdrehen","Kein Ergebnis gefunden.":"Kein Ergebnis gefunden.","kein Ordner ausgew\u00e4hlt":"kein Ordner ausgew\u00e4hlt","Kein passendes Element f\u00fcr Vollbildmodus.":"Kein passendes Element f\u00fcr Vollbildmodus.","Keine":"None","Keine Angabe beim Fach":"Keine Angabe beim Fach","Keine Auswahl":"No selection","Keine Dateien vorhanden":"No files available","keine n\u00e4chste Karte":"keine n\u00e4chste Karte","Keine \u00dcbereinstimmungen gefunden":"Keine \u00dcbereinstimmungen gefunden","keine vorherige Karte":"keine vorherige Karte","Keine weitere Auswahl m\u00f6glich":"Keine weitere Auswahl m\u00f6glich","klein":"small","Kommentar schreiben. Enter zum Abschicken.":"Kommentar schreiben. Enter zum Abschicken.","Kommentare":"Comments","Kommentare anzeigen":"Show comments","Kommt der Inhalt von einer anderen Plattform, z.B. Youtube?":"Kommt der Inhalt von einer anderen Plattform, z.B. Youtube?","K\u00f6nigin blau":"K\u00f6nigin blau","Konnte die Konversation nicht laden. Probieren Sie es nachher erneut.":"Konnte die Konversation nicht laden. Probieren Sie es nachher erneut.","Konnte die Suche nicht ausf\u00fchren. Probieren Sie es nachher erneut.":"Konnte die Suche nicht ausf\u00fchren. Probieren Sie es nachher erneut.","Kopieren":"Copy","Kreis hinzuf\u00fcgen":"Kreis hinzuf\u00fcgen","Kreisdiagramm":"Kreisdiagramm","K\u00fcrbis":"K\u00fcrbis","Lade mehr Ergebnisse...":"Lade mehr Ergebnisse...","Langsam":"Langsam","Layout":"Layout","Leguangr\u00fcn":"Leguangr\u00fcn","Lehrende in Stud.IP":"Lehrende in Stud.IP","Lesen":"Read","Lesen und Schreiben":"Lesen und Schreiben","Leser\/-innen":"Readers","Lesezeichen setzen":"Lesezeichen setzen","lila":"lila","Lila":"Lila","Liniendiagramm":"Liniendiagramm","Link wurde kopiert":"Link wurde kopiert","Liste":"List","Liste mit Beschreibung":"Liste mit Beschreibung","Lizenz der Plattform":"Lizenz der Plattform","Lizenztyp":"Lizenztyp","Mai":"May","M\u00e4r":"M\u00e4r","M\u00e4rz":"March","Maulbeere":"Maulbeere","Maximale H\u00f6he":"Maximale H\u00f6he","Mehr \u00fcber Courseware erfahren":"Mehr \u00fcber Courseware erfahren","Memcached-Server":"Memcached-Server","Metadaten":"Metadaten","Mi":"Wed.","Mikrosekunde":"Mikrosekunde","Millisekunde":"Millisekunde","Minute":"Minute","Minuten":"Minutes","Mittwoch":"Wednesday","Mo":"Mon.","M\u00f6chten Sie die Seite":"M\u00f6chten Sie die Seite","M\u00f6chten Sie die Seite wirklich l\u00f6schen?":"M\u00f6chten Sie die Seite wirklich l\u00f6schen?","M\u00f6chten Sie diesen Abschnitt wirklich l\u00f6schen?":"M\u00f6chten Sie diesen Abschnitt wirklich l\u00f6schen?","M\u00f6chten Sie diesen Block wirklich l\u00f6schen?":"M\u00f6chten Sie diesen Block wirklich l\u00f6schen?","Modul suchen":"Search for module","M\u00f6gliche Ursachen:":"M\u00f6gliche Ursachen:","Montag":"Monday","Multimedia":"Multimedia","nachm.":"nachm.","Nachricht schreiben. Enter zum Abschicken.":"Nachricht schreiben. Enter zum Abschicken.","Nachrichtenbox schlie\u00dfen":"Close message box","Name":"Name","Name der neuen Seite":"Name der neuen Seite","Name des \u00dcbergabeparameters":"Name des \u00dcbergabeparameters","Namensnennung":"Namensnennung","Namensnennung & Keine Bearbeitung":"Namensnennung & Keine Bearbeitung","Namensnennung & Nicht kommerziell":"Namensnennung & Nicht kommerziell","Namensnennung & Nicht kommerziell & Keine Bearbeitung":"Namensnennung & Nicht kommerziell & Keine Bearbeitung","Namensnennung & Nicht kommerziell & Weitergabe unter gleichen Bedingungen":"Namensnennung & Nicht kommerziell & Weitergabe unter gleichen Bedingungen","Namensnennung & Weitergabe unter gleichen Bedingungen":"Namensnennung & Weitergabe unter gleichen Bedingungen","Navigation":"Navigation","Neben der aktuellen Seite":"Neben der aktuellen Seite","Nein":"No","Neu laden":"Neu laden","Neuen Termin eintragen":"Neuen Termin eintragen","Nicht buchbare R\u00e4ume:":"Nicht buchbare R\u00e4ume:","Niveau":"Niveau","Noch nicht komplett ausgef\u00fcllt.":"Noch nicht komplett ausgef\u00fcllt.","normal":"normal","Normal":"Normal","Nov":"Nov","November":"November","Nr.":"No.","Nur buchbare R\u00e4ume anzeigen":"Display bookable rooms only","nur f\u00fcr Lehrede":"nur f\u00fcr Lehrede","Nur neue Inhalte anzeigen":"Nur neue Inhalte anzeigen","Nutzerspezifische ID \u00fcbergeben":"Nutzerspezifische ID \u00fcbergeben","oder":"or","Ok":"Ok","Okt":"Okt","Oktober":"October","Optional weitere Studiengangteile (max. 5)":"Optional weitere Studiengangteile (max. 5)","orange":"orange","Orange":"Orange","Ordner":"Folder","Ordner-Filter":"Ordner-Filter","Oval hinzuf\u00fcgen":"Oval hinzuf\u00fcgen","Polardiagramm":"Polardiagramm","Port":"Port","Position der neuen Seite":"Position der neuen Seite","Prima! Hier sind meine Vorschl\u00e4ge.":"Prima! Hier sind meine Vorschl\u00e4ge.","Quelle":"Resource","Quelle ausw\u00e4hlen":"Select source","Quelle nachher":"Quelle nachher","Quelle vorher":"Quelle vorher","Radius":"Radius","Rechte":"Rechte","Rechteck hinzuf\u00fcgen":"Rechteck hinzuf\u00fcgen","riesig":"riesig","Ringdiagramm":"Ringdiagramm","rot":"rot","Rot":"Red","R\u00fcckg\u00e4ngig":"R\u00fcckg\u00e4ngig","Sa":"Sat.","Samstag":"Saturday","S\u00e4ulendiagramm":"S\u00e4ulendiagramm","Schliessen":"Close","schlie\u00dfen":"schlie\u00dfen","Schlie\u00dfen":"Close","Schnell":"Schnell","Schreib was, frag was. Enter zum Abschicken.":"Schreib was, frag was. Enter zum Abschicken.","Schreiben Sie ein Feedback...":"Schreiben Sie ein Feedback...","Schriftart":"Schriftart","Schriftgr\u00f6\u00dfe":"Schriftgr\u00f6\u00dfe","schwarz":"schwarz","Schwarz":"Schwarz","Sehr schnell":"Sehr schnell","Seite auf":"Seite auf","Seite auf %{oerTitle} ver\u00f6ffentlichen":"Seite auf %{oerTitle} ver\u00f6ffentlichen","Seite bearbeiten":"Edit page","Seite exportieren":"Seite exportieren","Seite hinzuf\u00fcgen":"Seite hinzuf\u00fcgen","Seite l\u00f6schen":"Delete page","Seite unwiderruflich l\u00f6schen":"Seite unwiderruflich l\u00f6schen","Seite wurde an OER Campus gesendet.":"Seite wurde an OER Campus gesendet.","Seite wurde erstellt am":"Seite wurde erstellt am","Seite wurde erstellt von":"Seite wurde erstellt von","Seite zum kopieren f\u00fcr Lehrende freigeben":"Seite zum kopieren f\u00fcr Lehrende freigeben","Seiten":"Seiten","Seiten sortieren":"Seiten sortieren","Seiten, Abschnitte und Bl\u00f6cke lassen sich aus einer anderen Veranstaltung und Ihren\n eigenen Inhalten kopieren.\n Hierzu w\u00e4hlen Sie auf der linken Seite unter \"Diese Courseware\" die Schaltfl\u00e4che\n \"Seite an diese Stelle einf\u00fcgen\", \"Abschnitt an diese Stelle einf\u00fcgen\" oder\n \"Block an diese Stelle einf\u00fcgen\". W\u00e4hlen Sie dann auf der rechten Seite unter\n \"Kopieren\" erst die Veranstaltung aus der Sie kopieren m\u00f6chten oder Ihre eigenen\n Inhalte. W\u00e4hlen sie dann das Objekt aus das Sie kopieren m\u00f6chten. Kopierbare Objekte\n erkennen Sie an den zwei nach links zeigenden gelben Pfeilen.":"Seiten, Abschnitte und Bl\u00f6cke lassen sich aus einer anderen Veranstaltung und Ihren\n eigenen Inhalten kopieren.\n Hierzu w\u00e4hlen Sie auf der linken Seite unter \"Diese Courseware\" die Schaltfl\u00e4che\n \"Seite an diese Stelle einf\u00fcgen\", \"Abschnitt an diese Stelle einf\u00fcgen\" oder\n \"Block an diese Stelle einf\u00fcgen\". W\u00e4hlen Sie dann auf der rechten Seite unter\n \"Kopieren\" erst die Veranstaltung aus der Sie kopieren m\u00f6chten oder Ihre eigenen\n Inhalte. W\u00e4hlen sie dann das Objekt aus das Sie kopieren m\u00f6chten. Kopierbare Objekte\n erkennen Sie an den zwei nach links zeigenden gelben Pfeilen.","Seiten, Abschnitte und Bl\u00f6cke lassen sich in ihrer Reihenfolge sortieren.\n Hierzu w\u00e4hlen Sie auf der linken Seite unter \"Diese Courseware\" die Schaltfl\u00e4che \"Seiten sortieren\",\n \"Abschnitte sortieren\" oder \"Bl\u00f6cke sortieren\".\n An den Objekten werden Pfeile angezeigt, mit diesen k\u00f6nnen die Objekte an die gew\u00fcnschte\n Position gebracht werden. Um die neue Sortierung zu speichern w\u00e4hlen Sie \"Sortieren beenden\".\n Sie k\u00f6nnen die \u00c4nderungen auch r\u00fcckg\u00e4ngig machen indem Sie \"Sortieren abbrechen\" w\u00e4hlen.":"Seiten, Abschnitte und Bl\u00f6cke lassen sich in ihrer Reihenfolge sortieren.\n Hierzu w\u00e4hlen Sie auf der linken Seite unter \"Diese Courseware\" die Schaltfl\u00e4che \"Seiten sortieren\",\n \"Abschnitte sortieren\" oder \"Bl\u00f6cke sortieren\".\n An den Objekten werden Pfeile angezeigt, mit diesen k\u00f6nnen die Objekte an die gew\u00fcnschte\n Position gebracht werden. Um die neue Sortierung zu speichern w\u00e4hlen Sie \"Sortieren beenden\".\n Sie k\u00f6nnen die \u00c4nderungen auch r\u00fcckg\u00e4ngig machen indem Sie \"Sortieren abbrechen\" w\u00e4hlen.","Seiten, Abschnitte und Bl\u00f6cke lassen sich verschieben.\n Hierzu w\u00e4hlen Sie auf der linken Seite unter \"Diese Courseware\" die Schaltfl\u00e4che\n \"Seite an diese Stelle einf\u00fcgen\", \"Abschnitt an diese Stelle einf\u00fcgen\" oder\n \"Block an diese Stelle einf\u00fcgen\". W\u00e4hlen Sie dann auf der rechten Seite unter\n \"Verschieben\" das Objekt aus das Sie verschieben m\u00f6chten. Verschiebbare Objekte\n erkennen Sie an den zwei nach links zeigenden gelben Pfeilen.":"Seiten, Abschnitte und Bl\u00f6cke lassen sich verschieben.\n Hierzu w\u00e4hlen Sie auf der linken Seite unter \"Diese Courseware\" die Schaltfl\u00e4che\n \"Seite an diese Stelle einf\u00fcgen\", \"Abschnitt an diese Stelle einf\u00fcgen\" oder\n \"Block an diese Stelle einf\u00fcgen\". W\u00e4hlen Sie dann auf der rechten Seite unter\n \"Verschieben\" das Objekt aus das Sie verschieben m\u00f6chten. Verschiebbare Objekte\n erkennen Sie an den zwei nach links zeigenden gelben Pfeilen.","Seitenverh\u00e4ltnis":"Seitenverh\u00e4ltnis","Sektion entfernen":"Sektion entfernen","Sekunde":"Sekunde","Sekunden":"Seconds","Senden":"Senden","Sep":"Sep","September":"September","Sequentiell":"Sequentiell","Server hinzuf\u00fcgen":"Server hinzuf\u00fcgen","Sichtbar ab":"Visible from","sichtbar setzen":"sichtbar setzen","Sichtbarkeit":"Visibility","Sie haben <%= count %> Personen ausgew\u00e4hlt":"Sie haben <%= count %> Personen ausgew\u00e4hlt","Sie haben nicht angegeben, wer die Nachricht empfangen soll!":"You did not specify who should receive the message!","Sie haben noch keine Anmelderegeln festgelegt.":"You haven't yet defined any admission rule.","Sie haben noch keine eigenen Inhalte angelegt":"Sie haben noch keine eigenen Inhalte angelegt","Sie haben noch keine Lieblingsbl\u00f6cke ausgew\u00e4hlt.":"Sie haben noch keine Lieblingsbl\u00f6cke ausgew\u00e4hlt.","Sie haben noch niemanden hinzugef\u00fcgt.":"Sie haben noch niemanden hinzugef\u00fcgt.","Sie haben zur Zeit keine Veranstaltungen belegt, an denen Sie teilnehmen k\u00f6nnen.\n <br>\n Bitte nutzen Sie <a :href=\"searchCoursesUrl\"> <strong>Veranstaltung suchen \/ hinzuf\u00fcgen<\/strong> <\/a> um sich f\u00fcr Veranstaltungen anzumelden.\n <\/br>":"Sie haben zur Zeit keine Veranstaltungen belegt, an denen Sie teilnehmen k\u00f6nnen.\n <br>\n Bitte nutzen Sie <a :href=\"searchCoursesUrl\"> <strong>Veranstaltung suchen \/ hinzuf\u00fcgen<\/strong> <\/a> um sich f\u00fcr Veranstaltungen anzumelden.\n <\/br>","Sie k\u00f6nnen diese Daten unter \"Seite bearbeiten\" ver\u00e4ndern":"Sie k\u00f6nnen diese Daten unter \"Seite bearbeiten\" ver\u00e4ndern","Sie k\u00f6nnen nur %u Eintrag ausw\u00e4hlen":"Sie k\u00f6nnen nur %u Eintrag ausw\u00e4hlen","Sie k\u00f6nnen nur %u Eintr\u00e4ge ausw\u00e4hlen":"Sie k\u00f6nnen nur %u Eintr\u00e4ge ausw\u00e4hlen","Sie m\u00fcssen ein Mikrofon freigeben, um eine Aufnahme starten zu k\u00f6nnen.":"Sie m\u00fcssen ein Mikrofon freigeben, um eine Aufnahme starten zu k\u00f6nnen.","Sie sind nicht mehr im System angemeldet.":"Sie sind nicht mehr im System angemeldet.","Smileys":"Smileys","So":"Sun.","Soll der WYSIWYG Editor aktiviert werden?":"Soll der WYSIWYG Editor aktiviert werden?","Soll die ausgew\u00e4hlte Berechtigung wirklich entfernt werden?":"Soll die ausgew\u00e4hlte Berechtigung wirklich entfernt werden?","Sonnenschein":"Sonnenschein","Sonntag":"Sunday","Sonstiges":"Miscellanea","Sortieren abbrechen":"Sortieren abbrechen","Sortieren beenden":"Sortieren beenden","Speichern":"Save","Speicherort":"Speicherort","Sprache":"Language","Standard":"Standard","Starte die Konversation jetzt!":"Starte die Konversation jetzt!","Startpunkt w\u00e4hlen":"Startpunkt w\u00e4hlen","Status":"Status","Stellen Sie eine Frage oder kommentieren Sie...":"Stellen Sie eine Frage oder kommentieren Sie...","Studiengang suchen":"Search course of study","Studiengangteil suchen":"Search course of study","Studierende":"Students","Stunde":"Hour","Stunden":"Stunden","Suche zur\u00fccksetzen":"Reset search","Suche...":"Searching...","Suchergebnisse":"Search results","Tab hinzuf\u00fcgen":"Tab hinzuf\u00fcgen","Tab l\u00f6schen":"Tab l\u00f6schen","Tabellarische Ansicht":"Tabellarische Ansicht","Tag":"Day","Tage":"Days","Termindetails bearbeiten":"Termindetails bearbeiten","Text":"Text","Text R\u00fcckseite":"Text R\u00fcckseite","Text Vorderseite":"Text Vorderseite","Texte":"Texte","Texteingabe mit Enter-Taste best\u00e4tigen":"Texteingabe mit Enter-Taste best\u00e4tigen","Textfarbe":"Textfarbe","Textwerkzeug":"Textwerkzeug","Titel":"Title","Title":"Title","Transparent":"Transparent","t\u00fcrkis":"t\u00fcrkis","Typ":"Type","\u00dcberblick":"\u00dcberblick","\u00dcbernehmen":"Accept","\u00dcberschrift":"Header","Uhrzeit":"Time","Um die Veranstaltung sichtbar zu machen, w\u00e4hlen Sie den Punkt \"Sichtbarkeit\" im Administrationsbereich der Veranstaltung.":"In order to enable visibility of the course, please choose the tab \"visibility\" in the administration area of the course.","Um die Veranstaltung sichtbar zu machen, wenden Sie sich an Administrierende.":"Um die Veranstaltung sichtbar zu machen, wenden Sie sich an Administrierende.","Um weleche Art von Datei(en) handelt es sich?":"Um weleche Art von Datei(en) handelt es sich?","Unsichtbar ab":"Unsichtbar ab","unsichtbar f\u00fcr Nutzende ohne Schreibrecht":"unsichtbar f\u00fcr Nutzende ohne Schreibrecht","unsichtbar setzen":"unsichtbar setzen","Unterhalb der aktuellen Seite":"Unterhalb der aktuellen Seite","Unterseiten exportieren":"Unterseiten exportieren","Unterseiten ver\u00f6ffentlichen":"Unterseiten ver\u00f6ffentlichen","Untertitel":"Subtitle","URL":"URL","Veranstaltung ber\u00fccksichtigen":"Regard course","Veranstaltung nicht ber\u00fccksichtigen":"Do not regard course","Veranstaltungen":"Courses","Veranstaltungsdetails":"Course details","Veranstaltungstyp ausw\u00e4hlen (optional)":"Veranstaltungstyp ausw\u00e4hlen (optional)","Verhindern":"Verhindern","ver\u00f6ffentlichen":"ver\u00f6ffentlichen","Ver\u00f6ffentlichen":"Ver\u00f6ffentlichen","ver\u00f6ffentlicht auf":"ver\u00f6ffentlicht auf","Versteckte Veranstaltungen k\u00f6nnen \u00fcber die Suchfunktionen nicht gefunden werden.":"Hidden courses cannot be found via the search feature.","Video":"Video","Video startet automatisch":"Video startet automatisch","Voll":"Voll","Vollbild ausschalten":"Vollbild ausschalten","Vollbild einschalten":"Vollbild einschalten","von":"from","Vor":"Vor","Vor %s Minuten":"Vor %s Minuten","Vorlage":"Template","vorm.":"vorm.","Vorschaubild":"Vorschaubild","W\u00e4hlen Sie auf der linken Seite \"Diese Courseware\" aus.\n Beim laden der Seite ist dies immer gew\u00e4hlt. Die \u00dcberschrift\n gibt an welche Seite Sie grade ausgew\u00e4hlt haben. Darunter befinden\n sich die Abschnitte der Seite und innerhalb dieser dessen Bl\u00f6cke.\n M\u00f6chten Sie eine Seite die unterhalb der gew\u00e4hlten liegt bearbeiten,\n k\u00f6nnen Sie diese \u00fcber die Schaltfl\u00e4chen im Bereich \"Seiten\" w\u00e4hlen.\n \u00dcber der \u00dcberschrift wird eine Navigation eingeblendet, mit dieser k\u00f6nnen\n Sie beliebig weit hoch in der Hierarchie springen.":"W\u00e4hlen Sie auf der linken Seite \"Diese Courseware\" aus.\n Beim laden der Seite ist dies immer gew\u00e4hlt. Die \u00dcberschrift\n gibt an welche Seite Sie grade ausgew\u00e4hlt haben. Darunter befinden\n sich die Abschnitte der Seite und innerhalb dieser dessen Bl\u00f6cke.\n M\u00f6chten Sie eine Seite die unterhalb der gew\u00e4hlten liegt bearbeiten,\n k\u00f6nnen Sie diese \u00fcber die Schaltfl\u00e4chen im Bereich \"Seiten\" w\u00e4hlen.\n \u00dcber der \u00dcberschrift wird eine Navigation eingeblendet, mit dieser k\u00f6nnen\n Sie beliebig weit hoch in der Hierarchie springen.","Web-Adresse":"Web-Adresse","Wei\u00df":"Wei\u00df","weiter":"continue","Wenn Sie die regelm\u00e4\u00dfige Zeit \u00e4ndern, verlieren Sie die Raumbuchungen f\u00fcr alle in der Zukunft liegenden Termine! Sind Sie sicher, dass Sie die regelm\u00e4\u00dfige Zeit \u00e4ndern m\u00f6chten?":"Wenn Sie die regelm\u00e4\u00dfige Zeit \u00e4ndern, verlieren Sie die Raumbuchungen f\u00fcr alle in der Zukunft liegenden Termine! Sind Sie sicher, dass Sie die regelm\u00e4\u00dfige Zeit \u00e4ndern m\u00f6chten?","Werk":"Werk","Wert":"Value","Werte anderer Nutzer anzeigen":"Werte anderer Nutzer anzeigen","Wie finde ich die gew\u00fcnschte Stelle?":"Wie finde ich die gew\u00fcnschte Stelle?","Wie kopiere ich Objekte?":"Wie kopiere ich Objekte?","Wie sortiere ich Objekte?":"Wie sortiere ich Objekte?","Wie verschiebe ich Objekte?":"Wie verschiebe ich Objekte?","Willkommen bei Courseware":"Willkommen bei Courseware","Wird geladen":"Wird geladen","wirklich l\u00f6schen?":"wirklich l\u00f6schen?","Wo":"Wo","Wollen Sie die Aktion wirklich ausf\u00fchren?":"Wollen Sie die Aktion wirklich ausf\u00fchren?","Wollen Sie die gew\u00fcnschten Termine wirklich l\u00f6schen?":"Wollen Sie die gew\u00fcnschten Termine wirklich l\u00f6schen?","Wollen Sie die im Plan gezeigten Anfragen wirklich buchen?":"Wollen Sie die im Plan gezeigten Anfragen wirklich buchen?","Wollen Sie diese Termine wirklich l\u00f6schen?":"Wollen Sie diese Termine wirklich l\u00f6schen?","wurde erfolgreich angelegt.":"wurde erfolgreich angelegt.","Zeichen verbleibend: ":"Zeichen verbleibend: ","Zeichenwerkzeug":"Zeichenwerkzeug","Zeit":"Time","Zeit w\u00e4hlen":"Zeit w\u00e4hlen","Zeitzone":"Zeitzone","Ziel des Links":"Ziel des Links","Zufallszeichen f\u00fcr Verschl\u00fcsselung (Salt)":"Zufallszeichen f\u00fcr Verschl\u00fcsselung (Salt)","Zuletzt bearbeitet am":"Zuletzt bearbeitet am","Zuletzt bearbeitet von":"Zuletzt bearbeitet von","Zum Hauptordner":"Go to main folder","Zur Diskussion":"Zur Diskussion","Zur Gesamt\u00fcbersicht":"Zur Gesamt\u00fcbersicht","zur n\u00e4chsten Karte":"zur n\u00e4chsten Karte","zur vorherigen Karte":"zur vorherigen Karte","zur\u00fcck":"back","Zur\u00fcck":"Back","Zur\u00fccksetzen":"Reset","Zweck":"Purpose","zweites Element":"zweites Element"}
\ No newline at end of file diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js new file mode 100644 index 0000000..54d52fe --- /dev/null +++ b/resources/vue/base-components.js @@ -0,0 +1,29 @@ +import Quicksearch from './components/Quicksearch.vue'; +import StudipActionMenu from './components/StudipActionMenu.vue'; +import StudipAssetImg from './components/StudipAssetImg.vue'; +import StudipDateTime from './components/StudipDateTime.vue'; +import StudipDialog from './components/StudipDialog.vue'; +import StudipFileSize from './components/StudipFileSize.vue'; +import StudipIcon from './components/StudipIcon.vue'; +// import StudipLoadingIndicator from './StudipLoadingIndicator.vue'; +import StudipMessageBox from './components/StudipMessageBox.vue'; +import StudipProxyCheckbox from './components/StudipProxyCheckbox.vue'; +import StudipProxiedCheckbox from './components/StudipProxiedCheckbox.vue'; +import StudipTooltipIcon from './components/StudipTooltipIcon.vue'; + +const BaseComponents = { + Quicksearch, + StudipActionMenu, + StudipAssetImg, + StudipDateTime, + StudipDialog, + StudipFileSize, + StudipIcon, +// StudipLoadingIndicator, + StudipMessageBox, + StudipProxyCheckbox, + StudipProxiedCheckbox, + StudipTooltipIcon, +}; + +export default BaseComponents; diff --git a/resources/vue/base-directives.js b/resources/vue/base-directives.js new file mode 100644 index 0000000..a2b8ae1 --- /dev/null +++ b/resources/vue/base-directives.js @@ -0,0 +1,4 @@ +const BaseDirectives = { +}; + +export default BaseDirectives; diff --git a/resources/vue/components/BlubberGlobalstream.vue b/resources/vue/components/BlubberGlobalstream.vue new file mode 100644 index 0000000..0c4976c --- /dev/null +++ b/resources/vue/components/BlubberGlobalstream.vue @@ -0,0 +1,129 @@ +<template> + <div class="blubber_globalstream"> + <div class="scrollable_area" v-scroll> + <blubber-public-composer></blubber-public-composer> + <ol class="postings" aria-live="polite"> + <li class="more" v-if="stream_data.more_up"> + <studip-asset-img file="ajax-indicator-black.svg" width="20"></studip-asset-img> + </li> + + <li :class="blubber.class" + v-for="blubber in sortedPostings" + :data-thread_id="blubber.thread_id" + :key="blubber.thread_id"> + <div class="thread_posting" v-if="blubber.html"> + <div class="contextinfo"> + <studip-date-time :timestamp="blubber.mkdate" :relative="true"></studip-date-time> + <div>{{ blubber.user_name }}</div> + <div class="avatar" :style="{ backgroundImage: 'url(' + blubber.avatar + ')' }"></div> + </div> + <div class="content" v-html="blubber.html"></div> + <a class="link_to_comments" + :href="link(blubber.thread_id)" + @click.prevent="changeActiveThread" v-translate>Zur Diskussion</a> + </div> + </li> + + <li class="more" v-if="more_down"> + <studip-asset-img file="ajax-indicator-black.svg" width="20"></studip-asset-img> + </li> + </ol> + </div> + </div> +</template> + +<script> + export default { + name: 'blubber-globalstream', + data: function () { + return { + already_loading_down: 0 + }; + }, + props: ['stream_data', 'more_down'], + methods: { + changeActiveThread: function (event) { + let li = $(event.target).closest('li'); + this.$root.changeActiveThread(li.data('thread_id')); + }, + link: function (thread_id) { + return STUDIP.URLHelper.getURL(`dispatch.php/blubber/index/${thread_id}`); + }, + addPosting: function (posting) { + let exists = false; + for (let i in this.stream_data) { + if (this.stream_data[i].thread_id === posting.thread_id) { + exists = true; + return; + } + } + if (!exists) { + posting.class = posting.class + " new"; + this.stream_data.push(posting); + this.$nextTick(() => { + STUDIP.Markup.element($(this.$el).find(`.postings > li[data-thread_id="${posting.thread_id}"]`)); + }); + } + } + }, + mounted () { //when everything is initialized + this.$nextTick(function () { + $(this.$el).find('.postings .content').each(function () { + STUDIP.Markup.element(this); + }); + }); + }, + computed: { + sortedPostings: function () { + return this.stream_data.sort((a, b) => b.mkdate - a.mkdate); + } + }, + directives: { + scroll: { + // directive definition + inserted: function (el) { + let stream = $(el).closest(".blubber_globalstream")[0].__vue__; + $(el).on('scroll', function (event) { + let top = $(el).scrollTop(); + let height = $(el).find(".postings").height(); + + $(el).toggleClass('scrolled', top > 0); + + if (stream.more_down && (top > $(el).find(".postings").height() - 1000) + && !stream.already_loading_down) { + stream.already_loading_down = 1; + + let earliest_mkdate = null; + for (let i in stream.stream_data) { + if ((earliest_mkdate === null) || stream.stream_data[i].mkdate < earliest_mkdate) { + earliest_mkdate = stream.stream_data[i].mkdate; + } + } + //load older comments + $.ajax({ + url: STUDIP.ABSOLUTE_URI_STUDIP + "api.php/blubber/threads/global", + type: "get", + dataType: "json", + data: { + modifier: "olderthan", + timestamp: earliest_mkdate, + limit: 30 + }, + success: function (data) { + for (let i in data.postings) { + stream.addPosting(data.postings[i]); + } + stream.more_down = data.more_down; + }, + complete: function () { + stream.already_loading_down = 0; + } + }); + + } + }); + } + } + } + } +</script> diff --git a/resources/vue/components/BlubberPublicComposer.vue b/resources/vue/components/BlubberPublicComposer.vue new file mode 100644 index 0000000..15fce7c --- /dev/null +++ b/resources/vue/components/BlubberPublicComposer.vue @@ -0,0 +1,98 @@ +<template> + <div class="writer"> + <studip-icon shape="blubber" size="30" role="info"></studip-icon> + <textarea :placeholder="$gettext('Schreib was, frag was. Enter zum Abschicken.')" + @keyup.enter.exact="submit" + @keyup="saveCommentToSession" @change="saveCommentToSession"></textarea> + <label class="upload" :title="$gettext('Datei hochladen')"> + <input type="file" multiple style="display: none;" @change="upload"> + <studip-icon shape="upload" size="30"></studip-icon> + </label> + </div> +</template> +<script> + export default { + name: 'blubber-public-composer', + methods: { + submit (text) { + if (!text || typeof text !== "string") { + text = $(this.$el).find("textarea").val(); + $(this.$el).find("textarea").val(""); + sessionStorage.removeItem( + 'BlubberMemory-Writer-Public' + ); + } + if (!text.trim()) { + return false; + } + let thread = this; + + //AJAX-Request ... + STUDIP.api.POST(`blubber/threads`, { + data: { + content: text + } + }).done((data) => { + this.$parent.addPosting(data.thread_posting); + }); + }, + saveCommentToSession (event) { + let value = event.target.value; + sessionStorage.setItem( + `BlubberMemory-Writer-Public`, + value + ); + }, + upload (event) { + let files = typeof event.dataTransfer !== 'undefined' + ? event.dataTransfer.files // file drop + : event.target.files; // upload button + let writer = this; + let data = new FormData(); + for (let i in files) { + if (files[i].size > 0) { + data.append(`file_${i}`, files[i], files[i].name.normalize()); + } + } + + let request = new XMLHttpRequest(); + request.open('POST', `${STUDIP.ABSOLUTE_URI_STUDIP}dispatch.php/blubber/upload_files`); + request.upload.addEventListener('progress', (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 + $(writer.$el).css('background-size', `${percent}% 100%`); + }); + request.addEventListener('load', function (event) { + let output = JSON.parse(this.response); + $(writer.$el).find("textarea").val( + $(writer.$el).find("textarea").val() + + " " + + output.inserts.join(" ") + ); + }); + request.addEventListener('loadend', function (event) { + $(writer.$el).css('background-size', '0% 100%'); + }); + request.send(data); + } + }, + mounted () { //when everything is initialized + this.$nextTick(function () { + $(this.$el).find('textarea').autoResize({ + animateDuration: 0, + // More extra space: + extraSpace: 1 + }); + let memory = sessionStorage.getItem(`BlubberMemory-Writer-Public`); + if (memory) { + $(this.$el).find('textarea').val(memory); + } + }); + } + } +</script> diff --git a/resources/vue/components/BlubberThread.vue b/resources/vue/components/BlubberThread.vue new file mode 100644 index 0000000..f07e2e4 --- /dev/null +++ b/resources/vue/components/BlubberThread.vue @@ -0,0 +1,495 @@ +<template> + <div class="blubber_thread" + :id="'blubberthread_' + thread_data.thread_posting.thread_id" + @dragover.prevent="dragover" @dragleave.prevent="dragleave" + @drop.prevent="upload"> + <div class="responsive-visible context_info" v-if="thread_data.notifications"> + <a href="#" + @click.prevent="toggleFollow()" + class="followunfollow" + :class="{unfollowed: !thread_data.followed}" + :title="$gettext('Benachrichtigungen für diese Konversation abstellen.')" + :data-thread_id="thread_data.thread_posting.thread_id"> + <StudipIcon shape="remove/notification2" :size="20" class="follow text-bottom"></StudipIcon> + <StudipIcon shape="notification2" :size="20" class="unfollow text-bottom"></StudipIcon> + {{ $gettext('Benachrichtigungen aktiviert') }} + </a> + </div> + <div class="scrollable_area" v-scroll> + <div class="all_content"> + <div class="thread_posting" v-if="hasContent(thread_data.thread_posting.content)"> + <div class="contextinfo"> + <studip-date-time :timestamp="thread_data.thread_posting.mkdate" :relative="true"></studip-date-time> + <a :href="getUserProfileURL(thread_data.thread_posting.user_id, thread_data.thread_posting.user_username)">{{ thread_data.thread_posting.user_name }}</a> + <a :href="getUserProfileURL(thread_data.thread_posting.user_id, thread_data.thread_posting.user_username)" class="avatar" :style="{ backgroundImage: 'url(' + thread_data.thread_posting.avatar + ')' }"></a> + </div> + <div class="content" v-html="thread_data.thread_posting.html"></div> + <div class="link_to_comments"></div> + </div> + + <div v-if="!hasContent(thread_data.thread_posting.content) && !thread_data.comments.length" class="empty_blubber_background"> + <div v-translate>Starte die Konversation jetzt!</div> + </div> + + <ol class="comments" aria-live="polite"> + + <li class="more" v-if="thread_data.more_up"> + <studip-asset-img file="ajax-indicator-black.svg" width="20"></studip-asset-img> + </li> + + <li :class="comment.class" + v-for="comment in sortedComments" + :data-comment_id="comment.comment_id" + :key="comment.comment_id"> + <a :href="getUserProfileURL(comment.user_id, comment.user_username)" class="avatar" :title="comment.user_name" :style="{ backgroundImage: 'url(' + comment.avatar + ')' }"></a> + <div class="content"> + <a :href="getUserProfileURL(comment.user_id, comment.user_username)" class="name">{{ comment.user_name }}</a> + <div v-html="comment.html" class="html"></div> + <textarea class="edit" + v-html="comment.content" + @keyup.enter.exact="saveComment" + @keyup.escape.exact="editComment"></textarea> + </div> + <div class="time"> + <studip-date-time :timestamp="comment.mkdate" :relative="true"></studip-date-time> + <a href="" v-if="comment.writable" @click.prevent="editComment" class="edit_comment" :title="$gettext('Bearbeiten.')"> + <studip-icon shape="edit" size="14" role="inactive"></studip-icon> + </a> + <a href="" @click.prevent="answerComment" class="answer_comment" :title="$gettext('Hierauf antworten.')"> + <studip-icon shape="export" size="14" role="inactive"></studip-icon> + </a> + </div> + </li> + + <li class="more" v-if="thread_data.more_down"> + <studip-asset-img file="ajax-indicator-black.svg" width="20"></studip-asset-img> + </li> + + </ol> + </div> + </div> + <div class="writer" v-if="thread_data.thread_posting.commentable"> + <studip-icon shape="blubber" size="30" role="info"></studip-icon> + <textarea :placeholder="writerTextareaPlaceholder" + @keyup.enter.exact="submit" + @keyup.up.exact="editPreviousComment" + @keyup="saveCommentToSession" @change="saveCommentToSession"></textarea> + <a class="send" @click="submit" :title="$gettext('Abschicken')"> + <studip-icon shape="arr_2up" size="30"></studip-icon> + </a> + <label class="upload" :title="$gettext('Datei hochladen')"> + <input type="file" multiple style="display: none;" @change="upload"> + <studip-icon shape="upload" size="30"></studip-icon> + </label> + </div> + </div> +</template> + +<script> + export default { + name: 'blubber-thread', + data: function () { + return { + already_loading_up: 0, + already_loading_down: 0 + }; + }, + props: ['thread_data'], + methods: { + submit (text) { + if (!text || typeof text !== "string") { + text = $(this.$el).find(".writer textarea").val(); + $(this.$el).find(".writer textarea").val(""); + if (this.thread_data.thread_posting.thread_id) { + sessionStorage.removeItem( + 'BlubberMemory-Writer-' + this.thread_data.thread_posting.thread_id + ); + } + } + if (!text.trim()) { + return false; + } + let formatted_text = text.replace(/\n/g, "<br>"); + let comment = { + comment_id: Math.random().toString(36), + avatar: '', + html: formatted_text, + content: text, + mkdate: Math.floor(Date.now() / 1000), + name: 'Nobody', + class: 'mine new', + writable: 1 + }; + this.addComment(comment); + let thread = this; + + //AJAX-Request ... + STUDIP.api.POST(`blubber/threads/${this.thread_data.thread_posting.thread_id}/comments`, { + data: { + content: text + } + }).then(data => { + // Check following state + if (this.thread_data.notifications) { + STUDIP.api.GET(`blubber/threads/${this.thread_data.thread_posting.thread_id}/follow`).then(followed => { + jQuery('.followunfollow').toggleClass('unfollowed', !followed); + }); + } + return data; + }).done(data => { + comment.comment_id = data.comment_id; + comment.avatar = data.avatar; + comment.user_name = data.user_name; + comment.mkdate = data.mkdate; + comment.html = data.html; + comment.class = data.class; + + thread.$nextTick(() => { + STUDIP.Markup.element($(thread.$el).find(`.comments > li[data-comment_id="${data.comment_id}"]`)); + }); + }); + + this.$nextTick(() => { + // DOM updated + this.scrollDown(); + }); + }, + saveCommentToSession (event) { + let value = event.target.value; + if (this.thread_data.thread_posting.thread_id) { + sessionStorage.setItem( + `BlubberMemory-Writer-${this.thread_data.thread_posting.thread_id}`, + value + ); + } + $(this.$el).find('.writer').toggleClass( + 'filled', + value.trim() !== '' + ); + }, + scrollDown () { + this.$nextTick(function () { + let element = this.$el; + + let scroll = () => { + $(element).find('.scrollable_area').scrollTo( + $(element).find('.scrollable_area .all_content').height() + ); + }; + + $(element).find('.scrollable_area img').on('load', scroll); + scroll(); + }); + }, + addComments (comments, new_ones) { + comments.forEach((comment) => { + if (new_ones) { + comment.class += ' new'; + } + this.addComment(comment); + }); + }, + addComment (comment) { + this.$nextTick(() => { + STUDIP.Markup.element($(this.$el).find(`.comments > li[data-comment_id="${comment.comment_id}"]`)); + }); + for (let i in this.thread_data.comments) { + if (this.thread_data.comments[i].comment_id === comment.comment_id) { + this.thread_data.comments[i].content = comment.content; + this.thread_data.comments[i].html = comment.html; + return; + } + } + this.thread_data.comments.push(comment); + }, + removeComment (comment_id) { + this.thread_data.comments.forEach((comment, i) => { + if (comment.comment_id === comment_id) { + this.$delete(this.thread_data.comments, i); + } + }); + }, + upload (event) { + let files = typeof event.dataTransfer !== 'undefined' + ? event.dataTransfer.files // file drop + : event.target.files; // upload button + let thread = this; + let data = new FormData(); + for (let i in files) { + if (files[i].size > 0) { + data.append(`file_${i}`, files[i], files[i].name.normalize()); + } + } + + var request = new XMLHttpRequest(); + request.open('POST', `${STUDIP.ABSOLUTE_URI_STUDIP}dispatch.php/blubber/upload_files`); + request.upload.addEventListener('progress', (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 + $(thread.$el).find('.writer').css('background-size', `${percent}% 100%`); + }); + request.addEventListener('load', function (event) { + let output = JSON.parse(this.response); + thread.submit(output.inserts.join(" ")); + }); + request.addEventListener('loadend', function (event) { + $(thread.$el).find('.writer').css('background-size', '0% 100%'); + }); + request.send(data); + + this.dragleave(); + }, + dragover () { + $(this.$el).addClass('dragover'); + }, + dragleave () { + $(this.$el).removeClass('dragover'); + }, + getUserProfileURL (user_id, username) { + if (username) { + return STUDIP.URLHelper.getURL('dispatch.php/profile', { + username: username + }); + } else { + return STUDIP.URLHelper.getURL('dispatch.php/profile/extern/' + user_id); + } + }, + editComment (event) { + let li; + if (typeof event === 'string') { + let comment_id = event; + li = $(this.$el).find(`.comments > li[data-comment_id="${comment_id}"]`); + } else { + li = $(event.target).closest('li[data-comment_id]'); + let comment_id = $(event.target).closest('li[data-comment_id]').data('comment_id'); + } + li.find('.content').toggleClass('editing'); + let textarea = li.find('.content textarea').last()[0]; + textarea.focus(); + textarea.setSelectionRange(textarea.value.length, textarea.value.length); + li.find('.content textarea:not(.auto-resizable)').addClass('auto-resizable').autoResize({ + animateDuration: 0 + }); + }, + answerComment (event) { + let li; + if (typeof event === 'string') { + let comment_id = event; + li = $(this.$el).find(`.comments > li[data-comment_id="${comment_id}"]`); + } else { + li = $(event.target).closest('li[data-comment_id]'); + let comment_id = $(event.target).closest('li[data-comment_id]').data('comment_id'); + } + let comment_id = $(li).data('comment_id'); + let comment_data = null; + this.thread_data.comments.forEach((comment, i) => { + if (comment.comment_id === comment_id) { + comment_data = comment; + } + }); + if (comment_data) { + let quote = '[quote=' + comment_data.user_name + ']' + (comment_data.content.replace(/\[quote[^\]]*\].*\[\/quote\]/g, '')).trim() + "[/quote]\n"; + $(this.$el).find('.writer textarea').val(quote); + let textarea = $(this.$el).find('.writer textarea').last()[0]; + textarea.focus(); + textarea.setSelectionRange(textarea.value.length, textarea.value.length); + } + }, + saveComment (event) { + let thread = this; + let li = $(event.target).closest('li[data-comment_id]'); + let comment_id = li.data('comment_id'); + let content = li.find('textarea').val(); + + thread.thread_data.comments.forEach((comment) => { + if (comment.comment_id === comment_id) { + comment.html = content; + } + }); + + li.find('.content').removeClass('editing'); + + STUDIP.api.PUT(`blubber/threads/${this.thread_data.thread_posting.thread_id}/comments/${comment_id}`, { + data: { + content: content + }, + }).done((output) => { + if (this.hasContent(output.content)) { + thread.thread_data.comments.forEach((comment) => { + if (comment.comment_id === comment_id) { + comment.html = output.html; + comment.content = output.content; + + thread.$nextTick(() => { + STUDIP.Markup.element($(thread.$el).find(`.comments > li[data-comment_id="${comment_id}"]`)); + }); + } + }); + } else { + thread.removeComment(comment_id); + } + $(thread.$el).find('.writer textarea').focus(); + }); + }, + removeDeletedComments: function (comment_ids) { + for (let i in comment_ids) { + this.removeComment(comment_ids[i]); + } + }, + editPreviousComment () { + if (!$(this.$el).find('.writer textarea').val().trim()) { + let comment = $(this.$el).find('.comments li.mine').last(); + if (comment.length > 0) { + this.editComment(comment.data('comment_id')); + } + } + }, + toggleFollow () { + STUDIP.Blubber.followunfollow( + this.thread_data.thread_posting.thread_id, + !this.thread_data.followed + ).done(state => { + this.thread_data.followed = state; + }); + }, + hasContent (input) { + return input && input.trim().length > 0; + } + }, + directives: { + scroll: { + // directive definition + inserted: function (el) { + let thread = $(el).closest('.blubber_thread')[0].__vue__; + + $(el).on('scroll', (event) => { + let top = $(el).scrollTop(); + let height = $(el).find('.all_content').height(); + + $(el).toggleClass('scrolled', top > 0); + + thread.$root.display_context_posting = top >= $(el).find('.all_content .thread_posting').height() + ? 1 + : 0; + if (thread.thread_data.more_up && top < 1000 && !thread.already_loading_up) { + thread.already_loading_up = 1; + + let earliest_mkdate = thread.thread_data.comments.reduce((min, comment) => { + return min === null ? comment.mkdate : Math.min(min, comment.mkdate); + }, null); + + //load older comments + STUDIP.api.GET(`blubber/threads/${thread.thread_data.thread_posting.thread_id}/comments`, { + data: { + modifier: 'olderthan', + timestamp: earliest_mkdate, + limit: 50 + } + }).done((data) => { + top = $(el).scrollTop(); + thread.addComments(data.comments, false); + thread.thread_data.more_up = data.more_up; + thread.$nextTick(function () { + //scroll to the position where we were: + let new_height = $(el).find(".all_content").height(); + let new_scroll_top = new_height - height + top; + $(el).scrollTo( + new_scroll_top + ); + }); + }).done(() => { + thread.already_loading_up = 0; + }); + } + + if (thread.thread_data.more_down && (top > $(thread).find(".scrollable_area .all_content").height() - 1000) && !thread.already_loading_down) { + thread.already_loading_down = 1; + + let latest_mkdate = thread.thread_data.comments.reduce((max, comment) => { + return Math.max(max, comment.mkdate); + }, null); + + //load newer comments + STUDIP.api.GET(`blubber/threads/${thread.thread_data.thread_posting.thread_id}/comments`, { + data: { + modifier: 'newerthan', + timestamp: latest_mkdate, + limit: 50 + } + }).done((data) => { + thread.addComments(data.comments, false); + thread.thread_data.more_down = data.more_down; + }).always(() => { + thread.already_loading_down = 0; + }); + } + }); + } + } + }, + mounted () { //when everything is initialized + this.$nextTick(function () { + if (this.thread_data.comments.length > 0) { + this.scrollDown(); + } + + $(this.$el).find('.writer textarea').autoResize({ + animateDuration: 0, + // More extra space: + extraSpace: 1 + }); + + $(this.$el).find('.comments .content .html').each(function () { + STUDIP.Markup.element(this); + }); + + if (this.thread_data.thread_posting.thread_id) { + let memory = sessionStorage.getItem(`BlubberMemory-Writer-${this.thread_data.thread_posting.thread_id}`); + if (memory) { + $(this.$el) + .find('.writer').addClass('filled') + .find('textarea').val(memory); + } + } + }); + }, + computed: { + sortedComments () { + return this.thread_data.comments.sort((a, b) => a.mkdate - b.mkdate); + }, + writerTextareaPlaceholder() { + return this.hasContent(this.thread_data.thread_posting.content) + ? this.$gettext('Kommentar schreiben. Enter zum Abschicken.') + : this.$gettext('Nachricht schreiben. Enter zum Abschicken.'); + } + }, + updated () { + this.$nextTick(function () { + if (this.thread_data.thread_posting.thread_id) { + let memory = sessionStorage.getItem('BlubberMemory-Writer-' + this.thread_data.thread_posting.thread_id); + $(this.$el).find('.writer textarea').val(memory); + } + }); + }, + watch: { + thread_data (new_data, old_data) { + if (new_data.thread_posting.thread_id !== old_data.thread_posting.thread_id) { + //if the thread got reloaded by a new thread + //markup contents + this.$nextTick(function () { + $(this.$el).find(".comments .content .html").each(function () { + STUDIP.Markup.element(this); + }); + }); + //and scroll down: + this.scrollDown(); + } + } + } + } +</script> diff --git a/resources/vue/components/BlubberThreadWidget.vue b/resources/vue/components/BlubberThreadWidget.vue new file mode 100644 index 0000000..12514eb --- /dev/null +++ b/resources/vue/components/BlubberThreadWidget.vue @@ -0,0 +1,111 @@ +<template> + <div class="scrollable_area blubber_thread_widget" v-scroll> + <transition-group name="blubberthreadwidget-list" + tag="ol"> + <li v-for="thread in sortedThreads" + :key="thread.thread_id" + :data-thread_id="thread.thread_id" + :class="(active_thread === thread.thread_id ? 'active' : '') + (thread.unseen_comments > 0 ? ' unseen' : '')" + :data-unseen_comments="thread.unseen_comments" + @click.prevent="changeActiveThread"> + <a :href="link(thread.thread_id)"> + <div class="avatar" + :style="{ backgroundImage: 'url(' + thread.avatar + ')' }"> + </div> + <div class="info"> + <div class="name"> + {{ thread.name }} + </div> + <studip-date-time :timestamp="thread.timestamp" :relative="true"></studip-date-time> + </div> + </a> + </li> + <li class="more" v-if="display_more_down" key="more"> + <studip-asset-img file="ajax-indicator-black.svg" width="20"></studip-asset-img> + </li> + </transition-group> + </div> +</template> + +<script> + export default { + name: 'blubber-thread-widget', + props: ['threads', 'active_thread', 'more_down'], + data () { + return { + display_more_down: this.more_down, + already_loading_down: 0 + }; + }, + methods: { + changeActiveThread (event) { + let li = $(event.target).closest('li'); + if (!li.hasClass('active')) { + li.siblings('.active').removeClass('active'); + li.addClass('active'); + this.$root.changeActiveThread(li.data('thread_id')); + } + }, + link (thread_id) { + return STUDIP.URLHelper.getURL(`dispatch.php/blubber/index/${thread_id}`); + }, + addThread (thread) { + let thread_ids = this.threads.map((t) => t.thread_id); + if (thread_ids.indexOf(thread.thread_id) !== -1) { + return; + } + this.threads.push(thread); + } + }, + directives: { + scroll: { + // directive definition + inserted (el) { + let threads = el.__vue__; + $(el).parent().on('scroll', function (event) { + let top = $(el).parent().scrollTop(); + let height = $(el).height(); + + $(el).toggleClass('scrolled', top > 0); + + if (!threads.display_more_down || (top <= height - 1000) || threads.already_loading_down) { + return; + } + + threads.already_loading_down = true; + + let latest_timestamp = threads.threads.reduce((max, thread) => { + if (thread.thread_id === 'global') { + return max; + } + return max === null ? thread.timestamp : Math.min(max, thread.timestamp); + }, null); + + //load newer comments + STUDIP.api.GET('blubber/threads', { + data: { + modifier: 'olderthan', + timestamp: latest_timestamp, + limit: 50 + } + }).done((data) => { + data.threads.forEach((thread) => threads.addThread(thread)); + + threads.display_more_down = data.more_down; + }).always(() => { + threads.already_loading_down = false; + }); + }); + } + } + }, + mounted: function () { + + }, + computed: { + sortedThreads () { + return this.threads.sort((a, b) => b.timestamp - a.timestamp); + } + } + } +</script> diff --git a/resources/vue/components/CacheAdministration.vue b/resources/vue/components/CacheAdministration.vue new file mode 100644 index 0000000..14e1d53 --- /dev/null +++ b/resources/vue/components/CacheAdministration.vue @@ -0,0 +1,117 @@ +<template> + <form class="default" :action="actionUrl" method="post" ref="configForm"> + <fieldset> + <legend> + <translate>Cachetyp</translate> + </legend> + <label> + <translate>Cachetyp auswählen</translate> + + <select name="cachetype" v-model="selectedCacheType" @change="getCacheConfig"> + <option v-for="(type) in cacheTypes" :key="type.cache_id" :value="type.class_name"> + {{ type.display_name }} + </option> + </select> + </label> + </fieldset> + + <fieldset> + <legend>Konfiguration</legend> + <template v-if="configComponent != null"> + <component :is="configComponent" v-bind="configProps" ref="cacheConfig" @is-valid="setValid"></component> + </template> + <template v-else> + <translate>Für diesen Cachetyp ist keine Konfiguration erforderlich.</translate> + </template> + </fieldset> + <footer data-dialog-button> + <button class="button accept" @click.prevent="validateConfig" :disabled="!isValid"> + <translate>Speichern</translate> + </button> + </footer> + </form> +</template> + +<script> +import FileCacheConfig from './FileCacheConfig.vue' +import MemcachedCacheConfig from './MemcachedCacheConfig.vue' +import RedisCacheConfig from './RedisCacheConfig.vue' + +export default { + name: 'CacheAdministration', + components: { + FileCacheConfig, + MemcachedCacheConfig, + RedisCacheConfig + }, + props: { + cacheTypes: { + type: Array, + required: true + }, + currentCache: { + type: String, + required: true + }, + currentConfig: { + type: Object, + default: { + component: null, + props: [] + } + } + }, + data () { + return { + isValid: true, + selectedCacheType: this.currentCache, + configComponent: this.currentConfig.component, + configProps: this.currentConfig.props + } + }, + computed: { + actionUrl () { + return STUDIP.URLHelper.getURL('dispatch.php/admin/cache/store_settings'); + } + }, + methods: { + /** + * Fetches configuration template for selected cache + * @param event + */ + getCacheConfig (event) { + fetch(STUDIP.URLHelper.getURL(`dispatch.php/admin/cache/get_config/${this.selectedCacheType}`)) + .then((response) => { + if (!response.ok) { + throw response + } + + response.json() + .then((json) => { + this.configComponent = json.component + this.configProps = json.props + }).catch((error) => { + console.error(error) + console.error(error.status + ': ', error.statusText) + }) + }).catch((error) => { + console.error(error) + console.error(error.status + ': ', error.statusText) + }) + }, + validateConfig () { + if (this.configComponent == null || this.isValid) { + this.$refs.configForm.submit() + } + }, + setValid (state) { + this.isValid = state; + } + }, + watch: { + configComponent () { + this.isValid = true; + } + } +} +</script> diff --git a/resources/vue/components/FileCacheConfig.vue b/resources/vue/components/FileCacheConfig.vue new file mode 100644 index 0000000..82569ba --- /dev/null +++ b/resources/vue/components/FileCacheConfig.vue @@ -0,0 +1,33 @@ +<template> + <label class="col-4"> + <span class="required"> + <translate>Dateipfad</translate> + </span> + <input required type="text" name="path" v-model="thePath"> + </label> +</template> + +<script> +export default { + name: 'FileCacheConfig', + props: { + path: { + type: String, + default: '' + } + }, + data () { + return { + thePath: this.path + }; + }, + watch: { + thePath: { + handler (current) { + this.$emit('is-valid', current.trim().length > 0); + }, + immediate: true + } + } +} +</script> diff --git a/resources/vue/components/FilesTable.vue b/resources/vue/components/FilesTable.vue new file mode 100644 index 0000000..76d0be6 --- /dev/null +++ b/resources/vue/components/FilesTable.vue @@ -0,0 +1,388 @@ +<template> + <table class="default documents" + :data-folder_id="topfolder.folder_id" + data-shiftcheck> + <caption> + <div class="caption-container"> + <div v-if="breadcrumbs && !table_title"> + <a v-if="breadcrumbs[0]" :href="breadcrumbs[0].url" :title="$gettext('Zum Hauptordner')"> + <studip-icon shape="folder-home-full" + role="clickable" + class="text-bottom" + size="30"></studip-icon> + <span v-if="breadcrumbs.length == 1"> + {{ breadcrumbs[0].name }} + </span> + </a> + <span v-for="(breadcrumb, index) in breadcrumbs" + :key="breadcrumb.folder_id" + v-if="index > 0"> + /<a :href="breadcrumb.url"> + {{breadcrumb.name}} + </a> + </span> + </div> + <div v-if="table_title">{{table_title}}</div> + </div> + <div v-if="topfolder.description" style="font-size: small" v-html="topfolder.description"></div> + </caption> + + <colgroup> + <col v-if="show_bulk_actions" width="30px" data-filter-ignore> + <col width="60px" data-filter-ignore> + <col> + <col width="100px" class="responsive-hidden" data-filter-ignore> + <col v-if="showdownloads" width="100px" class="responsive-hidden" data-filter-ignore> + <col width="150px" class="responsive-hidden"> + <col width="120px" class="responsive-hidden" data-filter-ignore> + <col v-if="topfolder.additionalColumns" + v-for="(name, index) in topfolder.additionalColumns" + :key="index" + data-filter-ignore + class="responsive-hidden"> + <col width="80px" data-filter-ignore> + </colgroup> + <thead> + <tr class="sortable"> + <th v-if="show_bulk_actions" data-sort="false"> + <studip-proxy-checkbox + type="studip" + v-model="selectedIds" + :total="allIds" + ></studip-proxy-checkbox> + </th> + <th @click="sort('mime_type')" :class="sortClasses('mime_type')"> + <a href="#" @click.prevent> + {{ $gettext('Typ') }} + </a> + </th> + <th @click="sort('name')" :class="sortClasses('name')"> + <a href="#" @click.prevent> + {{ $gettext('Name') }} + </a> + </th> + <th @click="sort('size')" class="responsive-hidden" :class="sortClasses('size')"> + <a href="#" @click.prevent> + {{ $gettext('Größe') }} + </a> + </th> + <th v-if="showdownloads" @click="sort('downloads')" class="responsive-hidden" :class="sortClasses('downloads')"> + <a href="#" @click.prevent> + {{ $gettext('Downloads') }} + </a> + </th> + <th class="responsive-hidden" @click="sort('author_name')" :class="sortClasses('author_name')"> + <a href="#" @click.prevent> + {{ $gettext('Autor/-in') }} + </a> + </th> + <th class="responsive-hidden" @click="sort('chdate')" :class="sortClasses('chdate')"> + <a href="#" @click.prevent> + {{ $gettext('Datum') }} + </a> + </th> + <th v-if="topfolder.additionalColumns" + v-for="(name, index) in topfolder.additionalColumns" + :key="index" + @click="sort(index)" + class="responsive-hidden" + :class="sortClasses(index)"> + <a href="#" @click.prevent> + {{name}} + </a> + + </th> + <th data-sort="false">{{ $gettext('Aktionen') }}</th> + </tr> + </thead> + <tbody class="subfolders"> + <tr v-if="files.length + folders.length == 0" class="empty"> + <td :colspan="numberOfColumns"> + {{ $gettext('Dieser Ordner ist leer') }} + </td> + </tr> + <tr v-for="folder in sortedFolders" + :id="'row_folder_' + folder.id " + :data-permissions="folder.permissions"> + <td v-if="show_bulk_actions"> + <studip-proxied-checkbox + name="ids[]" + type="studip" + :value="folder.id" + v-model="selectedIds" + ></studip-proxied-checkbox> + </td> + <td class="document-icon"> + <a :href="folder.url"> + <studip-icon :shape="folder.icon" role="clickable" size="26" class="text-bottom"></studip-icon> + </a> + </td> + <td> + <a :href="folder.url">{{folder.name}}</a> + </td> + <td class="responsive-hidden"></td> + <td v-if="showdownloads" + class="responsive-hidden"> + </td> + <td v-if="folder.author_url" class="responsive-hidden"> + <a :href="folder.author_url">{{folder.author_name}}</a> + </td> + <td v-else class="responsive-hidden"> + {{folder.author_name}} + </td> + <td class="responsive-hidden" style="white-space: nowrap;"> + <studip-date-time :timestamp="folder.chdate" :relative="true"></studip-date-time> + </td> + <template v-if="topfolder.additionalColumns" + v-for="(name, index) in topfolder.additionalColumns"> + <td v-if="folder.additionalColumns && folder.additionalColumns[index] && folder.additionalColumns[index].html" + class="responsive-hidden" + v-html="folder.additionalColumns[index].html"></td> + <td v-else class="responsive-hidden"></td> + </template> + <td class="actions" v-html="folder.actions"> + </td> + </tr> + </tbody> + <tbody class="files"> + <tr v-for="file in sortedFiles" + :class="file.new ? 'new' : ''" + :id="'fileref_' + file.id" + role="row" + :data-permissions="getPermissions(file)"> + <td v-if="show_bulk_actions"> + <studip-proxied-checkbox + name="ids[]" + type="studip" + :value="file.id" + v-model="selectedIds" + ></studip-proxied-checkbox> + </td> + <td class="document-icon"> + <a v-if="file.download_url" :href="file.download_url" target="_blank" rel="noopener noreferrer"> + <studip-icon :shape="file.icon" role="clickable" size="24" class="text-bottom"></studip-icon> + </a> + <studip-icon v-else :shape="file.icon" role="clickable" size="24"></studip-icon> + + <a :href="file.download_url" + v-if="file.download_url && file.mime_type.indexOf('image/') === 0" + class="lightbox-image" + data-lightbox="gallery"></a> + </td> + <td> + <a :href="file.details_url" data-dialog>{{file.name}}</a> + + <studip-icon v-if="file.restrictedTermsOfUse" + shape="lock-locked" + role="info" + size="16" + :title="$gettext('Das Herunterladen dieser Datei ist nur eingeschränkt möglich.')"></studip-icon> + </td> + <td :data-sort-value="file.size" + class="responsive-hidden"> + <studip-file-size v-if="file.size !== null" :size="parseInt(file.size, 10)"></studip-file-size> + </td> + <td v-if="showdownloads" + class="responsive-hidden"> + {{file.downloads}} + </td> + <td v-if="file.author_url" class="responsive-hidden" > + <a :href="file.author_url"> + {{file.author_name}} + </a> + </td> + <td v-else class="responsive-hidden"> + {{file.author_name}} + </td> + <td data-sort-value="file.chdate" class="responsive-hidden" style="white-space: nowrap;"> + <studip-date-time :timestamp="file.chdate" :relative="true"></studip-date-time> + </td> + <template v-if="topfolder.additionalColumns" + v-for="(name, index) in topfolder.additionalColumns"> + <td v-if="file.additionalColumns && file.additionalColumns[index] && file.additionalColumns[index].html" + class="responsive-hidden" + v-html="file.additionalColumns[index].html"></td> + <td v-else class="responsive-hidden"></td> + </template> + <td class="actions" v-html="file.actions"> + </td> + </tr> + </tbody> + <tfoot v-if="(topfolder.buttons && show_bulk_actions) || tfoot_link"> + <tr> + <td :colspan="numberOfColumns - (tfoot_link ? 1 : 0)"> + <div class="footer-items"> + <span v-if="topfolder.buttons && show_bulk_actions" + v-html="topfolder.buttons" class="bulk-buttons" ref="buttons"></span> + <span v-if="tfoot_link" :colspan="(topfolder.buttons && show_bulk_actions ? 1 : numberOfColumns)"> + <a :href="tfoot_link.href">{{tfoot_link.text}}</a> + </span> + <span v-if="pagination" v-html="pagination" class="pagination"></span> + </div> + </td> + </tr> + </tfoot> + </table> +</template> +<script> +export default { + name: 'files-table', + props: { + topfolder: Object, + folders: { + type: Array, + required: false, + default: () => [], + }, + files: Array, + breadcrumbs: { + type: Array, + required: false, + default: () => [], + }, + showdownloads: { + type: Boolean, + required: false, + default: true + }, + table_title: { + type: String, + required: false, + default: '' + }, + show_bulk_actions: { + type: Boolean, + required: false, + default: true + }, + tfoot_link: { + type: Object, + required: false, + default: null + }, + pagination: { + type: String, + required: false, + default: '' + }, + initial_sort: { + type: Object, + required: false, + default: () => ({sortedBy: 'name', sortDirection: 'asc'}) + } + }, + data () { + return { + selectedIds: [undefined], // Includes invalid value to trigger watch on mounted + sortedBy: this.initial_sort.sortedBy, + sortDirection: this.initial_sort.sortDirection + }; + }, + methods: { + sort (column) { + let oldDirection = this.sortDirection; + if (this.sortedBy === column) { + this.sortDirection = oldDirection === "asc" ? "desc" : "asc"; + } + this.sortedBy = column; + }, + sortClasses (column) { + let classes = []; + if (this.sortedBy === column) { + classes.push(this.sortDirection === 'asc' ? 'sortasc' : 'sortdesc'); + } + return classes; + }, + removeFile (id) { + this.files = this.files.filter(file => file.id != id) + }, + removeFolder (id) { + this.folders = this.folders.filter(folder => folder.id != id) + }, + sortArray (array) { + if (!array.length) { + return []; + } + + // Determine whether the sorted array items have the key to sort by + const arrayHasKey = Object.keys(array.find(item => true)).includes(this.sortedBy); + + // Define sort direction by this factor + const directionFactor = this.sortDirection === "asc" ? 1 : -1; + + // Default sort function by string comparison of field + const collator = new Intl.Collator(String.locale, {numeric: true, sensitivity: 'base'}); + let sortFunction = (a, b) => collator.compare(a[this.sortedBy], b[this.sortedBy]); + + // Sort numerically by field + if (["size", "downloads", "chdate"].includes(this.sortedBy) && arrayHasKey) { + sortFunction = (a, b) => parseInt(a[this.sortedBy], 10) - parseInt(b[this.sortedBy], 10); + } + + // Additional sorting + if (this.topfolder.additionalColumns.hasOwnProperty(this.sortedBy) && arrayHasKey) { + const is_string = array.some(item => { + return typeof item.additionalColumns[this.sortedBy].order === "string" + && !isNaN(parseFloat(item.additionalColumns[this.sortedBy].order)); + }); + if (is_string) { + sortFunction = (a, b) => collator.compare(a.additionalColumns[this.sortedBy].order, b.additionalColumns[this.sortedBy].order); + } else { + sortFunction = (a, b) => a.additionalColumns[this.sortedBy].order - b.additionalColumns[this.sortedBy].order; + } + } + + // Actual sort on copy of array + return array.concat().sort((a, b) => directionFactor * sortFunction(a, b)); + }, + getPermissions (file) { + let permissions = ''; + if (file.download_url) { + permissions += 'dr'; + } + if (file.isEditable) { + permissions += 'w'; + } + return permissions; + } + }, + computed: { + numberOfColumns () { + return 7 + + (this.showdownloads ? 1 : 0) + + Object.keys(this.topfolder.additionalColumns).length; + }, + sortedFiles () { + return this.sortArray(this.files); + }, + sortedFolders () { + return this.sortArray(this.folders); + }, + allIds () { + return [].concat(this.files.map(file => file.id)).concat(this.folders.map(folder => folder.id)); + } + }, + mounted () { + // Trigger watch + this.selectedIds = []; + }, + watch: { + selectedIds (current) { + const activated = current.length > 0; + this.$nextTick(() => { // needs to be wrapped since we check the dom + this.$refs.buttons.querySelectorAll('.multibuttons .button').forEach(element => { + let condition = element.dataset.activatesCondition; + if (!condition || !activated) { + element.disabled = !activated; + } else { + condition = condition.replace(/:has\((.*?)\)/g, ' $1'); + condition = condition.replace(':checkbox', 'input[type="checkbox"]'); + + element.disabled = this.$el.querySelector(condition) === null; + } + }); + }); + }, + } +} +</script> diff --git a/resources/vue/components/MemcachedCacheConfig.vue b/resources/vue/components/MemcachedCacheConfig.vue new file mode 100644 index 0000000..84e8059 --- /dev/null +++ b/resources/vue/components/MemcachedCacheConfig.vue @@ -0,0 +1,85 @@ +<template> + <div> + <article v-for="(server, index) in serverConfig" :key="index" class="memcached-server"> + <header> + <h3> + <translate>Memcached-Server</translate> {{ index + 1 }} + <studip-icon shape="trash" class="remove-server" @click.prevent="removeServer($event, index)" + :size="24"></studip-icon> + </h3> + </header> + <section class="col-4"> + <label :for="'hostname-' + index"> + <translate>Hostname</translate> + </label> + <input type="text" :name="'servers[' + index + '][hostname]'" :id="'hostname-' + index" + placeholder="localhost" :value="server.hostname"> + </section> + <section class="col-2"> + <label :for="'port-' + index"> + <translate>Port</translate> + </label> + <input type="text" :name="'servers[' + index + '][port]'" :id="'port-' + index" + placeholder="11211" :value="server.port"> + </section> + </article> + <label class="add-server" @click="addServer"> + <studip-icon shape="add" :size="20"></studip-icon> + <translate>Server hinzufügen</translate> + </label> + </div> +</template> + +<script> +export default { + name: 'MemcachedCacheConfig', + props: { + servers: { + type: Array, + default: () => [] + } + }, + data () { + return { + serverConfig: this.servers + } + }, + methods: { + addServer () { + this.serverConfig.push({ server: '', port: null }) + }, + removeServer (event, index) { + this.serverConfig.splice(index, 1) + }, + isValid () { + return this.serverConfig.length > 0; + } + }, + watch: { + servers: { + handler (current) { + this.$emit('is-valid', this.isValid()); + }, + immediate: true + } + } +} +</script> + +<style lang="scss" scoped> +.memcached-server { + .remove-server { + vertical-align: text-bottom; + } +} + +.add-server { + &:not(:only-child) { + margin-top: 25px; + } + + img { + vertical-align: top; + } +} +</style> diff --git a/resources/vue/components/MyCourses.vue b/resources/vue/components/MyCourses.vue new file mode 100644 index 0000000..3e99ad7 --- /dev/null +++ b/resources/vue/components/MyCourses.vue @@ -0,0 +1,74 @@ +<template> + <div id="mycourses"> + <studip-message-box v-if="isEmpty" type="info" :hideClose="true"> + {{ $gettext('Es wurden keine Veranstaltungen gefunden.') }} + {{ $gettext('Mögliche Ursachen:') }} + <template #details> + <ul> + <li v-translate> + Sie haben zur Zeit keine Veranstaltungen belegt, an denen Sie teilnehmen können. + <br> + Bitte nutzen Sie <a :href="searchCoursesUrl"> <strong>Veranstaltung suchen / hinzufügen</strong> </a> um sich für Veranstaltungen anzumelden. + </li> + + <li v-translate> + In dem ausgewählten <strong>Semester</strong> wurden keine Veranstaltungen belegt. + <br> + Wählen Sie links im <strong>Semesterfilter</strong> ein anderes Semester aus! + </li> + </ul> + </template> + </studip-message-box> + <component v-else :is="displayComponent" :icon-size="iconSize"></component> + + <MountingPortal mount-to="#tiled-courses-sidebar-switch .sidebar-widget-content .widget-list" name="sidebar-switch"> + <my-courses-sidebar-switch></my-courses-sidebar-switch> + </MountingPortal> + + <MountingPortal mount-to="#tiled-courses-new-contents-toggle .sidebar-widget-content .widget-list" name="sidebar-content-toggle"> + <my-courses-new-content-toggle></my-courses-new-content-toggle> + </MountingPortal> + </div> +</template> + +<script> +import { sprintf } from 'sprintf-js'; +import MyCoursesTables from './MyCoursesTables.vue'; +import MyCoursesTiles from './MyCoursesTiles.vue'; +import MyCoursesMixin from '../mixins/MyCoursesMixin.js'; +import MyCoursesSidebarSwitch from "./MyCoursesSidebarSwitch.vue"; +import MyCoursesNewContentToggle from "./MyCoursesNewContentToggle.vue"; + +export default { + name: 'MyCourses', + mixins: [MyCoursesMixin], + components: { + MyCoursesTables, + MyCoursesTiles, + MyCoursesSidebarSwitch, + MyCoursesNewContentToggle, + }, + computed: { + displayComponent () { + return this.displayedType === 'tiles' + ? MyCoursesTiles + : MyCoursesTables; + }, + displayedType () { + return this.getConfig(this.viewConfig); + }, + iconSize () { + if (this.displayedType !== 'tiles' && !this.responsiveDisplay) { + return 20; + } + return 24; + }, + searchCoursesUrl () { + return STUDIP.URLHelper.getURL('dispatch.php/search/courses'); + }, + isEmpty () { + return this.groups.length === 0; + } + } +} +</script> diff --git a/resources/vue/components/MyCoursesColorPicker.vue b/resources/vue/components/MyCoursesColorPicker.vue new file mode 100644 index 0000000..54854ea --- /dev/null +++ b/resources/vue/components/MyCoursesColorPicker.vue @@ -0,0 +1,97 @@ +<template> + <ul class="my-courses-color-picker"> + <li v-for="(i, index) in color_count" :id="i" :class="getCSSClasses(index)"> + <a @click="selectColor(index)" :title="getTitle(i, index)"> + {{ getTitle(i) }} + </a> + </li> + </ul> +</template> + +<script> +export default { + name: "my-courses-color-picker", + props: { + course: { + type: Object, + required: true + }, + color_count: { + required: false, + default: 9 + }, + }, + methods: { + getCSSClasses (index) { + let classes = []; + classes.push(`gruppe${index}`); + + if (this.course.group === index) { + classes.push('color-selected'); + } + + return classes; + }, + getTitle (i, index) { + let title = this.$gettext('Gruppe') + ' ' + i; + if (this.course.group === index) { + title += ' (' + this.$gettext('ausgewählt') + ')'; + } + return title; + }, + selectColor (index) { + this.$emit('color-picked', this.course, index); + }, + }, + mounted () { + // Detect safari + if (!/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { + return; + } + + // Force a double redraw in css since safari won't display the + // colorpicker otherwise + setTimeout(() => { + this.$el.style.position = 'static'; + setTimeout(() => { + this.$el.style.position = ''; + }, 0); + }, 0); + } +} +</script> + +<style lang="scss"> +@use '../../assets/stylesheets/mixins.scss'; + +.my-courses-color-picker { + list-style: none; + margin: 0; + padding: 0; + + // Hide text in color groups + li { + text-indent: 100%; + overflow: hidden; + white-space: nowrap; + + position: relative; + } + + a { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + cursor: pointer; + } + + .color-selected { + @include mixins.background-icon(accept, info, 32px); + background-position: center; + background-repeat: no-repeat; + } +} +</style> diff --git a/resources/vue/components/MyCoursesNavigation.vue b/resources/vue/components/MyCoursesNavigation.vue new file mode 100644 index 0000000..3c899b1 --- /dev/null +++ b/resources/vue/components/MyCoursesNavigation.vue @@ -0,0 +1,84 @@ +<template> + <ul class="my-courses-navigation" v-if="navigationLength > 0"> + <li v-for="nav in navigation" class="my-courses-navigation-item" :class="nav.class"> + <a v-if="nav" :href="nav.url" v-bind="nav.attr"> + <studip-icon :shape="nav.icon.shape" :role="nav.icon.role" :size="iconSize"></studip-icon> + </a> + <span v-else class="empty-slot" :style="{width: `${iconSize}px`}"></span> + </li> + </ul> +</template> + +<script> +export default { + name: 'my-courses-navigation', + props: { + navigation: Object, + iconSize: { + type: Number, + required: false, + default: 24, + }, + }, + computed: { + navigationLength () { + return Object.keys(this.navigation).length; + } + } +} +</script> + +<style lang="scss"> +@use '../../assets/stylesheets/mixins.scss'; +$icon-padding: 3px; + +.my-courses-navigation { + list-style: none; + margin: 0; + margin-bottom: -10px; + padding: 0; + + display: flex; + flex-wrap: wrap; +} +.my-courses-navigation-item { + margin: 0 3px 10px 0; + + a { + display: inline-block; + padding: $icon-padding; + } + + &:last-child { + margin-right: 0; + } + + img { + vertical-align: bottom; + } + + .empty-slot { + display: inline-block; + padding-left: $icon-padding; + padding-right: $icon-padding; + } +} +.my-courses-navigation-important { + $border-width: 1px; + border: $border-width solid mixins.$red; + + a { + padding: $icon-padding - $border-width; + } + html.high-contrast-mode-activated & { + a { + border: 1px dashed mixins.$black; + } + + img, + svg { + filter: grayscale(100%) invert(1); + } + } +} +</style> diff --git a/resources/vue/components/MyCoursesNewContentToggle.vue b/resources/vue/components/MyCoursesNewContentToggle.vue new file mode 100644 index 0000000..49bf7ed --- /dev/null +++ b/resources/vue/components/MyCoursesNewContentToggle.vue @@ -0,0 +1,34 @@ +<template> + <ul class="widget-list widget-options"> + <li> + <a href="#" class="options-checkbox" :class="showNewContents ? 'options-checked' : 'options-unchecked'" @click.prevent="toggleNewContents"> + <translate>Nur neue Inhalte anzeigen</translate> + </a> + </li> + </ul> +</template> + +<script> +import Sidebar from "../../assets/javascripts/lib/sidebar.js"; +import MyCoursesMixin from '../mixins/MyCoursesMixin.js'; + +export default { + name: 'my-courses-new-content-toggle', + mixins: [MyCoursesMixin], + computed: { + showNewContents () { + return this.getConfig('navigation_show_only_new'); + }, + }, + methods: { + toggleNewContents() { + this.updateConfigValue({ + key: 'navigation_show_only_new', + value: !this.getConfig('navigation_show_only_new'), + }).then(() => { + Sidebar.close(); + }); + }, + }, +}; +</script> diff --git a/resources/vue/components/MyCoursesSidebarSwitch.vue b/resources/vue/components/MyCoursesSidebarSwitch.vue new file mode 100644 index 0000000..5abc1fb --- /dev/null +++ b/resources/vue/components/MyCoursesSidebarSwitch.vue @@ -0,0 +1,48 @@ +<template> + <ul class="widget-list widget-links sidebar-views"> + <li :class="{ active: tableView }"> + <a href="#" @click.prevent="setTableView"> + <translate>Tabellarische Ansicht</translate> + </a> + </li> + <li :class="{ active: tilesView }"> + <a href="#" @click.prevent="setTilesView"> + <translate>Kachelansicht</translate> + </a> + </li> + </ul> +</template> + +<script> +import Sidebar from "../../assets/javascripts/lib/sidebar.js"; +import MyCoursesMixin from '../mixins/MyCoursesMixin.js'; + +export default { + name: 'my-courses-sidebar-switch', + mixins: [MyCoursesMixin], + computed: { + tableView () { + return this.getConfig(this.viewConfig) === 'tables'; + }, + tilesView () { + return this.getConfig(this.viewConfig) === 'tiles'; + }, + }, + methods: { + setTableView () { + this.setView('tables'); + }, + setTilesView () { + this.setView('tiles'); + }, + setView (view) { + this.updateConfigValue({ + key: this.viewConfig, + value: view + }).then(() => { + Sidebar.close(); + }); + } + }, +}; +</script> diff --git a/resources/vue/components/MyCoursesTables.vue b/resources/vue/components/MyCoursesTables.vue new file mode 100644 index 0000000..c542e3d --- /dev/null +++ b/resources/vue/components/MyCoursesTables.vue @@ -0,0 +1,186 @@ +<template> + <div id="my_seminars"> + <table class="default collapsable mycourses" v-for="group in groups" :key="group.id"> + <caption> {{ group.name }}</caption> + <colgroup> + <col style="width: 7px"> + <col style="width: 25px"> + <col style="width: 70px" v-if="getConfig('sem_number') && !responsiveDisplay"> + <col> + <col v-if="!responsiveDisplay" :style="{width: (2 * 5 + numberOfNavElements * (iconSize + 2 * 3 + 3) - 3) + 'px'}"> + <col v-if="!responsiveDisplay" style="width: 24px"> + </colgroup> + <thead> + <tr class="sortable"> + <th></th> + <th></th> + <th v-if="getConfig('sem_number') && !responsiveDisplay" :class="getOrderClasses('number')"> + <a href="#" @click.prevent="changeOrder('number')"> + {{ $gettext('Nr.') }} + </a> + </th> + <th :class="getOrderClasses('name')"> + <a href="#" @click.prevent="changeOrder('name')"> + {{ $gettext('Name') }} + </a> + </th> + <th v-if="!responsiveDisplay" >{{ $gettext('Inhalt') }}</th> + <th v-if="!responsiveDisplay"></th> + </tr> + </thead> + <tbody v-for="subgroup in group.data" :key="subgroup.id" :class="{collapsed: !isGroupOpen(subgroup)}"> + <tr class="header-row" v-if="subgroup.label !== false"> + <th style="white-space: nowrap; text-align: left"></th> + <th class="toggle-indicator" :colspan="(getConfig('sem_number') && !responsiveDisplay) ? 3 : 2"> + <a href="#" @click.prevent.stop="toggleOpenGroup(subgroup)">{{ subgroup.label }}</a> + </th> + <th v-if="!responsiveDisplay" class="dont-hide" colspan="2"></th> + </tr> + <tr v-for="course in getOrderedCourses(subgroup.ids)" :data-course-id="course.id" :class="getCourseClasses(course)"> + <td :class="`gruppe${course.group}`"></td> + <td :class="{'subcourse-indented': isChild(course)}"> + <span :style="{backgroundImage: `url(${course.avatar}`}" class="my-courses-avatar course-avatar-small" :title="course.name" alt=""></span> + </td> + <td v-if="getConfig('sem_number') && !responsiveDisplay" :class="{'subcourse-indented': isChild(course)}"> + {{ course.number }} + </td> + <td :class="{'subcourse-indented': isChild(course)}"> + <a :href="urlFor('seminar_main.php', {auswahl: course.id})"> + {{ getCourseName(course, getConfig('sem_number') && responsiveDisplay) }} + <span v-if="course.is_deputy">{{ $gettext('[Vertretung]') }}</span> + + <span v-if="course.is_hidden"> + {{ $gettext('[versteckt]') }} + <studip-tooltip-icon :text="getHiddenTooltip(course)"></studip-tooltip-icon> + </span> + </a> + <div v-if="responsiveDisplay" class="mycourse_elements"> + <div class="special_nav"> + <studip-action-menu :items="getActionMenuForCourse(course)" + :collapseAt="false" + v-on:show-color-picker="shownColorPicker = course.id" + ></studip-action-menu> + </div> + + <my-courses-navigation :navigation="getNavigationForCourse(course)" :icon-size="iconSize"></my-courses-navigation> + </div> + </td> + <td v-if="!responsiveDisplay" class="course-navigation"> + <my-courses-navigation :navigation="getNavigationForCourse(course, true)" :icon-size="iconSize"></my-courses-navigation> + </td> + <td v-if="!responsiveDisplay" class="actions"> + <studip-action-menu :items="getActionMenuForCourse(course)" + :collapseAt="2" + ></studip-action-menu> + </td> + </tr> + </tbody> + </table> + </div> +</template> + +<script> +import MyCoursesMixin from '../mixins/MyCoursesMixin.js'; + +export default { + name: 'MyCoursesTables', + mixins: [MyCoursesMixin], + props: { + iconSize: { + type: Number, + required: false, + default: 16 + } + }, + data () { + return { + orderBy: 'group', + orderDir: 'asc' + } + }, + methods: { + changeOrder (by) { + if (this.orderBy === by) { + this.orderDir = this.orderDir === 'asc' ? 'desc' : 'asc'; + } else { + this.orderBy = by; + this.orderDir = 'asc'; + } + }, + getCourseClasses (course) { + return { + 'has-subcourses': this.isParent(course), + subcourses: this.isChild(course), + }; + }, + getOrderedCourses (ids) { + const sorted = this.getCourses(ids); + const dirFactor = this.orderDir === 'desc' ? -1 : 1; + if (this.orderBy === 'name') { + sorted.sort((a, b) => a.name.localeCompare(b.name) * dirFactor); + } else if (this.orderBy === 'number') { + sorted.sort((a, b) => a.number.localeCompare(b.number) * dirFactor); + } + + // Ensure parent / child relation + let courses = []; + sorted.forEach(course => { + if (!this.isChild(course)) { + courses.push(course); + } + if (this.isParent(course)) { + this.getCourses(course.children).forEach(c => { + courses.push(c); + }); + } + }); + + return courses; + }, + getOrderClasses (by) { + if (by !== this.orderBy) { + return []; + } + return this.orderDir === 'asc' ? ['sortasc'] : ['sortdesc']; + } + } +} +</script> + +<style lang="scss"> +@use '../../assets/stylesheets/mixins/colors.scss' as *; + +table.mycourses { + tbody td { + vertical-align: top; + + &.actions, + &.course-navigation { + vertical-align: middle; + } + } + + .special_nav { + float: right; + } + + tr.has-subcourses td { + border-bottom: 1px solid $dark-gray-color-75; + } + tr.subcourses { + background-color: $dark-gray-color-5; + + td.subcourse-indented { + padding-left: 20px; + } + } +} +.my-courses-avatar.course-avatar-small { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + display: inline-block; + height: 25px; + width: 25px; +} +</style> diff --git a/resources/vue/components/MyCoursesTiles.vue b/resources/vue/components/MyCoursesTiles.vue new file mode 100644 index 0000000..28ff6c6 --- /dev/null +++ b/resources/vue/components/MyCoursesTiles.vue @@ -0,0 +1,302 @@ +<template> + <div class="my-courses my-courses--tiles"> + <template v-for="group in groups"> + <div class="group-label">{{ group.name }}</div> + <article class="studip" v-for="subgroup in group.data" :key="subgroup.id" :class="getGroupCssClasses(subgroup)"> + <header v-if="subgroup.label"> + <h1> + <a href="#" @click.prevent.stop="toggleOpenGroup(subgroup)">{{ subgroup.label }}</a> + </h1> + </header> + <section class="studip-grid"> + <template v-for="course in getOrderedCourses(subgroup.ids)"> + <div class="course-group-label" v-if="isParent(course)"> + {{ getCourseName(course, getConfig('sem_number')) }} + </div> + + <article class="studip-grid-element" :data-course-id="course.id" :class="getCourseCssClasses(course)"> + <header class="tiles-grid-element-header"> + <span class="tiles-grid-element-options"> + <studip-action-menu :items="getActionMenuForCourse(course, true)" + :collapseAt="0" + class="tiles-action-menu" + @show-color-picker="shownColorPicker = course.id" + ></studip-action-menu> + </span> + + <a :href="urlFor('seminar_main.php', {auswahl: course.id})" class="tiles-grid-element-header-content" :title="getCourseName(course, getConfig('sem_number'))"> + <span :style="{backgroundImage: `url(${course.avatar})`}" class="tiles-grid-element-header-image"></span> + <span class="tiled-grid-element-header-title"> + {{ getCourseName(course, getConfig('sem_number')) }} + <span v-if="course.is_deputy">{{ $gettext('[Vertretung]') }}</span> + + <span v-if="course.is_hidden"> + {{ $gettext('[versteckt]') }} + <studip-tooltip-icon :text="getHiddenTooltip(course)"></studip-tooltip-icon> + </span> + </span> + </a> + </header> + <footer class="tiles-grid-element-footer"> + <my-courses-navigation :navigation="getNavigationForCourse(course)" :icon-size="iconSize"></my-courses-navigation> + </footer> + + <my-courses-color-picker v-if="showColorPickerForCourse(course)" :course="course" v-on:color-picked="changeColor"></my-courses-color-picker> + </article> + </template> + </section> + </article> + </template> + </div> +</template> + + +<script> +import MyCoursesMixin from '../mixins/MyCoursesMixin.js'; +import MyCoursesColorPicker from './MyCoursesColorPicker.vue'; + +export default { + name: 'my-courses-tiles', + mixins: [MyCoursesMixin], + components: {MyCoursesColorPicker}, + props: { + iconSize: { + type: Number, + required: false, + default: 16 + } + }, + data () { + return { + shownColorPicker: null, + }; + }, + methods: { + getGroupCssClasses(group) { + if (group.label === false) { + return ['my-courses--group-hidden']; + } + + let classes = ['toggle']; + if (this.isGroupOpen(group)) { + classes.push('open'); + } + + return classes; + }, + getCourseCssClasses(course) { + let classes = [`my-courses-group-${course.group}`]; + + if (this.isParent(course)) { + classes.push('has-subcourses'); + } + + if (this.isChild(course)) { + classes.push('subcourses'); + } + + if (this.showColorPickerForCourse(course)) { + classes.push('has-color-picker'); + } + + return classes; + }, + getOrderedCourses (ids) { + const sorted = this.getCourses(ids); + sorted.sort((a, b) => { + // Sort courses with subcourses at the end + if (this.isParent(a)) { + return 1; + } + if (this.isParent(b)) { + return -1; + } + + // Sort by number and name + return (a.group - b.group) || a.number.localeCompare(b.number) || a.name.localeCompare(b.name); + }); + + // Ensure parent / child relation + let courses = []; + sorted.forEach(course => { + if (!this.isChild(course)) { + courses.push(course); + } + if (this.isParent(course)) { + this.getCourses(course.children).forEach(c => { + courses.push(c); + }); + } + }); + + return courses; + }, + showColorPickerForCourse(course) { + return this.shownColorPicker === course.id; + }, + changeColor(course, index) { + STUDIP.jsonapi.PATCH(`course-memberships/${course.id}_${this.userid}`, { + data: { + data: { + type: 'course-memberships', + attributes: { + group: index + } + } + } + }).done(() => { + course.group = index; + }).always(() => { + this.shownColorPicker = null; + }); + }, + }, + computed: { + } +} +</script> + +<style lang="scss" scoped> +@use '../../assets/stylesheets/mixins.scss'; +@use '../../assets/stylesheets/scss/breakpoints.scss' as *; +@use '../../assets/stylesheets/scss/variables.scss'; +@import '../../assets/stylesheets/scss/visibility.scss'; // Needs to be imported (breakpoint variables are missing) + +$tile-border-width: 1px; +$tile-color-width: 15px; +$tile-padding: 10px; + +.studip-grid { + $avatar-size: 60px; + $header-size: $avatar-size; + $element-height: (100px + $header-size); + + &:not(:last-child) { + margin-bottom: 2rem; + } + + .studip-grid-element { + box-sizing: border-box; + display: flex; + flex-direction: column; + position: relative; // For color picker + + border: $tile-border-width solid mixins.$base-color-20; + + padding: $tile-padding; + } + + .tiles-grid-element-header { + flex: 0 $header-size; + } + + .tiled-grid-element-header-title { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + max-height: $header-size; + overflow: hidden; + } + + .tiles-grid-element-header-image { + float: left; + display: block; + + margin-right: $tile-padding; + + width: $avatar-size; + height: $avatar-size; + + background-position: center; + background-size: cover; + } + + .tiles-grid-element-options { + float: right; + } + + .tiles-grid-element-footer { + flex: 0 0 auto; + &:not(:empty) { + padding-top: 10px; + } + } + + .course-group-label { + grid-column: 1 / -1; + margin-bottom: -1em; + } +} + +.group-label, +.course-group-label { + color: mixins.$base-gray; +} + +.group-label { + font-size: variables.$font-size-h1; + + &:not(:first-child) { + margin-top: 1em; + } +} +.course-group-label { + font-size: variables.$font-size-h2; +} + +article.studip.my-courses--group-hidden { + border: 0; + padding: 0; + > header { + display: none; + } +} + +// Border below according to selected group +$group-colors: ( + 0: mixins.$group-color-0, + 1: mixins.$group-color-1, + 2: mixins.$group-color-2, + 3: mixins.$group-color-3, + 4: mixins.$group-color-4, + 5: mixins.$group-color-5, + 6: mixins.$group-color-6, + 7: mixins.$group-color-7, + 8: mixins.$group-color-8, +); +@for $i from 0 through 8 { + .studip-grid-element.my-courses-group-#{$i} { + padding-left: $tile-padding + $tile-color-width; + &::before { + position: absolute; + top: -$tile-border-width; + left: -$tile-border-width; + bottom: -$tile-border-width; + width: $tile-color-width; + content: ''; + background-color: map-get($group-colors, $i); + + } + } +} + +// Definitions for color picker +.my-courses-color-picker { + $gap: 0.5ex; + + display: grid; + grid-template-rows: 1fr 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr; + + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + + background: mixins.$white; + grid-gap: $gap; + padding: $gap; +} +</style> diff --git a/resources/vue/components/Quicksearch.vue b/resources/vue/components/Quicksearch.vue new file mode 100644 index 0000000..23fd5f4 --- /dev/null +++ b/resources/vue/components/Quicksearch.vue @@ -0,0 +1,178 @@ +<template> + <span :class="'quicksearch_container' + (containerclass ? ' ' + containerclass : '')"> + <input type="hidden" + :name="name" + :value="returnValue" + v-if="!autocomplete"> + <input type="text" + :name="autocomplete ? name : null" + v-model="inputValue" + autocomplete="off" + @blur="reset()" + @keydown.up="selectUp" + @keydown.down="selectDown" + @keydown.enter.prevent="selectByKey" + v-bind="$attrs"> + <div class="dropdownmenu"> + <ul class="autocomplete__results" v-if="isVisible"> + <li class="autocomplete__result" + v-for="(result, index) in results" + :key="index" + :class="index === selected ? 'autocomplete__result--selected' : ''" + @click="select(result)" + v-html="result.item_name"> + </li> + <li v-if="errorMessage !== null">{{errorMessage}}</li> + </ul> + </div> + </span> +</template> + +<script> +export default { + name: 'quicksearch', + props: { + searchtype: { + type: String, + required: true + }, + name: { + type: String, + required: true + }, + value: { + type: String, + required: false, + default: '' + }, + needle: { + type: String, + required: false, + default: '' + }, + autocomplete: { + type: Boolean, + required: false, + default: false + }, + containerclass: { + type: String, + required: false, + default: '' + } + }, + data () { + return { + searching: false, + debounceTimeout: null, + selected: null, + results: [], + errorMessage: null, + inputValue: null, + returnValue: null, + initialValue: null + }; + }, + methods: { + initialize (value) { + this.initialValue = value; + this.inputValue = value; + this.returnValue = value; + }, + search (needle) { + clearTimeout(this.debounceTimeout); + this.debounceTimeout = setTimeout(() => { + let data = [] + if ($(this.$el).closest("form").length > 0) { + data = $(this.$el).closest("form").serializeArray(); + } + data.push({ + name: "request", + value: needle + }); + + $.post( + STUDIP.URLHelper.getURL("dispatch.php/quicksearch/response/" + this.searchtype), + data + ).done(response => { + this.selected = null; + this.results = response; + this.errorMessage = null; + }).fail(response => { + this.errorMessage = response.responseText; + }).always(() => { + this.searching = false; + }); + + this.searching = true; + }, 500); + }, + select (result) { + this.inputValue = result.item_search_name; + this.initialValue = this.inputValue; + if (this.autocomplete) { + this.returnValue = result.item_search_name; + } else { + this.returnValue = result.item_id; + } + this.results = []; + + this.$emit('input', this.returnValue); + }, + selectUp () { + if (this.selected > 0) { + this.selected -= 1; + } else if (this.selected === null) { + this.selected = this.results.length - 1; + } else { + this.selected = null; + } + }, + selectDown () { + if (this.selected === null) { + this.selected = 0; + } else if (this.selected < this.results.length - 1) { + this.selected += 1; + } else { + this.selected = null; + } + }, + selectByKey () { + if (this.selected !== null) { + this.select(this.results[this.selected]); + } + return false; + }, + reset (clear = false) { + setTimeout(() => { + this.results = []; + this.selected = null; + + if (clear) { + this.returnValue = this.initialValue; + this.inputValue = this.initialValue; + } + }, clear ? 0 : 200); + } + }, + created () { + this.initialize(this.autocomplete ? this.value : this.needle); + }, + computed: { + isVisible() { + return this.results.length > 0 || this.errorMessage !== null; + } + }, + watch: { + value (val) { + this.reset(true); + this.initialize(val); + }, + inputValue (needle) { + if (this.initialValue !== needle && needle.length > 2) { + this.search(needle); + } + } + } +} +</script> diff --git a/resources/vue/components/RedisCacheConfig.vue b/resources/vue/components/RedisCacheConfig.vue new file mode 100644 index 0000000..ae254b9 --- /dev/null +++ b/resources/vue/components/RedisCacheConfig.vue @@ -0,0 +1,58 @@ +<template> + <div> + <label class="col-4"> + <span class="required"> + <translate>Hostname</translate> + </span> + <input required type="text" name="hostname" placeholder="localhost" v-model="theHostname"> + </label> + <label class="col-2"> + <span class="required"> + <translate>Port</translate> + </span> + <input required type="text" name="port" placeholder="6379" v-model="thePort"> + </label> + </div> +</template> + +<script> +export default { + name: 'RedisCacheConfig', + props: { + hostname: { + type: String, + default: 'localhost' + }, + port: { + type: Number, + default: 6379 + } + }, + data () { + return { + theHostname: this.hostname, + thePort: this.port, + } + }, + methods: { + isValid () { + return this.theHostname.trim().length > 0 + && !isNaN(parseInt(this.thePort, 10)); + } + }, + watch: { + theHostname: { + handler (current) { + this.$emit('is-valid', this.isValid()); + }, + immediate: true + }, + thePort: { + handler (current) { + this.$emit('is-valid', this.isValid()); + }, + immediate: true + } + } +} +</script> diff --git a/resources/vue/components/StudipActionMenu.vue b/resources/vue/components/StudipActionMenu.vue new file mode 100644 index 0000000..95ec6a5 --- /dev/null +++ b/resources/vue/components/StudipActionMenu.vue @@ -0,0 +1,114 @@ +<template> + <nav v-if="shouldCollapse" class="action-menu"> + <a class="action-menu-icon" :title="$gettext('Aktionen')" aria-expanded="false" :aria-label="$gettext('Aktionsmenü')" href="#"> + <div></div> + <div></div> + <div></div> + </a> + <div class="action-menu-content"> + <div class="action-menu-title"> + {{ $gettext('Aktionen') }} + </div> + <ul class="action-menu-list"> + <li v-for="item in navigationItems" :key="item.id" class="action-menu-item"> + <a v-if="item.type === 'link'" v-bind="linkAttributes(item)" v-on="linkEvents(item)"> + <studip-icon v-if="item.icon !== false" :shape="item.icon.shape" :role="item.icon.role"></studip-icon> + <span v-else class="action-menu-no-icon"></span> + + {{ item.label }} + </a> + </li> + </ul> + </div> + </nav> + <nav v-else> + <a v-for="item in navigationItems" :key="item.id" v-bind="linkAttributes(item)" v-on="linkEvents(item)"> + <studip-icon :title="item.label" :shape="item.icon.shape" :role="item.icon.role" :size="20"></studip-icon> + </a> + </nav> +</template> + +<script> +export default { + name: 'studip-action-menu', + props: { + items: Array, + collapseAt: { + default: true, + } + }, + data () { + return { + open: false + }; + }, + methods: { + linkAttributes (item) { + let attributes = item.attributes; + attributes.class = item.classes; + + if (item.disabled) { + attributes.disabled = true; + } + + if (item.url) { + attributes.href = item.url; + } + + return attributes; + }, + linkEvents (item) { + let events = {}; + if (item.emit) { + events.click = () => { + this.$emit.apply(this, [item.emit].concat(item.emitArguments)); + this.close(); + }; + } + return events; + }, + close () { + STUDIP.ActionMenu.closeAll(); + } + }, + computed: { + navigationItems () { + return this.items.map((item) => { + let classes = item.classes || ''; + if (item.disabled) { + classes += " action-menu-item-disabled"; + } + return { + label: item.label, + url: item.url || false, + emit: item.emit || false, + emitArguments: item.emitArguments || [], + icon: item.icon ? { + shape: item.icon, + role: item.disabled ? 'inactive' : 'clickable' + } : false, + type: item.type || 'link', + classes: classes.trim(), + attributes: item.attributes || {}, + disabled: item.disabled, + }; + }); + }, + shouldCollapse () { + if (this.collapseAt === false) { + return false; + } + if (this.collapseAt === true) { + return true; + } + return Number.parseInt(this.collapseAt) <= this.items.length; + } + } +} +</script> + +<style lang="scss"> +.action-menu .action-menu-item a { + cursor: pointer; +} +</style> diff --git a/resources/vue/components/StudipAssetImg.vue b/resources/vue/components/StudipAssetImg.vue new file mode 100644 index 0000000..6a250a9 --- /dev/null +++ b/resources/vue/components/StudipAssetImg.vue @@ -0,0 +1,16 @@ +<template> + <img :src="url" + :width="width"> +</template> + +<script> + export default { + name: 'studip-asset-img', + props: ['width', 'file'], + computed: { + url: function () { + return `${STUDIP.ASSETS_URL}images/${this.file}`; + } + } + } +</script> diff --git a/resources/vue/components/StudipDateTime.vue b/resources/vue/components/StudipDateTime.vue new file mode 100644 index 0000000..1cf852c --- /dev/null +++ b/resources/vue/components/StudipDateTime.vue @@ -0,0 +1,63 @@ +<template> + <time :datetime="datetime" v-if="timestamp !== 0" :title="title"> + {{ formatted_date() }} + </time> +</template> + +<script> + function pad(what, length = 2) { + return `00000000${what}`.substr(-length); + } + + export default { + name: 'studip-date-time', + props: { + timestamp: Number, + relative: { + type: Boolean, + required: false, + default: false + } + }, + computed: { + datetime () { + if (!Number.isInteger(this.timestamp)) { + return ''; + } + let date = new Date(this.timestamp * 1000); + return date.toISOString(); + }, + title () { + return this.display_relative() ? this.formatted_date(true) : false; + } + }, + methods: { + display_relative: function () { + return Date.now() - this.timestamp * 1000 < 12 * 60 * 60 * 1000; + }, + formatted_date: function (force_absolute = false) { + if (!Number.isInteger(this.timestamp)) { + return `Should be integer: ${this.timestamp}`; + } + let date = new Date(this.timestamp * 1000); + let now = Date.now(); + if (!force_absolute && this.relative && this.display_relative()) { + if (now - date < 1 * 60 * 1000) { + return this.$gettext('Jetzt'); + } + if (now - date < 2 * 60 * 60 * 1000) { + return this.$gettext('Vor %s Minuten').replace('%s', Math.floor((now - date) / (1000 * 60))); + } + return pad(date.getHours()) + ':' + pad(date.getMinutes()); + } else { + return pad(date.getDate()) + '.' + pad(date.getMonth() + 1) + '.' + date.getFullYear() + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes()); + } + } + }, + mounted: function () { + window.setInterval(() => { + this.$forceUpdate(); + }, 1000); + } + } +</script> diff --git a/resources/vue/components/StudipDialog.vue b/resources/vue/components/StudipDialog.vue new file mode 100755 index 0000000..0f62f03 --- /dev/null +++ b/resources/vue/components/StudipDialog.vue @@ -0,0 +1,236 @@ +<template> + <MountingPortal mountTo="body" append> + <focus-trap v-model="trap" :initial-focus="() => $refs.buttonB"> + <div class="studip-dialog" @keydown.esc="closeDialog"> + <transition name="dialog-fade"> + <div class="studip-dialog-backdrop"> + <vue-resizeable + class="resizable" + style="position: absolute" + ref="resizableComponent" + :dragSelector="dragSelector" + :active="handlers" + :fit-parent="fit" + :left="left" + :top="top" + :width="currentWidth" + :height="currentHeight" + :min-width="minW | checkEmpty" + :min-height="minH | checkEmpty" + @mount="initSize" + @resize:move="resizeHandler" + @resize:start="resizeHandler" + @resize:end="resizeHandler" + @drag:move="resizeHandler" + @drag:start="resizeHandler" + @drag:end="resizeHandler" + > + <div + :style="{ width: dialogWidth, height: dialogHeight, top: top, left: left }" + :class="{ 'studip-dialog-warning': question, 'studip-dialog-alert': alert }" + class="studip-dialog-body" + role="dialog" + :aria-modal="'true'" + :aria-labelledby="dialogTitleId" + :aria-describedby="dialogDescId" + ref="dialog" + > + <header + class="studip-dialog-header" + > + <span :id="dialogTitleId" class="studip-dialog-title" :title="dialogTitle"> + {{ dialogTitle }} + </span> + <slot name="dialogHeader"></slot> + <span + :aria-label="$gettext('Diesen Dialog schließen')" + class="studip-dialog-close-button" + :title="$gettext('Schließen')" + @click="closeDialog" + > + </span> + </header> + <section + :id="dialogDescId" + :style="{ height: contentHeight }" + class="studip-dialog-content" + > + <slot name="dialogContent"></slot> + <div v-if="message">{{ message }}</div> + <div v-if="question">{{ question }}</div> + <div v-if="alert">{{ alert }}</div> + </section> + <footer class="studip-dialog-footer" ref="footer"> + <button + v-if="buttonA" + :title="buttonA.text" + :class="[buttonA.class]" + class="button" + type="button" + @click="confirmDialog" + > + {{ buttonA.text }} + </button> + <slot name="dialogButtons"></slot> + <button + v-if="buttonB" + :title="buttonB.text" + :class="[buttonB.class]" + class="button" + type="button" + ref="buttonB" + @click="closeDialog" + > + {{ buttonB.text }} + </button> + </footer> + </div> + </vue-resizeable> + </div> + </transition> + </div> + </focus-trap> + </MountingPortal> +</template> + +<script> +import { FocusTrap } from 'focus-trap-vue'; +import VueResizeable from 'vrp-vue-resizable'; +let uuid = 0; + +export default { + name: 'studip-dialog', + components: { + FocusTrap, + VueResizeable, + }, + props: { + height: {type: String, default: '300'}, + width: {type: String, default: '450'}, + title: String, + confirmText: String, + closeText: String, + confirmClass: String, + closeClass: String, + question: String, + alert: String, + message: String, + }, + data() { + const dialogId = uuid++; + + return { + trap: true, + dialogTitleId: `studip-dialog-title-${dialogId}`, + dialogDescId: `studip-dialog-desc-${dialogId}`, + + currentWidth: 450, + currentHeight: 300, + minW: 100, + minH: 100, + left: 0, + top: 0, + dragSelector: ".studip-dialog-header", + handlers: ["r", "rb", "b", "lb", "l", "lt", "t", "rt"], + fit: false, + footerHeight: 68, + }; + }, + computed: { + buttonA() { + let button = false; + if (this.message) { + return false; + } + if (this.question || this.alert) { + button = {}; + button.text = this.$gettext('Ja'); + button.class = 'accept'; + } + if (this.confirmText) { + button = {}; + button.text = this.confirmText; + button.class = this.confirmClass; + } + + return button; + }, + buttonB() { + let button = false; + if (this.message) { + button = {}; + button.text = this.$gettext('Ok'); + button.class = ''; + } + if (this.question || this.alert) { + button = {}; + button.text = this.$gettext('Nein'); + button.class = 'cancel'; + } + if (this.closeText) { + button = {}; + button.text = this.closeText; + if (this.closeClass) { + button.class = this.closeClass; + } else { + button.class = 'cancel'; + } + } + + return button; + }, + dialogTitle() { + if (this.title) { + return this.title; + } + if (this.alert || this.question) { + return this.$gettext('Bitte bestätigen Sie die Aktion'); + } + if (this.message) { + return this.$gettext('Information'); + } + }, + dialogWidth() { + return this.currentWidth ? this.currentWidth + 'px' : 'unset'; + }, + dialogHeight() { + return this.currentHeight ? this.currentHeight + 'px' : 'unset'; + }, + contentHeight() { + return this.currentHeight ? this.currentHeight - this.footerHeight + 'px' : 'unset'; + } + }, + methods: { + closeDialog() { + this.$emit('close'); + }, + confirmDialog() { + this.$emit('confirm'); + }, + initSize() { + this.currentWidth = parseInt(this.width, 10); + this.currentHeight = parseInt(this.height, 10); + if (window.outerWidth > this.currentWidth) { + this.left = (window.outerWidth - this.currentWidth) / 2; + } else { + this.left = 5; + this.currentWidth = window.outerWidth - 16; + } + + this.top = (window.outerHeight - this.currentHeight) / 2; + this.footerHeight = this.$refs.footer.offsetHeight; + }, + resizeHandler(data) { + this.currentWidth = data.width; + this.currentHeight = data.height; + this.left = data.left; + this.top = data.top; + }, + }, + filters: { + checkEmpty(value) { + return typeof value !== "number" ? 0 : value; + } + }, +}; +</script> diff --git a/resources/vue/components/StudipFileSize.vue b/resources/vue/components/StudipFileSize.vue new file mode 100644 index 0000000..46a0699 --- /dev/null +++ b/resources/vue/components/StudipFileSize.vue @@ -0,0 +1,37 @@ +<template> + <span> + {{ formatted_size }} + </span> +</template> + +<script> + export default { + name: 'studip-file-size', + props: { + size: Number + }, + computed: { + formatted_size () { + if (this.size < 1024) { + return this.size + " Byte"; + } + if (this.size < 1024 * 1024) { + let rel = (this.size / 1024).toFixed(1); + return rel + " KB"; + } + if (this.size < 1024 * 1024 * 1024) { + let rel = (this.size / 1024 / 1024).toFixed(1); + return rel + " MB"; + } + if (this.size < 1024 * 1024 * 1024 * 1024) { + let rel = (this.size / 1024 / 1024 / 1024).toFixed(1); + return rel + " GB"; + } + if (this.size < 1024 * 1024 * 1024 * 1024 * 1024) { + let rel = (this.size / 1024 / 1024 / 1024 / 1024).toFixed(1); + return rel + " TB"; + } + } + } + } +</script> diff --git a/resources/vue/components/StudipIcon.vue b/resources/vue/components/StudipIcon.vue new file mode 100644 index 0000000..b968d4a --- /dev/null +++ b/resources/vue/components/StudipIcon.vue @@ -0,0 +1,68 @@ +<template> + <input v-if="name" type="image" :name="name" :src="url" + :width="size" :height="size" v-bind="$attrs" v-on="$listeners"> + <img v-else :src="url" :width="size" :height="size" v-bind="$attrs" v-on="$listeners"> +</template> + +<script> + export default { + name: 'studip-icon', + props: { + shape: String, + role: { + type: String, + required: false, + default: 'clickable' + }, + size: { + required: false, + default: 16 + }, + name: { + type: String, + required: false + } + }, + computed: { + url: function () { + if (this.shape.indexOf("http") === 0) { + return this.shape; + } + return `${STUDIP.ASSETS_URL}images/icons/${this.color}/${this.shape}.svg`; + }, + color: function () { + switch (this.role) { + case 'info': + return 'black'; + + case 'inactive': + return 'grey'; + + case 'accept': + case 'status-green': + return 'green'; + + case 'attention': + case 'new': + case 'status-red': + return 'red'; + + case 'info_alt': + return 'white'; + + case 'sort': + case 'status-yellow': + return 'yellow'; + + case 'lightblue': + return 'lightblue'; + + case 'clickable': + case 'navigation': + default: + return 'blue'; + } + } + } + } +</script> diff --git a/resources/vue/components/StudipMessageBox.vue b/resources/vue/components/StudipMessageBox.vue new file mode 100644 index 0000000..471225e --- /dev/null +++ b/resources/vue/components/StudipMessageBox.vue @@ -0,0 +1,68 @@ +<template> + <div :class="classNames" v-if="!closed"> + <div class="messagebox_buttons"> + <a v-if="hideDetails" class="details" href="" :title="$gettext('Detailanzeige umschalten')" @click.prevent.stop="closedDetails = !closedDetails"> + <span>{{ $gettext('Detailanzeige umschalten') }}</span> + </a> + <a v-if="!hideClose" class="close" href="" :title="$gettext('Nachrichtenbox schließen')" @click.prevent="closed = true"> + <span>{{ $gettext('Nachrichtenbox schließen') }}</span> + </a> + </div> + <slot></slot> + <div v-if="showDetails" class="messagebox_details"> + <slot name="details"> + <ul> + <li v-for="detail in details" v-html="detail"></li> + </ul> + </slot> + </div> + </div> +</template> + +<script> +export default { + name: 'studip-message-box', + props: { + type: { + type: String, // exception, error, success, info, warning + default: 'info', + validator (type) { + return ['exception', 'error', 'warning', 'success', 'info'].indexOf(type) !== -1; + } + }, + details: { + type: Array, + default: [], + }, + hideDetails: { + type: Boolean, + default: false + }, + hideClose: { + type: Boolean, + default: false, + }, + }, + computed: { + classNames() { + return { + messagebox: true, + [`messagebox_${this.type}`]: true, + details_hidden: !this.showDetails, + }; + }, + hasDetails() { + return !!this.$slots.details || this.details.length > 0; + }, + showDetails() { + return this.hasDetails && !this.closedDetails; + } + }, + data() { + return { + closed: false, + closedDetails: this.hideDetails, + }; + }, +}; +</script> diff --git a/resources/vue/components/StudipProxiedCheckbox.vue b/resources/vue/components/StudipProxiedCheckbox.vue new file mode 100644 index 0000000..fc8a532 --- /dev/null +++ b/resources/vue/components/StudipProxiedCheckbox.vue @@ -0,0 +1,79 @@ +<script> +let uuid = 0; +export default { + name: 'studip-proxied-checkbox', + model: { + prop: 'selected', + event: 'change', + }, + props: { + name: String, + id: String, + type: String, + value: { + required: true + }, + selected: { + type: Array, + required: true + } + }, + methods: { + changeCollection () { + const selected = new Set(this.selected); + + if (this.checked) { + selected.delete(this.value); + } else { + selected.add(this.value); + } + + this.$emit('change', [...selected.values()]); + } + }, + computed: { + proxiedId () { + return this.id ?? `proxied-checkbox-${uuid++}`; + }, + checked () { + return this.selected.includes(this.value); + }, + }, + render (createElement) { + const checkbox = createElement('input', { + class: { + 'studip-checkbox': this.type === 'studip' + }, + attrs: { + type: 'checkbox', + name: this.name, + id: this.proxiedId, + value: this.value, + }, + domProps: { + checked: this.checked, + }, + on: { + change: this.changeCollection, + } + }); + + if (this.type !== 'studip') { + return checkbox; + } + + return createElement('span', { + style: { + display: 'contents', + }, + }, [ + checkbox, + createElement('label', { + attrs: { + for: this.proxiedId + } + }), + ]); + } +}; +</script> diff --git a/resources/vue/components/StudipProxyCheckbox.vue b/resources/vue/components/StudipProxyCheckbox.vue new file mode 100644 index 0000000..91d49b6 --- /dev/null +++ b/resources/vue/components/StudipProxyCheckbox.vue @@ -0,0 +1,75 @@ +<script> +let uuid = 0; +export default { + name: 'studip-proxy-checkbox', + model: { + prop: 'selected', + event: 'change', + }, + props: { + id: String, + type: String, + total: { + type: Array, + required: true + }, + selected: { + type: Array, + required: true, + } + }, + methods: { + changeProxy () { + this.$emit('change', this.checked ? [] : [...this.total] ); + } + }, + computed: { + proxyId () { + return this.id ?? `proxy-checkbox-${uuid++}`; + }, + checked () { + return this.selected.length === this.total.length; + }, + indeterminate () { + return this.selected.length > 0 && this.selected.length !== this.total.length; + } + }, + render (createElement) { + const checkbox = createElement('input', { + class: { + 'studip-checkbox': this.type === 'studip' + }, + attrs: { + type: 'checkbox', + name: this.name, + id: this.proxyId + }, + domProps: { + checked: this.checked, + indeterminate: this.indeterminate, + }, + on: { + change: this.changeProxy, + } + }); + + if (this.type !== 'studip') { + return checkbox; + } + + return createElement('span', { + style: { + display: 'contents', + }, + }, [ + checkbox, + createElement('label', { + attrs: { + for: this.proxyId + } + }), + ]); + + } +}; +</script> diff --git a/resources/vue/components/StudipTooltipIcon.vue b/resources/vue/components/StudipTooltipIcon.vue new file mode 100644 index 0000000..de4485d --- /dev/null +++ b/resources/vue/components/StudipTooltipIcon.vue @@ -0,0 +1,43 @@ +<template> + <span data-tooltip class="tooltip tooltip-icon" :class="cssClass" :title="title"> + <span class="tooltip-content" v-if="isHtml" v-html="text"></span> + <studip-icon shape="info-circle" role="inactive" :size="size"></studip-icon> + </span> +</template> + +<script> + export default { + name: 'studip-tooltip-icon', + props: { + text: String, + isHtml: { + type: Boolean, + required: false, + default: false + }, + isImportant: { + type: Boolean, + required: false, + default: false + }, + size: { + type: Number, + default: 16 + } + }, + computed: { + cssClass () { + return this.isImportant ? 'tooltip-important' : ''; + }, + title () { + return !this.isHtml ? this.text : ''; + } + } + } +</script> + +<style lang="scss" scoped> +.tooltip.tooltip-icon::before { + display: none; +} +</style> diff --git a/resources/vue/components/StudipWysiwyg.vue b/resources/vue/components/StudipWysiwyg.vue new file mode 100755 index 0000000..707fb55 --- /dev/null +++ b/resources/vue/components/StudipWysiwyg.vue @@ -0,0 +1,55 @@ +<template> + <textarea + :value="value" + ref="studip_wysiwyg" + class="studip-wysiwyg" + @input="updateValue($event.target.value)"/> +</template> + +<script> +// need v-model to provide and get content -> <studip-wysiwyg v-model="content" /> +export default { + name: 'studip-wysiwyg', + props: { + value: String + }, + data() { + return { + fallbackActive: false + } + }, + mounted() { + let ckeInit = this.initCKE(); + if (!ckeInit) { + this.fallbackActive = true; + } + }, + methods: { + initCKE() { + if (!STUDIP.wysiwyg_enabled) { + return false; + } + let view = this; + STUDIP.wysiwyg.replace(view.$refs.studip_wysiwyg); + let wysiwyg_editor = CKEDITOR.instances[view.$refs.studip_wysiwyg.id]; + wysiwyg_editor.on('blur', function() { + //console.log('cke blur'); + }); + wysiwyg_editor.on('change', function() { + view.$emit('input', wysiwyg_editor.getData()); + }); + return true; + }, + updateValue(value) { + if (this.fallbackActive) { + this.$emit('input', value); + } + } + }, + +} +</script> + +<style> + +</style>
\ No newline at end of file diff --git a/resources/vue/components/courseware/CoursewareAccordionContainer.vue b/resources/vue/components/courseware/CoursewareAccordionContainer.vue new file mode 100755 index 0000000..d69425a --- /dev/null +++ b/resources/vue/components/courseware/CoursewareAccordionContainer.vue @@ -0,0 +1,169 @@ +<template> + <courseware-default-container + :container="container" + containerClass="cw-container-accordion" + :canEdit="canEdit" + :isTeacher="isTeacher" + @storeContainer="storeContainer" + @closeEdit="initCurrentData" + > + <template v-slot:containerContent> + <courseware-collapsible-box + v-for="(section, index) in container.attributes.payload.sections" + :key="index" + :title="section.name" + :icon="section.icon" + :open="index === 0" + > + <ul class="cw-container-accordion-block-list"> + <li v-for="block in blocks" :key="block.id" class="cw-block-item"> + <component + v-if="section.blocks.includes(block.id)" + :is="component(block)" + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + /> + </li> + <li v-if="showEditMode"> + <courseware-block-adder-area :container="container" :section="index" @updateContainerContent="updateContent"/> + </li> + </ul> + </courseware-collapsible-box> + </template> + <template v-slot:containerEditDialog> + <form class="default cw-container-dialog-edit-form" @submit.prevent=""> + <fieldset v-for="(section, index) in currentContainer.attributes.payload.sections" :key="index"> + <label> + <translate>Title</translate> + <input type="text" v-model="section.name" /> + </label> + <label> + <translate>Icon</translate> + <v-select :options="icons" v-model="section.icon" class="cw-vs-select"> + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span> + </template> + <template #no-options="{ search, searching, loading }"> + Es steht keine Auswahl zur Verfügung. + </template> + <template #selected-option="option"> + <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span> + </template> + <template #option="option"> + <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span> + </template> + </v-select> + </label> + <label + class="cw-container-section-delete" + v-if="currentContainer.attributes.payload.sections.length > 1" + > + <button class="button trash" @click="deleteSection(index)"><translate>Fach löschen</translate></button> + </label> + </fieldset> + </form> + <button class="button add" @click="addSection"><translate>Fach hinzufügen</translate></button> + </template> + </courseware-default-container> +</template> + +<script> +import ContainerComponents from './container-components.js'; +import containerMixin from '../../mixins/courseware/container.js'; +import contentIcons from './content-icons.js'; +import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue'; +import StudipIcon from './../StudipIcon.vue'; + +import { mapGetters, mapActions } from 'vuex'; + +export default { + name: 'courseware-accordion-container', + mixins: [containerMixin], + components: Object.assign(ContainerComponents, { + CoursewareCollapsibleBox, + StudipIcon, + }), + props: { + container: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentContainer: {}, + }; + }, + computed: { + ...mapGetters({ + blockById: 'courseware-blocks/byId', + }), + blocks() { + if (!this.container) { + return []; + } + + return this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id })); + }, + showEditMode() { + return this.$store.getters.viewMode === 'edit'; + }, + icons() { + return contentIcons; + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateContainer: 'updateContainer', + unlockObject: 'unlockObject', + }), + initCurrentData() { + // clone container to make edit reversible + this.currentContainer = JSON.parse(JSON.stringify(this.container)); + }, + addSection() { + this.currentContainer.attributes.payload.sections.push({ name: '', icon: '', blocks: [] }); + }, + deleteSection(index) { + if (this.currentContainer.attributes.payload.sections.length === 1) { + return; + } + if (this.currentContainer.attributes.payload.sections[index].blocks.length > 0) { + if (index === 0) { + this.currentContainer.attributes.payload.sections[ + index + 1 + ].blocks = this.currentContainer.attributes.payload.sections[index + 1].blocks.concat( + this.currentContainer.attributes.payload.sections[index].blocks + ); + } else { + this.currentContainer.attributes.payload.sections[ + index - 1 + ].blocks = this.currentContainer.attributes.payload.sections[index - 1].blocks.concat( + this.currentContainer.attributes.payload.sections[index].blocks + ); + } + } + this.currentContainer.attributes.payload.sections.splice(index, 1); + }, + async storeContainer() { + await this.updateContainer({ + container: this.currentContainer, + structuralElementId: this.currentContainer.relationships['structural-element'].data.id, + }); + await this.unlockObject({ id: this.container.id, type: 'courseware-containers' }); + this.initCurrentData(); + }, + component(block) { + return 'courseware-' + block.attributes["block-type"] + '-block'; + }, + updateContent(blockAdder) { + if(blockAdder.container.id === this.container.id) { + this.initCurrentData(); + } + } + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareActionWidget.vue b/resources/vue/components/courseware/CoursewareActionWidget.vue new file mode 100644 index 0000000..07238dd --- /dev/null +++ b/resources/vue/components/courseware/CoursewareActionWidget.vue @@ -0,0 +1,118 @@ +<template> + <ul class="widget-list widget-links cw-action-widget"> + <li v-show="canEdit" class="cw-action-widget-edit" @click="editElement"><translate>Seite bearbeiten</translate></li> + <li v-show="canEdit" class="cw-action-widget-add" @click="addElement"><translate>Seite hinzufügen</translate></li> + <li class="cw-action-widget-info" @click="showElementInfo"><translate>Informationen anzeigen</translate></li> + <li class="cw-action-widget-star" @click="createBookmark"><translate>Lesezeichen setzen</translate></li> + <li v-show="canEdit" @click="exportElement" class="cw-action-widget-export"><translate>Seite exportieren</translate></li> + <li v-show="canEdit" @click="oerElement" class="cw-action-widget-oer"><translate>Seite auf %{oerTitle} veröffentlichen</translate></li> + <li v-show="!isRoot && canEdit" class="cw-action-widget-trash" @click="deleteElement"><translate>Seite löschen</translate></li> + </ul> +</template> + +<script> +import StudipIcon from './../StudipIcon.vue'; +import CoursewareExport from '@/vue/mixins/courseware/export.js'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-action-widget', + components: { + StudipIcon + }, + mixins: [CoursewareExport], + data() { + return { + currentId: null, + currentElement: {}, + } + }, + computed: { + ...mapGetters({ + structuralElementById: 'courseware-structural-elements/byId', + oerTitle: 'oerTitle', + }), + structuralElement() { + if (!this.currentId) { + return null; + } + + return this.structuralElementById({ id: this.currentId }); + }, + isRoot() { + if (!this.structuralElement) { + return true; + } + + return this.structuralElement.relationships.parent.data === null; + }, + canEdit() { + if (!this.structuralElement) { + return false; + } + return this.structuralElement.attributes['can-edit']; + }, + }, + async mounted() { + if (!this.currentId) { + this.setCurrentId(this.$route.params.id); + } + }, + methods: { + ...mapActions({ + loadStructuralElement: 'loadStructuralElement', + showElementEditDialog: 'showElementEditDialog', + showElementAddDialog: 'showElementAddDialog', + showElementDeleteDialog: 'showElementDeleteDialog', + showElementInfoDialog: 'showElementInfoDialog', + showElementExportDialog: 'showElementExportDialog', + showElementOerDialog: 'showElementOerDialog', + companionInfo: 'companionInfo', + addBookmark: 'addBookmark', + lockObject: 'lockObject' + }), + async setCurrentId(id) { + this.currentId = id; + await this.loadStructuralElement(this.currentId); + this.initCurrent(); + }, + initCurrent() { + this.currentElement = JSON.parse(JSON.stringify(this.structuralElement)); + if (!this.currentElement.attributes.payload.meta) { + this.currentElement.attributes.payload.meta = {}; + } + }, + async editElement() { + await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.showElementEditDialog(true); + }, + async deleteElement() { + await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.showElementDeleteDialog(true); + }, + addElement() { + this.showElementAddDialog(true); + }, + exportElement() { + this.showElementExportDialog(true); + }, + showElementInfo() { + this.showElementInfoDialog(true); + }, + createBookmark() { + this.addBookmark(this.structuralElement); + this.companionInfo({ info: this.$gettext('Das Lesezeichen wurde gesetzt') }); + }, + oerElement() { + this.showElementOerDialog(true); + } + }, + watch: { + $route(to) { + this.setCurrentId(to.params.id); + }, + }, + + +} +</script>2
\ No newline at end of file diff --git a/resources/vue/components/courseware/CoursewareActivityItem.vue b/resources/vue/components/courseware/CoursewareActivityItem.vue new file mode 100755 index 0000000..6b18bfe --- /dev/null +++ b/resources/vue/components/courseware/CoursewareActivityItem.vue @@ -0,0 +1,48 @@ +<template> + <li class="cw-activity-item"> + <p v-if="item.username" class="cw-activity-item-user"> + <a><studip-icon role="inactive" shape="headache" />{{ item.username }}</a> + </p> + <p v-if="item.date" class="cw-activity-item-date"> + <studip-icon role="inactive" shape="timetable" />{{ item.date }} + </p> + <p class="cw-activity-item-element"> + <a :href="item.element_id"><studip-icon role="inactive" :shape="shape" />{{ item.element_breadcrumb }}</a> + </p> + <p v-if="item.text" class="cw-activity-item-text"> + {{ item.text }} + </p> + </li> +</template> + +<script> +import StudipIcon from './../StudipIcon.vue'; + +export default { + name: 'courseware-activity-item', + components: { + StudipIcon, + }, + props: { + item: Object, + }, + computed: { + shape() { + switch (this.item.type) { + case 'comment': + return 'item'; + case 'feedback': + return 'support'; + case 'new_block': + case 'new_element': + return 'add'; + case 'updated_block': + case 'updated_element': + return 'edit'; + default: + return 'question-circle-full'; + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareAudioBlock.vue b/resources/vue/components/courseware/CoursewareAudioBlock.vue new file mode 100755 index 0000000..33d8406 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareAudioBlock.vue @@ -0,0 +1,566 @@ +<template> + <div class="cw-block cw-block-audio"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div> + <audio + :src="currentURL" + class="cw-audio-player" + ref="audio" + @timeupdate="onTimeUpdateListener" + @loadeddata="setDuration" + @ended="onEndedListener" + /> + <div v-if="!emptyAudio" class="cw-audio-container"> + <div class="cw-audio-current-track"> + <p>{{ activeTrackName }}</p> + </div> + <div class="cw-audio-controls"> + <input + class="cw-audio-range" + ref="range" + type="range" + :value="currentSeconds" + min="0" + :max="Math.round(durationSeconds)" + @input="rangeAction" + /> + <span class="cw-audio-time">{{ currentTime }} / {{ durationTime }}</span> + + <button v-if="hasPlaylist" class="cw-audio-button cw-audio-prevbutton" @click="prevAudio" /> + <button v-if="!playing" class="cw-audio-button cw-audio-playbutton" @click="playAudio" /> + <button v-if="playing" class="cw-audio-button cw-audio-pausebutton" @click="pauseAudio" /> + <button v-if="hasPlaylist" class="cw-audio-button cw-audio-nextbutton" @click="nextAudio" /> + <button class="cw-audio-button cw-audio-stopbutton" @click="stopAudio" /> + </div> + </div> + <div v-if="hasPlaylist" class="cw-audio-playlist-wrapper"> + <ul class="cw-audio-playlist"> + <li + v-for="(file, index) in files" + :key="file.id" + :class="{ + 'is-playing': index === currentPlaylistItem && playing, + 'current-item': index === currentPlaylistItem, + }" + class="cw-playlist-item" + @click="setCurrentPlaylistItem(index)" + > + {{ file.name }} + </li> + </ul> + <div v-if="showRecorder && canGetMediaDevices" class="cw-audio-playlist-recorder"> + <button + v-show="!userRecorderEnabled" + class="button" + @click="enableRecorder" + > + <translate>Aufnahme aktivieren</translate> + </button> + <button + v-show="userRecorderEnabled && !isRecording && !newRecording" + class="button" + @click="startRecording" + > + <translate>Aufnahme starten</translate> + </button> + <button + v-show="newRecording && !isRecording" + class="button" + @click="startRecording" + > + <translate>Aufnahme wiederholen</translate> + </button> + <button + v-show="isRecording" + class="button" + @click="stopRecording" + > + <translate>Aufnahme beenden</translate> + </button> + <button + v-show="newRecording && !isRecording" + class="button" + @click="resetRecorder" + > + <translate>Aufnahme löschen</translate> + </button> + <button + v-show="newRecording && !isRecording" + class="button" + @click="storeRecording" + > + <translate>Aufnahme speichern</translate> + </button> + <span v-show="isRecording"> + <translate>Aufnahme läuft</translate>: {{seconds2time(timer)}} + </span> + </div> + </div> + <div v-if="emptyAudio" class="cw-audio-empty"> + <p><translate>Es ist keine Audio-Datei verfügbar</translate></p> + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Überschrift</translate> + <input type="text" v-model="currentTitle" /> + </label> + <label> + <translate>Quelle</translate> + <select v-model="currentSource"> + <option value="studip_file"><translate>Dateibereich Datei</translate></option> + <option value="studip_folder"><translate>Dateibereich Ordner</translate></option> + <option value="web"><translate>Web-Adresse</translate></option> + </select> + </label> + <label v-show="currentSource === 'web'"> + <translate>URL</translate> + <input type="text" v-model="currentWebUrl" /> + </label> + <label v-show="currentSource === 'studip_file'"> + <translate>Datei</translate> + <courseware-file-chooser + v-model="currentFileId" + :isAudio="true" + @selectFile="updateCurrentFile" + /> + </label> + <label v-show="currentSource === 'studip_folder'"> + <translate>Ordner</translate> + <courseware-folder-chooser v-model="currentFolderId" allowUserFolders /> + </label> + <label v-show="currentSource === 'studip_folder'"> + <translate>Audio Aufnahmen zulassen</translate> + <select v-model="currentRecorderEnabled"> + <option value="true"><translate>Ja</translate></option> + <option value="false"><translate>Nein</translate></option> + </select> + </label> + </form> + </template> + <template #info> + <p><translate>Informationen zum Audio-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareFileChooser from './CoursewareFileChooser.vue'; +import CoursewareFolderChooser from './CoursewareFolderChooser.vue'; + +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-audio-block', + components: { + CoursewareDefaultBlock, + CoursewareFileChooser, + CoursewareFolderChooser, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentTitle: '', + currentSource: '', + currentFileId: '', + currentFolderId: '', + currentWebUrl: '', + currentFile: {}, + currentSeconds: 0, + durationSeconds: 0, + playing: false, + currentPlaylistItem: 0, + currentRecorderEnabled: false, + userRecorderEnabled: false, + recorder: null, + chunks: [], + blob: null, + timer: 0, + isRecording: false, + newRecording: false, + }; + }, + computed: { + ...mapGetters({ + fileRefById: 'file-refs/byId', + relatedFileRefs: 'file-refs/related', + urlHelper: 'urlHelper', + userId: 'userId', + usersById: 'users/byId', + relatedTermOfUse: 'terms-of-use/related' + }), + files() { + const files = + this.relatedFileRefs({ + parent: { type: 'folders', id: this.currentFolderId }, + relationship: 'file-refs' + }) ?? []; + + return files + .filter((file) => { + if (this.relatedTermOfUse({parent: file, relationship: 'terms-of-use'}).attributes['download-condition'] !== 0) { + return false; + } + if (! file.attributes['mime-type'].includes('audio')) { + return false; + } + + return true; + }) + .map(({ id, attributes }) => { + return { + id, + name: attributes.name, + download_url: this.urlHelper.getURL( + 'sendfile.php/', + { type: 0, file_id: id, file_name: attributes.name }, + true + ), + }; + }); + }, + currentTime() { + return this.seconds2time(this.currentSeconds); + }, + durationTime() { + return this.seconds2time(this.durationSeconds); + }, + title() { + return this.block?.attributes?.payload?.title; + }, + source() { + return this.block?.attributes?.payload?.source; + }, + fileId() { + return this.block?.attributes?.payload?.file_id; + }, + folderId() { + return this.block?.attributes?.payload?.folder_id; + }, + webUrl() { + return this.block?.attributes?.payload?.web_url; + }, + recorderEnabled() { + return this.block?.attributes?.payload?.recorder_enabled; + }, + showRecorder() { + return this.currentRecorderEnabled === 'true'; + }, + hasPlaylist() { + return this.files.length > 0 && this.currentSource === 'studip_folder'; + }, + canGetMediaDevices() { + return navigator.mediaDevices !== undefined; + }, + currentURL() { + if (this.currentSource === 'studip_file') { + return this.currentFile.download_url; + } + if (this.currentSource === 'studip_folder') { + if (this.files.length > 0) { + return this.files[this.currentPlaylistItem].download_url; + } else { + return ''; + } + } + if (this.currentSource === 'web') { + return this.currentWebUrl; + } + + return ''; + }, + activeTrackName() { + if (this.currentSource === 'studip_file') { + return this.currentFile.name; + } + if (this.currentSource === 'studip_folder') { + if (this.files.length > 0) { + return this.files[this.currentPlaylistItem].name; + } else { + return ''; + } + } + if (this.currentSource === 'web') { + return this.currentWebUrl; + } + + return ''; + }, + emptyAudio() { + if (this.currentSource === 'studip_folder' && this.currentFolderId !== '') { + return false; + } + if (this.currentSource === 'studip_file' && this.currentFileId !== '') { + return false; + } + if (this.currentSource === 'web' && this.currentWebUrl !== '') { + return false; + } + return true; + } + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + loadFileRef: 'file-refs/loadById', + loadRelatedFileRefs: 'file-refs/loadRelated', + updateBlock: 'updateBlockInContainer', + companionWarning: 'companionWarning', + companionSuccess: 'companionSuccess', + createFile: 'createFile', + }), + initCurrentData() { + this.currentTitle = this.title; + this.currentSource = this.source; + this.currentFileId = this.fileId; + this.currentWebUrl = this.webUrl; + if (this.currentFileId !== '') { + this.loadFile(); + } + this.currentFolderId = this.folderId; + this.currentRecorderEnabled = this.recorderEnabled; + }, + updateCurrentFile(file) { + this.currentFile = file; + this.currentFileId = file.id; + }, + getFolderFiles() { + return this.loadRelatedFileRefs({ + parent: { type: 'folders', id: this.currentFolderId }, + relationship: 'file-refs', + options: { include: 'terms-of-use' } + }); + }, + storeBlock() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.title = this.currentTitle; + attributes.payload.source = this.currentSource; + attributes.payload.file_id = ''; + attributes.payload.web_url = ''; + attributes.payload.folder_id = ''; + attributes.payload.recorder_enabled = 'false'; + if (this.currentSource === 'studip_file') { + attributes.payload.file_id = this.currentFileId; + } else if (this.currentSource === 'web') { + attributes.payload.web_url = this.currentWebUrl; + } else if (this.currentSource === 'studip_folder') { + attributes.payload.folder_id = this.currentFolderId; + attributes.payload.recorder_enabled = this.currentRecorderEnabled; + } else { + return false; + } + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + rangeAction() { + if (this.$refs.range.value !== this.currentSeconds) { + this.$refs.audio.currentTime = this.$refs.range.value; + } + }, + setDuration() { + this.durationSeconds = this.$refs.audio.duration; + }, + playAudio() { + this.$refs.audio.play(); + this.playing = true; + }, + pauseAudio() { + this.$refs.audio.pause(); + this.playing = false; + }, + stopAudio() { + this.pauseAudio(); + this.$refs.audio.currentTime = 0; + }, + onTimeUpdateListener() { + this.currentSeconds = this.$refs.audio.currentTime; + }, + onEndedListener() { + this.stopAudio(); + if(this.hasPlaylist) { + this.nextAudio(); + } + }, + seconds2time(seconds) { + seconds = Math.round(seconds); + let hours = Math.floor(seconds / 3600); + let minutes = Math.floor((seconds - hours * 3600) / 60); + let time = ''; + seconds = seconds - hours * 3600 - minutes * 60; + if (hours !== 0) { + time = hours + ':'; + } + if (minutes !== 0 || time !== '') { + minutes = minutes < 10 && time !== '' ? '0' + minutes : String(minutes); + time += minutes + ':'; + } + if (time === '') { + time = seconds < 10 ? '0:0' + seconds : '0:' + seconds; + } else { + time += seconds < 10 ? '0' + seconds : String(seconds); + } + return time; + }, + setCurrentPlaylistItem(index) { + if (this.currentPlaylistItem === index) { + if (this.playing) { + this.pauseAudio(); + } else { + this.playAudio(); + } + } else { + this.currentPlaylistItem = index; + this.$nextTick(()=> { + this.playAudio(); + }); + } + }, + prevAudio() { + this.stopAudio(); + if (this.currentPlaylistItem !== 0) { + this.currentPlaylistItem = this.currentPlaylistItem - 1; + } else { + this.currentPlaylistItem = this.files.length - 1; + } + this.$nextTick(()=> { + this.playAudio(); + }); + }, + nextAudio() { + this.stopAudio(); + if (this.currentPlaylistItem < this.files.length - 1) { + this.currentPlaylistItem = this.currentPlaylistItem + 1; + } else { + this.currentPlaylistItem = 0; + } + this.$nextTick(()=> { + this.playAudio(); + }); + }, + + async loadFile() { + const id = this.currentFileId; + await this.loadFileRef({ id }); + const fileRef = this.fileRefById({ id }); + + if (fileRef) { + this.updateCurrentFile({ + id: fileRef.id, + name: fileRef.attributes.name, + download_url: this.urlHelper.getURL( + 'sendfile.php', + { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name }, + true + ), + }); + } + }, + enableRecorder() { + let view = this; + navigator.mediaDevices.getUserMedia({audio: true}).then(_stream => { + let stream = _stream; + view.recorder = new MediaRecorder(stream); + view.userRecorderEnabled = true; + + view.recorder.ondataavailable = e => { + view.chunks.push(e.data); + if(view.recorder.state == 'inactive') { + this.blob = new Blob(view.chunks, {type: 'audio/mpeg' }); + } + }; + view.recorder.start(); + view.recorder.stop(); + view.chunks = []; + view.blob = null; + + }).catch(error => { + view.companionWarning({ + info: view.$gettext('Sie müssen ein Mikrofon freigeben, um eine Aufnahme starten zu können.') + }); + console.debug(error); + }); + }, + startRecording() { + let view = this; + this.chunks = []; + this.timer = 0; + this.recorder.start(); + this.isRecording = true; + setTimeout(function(){ view.setTimer(); }, 1000); + }, + stopRecording() { + this.isRecording = false; + this.newRecording = true; + this.recorder.stop(); + }, + setTimer() { + let view = this; + if (this.recorder.state === 'recording') { + this.timer++; + setTimeout(function(){ view.setTimer(); }, 1000); + } + }, + async storeRecording() { + let view = this; + let user = this.usersById({id: this.userId}); + let file = {}; + file.attributes = {}; + file.attributes.name = (user.attributes["formatted-name"]).replace(/\s+/g, '_') + '.mp3'; + let fileObj = false; + try { + fileObj = await this.createFile({ + file: file, + filedata: view.blob, + folder: {id: this.currentFolderId} + }); + } + catch(e) { + this.companionError({ + info: this.$gettext('Es ist ein Fehler aufgetretten! Die Aufnahme konnte nicht gespeichert werden.') + }); + console.debug(e); + } + if(fileObj && fileObj.type === 'file-refs') { + this.companionSuccess({ + info: this.$gettext('Aufnahme wurde erfolgreich im Dateibereich abgelegt.') + }); + } + this.newRecording = false; + this.getFolderFiles(); + }, + resetRecorder() { + this.newRecording = false; + this.chunks = []; + this.timer = 0; + this.blob = null; + }, + }, + watch: { + currentFolderId() { + this.getFolderFiles(); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareBeforeAfterBlock.vue b/resources/vue/components/courseware/CoursewareBeforeAfterBlock.vue new file mode 100755 index 0000000..e8d7f91 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBeforeAfterBlock.vue @@ -0,0 +1,231 @@ +<template> + <div class="cw-block cw-block-before-after"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <TwentyTwenty :before="currentBeforeUrl" :after="currentAfterUrl" /> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Quelle vorher</translate> + <select v-model="currentBeforeSource"> + <option value="studip"><translate>Dateibereich</translate></option> + <option value="web"><translate>Web-Adresse</translate></option> + </select> + </label> + <label v-if="currentBeforeSource === 'web'"> + <translate>URL</translate>: + <input type="text" v-model="currentBeforeWebUrl" /> + </label> + <label v-if="currentBeforeSource === 'studip'"> + <translate>Datei</translate> + <courseware-file-chooser + v-model="currentBeforeFileId" + :isImage="true" + @selectFile="updateCurrentBeforeFile" + /> + </label> + <label> + <translate>Quelle nachher</translate> + <select v-model="currentAfterSource"> + <option value="studip"><translate>Dateibereich</translate></option> + <option value="web"><translate>Web-Adresse</translate></option> + </select> + </label> + <label v-if="currentAfterSource === 'web'"> + <translate>URL</translate> + <input type="text" v-model="currentAfterWebUrl" /> + </label> + <label v-if="currentAfterSource === 'studip'"> + <translate>Datei</translate> + <courseware-file-chooser + v-model="currentAfterFileId" + :isImage="true" + @selectFile="updateCurrentAfterFile" + /> + </label> + </form> + </template> + <template #info><translate>Informationen zum Bildvergleich-Block</translate></template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareFileChooser from './CoursewareFileChooser.vue'; +import TwentyTwenty from 'vue-twentytwenty'; +import 'vue-twentytwenty/dist/vue-twentytwenty.css'; +import { mapActions } from 'vuex'; + +export default { + name: 'courseware-before-after-block', + components: { + CoursewareDefaultBlock, + CoursewareFileChooser, + TwentyTwenty, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentBeforeSource: '', + currentBeforeFileId: '', + currentBeforeFile: {}, + currentBeforeWebUrl: '', + currentAfterSource: '', + currentAfterFileId: '', + currentAfterFile: {}, + currentAfterWebUrl: '', + afterFile: null, + beforeFile: null + }; + }, + computed: { + beforeSource() { + return this.block?.attributes?.payload?.before_source; + }, + beforeFileId() { + return this.block?.attributes?.payload?.before_file_id; + }, + beforeWebUrl() { + return this.block?.attributes?.payload?.before_web_url; + }, + afterSource() { + return this.block?.attributes?.payload?.after_source; + }, + afterFileId() { + return this.block?.attributes?.payload?.after_file_id; + }, + afterWebUrl() { + return this.block?.attributes?.payload?.after_web_url; + }, + currentBeforeUrl() { + if (this.currentBeforeSource === 'studip'&& this.currentBeforeFile?.meta) { + return this.currentBeforeFile.meta['download-url']; + } else if (this.currentBeforeSource === 'web') { + return this.currentBeforeWebUrl; + } else { + return ''; + } + }, + currentAfterUrl() { + if (this.currentAfterSource === 'studip'&& this.currentAfterFile?.meta) { + return this.currentAfterFile.meta['download-url']; + } else if (this.currentAfterSource === 'web') { + return this.currentAfterWebUrl; + } else { + return ''; + } + }, + }, + mounted() { + this.loadFileRefs(this.block.id).then((response) => { + for (let i = 0; i < response.length; i++) { + if (response[i].id === this.beforeFileId) { + this.beforeFile = response[i]; + } + + if (response[i].id === this.afterFileId) { + this.afterFile = response[i]; + } + } + + this.currentBeforeFile = this.beforeFile; + this.currentAfterFile = this.afterFile; + }); + + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + loadFileRefs: 'loadFileRefs', + companionWarning: 'companionWarning', + }), + initCurrentData() { + this.currentBeforeSource = this.beforeSource; + this.currentBeforeFileId = this.beforeFileId; + this.currentBeforeWebUrl = this.beforeWebUrl; + this.currentAfterSource = this.afterSource; + this.currentAfterFileId = this.afterFileId; + this.currentAfterWebUrl = this.afterWebUrl; + }, + updateCurrentBeforeFile(file) { + this.currentBeforeFile = file; + this.currentBeforeFileId = file.id; + }, + updateCurrentAfterFile(file) { + this.currentAfterFile = file; + this.currentAfterFileId = file.id; + }, + storeBlock() { + let cmpInfo = false; + let cmpInfoBefore = this.$gettext('Bitte wählen Sie ein Vorherbilder aus.'); + let cmpInfoAfter = this.$gettext('Bitte wählen Sie ein Nachherbilder aus.'); + let attributes = {}; + attributes.payload = {}; + attributes.payload.before_source = this.currentBeforeSource; + attributes.payload.after_source = this.currentAfterSource; + if (this.currentAfterSource === 'studip') { + if (this.currentAfterFile === null) { + cmpInfo = cmpInfoAfter; + } else { + attributes.payload.after_file_id = this.currentAfterFile.id; + attributes.payload.after_web_url = ''; + } + } else if (this.currentAfterSource === 'web') { + if (this.currentAfterWebUrl === '') { + cmpInfo = cmpInfoAfter; + } else { + attributes.payload.after_file_id = ''; + attributes.payload.after_web_url = this.currentAfterWebUrl; + } + + } else { + cmpInfo = cmpInfoAfter; + } + if (this.currentBeforeSource === 'studip') { + if (this.currentBeforeFile === null) { + cmpInfo = cmpInfoBefore; + } else { + attributes.payload.before_file_id = this.currentBeforeFile.id; + attributes.payload.before_web_url = ''; + } + } else if (this.currentBeforeSource === 'web') { + if (this.currentBeforeWebUrl === '') { + cmpInfo = cmpInfoBefore; + } else { + attributes.payload.before_file_id = ''; + attributes.payload.before_web_url = this.currentBeforeWebUrl; + } + } else { + cmpInfo = cmpInfoBefore; + } + + if (cmpInfo) { + this.companionWarning({ + info: cmpInfo + }); + return false; + } else { + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareBlockActions.vue b/resources/vue/components/courseware/CoursewareBlockActions.vue new file mode 100755 index 0000000..5b638ee --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBlockActions.vue @@ -0,0 +1,132 @@ +<template> + <div class="cw-block-actions"> + <studip-action-menu + :items="menuItems" + @editBlock="editBlock" + @setVisibility="setVisibility" + @showComments="showComments" + @showFeedback="showFeedback" + @showInfo="showInfo" + @deleteBlock="deleteBlock" + /> + </div> +</template> + +<script> +import StudipActionMenu from './../StudipActionMenu.vue'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-block-actions', + components: { + StudipActionMenu, + }, + props: { + canEdit: Boolean, + block: Object, + }, + data() { + return { + menuItems: [ + { id: 6, label: this.$gettext('Kommentare anzeigen'), icon: 'comment2', emit: 'showComments' }, + ], + }; + }, + computed: { + ...mapGetters({ + userId: 'userId', + }), + blocked() { + return this.block?.relationships['edit-blocker'].data !== null; + }, + blockerId() { + return this.blocked ? this.block?.relationships['edit-blocker'].data?.id : null; + }, + }, + mounted() { + if (this.canEdit) { + this.menuItems.push({ id: 1, label: this.$gettext('Block bearbeiten'), icon: 'edit', emit: 'editBlock' }); + this.menuItems.push({ + id: 2, + label: this.block.attributes.visible + ? this.$gettext('unsichtbar setzen') + : this.$gettext('sichtbar setzen'), + icon: this.block.attributes.visible ? 'visibility-visible' : 'visibility-invisible', // do we change the icons ? + emit: 'setVisibility', + }); + this.menuItems.push({ + id: 5, + label: this.$gettext('Feedback anzeigen'), + icon: 'comment', + emit: 'showFeedback', + }); + this.menuItems.push({ + id: 7, + label: this.$gettext('Informationen zum Block'), + icon: 'info', + emit: 'showInfo', + }); + this.menuItems.push({ + id: 9, + label: this.$gettext('Block löschen'), + icon: 'trash', + emit: 'deleteBlock' + }); + } + + this.menuItems.sort((a, b) => { + return a.id > b.id ? 1 : b.id > a.id ? -1 : 0; + }); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + lockObject: 'lockObject', + unlockObject: 'unlockObject', + }), + menuAction(action) { + this[action](); + }, + editBlock() { + this.$emit('editBlock'); + }, + showFeedback() { + this.$emit('showFeedback'); + }, + showComments() { + this.$emit('showComments'); + }, + showInfo() { + this.$emit('showInfo'); + }, + showExportOptions() { + this.$emit('showExportOptions'); + }, + async setVisibility() { + if (!this.blocked) { + await this.lockObject({ id: this.block.id, type: 'courseware-blocks' }); + } else { + if (this.blockerId !== this.userId) { + return false; + } + } + let attributes = {}; + attributes.visible = !this.block.attributes.visible; + + await this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + + await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' }); + }, + copyToClipboard() { + // use JSONAPI to copy to clipboard + }, + deleteBlock() { + this.$emit('deleteBlock'); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareBlockAdderArea.vue b/resources/vue/components/courseware/CoursewareBlockAdderArea.vue new file mode 100755 index 0000000..156048f --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBlockAdderArea.vue @@ -0,0 +1,55 @@ +<template> + <div + class="cw-block-adder-area" + :class="{ 'cw-block-adder-active': adderActive, 'cw-block-adder-disabled': adderDisable }" + @click="selectBlockAdder" + > + <translate>Block hinzufügen</translate> + </div> +</template> + +<script> +export default { + name: 'courseware-block-adder-area', + props: { + container: Object, + section: Number, + }, + data() { + return { + adderActive: false, + }; + }, + computed: { + adderDisable() { + return Object.keys(this.$store.getters.blockAdder).length !== 0 && !this.adderActive; + }, + adderStorage() { + return this.$store.getters.blockAdder; + }, + }, + methods: { + selectBlockAdder() { + if (this.adderDisable) { + return false; + } + if (this.adderActive) { + this.adderActive = false; + this.$store.dispatch('coursewareBlockAdder', {}); + } else { + this.adderActive = true; + this.$store.dispatch('coursewareBlockAdder', { container: this.container, section: this.section }); + this.$store.dispatch('coursewareShowToolbar', true); + } + }, + }, + watch: { + adderStorage(newValue, oldValue) { + if (Object.keys(newValue).length === 0) { + this.adderActive = false; + this.$emit('updateContainerContent', oldValue) + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareBlockComments.vue b/resources/vue/components/courseware/CoursewareBlockComments.vue new file mode 100755 index 0000000..f182aa8 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBlockComments.vue @@ -0,0 +1,85 @@ +<template> + <section class="cw-block-comments"> + <header><translate>Kommentare</translate></header> + <div class="cw-block-features-content"> + <div class="cw-block-comments-items" ref="comments"> + <courseware-talk-bubble + v-for="comment in comments" + :key="comment.id" + :payload="buildPayload(comment)" + /> + </div> + <div class="cw-block-comment-create"> + <textarea v-model="createComment" :placeholder="placeHolder" spellcheck="true"></textarea> + <button class="button" @click="postComment"><translate>Senden</translate></button> + <button class="button" @click="$emit('close')"><translate>Schließen</translate></button> + </div> + </div> + </section> +</template> + +<script> +import CoursewareTalkBubble from './CoursewareTalkBubble.vue'; +import { mapGetters } from 'vuex'; + +export default { + name: 'courseware-block-comments', + components: { + CoursewareTalkBubble, + }, + props: { + block: Object, + comments: Array, + }, + data() { + return { + createComment: '', + placeHolder: this.$gettext('Stellen Sie eine Frage oder kommentieren Sie...'), + }; + }, + computed: { + ...mapGetters({ + relatedUser: 'users/related', + userId: 'userId', + }), + }, + methods: { + async postComment() { + let data = {}; + data.attributes = {}; + data.attributes.comment = this.createComment; + data.relationships = {}; + data.relationships.block = {}; + data.relationships.block.data = {}; + data.relationships.block.data.id = this.block.id; + data.relationships.block.data.type = this.block.type; + + await this.$store.dispatch('courseware-block-comments/create', data); + this.$emit('postComment'); + this.createComment = ''; + }, + buildPayload(comment) { + const commenter = this.relatedUser({ + parent: { id: comment.id, type: comment.type }, + relationship: 'user', + }); + + const payload = { + id: comment.id, + own: comment.relationships.user.data.id === this.userId, + content: comment.attributes.comment, + chdate: comment.attributes.chdate, + mkdate: comment.attributes.mkdate, + user_id: commenter.id, + user_name: commenter.attributes['formatted-name'], + user_avatar: commenter.meta.avatar.small, + }; + + return payload; + }, + }, + updated() { + this.$refs.comments.scrollTop = this.$refs.comments.scrollHeight; + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareBlockEdit.vue b/resources/vue/components/courseware/CoursewareBlockEdit.vue new file mode 100755 index 0000000..2b1909f --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBlockEdit.vue @@ -0,0 +1,41 @@ +<template> + <section class="cw-block-edit" @click="deactivateToolbar"> + <header><translate>Bearbeiten</translate></header> + <div class="cw-block-features-content"> + <slot name="edit" /> + <div class="cw-button-box"> + <button class="button" @click="$emit('store'); exitHandler = false;"><translate>Speichern</translate></button> + <button class="button" @click="$emit('close'); exitHandler = false;"><translate>Abbrechen</translate></button> + </div> + </div> + </section> +</template> + +<script> +export default { + name: 'courseware-block-edit', + props: { + block: Object, + }, + data() { + return { + originalBlock: Object, + exitHandler: true + }; + }, + beforeMount() { + this.originalBlock = this.block; + }, + methods: { + deactivateToolbar() { + this.$store.dispatch('coursewareShowToolbar', false); + }, + }, + beforeDestroy() { + if (this.exitHandler) { + console.log('autosave'); + this.$emit('store'); + } + } +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareBlockExportOptions.vue b/resources/vue/components/courseware/CoursewareBlockExportOptions.vue new file mode 100755 index 0000000..ec47a6f --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBlockExportOptions.vue @@ -0,0 +1,18 @@ +<template> + <section class="cw-block-export-options"> + <header><translate>Export Options</translate></header> + <div class="cw-block-features-content"> + <button class="button" @click="$emit('close')"><translate>Schließen</translate></button> + </div> + </section> +</template> + +<script> +export default { + name: 'courseware-block-export-options', + components: {}, + props: { + block: Object, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareBlockFeedback.vue b/resources/vue/components/courseware/CoursewareBlockFeedback.vue new file mode 100755 index 0000000..05ec040 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBlockFeedback.vue @@ -0,0 +1,83 @@ +<template> + <section class="cw-block-feedback"> + <header><translate>Feedback</translate></header> + <div class="cw-block-features-content"> + <div class="cw-block-feedback-items" ref="feedbacks"> + <courseware-talk-bubble + v-for="feedback in feedback" + :key="feedback.id" + :payload="buildPayload(feedback)" + /> + </div> + <div class="cw-block-feedback-create"> + <textarea v-model="feedbackText" :placeholder="placeHolder" spellcheck="true"></textarea> + <button class="button" @click="postFeedback"><translate>Senden</translate></button> + <button class="button" @click="$emit('close')"><translate>Schließen</translate></button> + </div> + </div> + </section> +</template> + +<script> +import CoursewareTalkBubble from './CoursewareTalkBubble.vue'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-block-feedback', + components: { + CoursewareTalkBubble, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + feedbackText: '', + placeHolder: this.$gettext('Schreiben Sie ein Feedback...'), + }; + }, + computed: { + ...mapGetters({ + userId: 'userId', + getRelatedFeedback: 'courseware-block-feedback/related', + getRelatedUser: 'users/related', + }), + feedback() { + const { id, type } = this.block; + + return this.getRelatedFeedback({ parent: { id, type }, relationship: 'feedback' }); + }, + }, + methods: { + ...mapActions({ + loadFeedback: 'loadFeedback', + createFeedback: 'createFeedback', + }), + async postFeedback() { + this.createFeedback({ blockId: this.block.id, feedback: this.feedbackText }); + this.feedbackText = ''; + }, + buildPayload(feedback) { + const { id, type } = feedback; + const user = this.getRelatedUser({ parent: { id, type }, relationship: 'user' }); + + return { + own: user.id === this.userId, + content: feedback.attributes.feedback, + chdate: feedback.attributes.chdate, + mkdate: feedback.attributes.mkdate, + user_name: user?.attributes?.['formatted-name'] ?? '', + user_avatar: user?.meta?.avatar.small, + }; + }, + }, + async mounted() { + await this.loadFeedback(this.block.id); + }, + updated() { + this.$refs.feedbacks.scrollTop = this.$refs.feedbacks.scrollHeight; + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareBlockHelper.vue b/resources/vue/components/courseware/CoursewareBlockHelper.vue new file mode 100755 index 0000000..ba54f7d --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBlockHelper.vue @@ -0,0 +1,256 @@ +<template> + <div class="block-helper"> + <courseware-companion-box :msgCompanion="currentQuestion.text" :mood="companionMood"/> + <div v-if="showBlocks" class="cw-block-helper-results"> + <courseware-blockadder-item + v-for="(block, index) in selectedBlockTypes" + :key="index" + :title="block.title" + :type="block.type" + :description="block.description" + @blockAdded="resetQuestions" + /> + </div> + <div class="cw-block-helper-buttons"> + <button + v-for="(response, index) in currentQuestion.responses" + class="button" + :key="index" + @click="setQuestion(response.answer)" + > + {{ response.text }} + </button> + + <button v-if="currentQuestionId !== 'a'" class="button cw-block-helper-reset" @click="resetQuestions"> + zurücksetzen + </button> + </div> + </div> +</template> + +<script> +import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; +import CoursewareBlockadderItem from './CoursewareBlockadderItem.vue'; +import { mapGetters } from 'vuex'; + +export default { + name: 'courseware-block-helper', + components: { + CoursewareCompanionBox, + CoursewareBlockadderItem, + }, + + data() { + return { + companionMood: 'pointing', + questions: [ + { + id: 'a', + text: this.$gettext( + 'Ich helfe Ihnen bei der Auswahl des richtigen Blocks. Beantworten Sie mir einfach ein paar Fragen. Meine Vorschläge werden dann hier anzeigen.' + ), + responses: [ + { + text: this.$gettext('Ok'), + answer: 'b', + }, + ], + blockChooser: false, + }, + { + id: 'b', + text: this.$gettext('Kommt der Inhalt von einer anderen Plattform, z.B. Youtube?'), + responses: [ + { + text: this.$gettext('Ja'), + answer: 'c', + }, + { + text: this.$gettext('Nein'), + answer: 'd', + }, + ], + blockChooser: false, + }, + { + id: 'c', + text: this.$gettext('Prima! Hier sind meine Vorschläge.'), + responses: [], + blockChooser: { category: 'external', fileType: false }, + }, + { + id: 'd', + text: this.$gettext('Enthält der Inhalt eine oder mehrere Dateien?'), + responses: [ + { + text: this.$gettext('Ja'), + answer: 'e', + }, + { + text: this.$gettext('Nein'), + answer: 'f', + }, + ], + blockChooser: false, + }, + { + id: 'f', + text: this.$gettext('Handelt es sich bei dem Inhalt hauptsächlich um Text?'), + responses: [ + { + text: this.$gettext('Ja'), + answer: 'g', + }, + { + text: this.$gettext('Nein'), + answer: 'h', + }, + ], + blockChooser: false, + }, + { + id: 'g', + text: this.$gettext('Prima! Hier sind meine Vorschläge.'), + responses: [], + blockChooser: { category: 'text', fileType: false }, + }, + { + id: 'h', + text: this.$gettext('Prima! Hier sind meine Vorschläge.'), + responses: [], + blockChooser: { category: 'layout', fileType: false }, + }, + { + id: 'e', + text: this.$gettext('Um weleche Art von Datei(en) handelt es sich?'), + responses: [ + { + text: this.$gettext('Audio'), + answer: 'i', + }, + { + text: this.$gettext('Bild'), + answer: 'j', + }, + { + text: this.$gettext('Dokument'), + answer: 'k', + }, + { + text: this.$gettext('Video'), + answer: 'l', + }, + { + text: this.$gettext('beliebig'), + answer: 'm', + }, + ], + blockChooser: false, + }, + { + id: 'i', + text: this.$gettext('Prima! Hier sind meine Vorschläge.'), + responses: [], + blockChooser: { category: false, fileType: 'audio' }, + }, + { + id: 'j', + text: this.$gettext('Prima! Hier sind meine Vorschläge.'), + responses: [], + blockChooser: { category: false, fileType: 'image' }, + }, + { + id: 'k', + text: this.$gettext('Prima! Hier sind meine Vorschläge.'), + responses: [], + blockChooser: { category: false, fileType: 'document' }, + }, + { + id: 'l', + text: this.$gettext('Prima! Hier sind meine Vorschläge.'), + responses: [], + blockChooser: { category: false, fileType: 'video' }, + }, + { + id: 'm', + text: this.$gettext('Prima! Hier sind meine Vorschläge.'), + responses: [], + blockChooser: { category: false, fileType: 'all' }, + }, + ], + currentQuestionId: 'a', + showBlocks: false, + selectedBlockTypes: [], + }; + }, + computed: { + ...mapGetters({ blockTypes: 'blockTypes' }), + currentQuestion() { + let question = {}; + let view = this; + this.questions.forEach((q) => { + if (q.id === view.currentQuestionId) { + question = q; + } + }); + return question; + }, + }, + methods: { + blockChooser(choice) { + if (choice.category) { + this.setSelectedBlockTypesByCategory(choice.category); + this.showBlocks = true; + } else if (choice.fileType) { + this.setSelectedBlockTypesByFileTypes(choice.fileType); + this.showBlocks = true; + } + }, + setQuestion(q) { + this.currentQuestionId = q; + if(this.currentQuestion.responses.length === 0) { + this.companionMood= 'special'; + } else { + this.companionMood= 'unsure'; + } + }, + setSelectedBlockTypesByCategory(cat) { + this.selectedBlockTypes = []; + + this.blockTypes.forEach((block) => { + if (block.categories.includes(cat)) { + this.selectedBlockTypes.push(block); + } + }); + this.selectedBlockTypes.sort((a, b) => { + return a.title > b.title ? 1 : b.title > a.title ? -1 : 0; + }); + }, + setSelectedBlockTypesByFileTypes(type) { + this.selectedBlockTypes = []; + + this.blockTypes.forEach((block) => { + if (type === 'all' && block.file_types.length > 0) { + this.selectedBlockTypes.push(block); + } else if (block.file_types.includes(type)) { + this.selectedBlockTypes.push(block); + } + }); + this.selectedBlockTypes.sort((a, b) => { + return a.title > b.title ? 1 : b.title > a.title ? -1 : 0; + }); + }, + resetQuestions() { + this.currentQuestionId = 'a'; + this.showBlocks = false; + this.selectedBlockTypes = []; + this.companionMood= 'pointing'; + }, + }, + watch: { + currentQuestionId() { + this.blockChooser(this.currentQuestion.blockChooser); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareBlockInfo.vue b/resources/vue/components/courseware/CoursewareBlockInfo.vue new file mode 100755 index 0000000..1863c16 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBlockInfo.vue @@ -0,0 +1,61 @@ +<template> + <section class="cw-block-info"> + <header><translate>Informationen</translate></header> + <div class="cw-block-features-content cw-block-info-content"> + <table class="cw-block-info-table"> + <tr> + <td><translate>Blockbeschreibung</translate></td> + <td><slot name="info" /></td> + </tr> + <tr> + <td><translate>Block wurde erstellt von</translate></td> + <td>{{ owner }}</td> + </tr> + <tr> + <td><translate>Block wurde erstellt am</translate>:</td> + <td><iso-date :date="block.attributes.mkdate" /></td> + </tr> + <tr> + <td><translate>Zuletzt bearbeitet von</translate>:</td> + <td>{{ editor }}</td> + </tr> + <tr> + <td><translate>Zuletzt bearbeitet am</translate>:</td> + <td><iso-date :date="block.attributes.chdate" /></td> + </tr> + </table> + <button class="button" @click="$emit('close')"><translate>Schließen</translate></button> + </div> + </section> +</template> + +<script> +import IsoDate from './IsoDate.vue'; + +export default { + name: 'courseware-block-info', + components: { IsoDate }, + props: { + block: Object, + }, + computed: { + owner() { + const owner = this.$store.getters['users/related']({ + parent: this.block, + relationship: 'owner', + }); + + return owner?.attributes['formatted-name'] ?? ''; + }, + + editor() { + const editor = this.$store.getters['users/related']({ + parent: this.block, + relationship: 'editor', + }); + + return editor?.attributes['formatted-name'] ?? ''; + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareBlockadderItem.vue b/resources/vue/components/courseware/CoursewareBlockadderItem.vue new file mode 100755 index 0000000..1aefc23 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareBlockadderItem.vue @@ -0,0 +1,78 @@ +<template> + <div class="cw-blockadder-item" :class="['cw-blockadder-item-' + type]" @click="addBlock"> + <header class="cw-blockadder-item-title"> + {{ title }} + </header> + <p class="cw-blockadder-item-description"> + {{ description }} + </p> + </div> +</template> + +<script> +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-blockadder-item', + components: {}, + props: { + title: String, + description: String, + type: String, + }, + data() { + return { + showInfo: false, + }; + }, + computed: { + ...mapGetters({ + blockAdder: 'blockAdder', + }), + }, + methods: { + ...mapActions({ + createBlock: 'createBlockInContainer', + companionInfo: 'companionInfo', + companionWarning: 'companionWarning', + companionSuccess: 'companionSuccess', + updateContainer: 'updateContainer', + lockObject: 'lockObject', + unlockObject: 'unlockObject', + }), + async addBlock() { + if (Object.keys(this.blockAdder).length !== 0) { + // lock parent container + await this.lockObject({ id: this.blockAdder.container.id, type: 'courseware-containers' }); + // create new block + await this.createBlock({ + container: this.blockAdder.container, + section: this.blockAdder.section, + blockType: this.type, + }); + //get new Block + const newBlock = this.$store.getters['courseware-blocks/lastCreated']; + // update container information -> new block id in sections + let container = this.blockAdder.container; + container.attributes.payload.sections[this.blockAdder.section].blocks.push(newBlock.id); + const structuralElementId = container.relationships['structural-element'].data.id; + // update container + await this.updateContainer({ container, structuralElementId }); + // unlock container + await this.unlockObject({ id: this.blockAdder.container.id, type: 'courseware-containers' }); + this.companionSuccess({ + info: this.$gettext('Block wurde erfolgreich eingefügt.'), + }); + this.$emit('blockAdded'); + } else { + // companion action + this.companionWarning({ + info: this.$gettext('Bitte wählen Sie einen Ort aus, an dem der Block eingefügt werden soll.'), + }); + } + }, + }, +}; +</script> + +<style></style> diff --git a/resources/vue/components/courseware/CoursewareCanvasBlock.vue b/resources/vue/components/courseware/CoursewareCanvasBlock.vue new file mode 100755 index 0000000..8e749e9 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareCanvasBlock.vue @@ -0,0 +1,557 @@ +<template> + <div class="cw-block cw-block-canvas" ref="block"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div v-if="currentTitle" class="cw-block-title"> + {{ currentTitle }} + </div> + <div class="cw-canvasblock-toolbar"> + <div class="cw-canvasblock-buttonset"> + <button class="cw-canvasblock-reset" :title="$gettext('Zurücksetzen')" @click="reset"></button> + <button class="cw-canvasblock-undo" :title="$gettext('Rückgängig')" @click="undo"></button> + <button v-if="hasUploadFolder" class="cw-canvasblock-store" :title="$gettext('Bild im Dateibereich speichern')" @click="store"></button> + </div> + <div class="cw-canvasblock-buttonset"> + <button + v-for="(rgba, color) in colors" + :key="color" + class="cw-canvasblock-color" + :class="[currentColor === color ? 'selected-color' : '', color]" + @click="setColor(color)" + /> + </div> + <div class="cw-canvasblock-buttonset"> + <button + class="cw-canvasblock-size cw-canvasblock-size-small" + :class="{ 'selected-size': currentSize === 2 }" + :title="$gettext('klein')" + @click="setSize('small')" + /> + <button + class="cw-canvasblock-size cw-canvasblock-size-normal" + :class="{ 'selected-size': currentSize === 5 }" + :title="$gettext('normal')" + @click="setSize('normal')" + /> + <button + class="cw-canvasblock-size cw-canvasblock-size-large" + :class="{ 'selected-size': currentSize === 8 }" + :title="$gettext('groß')" + @click="setSize('large')" + /> + <button + class="cw-canvasblock-size cw-canvasblock-size-huge" + :class="{ 'selected-size': currentSize === 12 }" + :title="$gettext('riesig')" + @click="setSize('huge')" + /> + </div> + <div class="cw-canvasblock-buttonset"> + <button + class="cw-canvasblock-tool cw-canvasblock-tool-pen" + :class="{ 'selected-tool': currentTool === 'pen' }" + :title="$gettext('Zeichenwerkzeug')" + @click="setTool('pen')" + /> + <button + class="cw-canvasblock-tool cw-canvasblock-tool-text" + :class="{ 'selected-tool': currentTool === 'text' }" + :title="$gettext('Textwerkzeug')" + @click="setTool('text')" + > + T + </button> + </div> + </div> + <img :src="currentUrl" class="cw-canvasblock-original-img" ref="image" @load="buildCanvas" /> + <input + v-show="textInput" + class="cw-canvasblock-text-input" + ref="textInputField" + @keyup="textInputKeyUp" + /> + <canvas + class="cw-canvasblock-canvas" + :class="{ + 'cw-canvasblock-tool-selected-pen': currentTool === 'pen', + 'cw-canvasblock-tool-selected-text': currentTool === 'text', + }" + ref="canvas" + @mousedown="mouseDown" + @mousemove="mouseMove" + @mouseup="mouseUp" + @mouseout="mouseUp" + @mouseleave="mouseUp" + /> + <div class="cw-canvasblock-hints"> + <div v-show="write" class="messagebox messagebox_info cw-canvasblock-text-info"> + <translate>Texteingabe mit Enter-Taste bestätigen</translate> + </div> + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Überschrift</translate> + <input type="text" v-model="currentTitle" /> + </label> + <label> + <translate>Hintergrundbild</translate> + <select v-model="currentImage"> + <option value="true"><translate>Ja</translate></option> + <option value="false"><translate>Nein</translate></option> + </select> + </label> + <label v-if="currentImage === 'true'"> + <translate>Bilddatei</translate> + <courseware-file-chooser + v-model="currentFileId" + :isImage="true" + @selectFile="updateCurrentFile" + /> + </label> + <label> + <translate>Speicherort</translate> + <courseware-folder-chooser v-model="currentUploadFolderId" :unchoose="true"/> + </label> + <label> + <translate>Werte anderer Nutzer anzeigen</translate> + <select v-model="currentShowUserData"> + <option value="off"><translate>deaktiviert</translate></option> + <option value="teacher"><translate>nur für Lehrede</translate></option> + <option value="all"><translate>für alle</translate></option> + </select> + </label> + </form> + </template> + <template #info> + <p><translate>Informationen zum Leinwand-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareFileChooser from './CoursewareFileChooser.vue'; +import CoursewareFolderChooser from './CoursewareFolderChooser.vue'; + +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-canvas-block', + components: { + CoursewareDefaultBlock, + CoursewareFileChooser, + CoursewareFolderChooser, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentTitle: '', + currentImage: '', + currentFileId: '', + currentUploadFolderId: '', + currentShowUserData: '', + currentFile: {}, + + context: {}, + paint: false, + write: false, + clickX: [], + clickY: [], + clickDrag: [], + clickColor: [], + colors: { + white: 'rgba(255,255,255,1)', + blue: 'rgba(52,152,219,1)', + green: 'rgba(46,204,113,1)', + purple: 'rgba(155,89,182,1)', + red: 'rgba(231,76,60,1)', + yellow: 'rgba(254,211,48,1)', + orange: 'rgba(243,156,18,1)', + grey: 'rgba(149,165,166,1)', + darkgrey: 'rgba(52,73,94,1)', + black: 'rgba(0,0,0,1)', + }, + currentColor: '', + currentColorRGBA: '', + sizes: { small: 2, normal: 5, large: 8, huge: 12 }, + clickSize: [], + currentSize: '', + tools: { pen: 'pen', text: 'text' }, + currentTool: '', + clickTool: [], + Text: [], + textInput: false, + file: null + }; + }, + computed: { + ...mapGetters({ + userId: 'userId', + getUserDataById: 'courseware-user-data-fields/byId', + usersById: 'users/byId', + }), + userData() { + return this.getUserDataById({ id: this.block.relationships['user-data-field'].data.id }); + }, + canvasDraw() { + if (this.userData !== undefined && this.userData.attributes.payload.canvas_draw) { + return this.userData.attributes.payload.canvas_draw; + } else { + return false; + } + }, + title() { + return this.block?.attributes?.payload?.title; + }, + image() { + return this.block?.attributes?.payload?.image; + }, + fileId() { + return this.block?.attributes?.payload?.file_id; + }, + uploadFolderId() { + return this.block?.attributes?.payload?.upload_folder_id; + }, + showUsersData() { + return this.block?.attributes?.payload?.show_usersdata; + }, + currentUrl() { + if (this.currentFile?.meta) { + return this.currentFile.meta['download-url']; + } else if(this.currentFile?.download_url) { + return this.currentFile.download_url; + } else { + return ''; + } + }, + currentFileName() { + if (this.currentFile?.attributes?.name) { + return this.currentFile.attributes.name; + } else { + return this.currentTitle + '.jpg'; + } + }, + hasUploadFolder() { + return this.currentUploadFolderId !== ""; + }, + }, + mounted() { + this.loadFileRefs(this.block.id).then((response) => { + this.file = response[0]; + this.currentFile = this.file; + this.initCurrentData(); + this.buildCanvas(); + }); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + loadFileRefs: 'loadFileRefs', + createFile: 'createFile', + companionSuccess: 'companionSuccess', + companionError: 'companionError', + }), + initCurrentData() { + this.currentTitle = this.title; + this.currentImage = this.image; + this.currentFileId = this.fileId; + this.currentUploadFolderId = this.uploadFolderId; + this.currentShowUserData = this.showUsersData; + if (this.canvasDraw) { + this.clickX = JSON.parse(this.canvasDraw.clickX); + this.clickY = JSON.parse(this.canvasDraw.clickY); + this.clickDrag = JSON.parse(this.canvasDraw.clickDrag); + this.clickColor = JSON.parse(this.canvasDraw.clickColor); + this.clickSize = JSON.parse(this.canvasDraw.clickSize); + this.clickTool = JSON.parse(this.canvasDraw.clickTool); + this.Text = JSON.parse(this.canvasDraw.Text); + } + }, + updateCurrentFile(file) { + this.currentFile = file; + this.currentFileId = file.id; + this.buildCanvas(); + }, + setColor(color) { + if (this.write) { + return; + } + this.currentColor = color; + this.currentColorRGBA = this.colors[color]; + }, + setSize(size) { + if (this.textInput) { + return; + } + this.currentSize = this.sizes[size]; + }, + setTool(tool) { + if (this.write) { + this.clickX.pop(); + this.clickY.pop(); + this.clickDrag.pop(); + this.clickColor.pop(); + this.clickSize.pop(); + this.clickTool.pop(); + this.write = false; + this.textInput = false; + } + this.currentTool = this.tools[tool]; + }, + reset() { + this.clickX.length = 0; + this.clickY.length = 0; + this.clickDrag.length = 0; + this.clickColor.length = 0; + this.clickSize.length = 0; + this.clickTool.length = 0; + this.Text.length = 0; + this.paint = false; + this.write = false; + this.textInput = false; + this.redraw(); + }, + buildCanvas() { + let blockElem = this.$refs.block; + let image = this.$refs.image; + let canvas = this.$refs.canvas; + canvas.width = blockElem.offsetWidth - 2; + if (this.currentImage === 'true' && image.height > 0) { + canvas.height = Math.round((canvas.width / image.width) * image.height); + } else { + canvas.height = 500; + } + this.context = canvas.getContext('2d'); + this.currentColor = 'blue'; + this.currentColorRGBA = this.colors['blue']; + this.currentSize = this.sizes['normal']; + this.currentTool = this.tools['pen']; + this.redraw(); + }, + redraw() { + let view = this; + let context = view.context; + let clickX = view.clickX; + let clickY = view.clickY; + context.clearRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas + context.fillStyle = '#ffffff'; + context.fillRect(0, 0, context.canvas.width, context.canvas.height); // set background + if (view.currentImage === 'true') { + let outlineImage = new Image(); + outlineImage.src = this.currentUrl; + context.drawImage(outlineImage, 0, 0, context.canvas.width, context.canvas.height); + } + + context.lineJoin = 'round'; + for (var i = 0; i < clickX.length; i++) { + if (view.clickTool[i] === 'pen') { + context.beginPath(); + if (view.clickDrag[i] && i) { + context.moveTo(clickX[i - 1], clickY[i - 1]); + } else { + context.moveTo(clickX[i] - 1, clickY[i]); + } + context.lineTo(clickX[i], clickY[i]); + context.closePath(); + context.strokeStyle = view.clickColor[i]; + context.lineWidth = view.clickSize[i]; + context.stroke(); + } + if (view.clickTool[i] === 'text') { + let fontsize = view.clickSize[i] * 6; + context.font = fontsize + 'px Arial '; + context.fillStyle = view.clickColor[i]; + context.fillText(view.Text[i], clickX[i], clickY[i] + fontsize); + } + } + }, + mouseDown(e) { + if (this.write) { + let view = this; + this.$refs.textInputField.focus(); + window.setTimeout(function () { + view.$refs.textInputField.focus(); + }, 0); + return; + } + if (this.currentTool === 'pen') { + this.paint = true; + this.addClick(e.offsetX, e.offsetY, false); + this.redraw(); + } + if (this.currentTool === 'text') { + this.write = true; + this.addClick(e.offsetX, e.offsetY, false); + } + }, + mouseMove(e) { + if (this.paint) { + this.addClick(e.offsetX, e.offsetY, true); + this.redraw(); + } + }, + mouseUp(e) { + this.storeDraw(); + this.paint = false; + }, + addClick(x, y, dragging) { + this.clickX.push(x); + this.clickY.push(y); + this.clickDrag.push(dragging); + this.clickColor.push(this.currentColorRGBA); + this.clickSize.push(this.currentSize); + this.clickTool.push(this.currentTool); + if (this.currentTool === 'text') { + this.enableTextInput(x, y); + } else { + this.Text.push(''); + } + }, + undo() { + let dragging = this.clickDrag[this.clickDrag.length - 1]; + this.clickX.pop(); + this.clickY.pop(); + this.clickDrag.pop(); + this.clickColor.pop(); + this.clickSize.pop(); + this.clickTool.pop(); + if (this.write) { + this.textInput = false; + this.write = false; + } else { + this.Text.pop(''); + } + if (dragging) { + this.undo(); + } + this.redraw(); + }, + enableTextInput(x, y) { + let view = this; + let fontsize = this.currentSize * 6; + this.textInput = true; + let input = this.$refs.textInputField; + input.value = ''; + input.style.position = 'absolute'; + input.style.top = this.$refs.canvas.offsetTop + y + 'px'; + input.style.left = 320 + x + 'px'; + input.style.lineHeight = fontsize + 'px'; + input.style.fontSize = fontsize + 'px'; + input.style.width = '300px'; + window.setTimeout(function () { + view.$refs.textInputField.focus(); + }, 0); + }, + textInputKeyUp(e) { + if (e.defaultPrevented) { + return; + } + let key = e.key || e.keyCode; + if (key === 'Enter' || key === 13) { + this.Text.push(this.$refs.textInputField.value); + this.textInput = false; + this.write = false; + this.redraw(); + } + if (key === 'Escape' || key === 'Esc' || key === 27) { + this.clickX.pop(); + this.clickY.pop(); + this.clickDrag.pop(); + this.clickColor.pop(); + this.clickSize.pop(); + this.clickTool.pop(); + this.textInput = false; + this.write = false; + } + }, + async storeDraw() { + let data = {}; + data.type = 'courseware-user-data-fields'; + data.id = this.block.relationships['user-data-field'].data.id; + data.relationships = {}; + data.relationships.block = {}; + data.relationships.block.data = {}; + data.relationships.block.data.id = this.block.id; + data.relationships.block.data.type = this.block.type; + data.attributes = {}; + data.attributes.payload = {}; + data.attributes.payload.canvas_draw = {}; + data.attributes.payload.canvas_draw.clickX = JSON.stringify(this.clickX); + data.attributes.payload.canvas_draw.clickY = JSON.stringify(this.clickY); + data.attributes.payload.canvas_draw.clickDrag = JSON.stringify(this.clickDrag); + data.attributes.payload.canvas_draw.clickColor = JSON.stringify(this.clickColor); + data.attributes.payload.canvas_draw.clickSize = JSON.stringify(this.clickSize); + data.attributes.payload.canvas_draw.clickTool = JSON.stringify(this.clickTool); + data.attributes.payload.canvas_draw.Text = JSON.stringify(this.Text); + + await this.$store.dispatch('courseware-user-data-fields/update', data); + }, + storeBlock() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.title = this.currentTitle; + attributes.payload.image = this.currentImage; + if (this.currentImage === 'true') { + attributes.payload.file_id = this.currentFileId; + } else { + attributes.payload.file_id = ''; + } + attributes.payload.upload_folder_id = this.currentUploadFolderId; + attributes.payload.show_usersdata = this.currentShowUserData; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + async store() { + let user = this.usersById({id: this.userId}); + let imageBase64 = this.context.canvas.toDataURL("image/jpeg", 1.0); + let image = await fetch(imageBase64); + let imageBlob = await image.blob(); + let file = {}; + file.attributes = {}; + if(this.currentImage === 'true') { + file.attributes.name = (user.attributes["formatted-name"]).replace(/\s+/g, '_') + '_' + this.currentFile.attributes.name; + } else { + file.attributes.name = (user.attributes["formatted-name"]).replace(/\s+/g, '_') + '_' + this.block.attributes.title + '_' + this.block.id; + } + + let img = false; + try { + img = await this.createFile({ + file: file, + filedata: imageBlob, + folder: {id: this.currentUploadFolderId} + }); + } + catch(e) { + this.companionError({ + info: this.$gettext('Es ist ein Fehler aufgetretten! Das Bild konnte nicht gespeichert werden.') + }); + console.log(e); + } + if(img && img.type === 'file-refs') { + this.companionSuccess({ + info: this.$gettext('Bild wurde erfolgreich im Dateibereich abgelegt.') + }); + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareChartBlock.vue b/resources/vue/components/courseware/CoursewareChartBlock.vue new file mode 100755 index 0000000..6ff2f13 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareChartBlock.vue @@ -0,0 +1,293 @@ +<template> + <div class="cw-block cw-block-chart"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <canvas class="cw-chart-block-canvas" ref="chartCanvas" /> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Beschriftung</translate> + <input type="text" v-model="currentLabel" @focusout="buildChart" /> + </label> + <label> + <translate>Typ</translate> + <select v-model="currentType"> + <option value="bar"><translate>Säulendiagramm</translate></option> + <option value="horizontalBar"><translate>Balkendiagramm</translate></option> + <option value="pie"><translate>Kreisdiagramm</translate></option> + <option value="doughnut"><translate>Ringdiagramm</translate></option> + <option value="polarArea"><translate>Polardiagramm</translate></option> + <option value="line"><translate>Liniendiagramm</translate></option> + </select> + </label> + <fieldset v-for="(item, index) in currentContent" :key="index"> + <legend> + <translate>Datensatz</translate> {{ index + 1 }} + <span + v-if="!onlyRecord" + class="cw-block-chart-item-remove" + :title="textRecordRemove" + @click="removeItem(index)"> + <studip-icon shape="trash" /> + </span> + </legend> + <label> + <translate>Wert</translate> + <input type="number" v-model="item.value" @change="buildChart" /> + </label> + <label> + <translate>Bezeichnung</translate> + <input type="text" v-model="item.label" @focusout="buildChart" /> + </label> + <label> + <translate>Farbe</translate> + <v-select + :options="colors" + :reduce="colors => colors.value" + label="rgb" + :clearable="false" + v-model="item.color" + class="cw-vs-select" + @option:selected="buildChart" + > + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span> + </template> + <template #no-options="{ search, searching, loading }"> + <translate>Es steht keine Auswahl zur Verfügung</translate>. + </template> + <template #selected-option="{name, rgb}"> + <span class="vs__option-color" :style="{'background-color': 'rgb(' + rgb + ')'}"></span><span>{{name}}</span> + </template> + <template #option="{name, rgb}"> + <span class="vs__option-color" :style="{'background-color': 'rgb(' + rgb + ')'}"></span><span>{{name}}</span> + </template> + </v-select> + </label> + </fieldset> + </form> + <button class="button add" @click="addItem"><translate>Datensatz hinzufügen</translate></button> + </template> + <template #info> + <p><translate>Informationen zum Chart-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import Chart from 'chart.js'; +import { mapActions } from 'vuex'; +import StudipIcon from '../StudipIcon.vue'; + +export default { + name: 'courseware-chart-block', + components: { + CoursewareDefaultBlock, + StudipIcon, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + chart: null, + currentContent: [], + currentLabel: '', + currentType: '', + colors: [ + { name:this.$gettext('rot'), value: 'red', rgb: '192, 57, 43' }, + { name:this.$gettext('blau'), value: 'blue', rgb: '52, 152, 219' }, + { name:this.$gettext('gelb'), value: 'yellow', rgb: '241, 196, 15' }, + { name:this.$gettext('grün'), value: 'green', rgb: '46, 204, 113' }, + { name:this.$gettext('lila'), value: 'purple', rgb: '155, 89, 182' }, + { name:this.$gettext('orange'), value: 'orange', rgb: '230, 126, 34' }, + { name:this.$gettext('türkis'), value: 'turquoise', rgb: '26, 188, 156' }, + { name:this.$gettext('grau'), value: 'grey', rgb: '52, 73, 94' }, + { name:this.$gettext('hellgrau'), value: 'lightgrey', rgb: '149, 165, 166' }, + { name:this.$gettext('schwarz'), value: 'black', rgb: '0, 0, 0' }, + ], + textRecordRemove: this.$gettext('Datensatz entfernen'), + }; + }, + computed: { + content() { + return this.block?.attributes?.payload?.content; + }, + label() { + return this.block?.attributes?.payload?.label; + }, + type() { + return this.block?.attributes?.payload?.type; + }, + onlyRecord() { + return this.currentContent.length === 1; + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + this.currentContent = this.content; + this.currentLabel = this.label; + this.currentType = this.type; + }, + storeBlock() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.content = this.currentContent; + attributes.payload.label = this.currentLabel; + attributes.payload.type = this.currentType; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + + addItem() { + this.currentContent.push({ value: '0', label: '', color: 'blue' }); + }, + + removeItem(recordIndex) { + this.currentContent = this.currentContent.filter((val, index) => { + return !(index === recordIndex); + }); + this.buildChart(); + }, + + buildChart() { + if (this.chart !== null) { + this.chart.destroy(); + } + let ctx = this.$refs.chartCanvas.getContext('2d'); + let type = this.currentType; + let label = this.currentLabel; + let labels = []; + let data = []; + let backgroundColor = []; + let borderColor = []; + + this.currentContent.forEach((item) => { + labels.push(item.label); + data.push(item.value); + backgroundColor.push('rgba(' + this.colors.filter((color) => { return color.value === item.color })[0].rgb + ', 0.6)'); + borderColor.push('rgba(' + this.colors.filter((color) => { return color.value === item.color })[0].rgb + ', 1.0)'); + }); + + switch (type) { + case 'bar': + case 'horizontalBar': + this.chart = new Chart(ctx, { + type: type, + data: { + labels: labels, + datasets: [ + { + label: label, + data: data, + backgroundColor: backgroundColor, + borderColor: borderColor, + borderWidth: 1, + }, + ], + }, + options: { + scales: { + yAxes: [ + { + ticks: { + beginAtZero: true, + }, + }, + ], + xAxes: [ + { + ticks: { + beginAtZero: true, + }, + }, + ], + }, + legend: { + display: false, + }, + title: { + display: true, + text: label, + }, + }, + }); + break; + case 'pie': + case 'doughnut': + case 'polarArea': + this.chart = new Chart(ctx, { + type: type, + data: { + labels: labels, + datasets: [ + { + data: data, + backgroundColor: backgroundColor, + borderWidth: 1, + }, + ], + }, + options: { + title: { + display: true, + text: label, + }, + }, + }); + break; + case 'line': + this.chart = new Chart(ctx, { + type: type, + data: { + labels: labels, + datasets: [ + { + label: label, + data: data, + fill: false, + borderWidth: 2, + pointBackgroundColor: borderColor, + }, + ], + }, + options: { + title: { + display: true, + text: label, + }, + }, + }); + break; + } + }, + }, + watch: { + currentType() { + this.buildChart(); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareCodeBlock.vue b/resources/vue/components/courseware/CoursewareCodeBlock.vue new file mode 100755 index 0000000..275a77f --- /dev/null +++ b/resources/vue/components/courseware/CoursewareCodeBlock.vue @@ -0,0 +1,114 @@ +<template> + <div class="cw-block cw-block-code"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <pre v-show="currentContent !== ''" v-highlightjs="currentContent"><code ref="code" :class="[currentLang]"></code></pre> + <div v-show="currentLang !== ''" class="code-lang"> + <span>{{ currentLang }}</span> + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Sprache</translate> + <input type="text" v-model="currentLang" /> + </label> + <label> + <translate>Quelltext</translate> + <textarea v-model="currentContent"></textarea> + </label> + </form> + </template> + <template #info> + <p><translate>Informationen zum Quelltext-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import hljs from 'highlight.js'; + +import { mapActions } from 'vuex'; + +export default { + name: 'courseware-code-block', + components: { + CoursewareDefaultBlock, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentLang: '', + currentContent: '', + }; + }, + computed: { + content() { + return this.block?.attributes?.payload?.content; + }, + lang() { + return this.block?.attributes?.payload?.lang; + }, + }, + directives: { + highlightjs: { + deep: true, + bind(el, binding) { + let targets = el.querySelectorAll('code'); + targets.forEach((target) => { + if (binding.value) { + target.innerHTML = binding.value; + } + hljs.highlightBlock(target); + }); + }, + componentUpdated(el, binding) { + let targets = el.querySelectorAll('code'); + targets.forEach((target) => { + if (binding.value) { + target.innerHTML = binding.value; + hljs.highlightBlock(target); + } + }); + }, + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + this.currentLang = this.lang; + this.currentContent = this.content; + }, + storeBlock() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.lang = this.currentLang; + attributes.payload.content = this.currentContent; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareCollapsibleBox.vue b/resources/vue/components/courseware/CoursewareCollapsibleBox.vue new file mode 100755 index 0000000..70bcf03 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareCollapsibleBox.vue @@ -0,0 +1,38 @@ +<template> + <div class="cw-collapsible" :class="{ 'cw-collapsible-open': isOpen }"> + <header :class="{ 'cw-collapsible-open': isOpen }" class="cw-collapsible-title" @click="isOpen = !isOpen"> + <studip-icon v-if="icon !== ''" :shape="icon" /> {{ title }} + </header> + <div class="cw-collapsible-content" :class="{ 'cw-collapsible-content-open': isOpen }"> + <slot></slot> + </div> + </div> +</template> + +<script> +import StudipIcon from './../StudipIcon.vue'; + +export default { + name: 'courseware-collapsible-box', + components: { + StudipIcon, + }, + props: { + title: String, + icon: { + type: String, + default: '', + }, + open: { + type: Boolean, + default: false, + }, + }, + data() { + return { + isOpen: this.open, + }; + }, + methods: {}, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareCompanionBox.vue b/resources/vue/components/courseware/CoursewareCompanionBox.vue new file mode 100755 index 0000000..9d9c2c1 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareCompanionBox.vue @@ -0,0 +1,24 @@ +<template> + <div class="cw-companion-box" :class="[mood]"> + <div> + <p>{{ msgCompanion }}</p> + <slot name="companionActions"></slot> + </div> + </div> +</template> + +<script> +export default { + name: 'courseware-companion-box', + props: { + msgCompanion: String, + mood: { + type: String, + default: 'default', + validator: value => { + return ['default','unsure', 'special', 'sad', 'pointing'].includes(value); + } + } + }, +}; +</script>
\ No newline at end of file diff --git a/resources/vue/components/courseware/CoursewareCompanionOverlay.vue b/resources/vue/components/courseware/CoursewareCompanionOverlay.vue new file mode 100755 index 0000000..5c4fc97 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareCompanionOverlay.vue @@ -0,0 +1,46 @@ +<template> + <div + class="cw-companion-overlay" + :class="[showCompanion ? 'cw-companion-overlay-in' : '', showCompanion ? '' : 'cw-companion-overlay-out', styleCompanion]" + > + <div class="cw-companion-overlay-content" v-html="msgCompanion"></div> + <button class="cw-compantion-overlay-close" @click="hideCompanion"></button> + </div> +</template> + +<script> +import { mapGetters } from 'vuex'; + +export default { + name: 'courseware-companion-overlay', + computed: { + ...mapGetters({ + showCompanion: 'showCompanionOverlay', + msgCompanion: 'msgCompanionOverlay', + styleCompanion: 'styleCompanionOverlay', + showToolbar: 'showToolbar', + }), + }, + methods: { + hideCompanion() { + this.$store.dispatch('coursewareShowCompanionOverlay', false); + }, + }, + watch: { + showCompanion(newValue, oldValue) { + let view = this; + if (newValue === true && oldValue === false) { + setTimeout(() => { + view.hideCompanion(); + }, 4000); + } + }, + showToolbar(newValue, oldValue) { + // hide companion when toolbar is closed + if (oldValue === true && newValue === false) { + this.hideCompanion(); + } + } + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareConfirmBlock.vue b/resources/vue/components/courseware/CoursewareConfirmBlock.vue new file mode 100755 index 0000000..6ba1947 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareConfirmBlock.vue @@ -0,0 +1,118 @@ +<template> + <div class="cw-block cw-block-confirm"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + :defaultGrade="false" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div class="cw-block-title"> + <translate>Bestätigung</translate> + </div> + <div class="cw-block-confirm-content"> + <div class="cw-block-confirm-checkbox"> + <studip-icon v-if="!confirm" shape="checkbox-unchecked" role="info" @click="setConfirm" /> + <studip-icon v-if="confirm" shape="checkbox-checked" role="info" /> + </div> + <p class="cw-block-confirm-text"> + {{ currentText }} + </p> + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Text</translate> + <input type="text" v-model="currentText" /> + </label> + </form> + </template> + <template #info><translate>Informationen zum Bestätigungs-Block</translate></template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import { mapActions, mapGetters } from 'vuex'; +import { blockMixin } from './block-mixin.js'; +import StudipIcon from '../StudipIcon.vue'; + +export default { + name: 'courseware-confirm-block', + mixins: [blockMixin], + components: { + CoursewareDefaultBlock, + StudipIcon, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentText: '', + confirm: false, + }; + }, + computed: { + ...mapGetters({ + userId: 'userId', + getUserDataById: 'courseware-user-data-fields/byId', + }), + text() { + return this.block?.attributes?.payload?.text; + }, + userData() { + return this.getUserDataById({ id: this.block.relationships['user-data-field'].data.id }); + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + this.currentText = this.text; + if (this.userData.attributes.payload.confirm) { + this.confirm = this.userData.attributes.payload.confirm; + } + }, + async setConfirm() { + let data = {}; + data.type = 'courseware-user-data-fields'; + data.id = this.block.relationships['user-data-field'].data.id; + data.attributes = {}; + data.attributes.payload = {}; + data.attributes.payload.confirm = true; + data.relationships = {}; + data.relationships.block = {}; + data.relationships.block.data = {}; + data.relationships.block.data.id = this.block.id; + data.relationships.block.data.type = this.block.type; + + await this.$store.dispatch('courseware-user-data-fields/update', data); + this.userProgress = 1; + this.confirm = true; + }, + storeBlock() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.text = this.currentText; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareContainerActions.vue b/resources/vue/components/courseware/CoursewareContainerActions.vue new file mode 100755 index 0000000..be2c3dd --- /dev/null +++ b/resources/vue/components/courseware/CoursewareContainerActions.vue @@ -0,0 +1,42 @@ +<template> + <div v-if="canEdit" class="cw-container-actions"> + <studip-action-menu + :items="menuItems" + @editContainer="editContainer" + @deleteContainer="deleteContainer" + /> + </div> +</template> + +<script> +export default { + name: 'courseware-container-actions', + props: { + canEdit: Boolean, + container: Object, + }, + computed: { + menuItems() { + if (this.container.attributes["container-type"] === 'list') { + return [{ id: 1, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' }]; + } else { + return [ + { id: 1, label: this.$gettext('Abschnitt bearbeiten'), icon: 'edit', emit: 'editContainer' }, + { id: 2, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' }, + ]; + } + }, + }, + methods: { + menuAction(action) { + this[action](); + }, + editContainer() { + this.$emit('editContainer'); + }, + deleteContainer() { + this.$emit('deleteContainer'); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareContainerAdderItem.vue b/resources/vue/components/courseware/CoursewareContainerAdderItem.vue new file mode 100755 index 0000000..04b9ce8 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareContainerAdderItem.vue @@ -0,0 +1,50 @@ +<template> + <div class="cw-blockadder-item" :class="['cw-blockadder-item-' + type]" @click="addContainer"> + <header class="cw-blockadder-item-title"> + {{ title }} + </header> + <p class="cw-blockadder-item-description"> + {{ description }} + </p> + </div> +</template> +<script> +import { mapActions } from 'vuex'; +export default { + name: 'courseware-container-adder-item', + components: {}, + props: { + title: String, + description: String, + type: String, + colspan: String, + firstSection: String, + secondSection: String, + }, + methods: { + ...mapActions({ + createContainer: 'createContainer', + companionSuccess: 'companionSuccess', + }), + async addContainer() { + let attributes = {}; + attributes["container-type"] = this.type; + let sections = []; + if (this.type === 'list') { + sections = [{ name: this.firstSection, icon: '', blocks: [] }]; + } else { + sections = [{ name: this.firstSection, icon: '', blocks: [] },{ name: this.secondSection, icon: '', blocks: [] }]; + } + attributes.payload = { + colspan: this.colspan, + sections: sections, + }; + await this.createContainer({ structuralElementId: this.$route.params.id, attributes: attributes }); + this.companionSuccess({ + info: this.$gettext('Abschnitt wurde erfolgreich eingefügt.'), + }); + }, + }, + mounted() {}, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareCourseDashboard.vue b/resources/vue/components/courseware/CoursewareCourseDashboard.vue new file mode 100755 index 0000000..15dc293 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareCourseDashboard.vue @@ -0,0 +1,57 @@ +<template> + <div class="cw-dashboard cw-course-dashboard"> + <courseware-collapsible-box :title="$gettext('Überblick')" :open="true" class="cw-dashboard-box cw-dashboard-box-full"> + <div class="cw-dashboard-overview"> + <courseware-oblong :name="textChapterFinished" :icon="'accept'" :size="'small'"> + <template v-slot:oblongValue> {{ chapterCounter.finished }} </template> + </courseware-oblong> + <courseware-oblong :name="textChapterStarted" :icon="'play'" :size="'small'"> + <template v-slot:oblongValue> {{ chapterCounter.started }} </template> + </courseware-oblong> + <courseware-oblong :name="textChapterAhead" :icon="'timetable'" :size="'small'"> + <template v-slot:oblongValue> {{ chapterCounter.ahead }} </template> + </courseware-oblong> + </div> + </courseware-collapsible-box> + <courseware-collapsible-box :title="$gettext('Fortschritt')" :open="true" class="cw-dashboard-box cw-dashboard-box-half"> + <courseware-dashboard-progress /> + </courseware-collapsible-box> + <courseware-collapsible-box :title="$gettext('Aktivitäten')" :open="true" class="cw-dashboard-box cw-dashboard-box-half"> + <courseware-dashboard-activities :activitiesList="activitiesList"></courseware-dashboard-activities> + </courseware-collapsible-box> + </div> +</template> + +<script> +import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue'; +import CoursewareDashboardProgress from './CoursewareDashboardProgress.vue'; +import CoursewareDashboardActivities from './CoursewareDashboardActivities.vue'; +import CoursewareOblong from './CoursewareOblong.vue'; + +export default { + name: 'courseware-course-dashboard', + components: { + CoursewareCollapsibleBox, + CoursewareOblong, + CoursewareDashboardProgress, + CoursewareDashboardActivities, + }, + data() { + return { + textChapterAhead: this.$gettext('bevorstehende Seiten'), + textChapterStarted: this.$gettext('angefangene Seiten'), + textChapterFinished: this.$gettext('abgeschlossene Seiten'), + }; + }, + computed: { + chapterCounter() { + return STUDIP.courseware_chapter_counter; + }, + + activitiesList() { + // todo in 5.1 + return []; + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareCourseManager.vue b/resources/vue/components/courseware/CoursewareCourseManager.vue new file mode 100755 index 0000000..6005174 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareCourseManager.vue @@ -0,0 +1,300 @@ +<template> + <div class="cw-course-manager"> + <courseware-tabs class="cw-course-manager-tabs"> + <courseware-tab :name="$gettext('Diese Courseware')" :selected="true"> + <courseware-manager-element + type="current" + :currentElement="currentElement" + @selectElement="setCurrentId" + /> + </courseware-tab> + <courseware-tab :name="$gettext('Export')"> + <button + class="button" + @click.prevent="doExportCourseware" + :class="{ + disabled: exportRunning, + }" + > + <translate>Alles exportieren</translate> + </button> + <br> + <translate v-if="exportRunning"> + Export läuft, bitte haben sie einen Moment Geduld... + </translate> + </courseware-tab> + </courseware-tabs> + + <courseware-tabs class="cw-course-manager-tabs"> + <courseware-tab :name="$gettext('FAQ')"> + <courseware-collapsible-box :open="true" :title="$gettext('Wie finde ich die gewünschte Stelle?')"> + <p><translate> + Wählen Sie auf der linken Seite "Diese Courseware" aus. + Beim laden der Seite ist dies immer gewählt. Die Überschrift + gibt an welche Seite Sie grade ausgewählt haben. Darunter befinden + sich die Abschnitte der Seite und innerhalb dieser dessen Blöcke. + Möchten Sie eine Seite die unterhalb der gewählten liegt bearbeiten, + können Sie diese über die Schaltflächen im Bereich "Seiten" wählen. + Über der Überschrift wird eine Navigation eingeblendet, mit dieser können + Sie beliebig weit hoch in der Hierarchie springen. + </translate></p> + </courseware-collapsible-box> + <courseware-collapsible-box :title="$gettext('Wie sortiere ich Objekte?')"> + <p><translate> + Seiten, Abschnitte und Blöcke lassen sich in ihrer Reihenfolge sortieren. + Hierzu wählen Sie auf der linken Seite unter "Diese Courseware" die Schaltfläche "Seiten sortieren", + "Abschnitte sortieren" oder "Blöcke sortieren". + An den Objekten werden Pfeile angezeigt, mit diesen können die Objekte an die gewünschte + Position gebracht werden. Um die neue Sortierung zu speichern wählen Sie "Sortieren beenden". + Sie können die Änderungen auch rückgängig machen indem Sie "Sortieren abbrechen" wählen. + </translate></p> + </courseware-collapsible-box> + <courseware-collapsible-box :title="$gettext('Wie verschiebe ich Objekte?')"> + <p><translate> + Seiten, Abschnitte und Blöcke lassen sich verschieben. + Hierzu wählen Sie auf der linken Seite unter "Diese Courseware" die Schaltfläche + "Seite an diese Stelle einfügen", "Abschnitt an diese Stelle einfügen" oder + "Block an diese Stelle einfügen". Wählen Sie dann auf der rechten Seite unter + "Verschieben" das Objekt aus das Sie verschieben möchten. Verschiebbare Objekte + erkennen Sie an den zwei nach links zeigenden gelben Pfeilen. + </translate></p> + </courseware-collapsible-box> + <courseware-collapsible-box :title="$gettext('Wie kopiere ich Objekte?')"> + <p><translate> + Seiten, Abschnitte und Blöcke lassen sich aus einer anderen Veranstaltung und Ihren + eigenen Inhalten kopieren. + Hierzu wählen Sie auf der linken Seite unter "Diese Courseware" die Schaltfläche + "Seite an diese Stelle einfügen", "Abschnitt an diese Stelle einfügen" oder + "Block an diese Stelle einfügen". Wählen Sie dann auf der rechten Seite unter + "Kopieren" erst die Veranstaltung aus der Sie kopieren möchten oder Ihre eigenen + Inhalte. Wählen sie dann das Objekt aus das Sie kopieren möchten. Kopierbare Objekte + erkennen Sie an den zwei nach links zeigenden gelben Pfeilen. + </translate></p> + </courseware-collapsible-box> + </courseware-tab> + <courseware-tab name="Verschieben" :selected="true"> + <courseware-manager-element + type="self" + :currentElement="selfElement" + :moveSelfPossible="moveSelfPossible" + :moveSelfChildPossible="moveSelfChildPossible" + @selectElement="setSelfId" + @reloadElement="reloadElements" + /> + </courseware-tab> + + <courseware-tab :name="$gettext('Kopieren')"> + <courseware-manager-copy-selector @loadSelf="reloadElements"/> + </courseware-tab> + + <courseware-tab :name="$gettext('Importieren')"> + <button + class="button" + @click.prevent="chooseFile" + :class="{ + disabled: importRunning, + }" + > + Importdatei auswählen + </button> + + <div v-if="importZip"> + <b>{{ importZip.name }}</b + ><br /> + <translate>Größe</translate>: <span>{{ getFileSizeText(importZip.size) }}</span> + </div> + + <br v-else /> + + <div v-if="importState"> + {{ importState }} + </div> + + <button + class="button" + @click.prevent="doImportCourseware" + :class="{ + disabled: importRunning || !importZip, + }" + > + <translate>Alles importieren</translate> + </button> + + <input ref="importFile" type="file" accept=".zip" @change="setImport" style="visibility: hidden" /> + </courseware-tab> + </courseware-tabs> + </div> +</template> +<script> +import CoursewareTabs from './CoursewareTabs.vue'; +import CoursewareTab from './CoursewareTab.vue'; +import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue'; +import CoursewareManagerElement from './CoursewareManagerElement.vue'; +import CoursewareManagerCopySelector from './CoursewareManagerCopySelector.vue'; +import CoursewareImport from '@/vue/mixins/courseware/import.js'; +import CoursewareExport from '@/vue/mixins/courseware/export.js'; +import { mapActions, mapGetters } from 'vuex'; + +import JSZip from 'jszip'; +import FileSaver from 'file-saver'; + +export default { + name: 'courseware-course-manager', + components: { + CoursewareTabs, + CoursewareTab, + CoursewareCollapsibleBox, + CoursewareManagerElement, + CoursewareManagerCopySelector, + }, + + mixins: [CoursewareImport, CoursewareExport], + + data() { + return { + exportRunning: false, + importRunning: false, + importZip: null, + importState: '', + importPos: 0, + currentElement: {}, + currentId: null, + selfElement: {}, + selfId: null, + zip: null + }; + }, + + computed: { + ...mapGetters({ + courseware: 'courseware', + structuralElementById: 'courseware-structural-elements/byId', + }), + moveSelfPossible() { + if (this.selfElement.relationships === undefined) { + return false + } else if (this.selfElement.relationships.parent.data === null) { + return false; + } else if (this.currentElement.id === this.selfElement.relationships.parent.data.id) { + return false; + } else if (this.currentId === this.selfId) { + return false; + } else { + return true; + } + }, + moveSelfChildPossible() { + return this.currentId !== this.selfId; + }, + }, + + methods: { + ...mapActions({ + loadCoursewareStructure: 'loadCoursewareStructure', + createStructuralElement: 'createStructuralElement', + updateStructuralElement: 'updateStructuralElement', + deleteStructuralElement: 'deleteStructuralElement', + loadStructuralElement: 'loadStructuralElement', + lockObject: 'lockObject', + unlockObject: 'unlockObject', + addBookmark: 'addBookmark', + companionInfo: 'companionInfo', + }), + async reloadElements() { + await this.setCurrentId(this.currentId); + await this.setSelfId(this.selfId); + }, + async setCurrentId(target) { + this.currentId = target; + await this.loadStructuralElement(this.currentId); + this.initCurrent(); + }, + async initCurrent() { + this.currentElement = await this.structuralElementById({ id: this.currentId }); + }, + async setSelfId(target) { + this.selfId = target; + await this.loadStructuralElement(this.selfId); + this.initSelf(); + }, + initSelf() { + this.selfElement = this.structuralElementById({ id: this.selfId }); + }, + animateImport() { + // get number of dots + this.importPos++; + + if (this.importPos > 3) { + this.importPos = 0; + } + + this.importState = this.$gettext('Import läuft') + '.'.repeat(this.importPos); + }, + + async doExportCourseware() { + if (this.exportRunning) { + return false; + } + + this.exportRunning = true; + + await this.loadCoursewareStructure(); + await this.sendExportZip(); + + this.exportRunning = false; + }, + + setImport() { + this.importZip = event.target.files[0]; + }, + + async doImportCourseware() { + if (this.importZip === null) { + return false; + } + + this.importRunning = true; + this.animateImport(); + + let view = this; + + view.zip = new JSZip(); + + await view.zip.loadAsync(this.importZip).then(async function () { + let data = await view.zip.file('courseware.json').async('string'); + let courseware = JSON.parse(data); + + let data_files = await view.zip.file('files.json').async('string'); + let files = JSON.parse(data_files); + + await view.loadCoursewareStructure(); + let parent_id = view.courseware.relationships.root.data.id; + + await view.importCourseware(courseware, parent_id, files); + }); + + this.importState = this.$gettext('Import erfolgreich!'); + this.importZip = null; + this.importRunning = false; + }, + + chooseFile() { + this.$refs.importFile.click(); + }, + getFileSizeText(size) { + if (size / 1024 < 1000) { + return (size / 1024).toFixed(2) + ' kB'; + } else { + return (size / 1048576).toFixed(2) + ' MB'; + } + } + }, + watch: { + courseware(newValue, oldValue) { + let currentId = newValue.relationships.root.data.id; + this.setCurrentId(currentId); + this.setSelfId(currentId); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareDashboardActivities.vue b/resources/vue/components/courseware/CoursewareDashboardActivities.vue new file mode 100755 index 0000000..3e9a8ee --- /dev/null +++ b/resources/vue/components/courseware/CoursewareDashboardActivities.vue @@ -0,0 +1,19 @@ +<template> + <ul class="cw-dashboard-activities"> + <courseware-activity-item v-for="(item, index) in activitiesList" :key="index" :item="item" /> + </ul> +</template> + +<script> +import CoursewareActivityItem from './CoursewareActivityItem.vue'; + +export default { + name: 'courseware-dashboard-activities', + components: { + CoursewareActivityItem, + }, + props: { + activitiesList: Array, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareDashboardProgress.vue b/resources/vue/components/courseware/CoursewareDashboardProgress.vue new file mode 100755 index 0000000..6f2f4f8 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareDashboardProgress.vue @@ -0,0 +1,91 @@ +<template> + <div class="cw-dashboard-progress"> + <div class="cw-dashboard-progress-breadcrumb"> + <span v-if="currentChapter.parent_id !== null" @click="getRoot"><studip-icon shape="home" /></span> + <span v-if="currentChapter.parent_id !== null" @click="selectChapter(currentChapter.parent_id)"> + / {{ currentChapter.parent_name }}</span + > + </div> + <div class="cw-dashboard-progress-chapter"> + <h1><a :href="chapterUrl">{{ currentChapter.name }}</a></h1> + <courseware-progress-circle + :title="$gettext('diese Seite inkl. darunter liegende Seiten')" + :value="parseInt(currentChapter.progress.total)" + /> + <courseware-progress-circle + :title="$gettext('diese Seite')" + class="cw-dashboard-progress-current" + :value="parseInt(currentChapter.progress.current)" + /> + </div> + <div class="cw-dashboard-progress-subchapter-list"> + <courseware-dashboard-progress-item + v-for="chapter in currentChapter.children" + :key="chapter.id" + :name="chapter.name" + :value="chapter.progress.total" + :chapterId="chapter.id" + @selectChapter="selectChapter" + /> + <div v-if="currentChapter.children.length === 0"> + <translate>Dieses Seite enthält keine darunter liegenden Seiten</translate> + </div> + </div> + </div> +</template> + +<script> +import StudipIcon from '../StudipIcon.vue'; +import CoursewareDashboardProgressItem from './CoursewareDashboardProgressItem.vue'; +import CoursewareProgressCircle from './CoursewareProgressCircle.vue'; + +export default { + name: 'courseware-dashboard-progress', + components: { + CoursewareDashboardProgressItem, + CoursewareProgressCircle, + StudipIcon, + }, + data() { + return { + currentProgressData: 0, + }; + }, + computed: { + progressData() { + return STUDIP.courseware_progress_data; + }, + currentChapter() { + return this.progressData[this.currentProgressData]; + }, + chapterUrl() { + return STUDIP.URLHelper.base_url + 'dispatch.php/course/courseware/?cid=' + STUDIP.URLHelper.parameters.cid + '#/structural_element/' + this.currentChapter.id; + }, + }, + methods: { + getRoot() { + this.progressData.every((element, index) => { + if (element.parent_id === null) { + this.currentProgressData = index; + return false; + } else { + return true; + } + }); + }, + selectChapter(id) { + this.progressData.every((element, index) => { + if (element.id === id) { + this.currentProgressData = index; + return false; + } else { + return true; + } + }); + }, + }, + mounted() { + this.getRoot(); + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareDashboardProgressItem.vue b/resources/vue/components/courseware/CoursewareDashboardProgressItem.vue new file mode 100755 index 0000000..1d34020 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareDashboardProgressItem.vue @@ -0,0 +1,26 @@ +<template> + <div class="cw-dashboard-progress-item" @click="$emit('selectChapter', chapterId)"> + <div class="cw-dashboard-progress-item-value"> + <courseware-progress-circle :value="parseInt(value)" /> + </div> + <div class="cw-dashboard-progress-item-description"> + {{ name }} + </div> + </div> +</template> + +<script> +import CoursewareProgressCircle from './CoursewareProgressCircle.vue'; + +export default { + name: 'courseware-dashboard-progress-item', + components: { + CoursewareProgressCircle, + }, + props: { + name: String, + value: Number, + chapterId: String, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareDateBlock.vue b/resources/vue/components/courseware/CoursewareDateBlock.vue new file mode 100755 index 0000000..2b77fd3 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareDateBlock.vue @@ -0,0 +1,204 @@ +<template> + <div class="cw-block cw-block-date"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div v-if="currentStyle === 'countdown'" class="cw-date-countdown"> + <div class="cw-date-countdown-digit" data-countdown="days"> + <div class="cw-date-countdown-number">{{ countdownDays }}</div> + <div v-show="countdownDays === '01'" class="cw-date-countdown-label-sg"> + <translate>Tag</translate> + </div> + <div v-show="countdownDays !== '01'" class="cw-date-countdown-label-pl"> + <translate>Tage</translate> + </div> + </div> + <div class="cw-date-countdown-digit" data-countdown="hours"> + <div class="cw-date-countdown-number">{{ countdownHours }}</div> + <div v-show="countdownHours === '01'" class="cw-date-countdown-label-sg"> + <translate>Stunde</translate> + </div> + <div v-show="countdownHours !== '01'" class="cw-date-countdown-label-pl"> + <translate>Stunden</translate> + </div> + </div> + <div class="cw-date-countdown-digit" data-countdown="minutes"> + <div class="cw-date-countdown-number">{{ countdownMinutes }}</div> + <div v-show="countdownMinutes === '01'" class="cw-date-countdown-label-sg"> + <translate>Minute</translate> + </div> + <div v-show="countdownMinutes !== '01'" class="cw-date-countdown-label-pl"> + <translate>Minuten</translate> + </div> + </div> + <div class="cw-date-countdown-digit" data-countdown="seconds"> + <div class="cw-date-countdown-number">{{ countdownSeconds }}</div> + <div v-show="countdownSeconds === '01'" class="cw-date-countdown-label-sg"> + <translate>Sekunde</translate> + </div> + <div v-show="countdownSeconds !== '01'" class="cw-date-countdown-label-pl"> + <translate>Sekunden</translate> + </div> + </div> + </div> + <div v-if="currentStyle === 'date'" class="cw-date-date"> + <div class="cw-date-date-digits" data-date="date"> + <div class="cw-date-date-number">{{ currentDeDate }}</div> + </div> + <div class="cw-date-date-space"></div> + <div class="cw-date-date-digits" data-date="time"> + <div class="cw-date-date-number">{{ currentTime }}</div> + </div> + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Datum</translate> + <input type="date" v-model="currentDate" @change="computeTimestamp" /> + </label> + <label> + <translate>Uhrzeit</translate> + <input type="time" v-model="currentTime" @change="computeTimestamp" /> + </label> + <label> + <translate>Layout</translate> + <select v-model="currentStyle"> + <option value="countdown"><translate>Countdown</translate></option> + <option value="date"><translate>Datum</translate></option> + </select> + </label> + </form> + </template> + <template #info><translate>Informationen zum Date-Block</translate></template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import { mapActions } from 'vuex'; + +export default { + name: 'courseware-date-block', + components: { + CoursewareDefaultBlock, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentTitle: '', + currentTimestamp: 0, + currentStyle: '', + currentTime: '', + currentDate: '', + currentDeDate: '', + + countdownDays: '00', + countdownHours: '00', + countdownMinutes: '00', + countdownSeconds: '00', + }; + }, + computed: { + title() { + return this.block?.attributes?.payload?.title; + }, + timestamp() { + return this.block?.attributes?.payload?.timestamp; + }, + style() { + return this.block?.attributes?.payload?.style; + }, + date() { + return new Date(this.currentTimestamp); + }, + }, + mounted() { + this.initCurrentData(); + if (this.currentStyle === 'countdown') { + this.countdown(); + } + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + companionWarning: 'companionWarning', + }), + initCurrentData() { + this.currentTitle = this.title; + this.currentTimestamp = this.timestamp; + this.currentDate = + this.date.getFullYear() + + '-' + + ('0' + (this.date.getMonth() + 1)).slice(-2) + + '-' + + ('0' + this.date.getDate()).slice(-2); + this.currentDeDate = + ('0' + this.date.getDate()).slice(-2) + + '.' + + ('0' + (this.date.getMonth() + 1)).slice(-2) + + '.' + + this.date.getFullYear(); + this.currentTime = ('0' + this.date.getHours()).slice(-2) + ':' + ('0' + this.date.getMinutes()).slice(-2); + this.currentStyle = this.style; + }, + countdown() { + let view = this; + setInterval(function () { + let now = new Date().getTime(); + let distance = view.currentTimestamp - now; + if (distance < 0) { + return; + } + view.countdownDays = Math.floor(distance / (1000 * 60 * 60 * 24)); + view.countdownHours = ('0' + Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))).slice( + -2 + ); + view.countdownMinutes = ('0' + Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60))).slice(-2); + view.countdownSeconds = ('0' + Math.floor((distance % (1000 * 60)) / 1000)).slice(-2); + }, 1000); + }, + computeTimestamp() { + this.currentTimestamp = new Date(this.currentDate + ' ' + this.currentTime).getTime(); + }, + storeBlock() { + let cmpInfo = false; + if (this.currentDate === '') { + cmpInfo = this.$gettext('Bitte geben Sie ein Datum an'); + } else if (this.currentTime === '') { + cmpInfo = this.$gettext('Bitte geben Sie eine Uhrzeit an'); + } + if (cmpInfo) { + this.companionWarning({ + info: cmpInfo + }); + return false; + } else { + let attributes = {}; + attributes.payload = {}; + attributes.payload.timestamp = this.currentTimestamp; + attributes.payload.style = this.currentStyle; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + } + + + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareDefaultBlock.vue b/resources/vue/components/courseware/CoursewareDefaultBlock.vue new file mode 100755 index 0000000..ff9c66e --- /dev/null +++ b/resources/vue/components/courseware/CoursewareDefaultBlock.vue @@ -0,0 +1,262 @@ +<template> + <div v-if="block.attributes.visible || canEdit" class="cw-default-block"> + <div class="cw-content-wrapper" :class="[showEditMode ? 'cw-content-wrapper-active' : '']"> + <header v-if="showEditMode" class="cw-block-header"> + <span v-if="!block.attributes.visible" class="cw-default-block-invisible-info"> + <studip-icon shape="visibility-invisible" /> + </span> + <span>{{ blockTitle }}</span> + <span v-if="!block.attributes.visible" class="cw-default-block-invisible-info"> + (<translate>unsichtbar für Nutzende ohne Schreibrecht</translate>) + </span> + <courseware-block-actions + :block="block" + :canEdit="canEdit" + @editBlock="displayFeature('Edit')" + @showFeedback="displayFeature('Feedback')" + @showComments="displayFeature('Comments')" + @showInfo="displayFeature('Info')" + @showExportOptions="displayFeature('ExportOptions')" + @deleteBlock="displayDeleteDialog()" + /> + </header> + <div v-if="showContent" class="cw-block-content"> + <slot name="content" /> + </div> + <div v-if="showFeatures" class="cw-block-features cw-block-features-default"> + <courseware-block-feedback + v-if="canEdit && showFeedback" + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + @close="displayFeature(false)" + /> + <courseware-block-comments + v-if="showComments" + :block="block" + :comments="currentComments" + @postComment="updateComments" + @close="displayFeature(false)" + ref="comments" + /> + <courseware-block-export-options + v-if="canEdit && showExportOptions" + :block="block" + @close="displayFeature(false)" + /> + <courseware-block-edit + v-if="canEdit && showEdit" + :block="block" + @store="$emit('storeEdit')" + @close="closeEdit" + > + <template #edit> + <slot name="edit" /> + </template> + </courseware-block-edit> + <courseware-block-info v-if="showInfo" :block="block" @close="displayFeature(false)"> + <template #info> + <slot name="info" /> + </template> + </courseware-block-info> + </div> + </div> + <studip-dialog + v-if="showDeleteDialog" + :title="textDeleteTitle" + :question="textDeleteAlert" + height="180" + width="350" + @confirm="executeDelete" + @close="showDeleteDialog = false" + ></studip-dialog> + </div> +</template> + +<script> +import CoursewareBlockComments from './CoursewareBlockComments.vue'; +import CoursewareBlockEdit from './CoursewareBlockEdit.vue'; +import CoursewareBlockExportOptions from './CoursewareBlockExportOptions.vue'; +import CoursewareBlockFeedback from './CoursewareBlockFeedback.vue'; +import CoursewareBlockInfo from './CoursewareBlockInfo.vue'; +import CoursewareBlockActions from './CoursewareBlockActions.vue'; +import StudipDialog from '../StudipDialog.vue'; +import StudipIcon from '../StudipIcon.vue'; +import { blockMixin } from './block-mixin.js'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-default-block', + mixins: [blockMixin], + components: { + CoursewareBlockComments, + CoursewareBlockEdit, + CoursewareBlockExportOptions, + CoursewareBlockFeedback, + CoursewareBlockActions, + CoursewareBlockInfo, + StudipDialog, + StudipIcon, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + preview: Boolean, + defaultGrade: { + default: true, + }, + }, + data() { + return { + showFeatures: false, + showFeedback: false, + showComments: false, + showExportOptions: false, + showEdit: false, + showInfo: false, + showContent: true, + showEditModeShortcut: false, + showDeleteDialog: false, + currentComments: [], + textDeleteTitle: this.$gettext('Block unwiderruflich löschen'), + textDeleteAlert: this.$gettext('Möchten Sie diesen Block wirklich löschen?'), + }; + }, + computed: { + ...mapGetters({ + blockTypes: 'blockTypes', + userId: 'userId', + viewMode: 'viewMode', + getComments: 'courseware-block-comments/related', + }), + showEditMode() { + let show = this.viewMode === 'edit' || this.blockedByThisUser; + if (!show) { + this.displayFeature(false); + } + return show; + }, + blocked() { + return this.block?.relationships['edit-blocker'].data !== null; + }, + blockerId() { + return this.blocked ? this.block?.relationships['edit-blocker'].data?.id : null; + }, + blockedByThisUser() { + return this.userId === this.blockerId; + }, + blockedByAnotherUser() { + return this.userId !== this.blockerId; + }, + blockTitle() { + const type = this.block.attributes['block-type']; + + return this.blockTypes.find((blockType) => blockType.type === type)?.title || ''; + }, + }, + mounted() { + if (this.blocked) { + if (this.blockedByThisUser) { + this.displayFeature('Edit'); + } + } + if (this.userProgress && this.userProgress.attributes.grade === 0 && this.defaultGrade) { + this.userProgress = 1; + } + }, + methods: { + ...mapActions({ + companionInfo: 'companionInfo', + deleteBlock: 'deleteBlockInContainer', + lockObject: 'lockObject', + unlockObject: 'unlockObject', + loadComments: 'courseware-block-comments/loadRelated', + loadContainer: 'loadContainer', + }), + async displayFeature(element) { + this.showFeatures = false; + this.showFeedback = false; + this.showComments = false; + this.showExportOptions = false; + this.showEdit = false; + this.showInfo = false; + this.showContent = true; + if (element) { + if (element === 'Edit') { + if (!this.blocked) { + await this.lockObject({ id: this.block.id, type: 'courseware-blocks' }); + if (!this.preview) { + this.showContent = false; + } + this['show' + element] = true; + this.showFeatures = true; + } else { + if (this.userId === this.blockerId) { + if (!this.preview) { + this.showContent = false; + } + this['show' + element] = true; + this.showFeatures = true; + } else { + this.companionInfo({ info: this.$gettext('Dieser Block wird bereits bearbeitet.') }); + } + } + } else { + this['show' + element] = true; + this.showFeatures = true; + } + + if (element === 'Comments') { + this.loadComments(); + } + } + }, + async closeEdit() { + this.displayFeature(false); + this.$emit('closeEdit'); + await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.loadContainer(this.block.relationships.container.data.id); // to update block editor lock + }, + async displayDeleteDialog() { + if (!this.blocked) { + await this.lockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.showDeleteDialog = true; + } else { + if (this.userId === this.blockerId) { + this.showDeleteDialog = true; + } else { + this.companionInfo({ info: 'Dieser Block wird bereits bearbeitet.' }); + } + } + }, + async executeDelete() { + await this.deleteBlock({ + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + // this.showDeleteDialog = false; + }, + + async loadComments() { + const parent = { + type: this.block.type, + id: this.block.id, + }; + await this.$store.dispatch('courseware-block-comments/loadRelated', { + parent, + relationship: 'comments', + options: { + include: 'user', + }, + }); + + this.currentComments = await this.getComments({ parent, relationship: 'comments' }); + }, + async updateComments() { + await this.loadComments(); + this.$refs.comments.$refs.comments.scrollTo(0, this.$refs.comments.$refs.comments.scrollHeight); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareDefaultBlockElements.vue b/resources/vue/components/courseware/CoursewareDefaultBlockElements.vue new file mode 100755 index 0000000..c798366 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareDefaultBlockElements.vue @@ -0,0 +1,51 @@ +<template> + <div class="cw-default-block-elements"> + <courseware-block-actions + :block="block" + :canEdit="canEdit" + @editBlock="editBlock" + @showFeedback="showFeedback" + @showComments="showComments" + @showExportOptions="showExportOptions" + /> + <courseware-block-feedback v-if="canEdit" :block="block" :canEdit="canEdit" :isTeacher="isTeacher" /> + <courseware-block-comments :block="block" /> + <courseware-block-export-options v-if="canEdit" :block="block" /> + </div> +</template> + +<script> +import CoursewareBlockActions from './CoursewareBlockActions.vue'; +import CoursewareBlockComments from './CoursewareBlockComments.vue'; +import CoursewareBlockExportOptions from './CoursewareBlockExportOptions.vue'; +import CoursewareBlockFeedback from './CoursewareBlockFeedback.vue'; + +export default { + name: 'courseware-default-block-elements', + components: { + CoursewareBlockActions, + CoursewareBlockComments, + CoursewareBlockExportOptions, + CoursewareBlockFeedback, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + showFeatures: Boolean, + }, + data() { + return { + displayFeatures: false, + }; + }, + methods: { + editBlock() { + this.$emit('editBlock'); + }, + showFeedback() {}, + showComments() {}, + showExportOptions() {}, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/CoursewareDefaultContainer.vue new file mode 100755 index 0000000..a1d41c3 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareDefaultContainer.vue @@ -0,0 +1,126 @@ +<template> + <div + class="cw-container cw-container-list" + :class="['cw-container-colspan-' + colSpan, showEditMode && canEdit ? 'cw-container-active' : '']" + > + <div class="cw-container-content"> + <header v-if="showEditMode && canEdit" class="cw-container-header"> + <span>{{ container.attributes.title }} ({{container.attributes.width}})</span> + <courseware-container-actions + :canEdit="canEdit" + :container="container" + @editContainer="displayEditDialog" + @deleteContainer="displayDeleteDialog" + /> + </header> + <div class="cw-block-wrapper" :class="{ 'cw-block-wrapper-active': showEditMode }"> + <slot name="containerContent"></slot> + </div> + + <studip-dialog + v-if="showEditDialog" + :title="textEditTitle" + :confirmText="textEditConfirm" + :confirmClass="'accept'" + :closeText="textEditClose" + :closeClass="'cancel'" + @close="closeEdit" + @confirm="storeContainer" + height="400" + width="680" + > + <template v-slot:dialogContent> + <slot name="containerEditDialog"></slot> + </template> + </studip-dialog> + + <studip-dialog + v-if="showDeleteDialog" + :title="textDeleteTitle" + :question="textDeleteAlert" + height="180" + width="380" + @confirm="executeDelete" + @close="closeDeleteDialog" + ></studip-dialog> + </div> + </div> +</template> + +<script> +import CoursewareContainerActions from './CoursewareContainerActions.vue'; +import StudipDialog from '../StudipDialog.vue'; +import { mapActions } from 'vuex'; + +export default { + name: 'courseware-default-container', + components: { + CoursewareContainerActions, + StudipDialog, + }, + props: { + containerClass: String, + container: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + showDeleteDialog: false, + showEditDialog: false, + textEditConfirm: this.$gettext('Speichern'), + textEditClose: this.$gettext('Schließen'), + textEditTitle: this.$gettext('Abschnitt bearbeiten'), + textDeleteTitle: this.$gettext('Abschnitt unwiderruflich löschen'), + textDeleteAlert: this.$gettext('Möchten Sie diesen Abschnitt wirklich löschen?'), + }; + }, + computed: { + showEditMode() { + return this.$store.getters.viewMode === 'edit'; + }, + colSpan() { + return this.container.attributes.payload.colspan ? this.container.attributes.payload.colspan : 'full'; + }, + }, + methods: { + ...mapActions({ + deleteContainer: 'deleteContainer', + lockObject: 'lockObject', + unlockObject: 'unlockObject', + }), + async displayEditDialog() { + await this.lockObject({ id: this.container.id, type: 'courseware-containers' }); + this.showEditDialog = true; + }, + async closeEdit() { + this.$emit('closeEdit'); + this.showEditDialog = false; + await this.unlockObject({ id: this.container.id, type: 'courseware-containers' }); + }, + async storeContainer() { + this.$emit('storeContainer'); + this.showEditDialog = false; + // await this.unlockObject({ id: this.container.id, type: 'courseware-containers' }); + }, + async displayDeleteDialog() { + await this.lockObject({ id: this.container.id, type: 'courseware-containers' }); + this.showDeleteDialog = true; + }, + async closeDeleteDialog() { + await this.unlockObject({ id: this.container.id, type: 'courseware-containers' }); + this.showDeleteDialog = false; + }, + async executeDelete() { + await this.deleteContainer({ + containerId: this.container.id, + structuralElementId: this.container.relationships['structural-element'].data.id, + }); + if(Object.keys(this.$store.getters.blockAdder).length !== 0 && this.$store.getters.blockAdder.container.id === this.container.id) { + this.$store.dispatch('coursewareBlockAdder', {}); + } + this.showDeleteDialog = false; + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareDialogCardsBlock.vue b/resources/vue/components/courseware/CoursewareDialogCardsBlock.vue new file mode 100755 index 0000000..8feeb60 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareDialogCardsBlock.vue @@ -0,0 +1,265 @@ +<template> + <div class="cw-block cw-block-dialog-cards"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div class="cw-block-dialog-cards-content"> + <button + class="cw-dialogcards-prev cw-dialogcards-navbutton" + :class="{'cw-dialogcards-prev-disabled': hasNoPerv}" + @click="prevCard" + :title="hasNoPerv ? $gettext('keine vorherige Karte') : $gettext('zur vorherigen Karte')" + > + </button> + <div class="cw-dialogcards"> + <div + class="scene scene--card" + :class="[card.active ? 'active' : '']" + v-for="card in currentCards" + :key="card.index" + > + <div + class="card" + tabindex="0" + :title="$gettext('Karte umdrehen')" + @click="flipCard" + @keydown.enter="flipCard" + @keydown.space="flipCard" + > + <div class="card__face card__face--front"> + <img v-if="card.front_file.length !== 0" :src="card.front_file.download_url" /> + <div v-else class="cw-dialogcards-front-no-image"></div> + <p>{{ card.front_text }}</p> + </div> + <div class="card__face card__face--back"> + <img v-if="card.back_file.length !== 0" :src="card.back_file.download_url" /> + <div v-else class="cw-dialogcards-back-no-image"></div> + <p>{{ card.back_text }}</p> + </div> + </div> + </div> + </div> + <button + class="cw-dialogcards-next cw-dialogcards-navbutton" + :class="{'cw-dialogcards-next-disabled': hasNoNext}" + @click="nextCard" + :title="hasNoNext ? $gettext('keine nächste Karte') : $gettext('zur nächsten Karte')" + > + </button> + </div> + </template> + <template v-if="canEdit" #edit> + <button class="button add" @click="addCard"><translate>Karte hinzufügen</translate></button> + <courseware-tabs + v-if="currentCards.length > 0" + @selectTab="activateCard(parseInt($event.replace($gettext('Karte') + ' ', '')) - 1)" + > + <courseware-tab + v-for="(card, index) in currentCards" + :key="index" + :name="$gettext('Karte') + ' ' + (index + 1).toString()" + :selected="index === 0" + canBeEmpty + > + <form class="default" @submit.prevent=""> + <label> + <translate>Bild Vorderseite</translate>: + <courseware-file-chooser + v-model="card.front_file_id" + :isImage="true" + @selectFile="updateFile(index, 'front', $event)" + /> + </label> + <label> + <translate>Text Vorderseite</translate>: + <input type="text" v-model="card.front_text" /> + </label> + <label> + <translate>Bild Rückseite</translate>: + <courseware-file-chooser + v-model="card.back_file_id" + :isImage="true" + @selectFile="updateFile(index, 'back', $event)" + /> + </label> + <label> + <translate>Text Rückseite</translate>: + <input type="text" v-model="card.back_text" /> + </label> + <label v-if="!onlyCard"> + <button class="button trash" @click="removeCard(index)"> + <translate>Karte entfernen</translate> + </button> + </label> + </form> + </courseware-tab> + </courseware-tabs> + </template> + <template #info> + <p><translate>Informationen zum DialogCards-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareFileChooser from './CoursewareFileChooser.vue'; +import CoursewareTabs from './CoursewareTabs.vue'; +import CoursewareTab from './CoursewareTab.vue'; + +import { mapActions } from 'vuex'; +import StudipIcon from '../StudipIcon.vue'; + +export default { + name: 'courseware-dialog-cards-block', + components: { + CoursewareDefaultBlock, + CoursewareFileChooser, + CoursewareTabs, + CoursewareTab, + StudipIcon, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentCards: [], + }; + }, + computed: { + cards() { + return this.block?.attributes?.payload?.cards; + }, + onlyCard() { + return this.currentCards.length === 1; + }, + hasNoPerv() { + if(this.currentCards[0] !== undefined) { + return this.currentCards[0].active; + } else { + return true; + } + }, + hasNoNext() { + if(this.currentCards[this.currentCards.length -1] !== undefined) { + return this.currentCards[this.currentCards.length -1].active; + } else { + return true; + } + } + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + if (this.cards !== '') { + this.currentCards = JSON.parse(JSON.stringify(this.cards)); + } + this.activateCard(0); + }, + storeBlock() { + let cards = JSON.parse(JSON.stringify(this.currentCards)); + // don't store the file object + cards.forEach((card) => { + delete card.front_file; + delete card.back_file; + }); + let attributes = {}; + attributes.payload = {}; + attributes.payload.cards = cards; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + updateFile(cardIndex, side, file) { + if (side === 'front') { + this.currentCards[cardIndex].front_file_id = file.id; + this.currentCards[cardIndex].front_file = file; + } + if (side === 'back') { + this.currentCards[cardIndex].back_file_id = file.id; + this.currentCards[cardIndex].back_file = file; + } + }, + addCard() { + this.currentCards.push({ + index: this.currentCards.length, + front_file_id: '', + front_file: [], + front_text: '', + back_file_id: '', + back_text: '', + back_file: [] + }); + }, + removeCard(cardIndex){ + this.currentCards = this.currentCards.filter((val, index) => { + return !(index === cardIndex); + }); + this.activateCard(0); + }, + flipCard(event) { + event.currentTarget.classList.toggle('is-flipped'); + }, + nextCard() { + let view = this; + this.currentCards.every((card, index) => { + if (card.active) { + if (view.currentCards.length > index + 1) { + card.active = false; + view.currentCards[index + 1].active = true; + } + return false; // end every + } else { + return true; // continue every + } + }); + }, + prevCard() { + let view = this; + this.currentCards.every((card, index) => { + if (card.active) { + if (index > 0) { + card.active = false; + view.currentCards[index - 1].active = true; + } + return false; // end every + } else { + return true; // continue every + } + }); + }, + activateCard(selectedIndex) { + selectedIndex = parseInt(selectedIndex); + if (selectedIndex > this.currentCards.length - 1) { + console.log('can not select this card'); + return false; + } + this.currentCards.forEach((card, index) => { + if (index === selectedIndex) { + card.active = true; + } else { + card.active = false; + } + }); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareDocumentBlock.vue b/resources/vue/components/courseware/CoursewareDocumentBlock.vue new file mode 100755 index 0000000..1967387 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareDocumentBlock.vue @@ -0,0 +1,263 @@ +<template> + <div class="cw-block cw-block-document"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="false" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div v-if="hasFile" class="cw-pdf-header cw-block-title"> + <button class="cw-pdf-button-prev" :class="{ inactive: pageNum - 1 === 0 }" @click="prevPage" /> + <span class="cw-pdf-title">{{ currentTitle }}</span> + <a :href="currentUrl" class="cw-pdf-download" download></a> + <span> + <translate :translate-params="{pageNum, pageCount}"> + (Seite %{pageNum} von %{pageCount}) + </translate> + </span> + <button class="cw-pdf-button-next" :class="{ inactive: pageNum === pageCount }" @click="nextPage" /> + </div> + <canvas + v-if="hasFile" + ref="pdfcanvas" + class="cw-pdf-canvas" + @mousedown="browse = true" + @mouseup="browse = false" + @mouseleave="browse = false" + @mousemove="browsePdf" + /> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Überschrift</translate> + <input type="text" v-model="currentTitle" /> + </label> + <label> + <translate>Datei</translate> + <courseware-file-chooser + v-model="currentFileId" + :isDocument="true" + @selectFile="updateCurrentFile" + /> + </label> + <label> + <translate>Download-Icon anzeigen</translate> + <select v-model="currentDownloadable"> + <option value="true"><translate>Ja</translate></option> + <option value="false"><translate>Nein</translate></option> + </select> + </label> + <label> + <translate>Dateityp</translate> + <select v-model="currentDocType"> + <option value="pdf">PDF</option> + </select> + </label> + </form> + </template> + <template #info> + <p><translate>Informationen zum Document-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareFileChooser from './CoursewareFileChooser.vue'; +import * as pdfjsLib from 'pdfjs-dist'; +import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry'; + +import { mapActions } from 'vuex'; + +export default { + name: 'courseware-document-block', + components: { + CoursewareDefaultBlock, + CoursewareFileChooser, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentTitle: '', + currentFileId: '', + currentFile: {}, + currentDownloadable: '', + currentDocType: '', + + PdfViewer: true, + pdfDoc: null, + pageNum: 1, + pageRendering: false, + pageNumPending: null, + pageCount: 0, + scale: 2, + canvas: {}, + context: {}, + browse: false, + browseDirection: [], + file: null + }; + }, + computed: { + title() { + return this.block?.attributes?.payload?.title; + }, + downloadable() { + return this.block?.attributes?.payload?.downloadable; + }, + fileId() { + return this.block?.attributes?.payload?.file_id; + }, + docType() { + return this.block?.attributes?.payload?.doc_type; + }, + currentUrl() { + if (this.currentFile?.meta) { + return this.currentFile.meta['download-url']; + } else { + return ''; + } + }, + hasFile() { + return this.currentFileId !== ''; + } + }, + watch: { + browseDirection: function (val) { + if (val.length > 6) { + this.evaluateBrowseAction(); + } + }, + }, + mounted() { + this.loadFileRefs(this.block.id).then((response) => { + this.file = response[0]; + this.currentFile = this.file; + this.loadPdfViewer(); + }); + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + loadFileRefs: 'loadFileRefs', + companionWarning: 'companionWarning', + }), + initCurrentData() { + this.currentTitle = this.title; + this.currentDownloadable = this.downloadable; + this.currentFileId = this.fileId; + this.currentDocType = this.docType; + }, + updateCurrentFile(file) { + this.currentFile = file; + this.currentFileId = file.id; + }, + loadPdfViewer() { + if (this.PdfViewer && this.currentUrl) { + let view = this; + this.canvas = this.$refs.pdfcanvas; + this.context = this.canvas.getContext('2d'); + pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker; + pdfjsLib.getDocument(this.currentUrl).promise.then(function (pdf) { + view.pdfDoc = pdf; + view.pageCount = view.pdfDoc.numPages; + view.renderPage(view.pageNum); + }); + } + }, + renderPage(num) { + let view = this; + this.pageRendering = true; + this.pdfDoc.getPage(num).then(function (page) { + let viewport = page.getViewport({ scale: view.scale }); + view.canvas.height = viewport.height; + view.canvas.width = viewport.width; + + let renderContext = { + canvasContext: view.context, + viewport: viewport, + }; + let renderTask = page.render(renderContext); + + renderTask.promise.then(function () { + view.pageRendering = false; + if (view.pageNumPending !== null) { + view.renderPage(view.pageNumPending); + view.pageNumPending = null; + } + }); + }); + }, + queueRenderPage(num) { + if (this.pageRendering) { + this.pageNumPending = num; + } else { + this.renderPage(num); + } + }, + prevPage() { + if (this.pageNum <= 1) { + return; + } + this.pageNum--; + this.queueRenderPage(this.pageNum); + }, + nextPage() { + if (this.pageNum >= this.pdfDoc.numPages) { + return; + } + this.pageNum++; + this.queueRenderPage(this.pageNum); + }, + browsePdf(e) { + if (this.browse) { + this.browseDirection.push(e.clientX); + } + }, + evaluateBrowseAction() { + this.browse = false; + let first = this.browseDirection[0]; + let last = this.browseDirection.pop(); + this.browseDirection = []; + if (first < last) { + this.prevPage(); + } else { + this.nextPage(); + } + }, + + storeBlock() { + if (this.currentFile === undefined) { + this.companionWarning({ + info: this.$gettext('Bitte wählen Sie eine Datei aus') + }); + return false; + } else { + let attributes = {}; + attributes.payload = {}; + attributes.payload.title = this.currentTitle; + attributes.payload.file_id = this.currentFile.id; + attributes.payload.downloadable = this.currentDownloadable; + attributes.payload.doc_type = this.currentDocType; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + } + + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareDownloadBlock.vue b/resources/vue/components/courseware/CoursewareDownloadBlock.vue new file mode 100755 index 0000000..238e005 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareDownloadBlock.vue @@ -0,0 +1,225 @@ +<template> + <div class="cw-block cw-block-download"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div> + <div v-if="currentFile !== null" class="cw-block-download-content"> + <div v-if="currentInfo !== '' && !userHasDownloaded" class="messagebox messagebox_info"> + {{ currentInfo }} + </div> + <div v-if="currentSuccess !== '' && userHasDownloaded" class="messagebox messagebox_info"> + {{ currentSuccess }} + </div> + <div class="cw-block-download-file-item"> + <a target="_blank" :download="currentFile.name" :href="currentFile.download_url"> + <span class="cw-block-file-info" :class="['cw-block-file-icon-' + currentFile.icon]"> + {{ currentFile.name }} + </span> + <span class="cw-block-download-download-icon"></span> + </a> + </div> + </div> + <div v-else class="cw-block-download-content"> + <div class="cw-block-download-file-item-not-available"> + <span class="cw-block-file-info cw-block-file-icon-none"> + <translate>Datei ist nicht verfügbar</translate> + </span> + </div> + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Überschrift</translate> + <input type="text" v-model="currentTitle" /> + </label> + <label> + <translate>Datei</translate> + <courseware-file-chooser v-model="currentFileId" @selectFile="updateCurrentFile" /> + </label> + <label> + <translate>Infobox vor Download</translate> + <input type="text" v-model="currentInfo" /> + </label> + <label> + <translate>Infobox nach Download</translate> + <input type="text" v-model="currentSuccess" /> + </label> + <label> + <translate>Fortschritt erst beim Herunterladen</translate> + <select v-model="currentGrade"> + <option value="false"><translate>Nein</translate></option> + <option value="true"><translate>Ja</translate></option> + </select> + </label> + </form> + </template> + <template #info> + <p><translate>Informationen zum Download-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareFileChooser from './CoursewareFileChooser.vue'; + +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-download-block', + components: { + CoursewareDefaultBlock, + CoursewareFileChooser, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentTitle: '', + currentInfo: '', + currentSuccess: '', + currentGrade: '', + currentFileId: '', + currentFile: null, + userHasDownloaded: false, // Todo set and get user_data + }; + }, + computed: { + ...mapGetters({ + fileRefById: 'file-refs/byId', + urlHelper: 'urlHelper', + relatedTermOfUse: 'terms-of-use/related', + }), + title() { + return this.block?.attributes?.payload?.title; + }, + info() { + return this.block?.attributes?.payload?.info; + }, + success() { + return this.block?.attributes?.payload?.success; + }, + grade() { + return this.block?.attributes?.payload?.grade; + }, + fileId() { + return this.block?.attributes?.payload?.file_id; + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + loadFileRef: 'file-refs/loadById', + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + this.currentTitle = this.title; + this.currentInfo = this.info; + this.currentFileId = this.fileId; + this.currentSuccess = this.success; + this.currentGrade = this.grade; + if (this.currentFileId !== '') { + this.loadFile(); + } + }, + async loadFile() { + const id = `${this.currentFileId}`; + const options = { include: 'terms-of-use' }; + await this.loadFileRef({ id: id, options }); + const fileRef = this.fileRefById({ id: id }); + if (fileRef && this.relatedTermOfUse({parent: fileRef, relationship: 'terms-of-use'}).attributes['download-condition'] === 0) { + this.updateCurrentFile({ + id: fileRef.id, + name: fileRef.attributes.name, + icon: this.getIcon(fileRef.attributes['mime-type']), + download_url: this.urlHelper.getURL( + 'sendfile.php', + { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name }, + true + ), + }); + } + }, + updateCurrentFile(file) { + this.currentFile = file; + this.currentFileId = file.id; + if (!this.currentFile.icon) { + this.currentFile.icon = this.getIcon(file.mime_type); + } + }, + getIcon(mimeType) { + let icon = 'file'; + if (mimeType.includes('audio')) { + icon = 'audio'; + } + if (mimeType.includes('image')) { + icon = 'pic'; + } + if (mimeType.includes('video')) { + icon = 'video'; + } + if (mimeType.includes('text')) { + icon = 'text'; + } + if (mimeType.includes('pdf')) { + icon = 'pdf'; + } + if (mimeType.includes('msword')) { + icon = 'word'; + } + if (mimeType.includes('opendocument.text')) { + icon = 'word'; + } + if (mimeType.includes('openxmlformats-officedocument. wordprocessingml.document')) { + icon = 'word'; + } + if (mimeType.includes('msexcel')) { + icon = 'spreadsheet'; + } + if (mimeType.includes('opendocument.spreadsheet')) { + icon = 'spreadsheet'; + } + if (mimeType.includes('openxmlformats-officedocument. spreadsheetml.sheet')) { + icon = 'spreadsheet'; + } + if (mimeType.includes('mspowerpoint')) { + icon = 'ppt'; + } + if (mimeType.includes('zip')) { + icon = 'archive'; + } + + return icon; + }, + storeBlock() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.title = this.currentTitle; + attributes.payload.info = this.currentInfo; + attributes.payload.success = this.currentSuccess; + attributes.payload.grade = this.currentGrade; + attributes.payload.file_id = this.currentFileId; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareEmbedBlock.vue b/resources/vue/components/courseware/CoursewareEmbedBlock.vue new file mode 100755 index 0000000..521f902 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareEmbedBlock.vue @@ -0,0 +1,231 @@ +<template> + <div class="cw-block cw-block-embed" ref="block"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="false" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div> + <div v-if="oembedData !== null"> + <div + v-if="oembedData.type === 'rich' || oembedData.type === 'video'" + v-html="oembedData.html" + class="cw-block-embed-iframe-wrapper" + :style="{ height: contentHeight + 'px' }" + ></div> + + <div v-if="oembedData.type === 'photo'" :style="{ height: contentHeight + 'px' }"> + <img :src="oembedData.url" /> + </div> + + <div v-if="oembedData.type === 'link' && oembedData.provider_name === 'DeviantArt'"> + <img :src="oembedData.fullsize_url" /> + </div> + </div> + <div class="cw-block-embed-info" v-if="oembedData !== null"> + <span class="cw-block-embed-title">{{ oembedData.title }}</span> + <span class="cw-block-embed-author-name"> + <translate>erstellt von</translate> + <a :href="oembedData.author_url" target="_blank">{{ oembedData.author_name }}</a></span + > + <span class="cw-block-embed-source"> + <translate>veröffentlicht auf</translate> + <a :href="oembedData.provider_url" target="_blank">{{ oembedData.provider_name }}</a></span + > + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Überschrift</translate> + <input type="text" v-model="currentTitle" /> + </label> + <label> + <translate>Quelle</translate> + <select v-model="currentSource"> + <option v-for="(value, key) in endPoints" :key="key" :value="key">{{ key }}</option> + </select> + </label> + <label> + <translate>URL</translate> + <input type="text" v-model="currentUrl" /> + </label> + <label v-if="currentSource === 'youtube'"> + <translate>Startpunkt wählen</translate> + <input + type="time" + v-model="currentStartTime" + step="1" + min="00:00:00" + max="24:00:00" + @change="updateTime" + /> + </label> + <label v-if="currentSource === 'youtube'"> + <translate>Endpunkt wählen</translate> + <input + type="time" + v-model="currentEndTime" + step="1" + :min="currentStartTime" + max="24:00:00" + @change="updateTime" + /> + </label> + </form> + </template> + <template #info> + <p><translate>Informationen zum Embed-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; + +import { mapActions } from 'vuex'; + +export default { + name: 'courseware-embed-block', + components: { + CoursewareDefaultBlock, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentTitle: '', + currentSource: '', + currentUrl: '', + currentStartTime: '', + currentEndTime: '', + + endPoints: { + audiomack: 'https://www.audiomack.com/oembed', + codepen: 'https://codepen.io/api/oembed', + codesandbox: 'https://codesandbox.io/oembed', + deviantart: 'https://backend.deviantart.com/oembed', + ethfiddle: 'https://ethfiddle.com/services/oembed/', + flickr: 'https://www.flickr.com/services/oembed/', + giphy: 'https://giphy.com/services/oembed', + kidoju: 'https://www.kidoju.com/api/oembed', + learningapps: 'https://learningapps.org/oembed.php', + sketchfab: 'https://sketchfab.com/oembed', + slideshare: 'https://www.slideshare.net/api/oembed/2', + soundcloud: 'https://soundcloud.com/oembed', + speakerdeck: 'https://speakerdeck.com/oembed.json', + sway: 'https://sway.com/api/v1.0/oembed', + 'sway.office': 'https://sway.office.com/api/v1.0/oembed', + spotify: 'https://embed.spotify.com/oembed/', + vimeo: 'https://vimeo.com/api/oembed.json', + youtube: 'https://www.youtube.com/oembed', + }, + oembedData: {}, + contentHeight: 300, + }; + }, + computed: { + url() { + return this.block?.attributes?.payload?.url; + }, + source() { + return this.block?.attributes?.payload?.source; + }, + title() { + return this.block?.attributes?.payload?.title; + }, + startTime() { + return this.block?.attributes?.payload?.starttime; + }, + endTime() { + return this.block?.attributes?.payload?.endtime; + }, + oembed() { + return this.block?.attributes?.payload?.oembed; + }, + }, + mounted() { + this.initCurrentData(); + + window.addEventListener('resize', this.calcContentHeight); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + this.currentTitle = this.title; + this.currentSource = this.source; + this.currentUrl = this.url; + this.currentStartTime = this.startTime; + this.currentEndTime = this.endTime; + this.oembedData = this.oembed; + if (this.oembedData !== null) { + this.calcContentHeight(); + this.updateTime(); + } + }, + addTimeData(data) { + if (this.currentSource === 'youtube') { + if (this.currentStartTime !== '') { + let start = this.currentStartTime.split(':'); + let s = parseInt(start[0], 10) * 3600 + parseInt(start[1], 10) * 60 + parseInt(start[2], 10); + let query = '?feature=oembed&start=' + s; + if (this.currentEndTime !== '') { + let end = this.currentEndTime.split(':'); + let e = parseInt(end[0], 10) * 3600 + parseInt(end[1], 10) * 60 + parseInt(end[2], 10); + query = query + '&end=' + e; + } + data.html = data.html.replace('?feature=oembed', query); + } + } + return data; + }, + updateTime() { + this.oembedData = this.addTimeData(this.oembedData); + }, + validateCurrentSource() { + var validSource = false; + let view = this; + for (const key of Object.keys(this.endPoints)) { + if (view.currentUrl.includes(key)) { + view.currentSource = key; + validSource = true; + break; + } + } + + return validSource; + }, + calcContentHeight() { + if (this.oembedData.height && this.oembedData.width) { + this.contentHeight = + ((this.$refs.block.offsetWidth - 4) / this.oembedData.width) * this.oembedData.height; + } + }, + storeBlock() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.title = this.currentTitle; + attributes.payload.url = this.currentUrl; + attributes.payload.source = this.currentSource; + attributes.payload.starttime = this.currentStartTime; + attributes.payload.endtime = this.currentEndTime; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareEmptyElementBox.vue b/resources/vue/components/courseware/CoursewareEmptyElementBox.vue new file mode 100755 index 0000000..8680a55 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareEmptyElementBox.vue @@ -0,0 +1,50 @@ +<template> + <div class="cw-wellcome-screen"> + <courseware-companion-box :msgCompanion="this.$gettext('Es wurden bisher noch keine Inhalte eingepflegt.')"> + <template v-slot:companionActions> + <button v-if="canEdit && noContainers" class="button" @click="addContainer"><translate>Einen Abschnitt hinzufügen</translate></button> + <button v-if="canEdit && !noContainers && !editMode" class="button" @click="switchToEditView"><translate>Seite bearbeiten</translate></button> + </template> + </courseware-companion-box> + </div> +</template> + +<script> +import { mapGetters } from 'vuex'; +import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; + +export default { + name: 'courseware-empty-element-box', + components: { + CoursewareCompanionBox, + }, + props: { + canEdit: Boolean, + noContainers: Boolean + }, + data() { + return{} + }, + computed: { + ...mapGetters({ + viewMode: 'viewMode' + }), + editMode() { + return this.viewMode === 'edit'; + } + }, + methods: { + addContainer() { + this.$store.dispatch('coursewareViewMode', 'edit'); + this.$store.dispatch('coursewareConsumeMode', false); + this.$store.dispatch('coursewareContainerAdder', true); + this.$store.dispatch('coursewareShowToolbar', true); + }, + switchToEditView() { + this.$store.dispatch('coursewareViewMode', 'edit'); + this.$store.dispatch('coursewareConsumeMode', false); + } + } + +} +</script>
\ No newline at end of file diff --git a/resources/vue/components/courseware/CoursewareFileChooser.vue b/resources/vue/components/courseware/CoursewareFileChooser.vue new file mode 100755 index 0000000..0daaa54 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareFileChooser.vue @@ -0,0 +1,153 @@ +<template> + <div class="cw-file-chooser"> + <span v-translate>Ordner-Filter</span> + <courseware-folder-chooser allowUserFolders unchoose v-model="selectedFolderId" /> + <span v-translate>Datei</span> + <select v-model="currentValue" @change="selectFile"> + <option v-show="canBeEmpty" value=""> + <translate>Keine Auswahl</translate> + </option> + <optgroup v-if="this.context.type === 'courses' && courseFiles.length !== 0" :label="textOptGroupCourse"> + <option v-for="(file, index) in courseFiles" :key="index" :value="file.id"> + {{ file.name }} + </option> + </optgroup> + <optgroup v-if="userFiles.length !== 0" :label="textOptGroupUser"> + <option v-for="(file, index) in userFiles" :key="index" :value="file.id"> + {{ file.name }} + </option> + </optgroup> + <option v-show="userFiles.length === 0 && courseFiles.length === 0" disabled> + <translate>Keine Dateien vorhanden</translate> + </option> + </select> + </div> +</template> + +<script> +import CoursewareFolderChooser from './CoursewareFolderChooser.vue'; + +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-file-chooser', + components: { CoursewareFolderChooser }, + props: { + value: String, + mimeType: { type: String, default: '' }, + isImage: { type: Boolean, default: false }, + isVideo: { type: Boolean, default: false }, + isAudio: { type: Boolean, default: false }, + isDocument: { type: Boolean, default: false }, + canBeEmpty: { type: Boolean, default: false }, + }, + data() { + return { + currentValue: '', + selectedFolderId: '', + loadedCourseFiles: [], + courseFiles: [], + loadedUserFiles: [], + userFiles: [], + textOptGroupCourse: this.$gettext('Dateibereich der Veranstaltung'), + textOptGroupUser: this.$gettext('eigener Dateibereich'), + }; + }, + computed: { + ...mapGetters({ + context: 'context', + relatedFileRefs: 'file-refs/related', + urlHelper: 'urlHelper', + userId: 'userId', + relatedTermOfUse: 'terms-of-use/related' + }), + }, + methods: { + ...mapActions({ + loadRelatedFileRefs: 'file-refs/loadRelated', + }), + selectFile() { + this.$emit( + 'selectFile', + this.userFiles.concat(this.courseFiles).find((file) => file.id === this.currentValue) + ); + }, + filterFiles(loadArray) { + const filterFile = (file) => { + if (this.relatedTermOfUse({parent: file, relationship: 'terms-of-use'}).attributes['download-condition'] !== 0) { + return false; + } + if (this.selectedFolderId !== '' && this.selectedFolderId !== file.relationships.parent.data.id) { + return false; + } + if (this.mimeType !== '' && this.mimeType !== file.attributes['mime-type']) { + return false; + } + if (this.isImage && !file.attributes['mime-type'].includes('image')) { + return false; + } + const videoConditions = ['video/mp4', 'video/ogg', 'video/webm']; + if (this.isVideo && !videoConditions.some(condition => file.attributes['mime-type'].includes(condition))) { + return false; + } + const audioConditions = ['audio/wav', 'audio/ogg', 'audio/webm','audio/flac', 'audio/mpeg']; + if (this.isAudio && !audioConditions.some(condition => file.attributes['mime-type'].includes(condition)) ) { + return false; + } + const officeConditions = ['application/pdf']; //TODO enable more mime types + if (this.isDocument && !officeConditions.some(condition => file.attributes['mime-type'].includes(condition)) ) { + return false; + } + + return true; + }; + + return loadArray.filter(filterFile).map((file) => ({ + id: file.id, + name: file.attributes.name, + mime_type: file.attributes['mime-type'], + download_url: this.urlHelper.getURL( + 'sendfile.php', + { type: 0, file_id: file.id, file_name: file.attributes.name }, + true + ), + })); + }, + updateFiles() { + this.courseFiles = this.filterFiles(this.loadedCourseFiles); + this.userFiles = this.filterFiles(this.loadedUserFiles); + }, + async getCourseFiles() { + const parent = { type: 'courses', id: `${this.context.id}` }; + const relationship = 'file-refs'; + const options = { include: 'terms-of-use' }; + await this.loadRelatedFileRefs({ parent, relationship, options }); + + this.loadedCourseFiles = this.relatedFileRefs({ parent, relationship }); + this.updateFiles(); + }, + async getUserFiles() { + const parent = { type: 'users', id: `${this.userId}` }; + const relationship = 'file-refs'; + const options = { include: 'terms-of-use' }; + await this.loadRelatedFileRefs({ parent, relationship, options }); + + this.loadedUserFiles = this.relatedFileRefs({ parent, relationship }); + this.updateFiles(); + }, + }, + mounted() { + if (this.context.type !== 'users') { + this.getCourseFiles(); + } + this.getUserFiles(); + + this.currentValue = this.value; + }, + watch: { + selectedFolderId() { + this.updateFiles(); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareFolderBlock.vue b/resources/vue/components/courseware/CoursewareFolderBlock.vue new file mode 100755 index 0000000..a1b9749 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareFolderBlock.vue @@ -0,0 +1,194 @@ +<template> + <div class="cw-block cw-block-folder"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div> + <ul class="cw-block-folder-list"> + <li v-for="file in files" :key="file.id" class="cw-block-folder-file-item"> + <a target="_blank" :download="file.name" :href="file.download_url"> + <span class="cw-block-file-info" :class="['cw-block-file-icon-' + file.icon]"> + {{ file.name }} + </span> + <span class="cw-block-folder-download-icon"></span> + </a> + </li> + <li v-if="files.length === 0"> + <span class="cw-block-file-info cw-block-file-icon-empty"> + <translate>Dieser Ordner ist leer</translate> + </span> + </li> + </ul> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Überschrift</translate> + <input type="text" v-model="currentTitle" /> + </label> + <label> + <translate>Ordner</translate> + <courseware-folder-chooser v-model="currentFolderId" allowUserFolders /> + </label> + </form> + </template> + <template #info> + <p><translate>Informationen zum Dateiordner-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareFolderChooser from './CoursewareFolderChooser.vue'; + +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-folder-block', + components: { + CoursewareDefaultBlock, + CoursewareFolderChooser, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentTitle: '', + currentFolderId: '', + currentFileType: '', + files: [], + }; + }, + computed: { + ...mapGetters({ + relatedFileRefs: 'file-refs/related', + urlHelper: 'urlHelper', + relatedTermOfUse: 'terms-of-use/related', + }), + folderType() { + return this.block?.attributes?.payload?.type; + }, + folderId() { + return this.block?.attributes?.payload?.folder_id; + }, + title() { + return this.block?.attributes?.payload?.title; + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + loadRelatedFileRefs: 'file-refs/loadRelated', + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + this.currentTitle = this.title; + this.currentFolderId = this.folderId; + this.currentFolderType = this.folderType; + }, + async getFolderFiles() { + const parent = { type: 'folders', id: `${this.currentFolderId}` }; + const relationship = 'file-refs'; + const options = { include: 'terms-of-use' }; + await this.loadRelatedFileRefs({ parent, relationship, options }); + const fileRefs = this.relatedFileRefs({ parent, relationship }) ?? []; + this.processFiles(fileRefs); + }, + processFiles(files) { + this.files = files + .filter((file) => { + if (this.relatedTermOfUse({parent: file, relationship: 'terms-of-use'}).attributes['download-condition'] !== 0) { + return false; + } else { + return true; + } + }) + .map(({ id, attributes }) => ({ + id, + name: attributes.name, + icon: this.getIcon(attributes['mime-type']), + download_url: this.urlHelper.getURL( + `sendfile.php/`, + { type: 0, file_id: id, file_name: attributes.name }, + true + ), + })); + }, + getIcon(mimeType) { + let icon = 'file'; + if (mimeType.includes('audio')) { + icon = 'audio'; + } + if (mimeType.includes('image')) { + icon = 'pic'; + } + if (mimeType.includes('video')) { + icon = 'video'; + } + if (mimeType.includes('text')) { + icon = 'text'; + } + if (mimeType.includes('pdf')) { + icon = 'pdf'; + } + if (mimeType.includes('msword')) { + icon = 'word'; + } + if (mimeType.includes('opendocument.text')) { + icon = 'word'; + } + if (mimeType.includes('openxmlformats-officedocument. wordprocessingml.document')) { + icon = 'word'; + } + if (mimeType.includes('msexcel')) { + icon = 'spreadsheet'; + } + if (mimeType.includes('opendocument.spreadsheet')) { + icon = 'spreadsheet'; + } + if (mimeType.includes('openxmlformats-officedocument. spreadsheetml.sheet')) { + icon = 'spreadsheet'; + } + if (mimeType.includes('mspowerpoint')) { + icon = 'ppt'; + } + if (mimeType.includes('zip')) { + icon = 'archive'; + } + + return icon; + }, + storeBlock() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.title = this.currentTitle; + attributes.payload.folder_id = this.currentFolderId; + attributes.payload.type = this.currentFileType; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + }, + watch: { + currentFolderId() { + this.getFolderFiles(); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareFolderChooser.vue b/resources/vue/components/courseware/CoursewareFolderChooser.vue new file mode 100755 index 0000000..d176ebb --- /dev/null +++ b/resources/vue/components/courseware/CoursewareFolderChooser.vue @@ -0,0 +1,107 @@ +<template> + <select v-model="currentValue" @change="changeSelection"> + <option v-if="unchoose" value=""><translate>kein Ordner ausgewählt</translate></option> + <optgroup v-if="this.context.type === 'courses'" :label="textOptGroupCourse"> + <option v-for="folder in loadedCourseFolders" :key="folder.id" :value="folder.id"> + {{ folder.attributes.name }} + </option> + </optgroup> + <optgroup v-if="allowUserFolders" :label="textOptGroupUser"> + <option v-for="folder in loadedUserFolders" :key="folder.id" :value="folder.id"> + {{ folder.attributes.name }} + </option> + </optgroup> + </select> +</template> + +<script> +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-folder-chooser', + props: { + value: String, + allowUserFolders: { type: Boolean, default: false }, + allowHomeworkFolders: { type: Boolean, default: false }, + unchoose: { type: Boolean, default: false }, + }, + data() { + return { + currentValue: Object, + textOptGroupCourse: this.$gettext('Dateibereich dieser Veranstaltung'), + textOptGroupUser: this.$gettext('Eigener Dateibereich'), + }; + }, + computed: { + ...mapGetters({ + context: 'context', + relatedFolders: 'folders/related', + userId: 'userId', + }), + courseObject() { + return { type: 'courses', id: `${this.context.id}` }; + }, + userObject() { + return { type: 'users', id: `${this.userId}` }; + }, + loadedCourseFolders() { + let loadedCourseFolders = []; + let CourseFolders = this.relatedFolders({ parent: this.courseObject, relationship: 'folders' }) ?? []; + CourseFolders.forEach(folder => { + switch (folder.attributes['folder-type']) { + case 'HiddenFolder': + if (folder.attributes['data-content']['download_allowed'] === 1) { + loadedCourseFolders.push(folder); + } + break; + case 'HomeworkFolder': + if(this.allowHomeworkFolders) { + loadedCourseFolders.push(folder); + } + default: + loadedCourseFolders.push(folder); + } + }); + + return loadedCourseFolders; + }, + loadedUserFolders() { + let loadedUserFolders = []; + let UserFolders = this.relatedFolders({ parent: this.userObject, relationship: 'folders' }) ?? []; + UserFolders.forEach(folder => { + if (folder.attributes['folder-type'] === 'PublicFolder') { + loadedUserFolders.push(folder); + } + }); + + return loadedUserFolders; + }, + }, + methods: { + ...mapActions({ loadRelatedFolders: 'folders/loadRelated' }), + changeSelection() { + this.$emit('input', this.currentValue); + }, + + getCourseFolders() { + return this.loadRelatedFolders({ + parent: this.courseObject, + relationship: 'folders', + }); + }, + getUserFolders() { + return this.loadRelatedFolders({ + parent: this.userObject, + relationship: 'folders', + }); + }, + }, + mounted() { + this.currentValue = this.value; + if (this.context.type !== 'users') { + this.getCourseFolders(); + } + this.getUserFolders(); + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareGalleryBlock.vue b/resources/vue/components/courseware/CoursewareGalleryBlock.vue new file mode 100755 index 0000000..9d0549b --- /dev/null +++ b/resources/vue/components/courseware/CoursewareGalleryBlock.vue @@ -0,0 +1,259 @@ +<template> + <div class="cw-block cw-block-gallery"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div v-if="files.length !== 0" class="cw-block-gallery-content" :style="{ 'max-height': currentHeight + 'px' }"> + <div + v-for="(image, index) in files" + :key="image.id" + ref="images" + class="cw-block-gallery-slides cw-block-gallery-fade" + > + <div class="cw-block-gallery-number-text">{{ index + 1 }} / {{ files.length }}</div> + <img + :src="image.download_url" + :style="{ 'max-height': currentHeight + 'px' }" + @load=" + if (files.length - 1 === index) { + startGallery(); + } + " + /> + <div v-if="currentShowFileNames === 'true'" class="cw-block-gallery-file-name"> + <span>{{ image.name }}</span> + </div> + </div> + <div v-if="currentNav === 'true'"> + <a class="cw-block-gallery-prev" @click="plusSlides(-1)"></a> + <a class="cw-block-gallery-next" @click="plusSlides(1)"></a> + </div> + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Ordner</translate> + <courseware-folder-chooser v-model="currentFolderId" allowUserFolders /> + </label> + <label> + <translate>Maximale Höhe</translate> + <input type="number" min="0" max="800" v-model="currentHeight" /> + </label> + <label> + <translate>Autoplay</translate> + <select v-model="currentAutoplay"> + <option value="true"><translate>Ja</translate></option> + <option value="false"><translate>Nein</translate></option> + </select> + </label> + <label v-if="currentAutoplay === 'true'"> + <translate>Autoplay Timer in Sekunden</translate> + <input type="number" min="1" max="60" v-model="currentAutoplayTimer" /> + </label> + <label v-if="currentAutoplay === 'true'"> + <translate>Navigation</translate> + <select v-model="currentNav"> + <option value="true"><translate>Ja</translate></option> + <option value="false"><translate>Nein</translate></option> + </select> + </label> + <label> + <translate>Dateinamen anzeigen</translate> + <select v-model="currentShowFileNames"> + <option value="true"><translate>Ja</translate></option> + <option value="false"><translate>Nein</translate></option> + </select> + </label> + </form> + </template> + <template #info> + <p><translate>Informationen zum Galerie-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareFolderChooser from './CoursewareFolderChooser.vue'; + +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-gallery-block', + components: { + CoursewareDefaultBlock, + CoursewareFolderChooser, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentFolderId: '', + currentAutoplay: '', + currentNav: '', + currentHeight: '', + currentShowFileNames: '', + currentAutoplayTimer: '', + files: [], + slideIndex: 0, + }; + }, + computed: { + ...mapGetters({ + urlHelper: 'urlHelper', + relatedFileRefs: 'file-refs/related', + relatedTermOfUse: 'terms-of-use/related', + }), + folderId() { + return this.block?.attributes?.payload?.folder_id; + }, + autoplay() { + return this.block?.attributes?.payload?.autoplay; + }, + autoplayTimer() { + return this.block?.attributes?.payload?.autoplay_timer; + }, + nav() { + return this.block?.attributes?.payload?.nav; + }, + height() { + return this.block?.attributes?.payload?.height; + }, + showFileNames() { + return this.block?.attributes?.payload?.show_filenames; + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + loadRelatedFileRefs: 'file-refs/loadRelated', + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + this.currentFolderId = this.folderId; + this.currentAutoplay = this.autoplay; + this.currentAutoplayTimer = this.autoplayTimer; + this.currentNav = this.nav; + this.currentHeight = this.height; + this.currentShowFileNames = this.showFileNames; + }, + startGallery() { + this.slideIndex = 0; + this.showSlides(0); + if (this.currentAutoplay === 'true') { + this.playSlides(); + } + }, + async getFolderFiles() { + const parent = { type: 'folders', id: `${this.currentFolderId}` }; + const relationship = 'file-refs'; + const options = { include: 'terms-of-use'} + await this.loadRelatedFileRefs({ parent, relationship, options }); + + const files = this.relatedFileRefs({ parent, relationship }); + this.processFiles(files); + }, + processFiles(files) { + this.files = files + .filter((file) => { + if (this.relatedTermOfUse({parent: file, relationship: 'terms-of-use'}).attributes['download-condition'] !== 0) { + return false; + } + if (! file.attributes['mime-type'].includes('image')) { + return false; + } + + return true; + }) + .map((file) => ({ + id: file.id, + name: file.attributes.name, + download_url: this.urlHelper.getURL( + 'sendfile.php', + { type: 0, file_id: file.id, file_name: file.attributes.name }, + true + ), + })); + }, + storeBlock() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.folder_id = this.currentFolderId; + attributes.payload.autoplay = this.currentAutoplay; + attributes.payload.autoplay_timer = this.currentAutoplayTimer; + attributes.payload.nav = this.currentNav; + attributes.payload.height = this.currentHeight; + attributes.payload.show_filenames = this.currentShowFileNames; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + plusSlides: function (n) { + this.showSlides((this.slideIndex += n)); + }, + showSlides: function (n) { + let slides = this.$refs.images; + if (slides === undefined) { + return false; + } + if (n > slides.length - 1) { + this.slideIndex = 0; + } + if (n < 0) { + this.slideIndex = slides.length - 1; + } + slides.forEach((slide) => { + slide.style.display = 'none'; + }); + slides[this.slideIndex].style.display = 'block'; + }, + playSlides: function () { + let slides = this.$refs.images; + slides.forEach((slide) => { + slide.style.display = 'none'; + }); + if (this.slideIndex > slides.length - 1) { + this.slideIndex = 0; + } + if (slides[this.slideIndex]) { + slides[this.slideIndex].style.display = 'block'; + } + this.slideIndex++; + if (this.currentAutoplay === 'true') { + setTimeout(this.playSlides, this.currentAutoplayTimer * 1000); + } + }, + }, + watch: { + currentFolderId() { + this.getFolderFiles(); + }, + currentAutoplay(value) { + if (value === 'false') { + this.currentNav = 'true'; + } + }, + currentAutoplayTimer(value) { + if (value > 60 || value < 1) { + this.currentAutoplayTimer = '2'; + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareHeadlineBlock.vue b/resources/vue/components/courseware/CoursewareHeadlineBlock.vue new file mode 100755 index 0000000..4f6cc76 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareHeadlineBlock.vue @@ -0,0 +1,385 @@ +<template> + <div class="cw-block cw-block-headline"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeText" + @closeEdit="closeEdit" + > + <template #content> + <div + class="cw-block-headline-content" + :class="[currentStyle, currentHeight === 'half' ? 'half' : 'full']" + :style="headlineStyle" + > + <div + class="icon-layer" + :class="['icon-' + currentIconColor + '-' + currentIcon, currentHeight === 'half' ? 'half' : 'full']" + > + <div class="cw-block-headline-textbox"> + <div class="cw-block-headline-title"> + <h1 :style="textStyle">{{ currentTitle }}</h1> + </div> + <div class="cw-block-headline-subtitle"> + <h2 :style="textStyle">{{ currentSubtitle }}</h2> + </div> + </div> + </div> + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Layout</translate> + <select v-model="currentStyle"> + <option value="heavy"><translate>Große Schrift</translate></option> + <option value="ribbon"><translate>Band</translate></option> + <option value="bigicon_top"><translate>Großes Icon oben</translate></option> + <option value="bigicon_before"><translate>Großes Icon davor</translate></option> + </select> + </label> + <label> + <translate>Höhe</translate> + <select v-model="currentHeight"> + <option value="full"><translate>Voll</translate></option> + <option value="half"><translate>Halb</translate></option> + </select> + </label> + <label> + <translate>Haupttitel</translate> + <input type="text" v-model="currentTitle" /> + </label> + <label> + <translate>Untertitel</translate> + <input type="text" v-model="currentSubtitle" /> + </label> + <label> + <translate>Textfarbe</translate> + <v-select + :options="colors" + label="hex" + :reduce="color => color.hex" + :clearable="false" + v-model="currentTextColor" + class="cw-vs-select" + > + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span> + </template> + <template #no-options="{ search, searching, loading }"> + <translate>Es steht keine Auswahl zur Verfügung</translate>. + </template> + <template #selected-option="{name, hex}"> + <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span> + </template> + <template #option="{name, hex}"> + <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span> + </template> + </v-select> + </label> + <label> + <translate>Icon</translate> + <v-select :clearable="false" :options="icons" v-model="currentIcon" class="cw-vs-select"> + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span> + </template> + <template #no-options="{ search, searching, loading }"> + <translate>Es steht keine Auswahl zur Verfügung</translate>. + </template> + <template #selected-option="option"> + <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span> + </template> + <template #option="option"> + <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span> + </template> + </v-select> + </label> + <label> + <translate>Icon-Farbe</translate> + <v-select + :options="iconColors" + label="value" + :reduce="iconColor => iconColor.class" + :clearable="false" + v-model="currentIconColor" + class="cw-vs-select" + > + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span> + </template> + <template #no-options="{ search, searching, loading }"> + <translate>Es steht keine Auswahl zur Verfügung</translate>. + </template> + <template #selected-option="{name, hex}"> + <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span> + </template> + <template #option="{name, hex}"> + <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span> + </template> + </v-select> + </label> + <label> + <translate>Hintergrundtyp</translate> + <select v-model="currentBackgroundType"> + <option value="color"><translate>Farbe</translate></option> + <option value="image"><translate>Bild</translate></option> + </select> + </label> + <label v-if="currentBackgroundType === 'color'"> + <translate>Hintergrundfarbe</translate> + <v-select + :options="colors" + label="hex" + :reduce="color => color.hex" + v-model="currentBackgroundColor" + :clearable="false" + class="cw-vs-select" + > + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span> + </template> + <template #no-options="{ search, searching, loading }"> + <translate>Es steht keine Auswahl zur Verfügung</translate>. + </template> + <template #selected-option="{name, hex}"> + <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span> + </template> + <template #option="{name, hex}"> + <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span> + </template> + </v-select> + </label> + <label v-if="currentBackgroundType === 'image'"> + <translate>Hintergrundbild</translate> + <courseware-file-chooser + v-model="currentBackgroundImageId" + :isImage="true" + @selectFile="updateCurrentBackgroundImage" + /> + </label> + </form> + </template> + <template #info><translate>Informationen zum Blickfang-Block</translate></template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareFileChooser from './CoursewareFileChooser.vue'; +import { mapActions } from 'vuex'; +import contentIcons from './content-icons.js'; + +export default { + name: 'courseware-headline-block', + components: { + CoursewareDefaultBlock, + CoursewareFileChooser, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentTitle: '', + currentSubtitle: '', + currentStyle: '', + currentHeight: '', + currentBackgroundColor: '', + currentTextColor: '', + currentIcon: '', + currentIconColor: '', + currentBackgroundType: '', + currentBackgroundImageId: '', + currentBackgroundImage: {}, + }; + }, + computed: { + title() { + return this.block?.attributes?.payload?.title; + }, + subtitle() { + return this.block?.attributes?.payload?.subtitle; + }, + style() { + return this.block?.attributes?.payload?.style; + }, + height() { + return this.block?.attributes?.payload?.height; + }, + backgroundColor() { + return this.block?.attributes?.payload?.background_color; + }, + textColor() { + return this.block?.attributes?.payload?.text_color; + }, + icon() { + return this.block?.attributes?.payload?.icon; + }, + iconColor() { + return this.block?.attributes?.payload?.icon_color; + }, + backgroundImageId() { + return this.block?.attributes?.payload?.background_image_id; + }, + backgroundImage() { + return this.block?.attributes?.payload?.background_image; + }, + backgroundType() { + return this.block?.attributes?.payload?.background_type; + }, + complementBackgroundColor() { + return this.calcComplement(this.backgroundColor); + }, + icons() { + return contentIcons; + }, + colors() { + const colors = [ + {name: this.$gettext('Schwarz'), class: 'black', hex: '#000000', level: 100, icon: 'black', darkmode: true}, + {name: this.$gettext('Weiß'), class: 'white', hex: '#ffffff', level: 100, icon: 'white', darkmode: false}, + + {name: this.$gettext('Blau'), class: 'studip-blue', hex: '#28497c', level: 100, icon: 'blue', darkmode: true}, + {name: this.$gettext('Hellblau'), class: 'studip-lightblue', hex: '#e7ebf1', level: 40, icon: 'lightblue', darkmode: false}, + {name: this.$gettext('Rot'), class: 'studip-red', hex: '#d60000', level: 100, icon: 'red', darkmode: false}, + {name: this.$gettext('Grün'), class: 'studip-green', hex: '#008512', level: 100, icon: 'green', darkmode: true}, + {name: this.$gettext('Gelb'), class: 'studip-yellow', hex: '#ffbd33', level: 100, icon: 'yellow', darkmode: false}, + {name: this.$gettext('Grau'), class: 'studip-gray', hex: '#636a71', level: 100, icon: 'grey', darkmode: true}, + + {name: this.$gettext('Holzkohle'), class: 'charcoal', hex: '#3c454e', level: 100, icon: false, darkmode: true}, + {name: this.$gettext('Königliches Purpur'), class: 'royal-purple', hex: '#8656a2', level: 80, icon: false, darkmode: true}, + {name: this.$gettext('Leguangrün'), class: 'iguana-green', hex: '#66b570', level: 60, icon: false, darkmode: true}, + {name: this.$gettext('Königin blau'), class: 'queen-blue', hex: '#536d96', level: 80, icon: false, darkmode: true}, + {name: this.$gettext('Helles Seegrün'), class: 'verdigris', hex: '#41afaa', level: 80, icon: false, darkmode: true}, + {name: this.$gettext('Maulbeere'), class: 'mulberry', hex: '#bf5796', level: 80, icon: false, darkmode: true}, + {name: this.$gettext('Kürbis'), class: 'pumpkin', hex: '#f26e00', level: 100, icon: false, darkmode: true}, + {name: this.$gettext('Sonnenschein'), class: 'sunglow', hex: '#ffca5c', level: 80, icon: false, darkmode: false}, + {name: this.$gettext('Apfelgrün'), class: 'apple-green', hex: '#8bbd40', level: 80, icon: false, darkmode: true}, + ]; + + return colors; + }, + iconColors() { + const iconColors = [ + {name: this.$gettext('Schwarz'), class: 'black', hex: '#000000'}, + {name: this.$gettext('Weiß'), class: 'white', hex: '#ffffff'}, + {name: this.$gettext('Blau'), class: 'studip-blue', hex: '#28497c'}, + {name: this.$gettext('Rot'), class: 'studip-red', hex: '#d60000'}, + {name: this.$gettext('Grün'), class: 'studip-green', hex: '#008512'}, + {name: this.$gettext('Gelb'), class: 'studip-yellow', hex: '#ffbd33'}, + ]; + + return iconColors; + }, + textStyle() { + let style = {}; + style.color = this.currentTextColor; + + return style; + }, + headlineStyle() { + let style = {}; + if (this.currentBackgroundType === 'color') { + style['background-color'] = this.currentBackgroundColor; + } + if (this.currentBackgroundType === 'image') { + style['background-color'] = this.currentBackgroundColor; + style['background-image'] = 'url(' + this.currentBackgroundURL + ')'; + } + + return style; + }, + currentBackgroundURL() { + return this.currentBackgroundImage.download_url; + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + this.currentTitle = this.title; + this.currentSubtitle = this.subtitle; + this.currentStyle = this.style; + this.currentHeight = this.height; + this.currentBackgroundColor = this.backgroundColor; + this.currentTextColor = this.textColor; + this.currentIcon = this.icon; + this.currentIconColor = this.iconColor; + this.currentBackgroundType = this.backgroundType; + this.currentBackgroundImageId = this.backgroundImageId; + if (typeof this.backgroundImage === 'object' && !Array.isArray(this.backgroundImage)) { + this.currentBackgroundImage = this.backgroundImage; + } + }, + updateCurrentBackgroundImage(file) { + this.currentBackgroundImage = file; + this.currentBackgroundImageId = file.id; + }, + closeEdit() { + this.initCurrentData(); + }, + storeText() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.title = this.currentTitle; + attributes.payload.subtitle = this.currentSubtitle; + attributes.payload.style = this.currentStyle; + attributes.payload.height = this.currentHeight; + attributes.payload.background_color = this.currentBackgroundColor; + attributes.payload.text_color = this.currentTextColor; + attributes.payload.icon = this.currentIcon; + attributes.payload.icon_color = this.currentIconColor; + attributes.payload.background_image_id = this.currentBackgroundImageId; + attributes.payload.background_type = this.currentBackgroundType; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + calcComplement(color) { + let RGB = this.calcRGB(color); + + return '#' + this.compToHex(255 - RGB.r) + this.compToHex(255 - RGB.g) + this.compToHex(255 - RGB.b); + }, + calcIconColor(color) { + let RGB = this.calcRGB(color); + + return (RGB.r + RGB.g + RGB.b) / 3 > 129 ? 'black' : 'white'; + }, + calcRGB(color) { + color = color.slice(1); // remove # + let val = parseInt(color, 16); + let r = val >> 16; + let g = (val >> 8) & 0x00ff; + let b = val & 0x0000ff; + + if (g > 255) { + g = 255; + } else if (g < 0) { + g = 0; + } + if (b > 255) { + b = 255; + } else if (b < 0) { + b = 0; + } + + return { r: r, g: g, b: b }; + }, + compToHex(comp) { + let hex = comp.toString(16); + return hex.length === 1 ? '0' + hex : hex; + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareIframeBlock.vue b/resources/vue/components/courseware/CoursewareIframeBlock.vue new file mode 100755 index 0000000..2035276 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareIframeBlock.vue @@ -0,0 +1,233 @@ +<template> + <div class="cw-block cw-block-iframe"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div> + <iframe + v-show="currentUrl.includes('http')" + :src="activeUrl" + :height="currentHeight" + width="100%" + allowfullscreen + sandbox="allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts" + /> + <div v-if="currentCcInfo" class="cw-block-iframe-cc-data"> + <span class="cw-block-iframe-cc" :class="['cw-block-iframe-cc-' + currentCcInfo]"></span> + <div class="cw-block-iframe-cc-infos"> + <p v-if="currentCcWork !== ''"><translate>Werk</translate> {{ currentCcWork }}</p> + <p v-if="currentCcAuthor !== ''"><translate>Autor</translate> {{ currentCcAuthor }}</p> + <p v-if="currentCcBase !== ''"><translate>Lizenz der Plattform</translate> {{ currentCcBase }}</p> + </div> + </div> + <div v-show="!currentUrl.includes('http')" :style="{ height: currentHeight + 'px' }"></div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Titel</translate> + <input type="text" v-model="currentTitle" /> + </label> + <label> + <translate>URL</translate> + <input type="text" v-model="currentUrl" @change="setProtocol" /> + </label> + <label> + <translate>Höhe</translate> + <input type="number" v-model="currentHeight" min="0" /> + </label> + <label> + <translate>Nutzerspezifische ID übergeben</translate> + <select v-model="currentSubmitUserId"> + <option value="false"><translate>Nein</translate></option> + <option value="true"><translate>Ja</translate></option> + </select> + </label> + + <label v-if="currentSubmitUserId === 'true'"> + <translate>Name des Übergabeparameters</translate> + <input type="text" v-model="currentSubmitParam" /> + </label> + <label v-if="currentSubmitUserId === 'true'"> + <translate>Zufallszeichen für Verschlüsselung (Salt)</translate> + <input type="text" v-model="currentSalt" /> + </label> + <label> + <translate>Creative Commons Angaben</translate> + <select v-model="currentCcInfo"> + <option value="false"><translate>Keine</translate></option> + <option value="by">(by) <translate>Namensnennung</translate></option> + <option value="by-sa"> + (by-sa) <translate>Namensnennung & Weitergabe unter gleichen Bedingungen</translate> + </option> + <option value="by-nc"> + (by-nc) <translate>Namensnennung & Nicht kommerziell</translate> + </option> + <option value="by-nd"> + (by-nd) <translate>Namensnennung & Keine Bearbeitung</translate> + </option> + <option value="by-nc-nd"> + (by-nc-nd) <translate>Namensnennung & Nicht kommerziell & Keine Bearbeitung</translate> + </option> + <option value="by-nc-sa"> + (by-nc-sa) + <translate>Namensnennung & Nicht kommerziell & Weitergabe unter gleichen Bedingungen</translate> + </option> + </select> + </label> + <label v-if="currentCcInfo !== 'false'"> + CC <translate>Werk</translate> + <input type="text" v-model="currentCcWork" /> + </label> + <label v-if="currentCcInfo !== 'false'"> + CC <translate>Author</translate> + <input type="text" v-model="currentCcAuthor" /> + </label> + <label v-if="currentCcInfo !== 'false'"> + CC <translate>Lizenz der Plattform</translate> + <input type="text" v-model="currentCcBase" /> + </label> + </form> + </template> + <template #info> + <p><translate>Informationen zum IFrame-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; + +import { mapActions, mapGetters } from 'vuex'; +import md5 from 'md5'; + +export default { + name: 'courseware-iframe-block', + components: { + CoursewareDefaultBlock, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentTitle: '', + currentUrl: '', + currentHeight: '', + currentSubmitUserId: '', + currentSubmitParam: '', + currentSalt: '', + currentCcInfo: '', + currentCcWork: '', + currentCcAuthor: '', + currentCcBase: '', + }; + }, + computed: { + ...mapGetters(['userId']), + url() { + return this.block?.attributes?.payload?.url; + }, + title() { + return this.block?.attributes?.payload?.title; + }, + height() { + return this.block?.attributes?.payload?.height; + }, + submitUserId() { + return this.block?.attributes?.payload?.submit_user_id; + }, + submitParam() { + return this.block?.attributes?.payload?.submit_param; + }, + salt() { + return this.block?.attributes?.payload?.salt; + }, + ccInfo() { + return this.block?.attributes?.payload?.cc_info; + }, + ccWork() { + return this.block?.attributes?.payload?.cc_work; + }, + ccAuthor() { + return this.block?.attributes?.payload?.cc_author; + }, + ccBase() { + return this.block?.attributes?.payload?.cc_base; + }, + activeUrl() { + if (this.currentSubmitUserId) { + return this.currentUrl + '?' + this.currentSubmitParam + '=' + md5(this.userId + this.currentSalt); + } else { + return this.currentUrl; + } + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + this.currentTitle = this.title; + this.currentUrl = this.url; + this.currentHeight = this.height; + this.currentSubmitUserId = this.submitUserId; + this.currentSubmitParam = this.submitParam; + this.currentSalt = this.salt; + this.currentCcInfo = this.ccInfo; + this.currentCcWork = this.ccWork; + this.currentCcAuthor = this.ccAuthor; + this.currentCcBase = this.ccBase; + this.setProtocol(); + }, + setProtocol() { + if (location.protocol === 'https:') { + if (!this.currentUrl.includes('https:')) { + if (this.currentUrl.includes('http:')) { + this.currentUrl = this.currentUrl.replace('http', 'https'); + } else { + this.currentUrl = 'https://' + this.currentUrl; + } + } + } else if (location.protocol === 'http:') { + if (!this.currentUrl.includes('http:') && !this.currentUrl.includes('https:')) { + this.currentUrl = 'http://' + this.currentUrl; + } + } + }, + + storeBlock() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.title = this.currentTitle; + attributes.payload.url = this.currentUrl; + attributes.payload.height = this.currentHeight; + attributes.payload.submit_user_id = this.currentSubmitUserId; + attributes.payload.submit_param = this.currentSubmitParam; + attributes.payload.salt = this.currentSalt; + attributes.payload.cc_info = this.currentCcInfo; + attributes.payload.cc_work = this.currentCcWork; + attributes.payload.cc_author = this.currentCcAuthor; + attributes.payload.cc_base = this.currentCcBase; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareImageMapBlock.vue b/resources/vue/components/courseware/CoursewareImageMapBlock.vue new file mode 100755 index 0000000..f82bd41 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareImageMapBlock.vue @@ -0,0 +1,517 @@ +<template> + <div class="cw-block cw-block-image-map"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <img :src="currentUrl" class="cw-image-map-original-img" ref="original_img" @load="buildCanvas" /> + <canvas class="cw-image-map-canvas" ref="canvas"></canvas> + <img + class="cw-image-from-canvas" + :src="image_from_canvas" + ref="image_from_canvas" + :usemap="'#' + map_name" + /> + <map ref="map" :name="map_name"> + <area + v-for="area in areas" + :key="area.id" + :id="area.id" + :shape="area.shape" + :coords="area.coords" + :title="area.title" + :href="area.external_target" + :target="area.link_target" + @click=" + if (area.target_type === 'internal') { + areaLink(area.internal_target); + } + " + /> + </map> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Bilddatei</translate> + <courseware-file-chooser + v-model="currentFileId" + :isImage="true" + @selectFile="updateCurrentFile" + /> + </label> + <label> + <a class="button add" @click="addShape('arc')"><translate>Kreis hinzufügen</translate></a> + <a class="button add" @click="addShape('ellipse')"><translate>Oval hinzufügen</translate></a> + <a class="button add" @click="addShape('rect')"><translate>Rechteck hinzufügen</translate></a> + </label> + <courseware-tabs v-if="currentShapes.length > 0"> + <courseware-tab + v-for="(shape, index) in currentShapes" + :key="index" + :name="shape.title" + :icon="shape.title === '' ? 'link-extern' : ''" + :selected="index === 0" + > + <label> + <translate>Farbe</translate> + <v-select + :options="colors" + label="name" + :reduce="color => color.class" + :clearable="false" + v-model="shape.data.color" + class="cw-vs-select" + > + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span> + </template> + <template #no-options="{ search, searching, loading }"> + <translate>Es steht keine Auswahl zur Verfügung</translate>. + </template> + <template #selected-option="{name, rgba}"> + <span class="vs__option-color" :style="{'background-color': rgba}"></span><span>{{name}}</span> + </template> + <template #option="{name, rgba}"> + <span class="vs__option-color" :style="{'background-color': rgba}"></span><span>{{name}}</span> + </template> + </v-select> + </label> + <label v-if="shape.type === 'arc'" class="cw-block-image-map-dimensions"> + X: <input type="number" v-model="shape.data.centerX" @change="drawScreen" /> Y: + <input type="number" v-model="shape.data.centerY" @change="drawScreen" /> + <translate>Radius</translate> + <input type="number" v-model="shape.data.radius" @change="drawScreen" /> + </label> + <label v-if="shape.type === 'rect'" class="cw-block-image-map-dimensions"> + X: <input type="number" v-model="shape.data.X" @change="drawScreen" /> Y: + <input type="number" v-model="shape.data.Y" @change="drawScreen" /> + <translate>Höhe</translate> + <input type="number" v-model="shape.data.height" @change="drawScreen" /> + <translate>Breite</translate> + <input type="number" v-model="shape.data.width" @change="drawScreen" /> + </label> + <label v-if="shape.type === 'ellipse'" class="cw-block-image-map-dimensions"> + X: <input type="number" v-model="shape.data.X" @change="drawScreen" /> Y: + <input type="number" v-model="shape.data.Y" @change="drawScreen" /> + <translate>Radius</translate> X: + <input type="number" v-model="shape.data.radiusX" @change="drawScreen" /> + <translate>Radius</translate> Y: + <input type="number" v-model="shape.data.radiusY" @change="drawScreen" /> + </label> + <label> + <translate>Bezeichnung</translate> + <input type="text" v-model="shape.title" /> + </label> + <label> + <translate>Beschriftung</translate> + <input type="text" v-model="shape.data.text" @change="drawScreen" /> + </label> + <label> + <translate>Art des Links</translate> + <select v-model="shape.link_type"> + <option value="internal"><translate>Interner Link</translate></option> + <option value="external"><translate>Externer Link</translate></option> + </select> + </label> + <label v-if="shape.link_type === 'internal'"> + <translate>Ziel des Links</translate> + <select v-model="shape.target_internal" @change="drawScreen"> + <option v-for="(el, index) in courseware" :key="index" :value="el.id"> + {{ el.attributes.title }} + </option> + </select> + </label> + <label v-if="shape.link_type === 'external'"> + <translate>Ziel des Links</translate> + <input + type="text" + placeholder="https://www.studip.de" + v-model="shape.target_external" + @change=" + drawScreen(); + fixUrl(index); + " + /> + </label> + <label> + <a class="button cancel" @click="removeShape(index)" + ><translate>Form entfernen</translate></a + > + </label> + </courseware-tab> + </courseware-tabs> + </form> + </template> + <template #info> + <p><translate>Informationen zum Verweissensitive-Grafik-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareFileChooser from './CoursewareFileChooser.vue'; +import CoursewareTabs from './CoursewareTabs.vue'; +import CoursewareTab from './CoursewareTab.vue'; + +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-image-map-block', + components: { + CoursewareDefaultBlock, + CoursewareFileChooser, + CoursewareTabs, + CoursewareTab, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentFileId: '', + currentFile: {}, + currentShapes: {}, + context: {}, + image_from_canvas: '', + map_name: '', + areas: [], + darkColors: ['black', 'darkgrey', 'purple'], + colors: [ + { name: this.$gettext('Transparent'), class: 'transparent', rgba: 'rgba(0,0,0,0)' }, + { name: this.$gettext('Weiß'), class: 'white', rgba: 'rgba(255,255,255,1)' }, + { name: this.$gettext('Blau'), class: 'blue', rgba: 'rgba(52,152,219,1)' }, + { name: this.$gettext('Grün'), class: 'green', rgba: 'rgba(46,204,113,1)' }, + { name: this.$gettext('Lila'), class: 'purple', rgba: 'rgba(155,89,182,1)' }, + { name: this.$gettext('Rot'), class: 'red', rgba: 'rgba(231,76,60,1)' }, + { name: this.$gettext('Gelb'), class: 'yellow', rgba: 'rgba(254,211,48,1)' }, + { name: this.$gettext('Orange'), class: 'orange', rgba: 'rgba(243,156,18,1)' }, + { name: this.$gettext('Grau'), class: 'grey', rgba: 'rgba(236, 240, 241,1)' }, + { name: this.$gettext('Dunkelgrau'), class: 'darkgrey', rgba: 'rgba(52,73,94,1)' }, + { name: this.$gettext('Schwarz'), class: 'black', rgba: 'rgba(0,0,0,1)' } + ], + file: null + }; + }, + computed: { + ...mapGetters({ + courseware: 'courseware-structural-elements/all', + fileRefById: 'file-refs/byId', + urlHelper: 'urlHelper', + }), + fileId() { + return this.block?.attributes?.payload?.file_id; + }, + shapes() { + return this.block?.attributes?.payload?.shapes; + }, + currentUrl() { + if (this.currentFile.download_url !== 'undefined') { + return this.currentFile.download_url; + } else { + return ''; + } + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + loadFileRef: 'file-refs/loadById', + }), + async initCurrentData() { + this.currentFileId = this.fileId; + this.currentShapes = JSON.parse(JSON.stringify(this.shapes)); + await this.loadFile(); + this.buildCanvas(); + }, + async loadFile() { + const id = this.currentFileId; + await this.loadFileRef({ id }); + const fileRef = this.fileRefById({ id }); + + if (fileRef) { + this.updateCurrentFile({ + id: fileRef.id, + name: fileRef.attributes.name, + download_url: this.urlHelper.getURL( + 'sendfile.php', + { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name }, + true + ), + }); + } + }, + updateCurrentFile(file) { + this.currentFile = file; + this.currentFileId = file.id; + }, + storeBlock() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.file_id = this.currentFileId; + attributes.payload.shapes = this.currentShapes; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + + buildCanvas() { + let canvas = this.$refs.canvas; + let original_img = this.$refs.original_img; + canvas.width = 1085; + if (original_img.height > 0) { + canvas.height = Math.round((canvas.width / original_img.width) * original_img.height); + } else { + canvas.height = 484; + } + this.context = canvas.getContext('2d'); + this.drawScreen(); + }, + drawScreen() { + let context = this.context; + let view = this; + let outlineImage = new Image(); + outlineImage.src = this.currentUrl; + outlineImage.onload = function () { + context.clearRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas + context.fillStyle = '#ffffff'; + context.fillRect(0, 0, context.canvas.width, context.canvas.height); // set background + if (outlineImage.src !== '') { + context.drawImage(outlineImage, 0, 0, context.canvas.width, context.canvas.height); + } + view.drawShapes(); + + if (!(view.$refs.canvas.length > 0)) { + view.image_from_canvas = view.context.canvas.toDataURL('image/jpeg', 1.0); + view.mapImage(); + } + }; + }, + drawShapes() { + let context = this.context; + let view = this; + this.currentShapes.forEach((value) => { + let shape = value; + let text = shape.data.text; + let shape_width = 0; + let shape_height = 0; + let text_X = 0; + let text_Y = 0; + + context.beginPath(); + switch (shape.type) { + case 'arc': + shape_width = Math.round((2 * shape.data.radius) / Math.sqrt(2)) * 0.85; + shape_height = shape_width / 0.85; + text_X = shape.data.centerX; + text_Y = shape.data.centerY - shape.data.radius * 0.75; + context.arc(shape.data.centerX, shape.data.centerY, shape.data.radius, 0, 2 * Math.PI); // x, y, r, startAngle, endAngle ... Angle in radians! + context.fillStyle = view.colors.filter((color) => {return color.class === shape.data.color})[0].rgba; + context.fill(); + break; + case 'ellipse': + shape_width = shape.data.radiusX; + shape_height = shape.data.radiusY * 1.75; + text_X = shape.data.X; + text_Y = shape.data.Y - shape.data.radiusY * 0.8; + context.ellipse( + shape.data.X, + shape.data.Y, + shape.data.radiusX, + shape.data.radiusY, + 0, + 0, + 2 * Math.PI + ); + context.fillStyle = view.colors.filter((color) => {return color.class === shape.data.color})[0].rgba; + context.fill(); + break; + case 'rect': + shape_width = shape.data.width; + shape_height = shape.data.height; + text_X = shape.data.X + shape.data.width / 2; + text_Y = shape.data.Y; + context.rect(shape.data.X, shape.data.Y, shape.data.width, shape.data.height); + context.fillStyle = view.colors.filter((color) => {return color.class === shape.data.color})[0].rgba; + context.fill(); + break; + default: + return; + } + + if (text && shape.data.color !== 'transparent') { + text = view.fitTextToShape(context, text, shape_width); + context.textAlign = 'center'; + context.font = '14px Arial'; + if (view.darkColors.indexOf(shape.data.color) > -1) { + context.fillStyle = '#ffffff'; + } else { + context.fillStyle = '#000000'; + } + let lineHeight = shape_height / (text.length + 1); + text.forEach((value, key) => { + context.fillText(value, text_X, text_Y + lineHeight * (key + 1)); + }); + } + + context.closePath(); + }); + }, + fitTextToShape(context, text, shape_width) { + let text_width = context.measureText(text).width; + if (text_width > shape_width) { + text = text.split(' '); + let line = ''; + let word = ' '; + let new_text = []; + do { + word = text.shift(); + if (context.measureText(word).width >= shape_width) { + return ['']; + } + line = line + word + ' '; + if (context.measureText(line).width > shape_width) { + text.unshift(word); + line = line.substring(0, line.lastIndexOf(word)); + new_text.push(line.trim()); + line = ''; + } + } while (text.length > 0); + new_text.push(line.trim()); + return new_text; + } else { + return [text]; + } + }, + mapImage() { + let view = this; + // generate map name + let map_name = 'cw-image-map-' + Math.round(Math.random() * 100); + this.map_name = map_name; + + // insert areas + this.areas = []; + this.currentShapes.forEach((value, key) => { + let shape = value; + let area = {}; + area.id = 'shape-' + key; + + switch (shape.type) { + case 'arc': + area.shape = 'circle'; + area.coords = shape.data.centerX + ', ' + shape.data.centerY + ', ' + shape.data.radius; + break; + case 'ellipse': + let coords = ''; + let x = 0; + let y = 0; + for (let theta = 0; theta < 2 * Math.PI; theta += (2 * Math.PI) / 20) { + x = parseInt(shape.data.X) + Math.round(parseInt(shape.data.radiusX) * Math.cos(theta)); + y = parseInt(shape.data.Y) + Math.round(parseInt(shape.data.radiusY) * Math.sin(theta)); + coords = coords + x + ',' + y + ','; + } + area.shape = 'poly'; + area.coords = coords; + break; + case 'rect': + case 'text': + let x2 = parseInt(shape.data.X) + parseInt(shape.data.width); + let y2 = parseInt(shape.data.Y) + parseInt(shape.data.height); + area.shape = 'rect'; + area.coords = shape.data.X + ', ' + shape.data.Y + ', ' + x2 + ', ' + y2; + break; + } + area.title = shape.title; + shape.link_type === 'external' + ? (area.external_target = shape.target_external) + : (area.external_target = '#'); + if (shape.link_type === 'internal') { + area.internal_target = shape.target_internal; + } else { + area.internal_target = ''; + } + shape.link_type === 'external' ? (area.link_target = '_blank') : (area.link_target = '_self'); + area.link_type = shape.link_type; + area.target_type = shape.link_type; + view.areas.push(area); + }); + }, + areaLink(target) { + this.$router.push(target); + }, + + //edit methods + addShape(addtype) { + let data = {}; + switch (addtype) { + case 'arc': + data = { + centerX: 50, + centerY: 50, + radius: 50, + color: 'blue', + border: false, + text: '', + }; + break; + case 'rect': + data = { + X: 50, + Y: 50, + height: 100, + width: 50, + color: 'blue', + border: false, + text: '', + }; + break; + case 'ellipse': + data = { + X: 50, + Y: 50, + radiusX: 50, + radiusY: 20, + color: 'blue', + border: false, + text: '', + }; + break; + } + this.currentShapes.push({ + type: addtype, + data: data, + title: '', + link_type: 'external', + target_internal: '', + target_external: '', + }); + this.buildCanvas(); + }, + removeShape(index) { + this.currentShapes.splice(index, 1); + }, + fixUrl(index) { + let url = this.currentShapes[index].target_external; + if (url !== '' && url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) { + url = 'https://' + url; + } + this.currentShapes[index].target_external = url; + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareKeyPointBlock.vue b/resources/vue/components/courseware/CoursewareKeyPointBlock.vue new file mode 100755 index 0000000..7c57f2a --- /dev/null +++ b/resources/vue/components/courseware/CoursewareKeyPointBlock.vue @@ -0,0 +1,206 @@ +<template> + <div class="cw-block cw-block-key-point"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="closeEdit" + > + <template #content> + <div class="cw-keypoint-content" :class="['cw-keypoint-' + currentColor]"> + <studip-icon v-if="currentIcon" size="48" :shape="currentIcon" :role="currentRole"/> + <p class="cw-keypoint-sentence">{{currentText}}</p> + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label for="cw-keypoint-content"> + <translate>Merksatz</translate> + <input + type="text" + name="cw-keypoint-content" + class="cw-keypoint-set-content" + v-model="currentText" + spellcheck="true" + /> + </label> + + <label for="cw-keypoint-color"> + <translate>Farbe</translate> + <v-select + :options="colors" + label="icon" + :clearable="false" + :reduce="option => option.icon" + v-model="currentColor" + class="cw-vs-select" + > + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span> + </template> + <template #no-options="{ search, searching, loading }"> + <translate>Es steht keine Auswahl zur Verfügung</translate>. + </template> + <template #selected-option="{name, hex}"> + <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span> + </template> + <template #option="{name, hex}"> + <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span> + </template> + </v-select> + </label> + + <label for="cw-keypoint-icons"> + <translate>Icon</translate> + <v-select :options="icons" :clearable="false" v-model="currentIcon" class="cw-vs-select"> + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span> + </template> + <template #no-options="{ search, searching, loading }"> + <translate>Es steht keine Auswahl zur Verfügung</translate>. + </template> + <template #selected-option="option"> + <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span> + </template> + <template #option="option"> + <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span> + </template> + </v-select> + </label> + </form> + </template> + <template #info> + <p><translate>Informationen zum Merksatz-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import { mapActions } from 'vuex'; +import contentIcons from './content-icons.js'; + +export default { + name: 'courseware-key-point-block', + components: { + CoursewareDefaultBlock, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentText: '', + currentColor: '', + currentIcon: '', + }; + }, + computed: { + file() { + return `icons/${this.color}/${this.icon}.svg`; + }, + icons() { + return contentIcons; + }, + colors() { + const colors = [ + {name: this.$gettext('Schwarz'), class: 'black', hex: '#000000', level: 100, icon: 'black', darkmode: true}, + {name: this.$gettext('Weiß'), class: 'white', hex: '#ffffff', level: 100, icon: 'white', darkmode: false}, + + {name: this.$gettext('Blau'), class: 'studip-blue', hex: '#28497c', level: 100, icon: 'blue', darkmode: true}, + {name: this.$gettext('Hellblau'), class: 'studip-lightblue', hex: '#e7ebf1', level: 40, icon: 'lightblue', darkmode: false}, + {name: this.$gettext('Rot'), class: 'studip-red', hex: '#d60000', level: 100, icon: 'red', darkmode: false}, + {name: this.$gettext('Grün'), class: 'studip-green', hex: '#008512', level: 100, icon: 'green', darkmode: true}, + {name: this.$gettext('Gelb'), class: 'studip-yellow', hex: '#ffbd33', level: 100, icon: 'yellow', darkmode: false}, + {name: this.$gettext('Grau'), class: 'studip-gray', hex: '#636a71', level: 100, icon: 'grey', darkmode: true}, + + {name: this.$gettext('Holzkohle'), class: 'charcoal', hex: '#3c454e', level: 100, icon: false, darkmode: true}, + {name: this.$gettext('Königliches Purpur'), class: 'royal-purple', hex: '#8656a2', level: 80, icon: false, darkmode: true}, + {name: this.$gettext('Leguangrün'), class: 'iguana-green', hex: '#66b570', level: 60, icon: false, darkmode: true}, + {name: this.$gettext('Königin blau'), class: 'queen-blue', hex: '#536d96', level: 80, icon: false, darkmode: true}, + {name: this.$gettext('Helles Seegrün'), class: 'verdigris', hex: '#41afaa', level: 80, icon: false, darkmode: true}, + {name: this.$gettext('Maulbeere'), class: 'mulberry', hex: '#bf5796', level: 80, icon: false, darkmode: true}, + {name: this.$gettext('Kürbis'), class: 'pumpkin', hex: '#f26e00', level: 100, icon: false, darkmode: true}, + {name: this.$gettext('Sonnenschein'), class: 'sunglow', hex: '#ffca5c', level: 80, icon: false, darkmode: false}, + {name: this.$gettext('Apfelgrün'), class: 'apple-green', hex: '#8bbd40', level: 80, icon: false, darkmode: true}, + ]; + let iconColors = []; + + colors.forEach(color => { + if(color.icon && color.class !== 'white' && color.class !== 'studip-lightblue') { + iconColors.push(color); + } + }); + + return iconColors; + }, + text() { + return this.block?.attributes?.payload?.text; + }, + color() { + return this.block?.attributes?.payload?.color; + }, + icon() { + return this.block?.attributes?.payload?.icon; + }, + currentRole() { + switch (this.currentColor) { + case 'black': + return 'info'; + + case 'grey': + return 'inactive'; + + case 'green': + return 'status-green'; + + case 'red': + return 'status-red'; + + case 'white': + return 'info_alt'; + + case 'yellow': + return 'status-yellow'; + + case 'blue': + return 'clickable'; + } + } + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + this.currentText = this.text; + this.currentColor = this.color; + this.currentIcon = this.icon; + }, + storeBlock() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.text = this.currentText; + attributes.payload.color = this.currentColor; + attributes.payload.icon = this.currentIcon; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + closeEdit() { + this.initCurrentData(); + }, + }, + mounted() { + this.initCurrentData(); + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareLinkBlock.vue b/resources/vue/components/courseware/CoursewareLinkBlock.vue new file mode 100755 index 0000000..02922d3 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareLinkBlock.vue @@ -0,0 +1,149 @@ +<template> + <div class="cw-block cw-block-link"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div v-if="currentType === 'external'"> + <a :href="currentUrl" target="_blank"> + <div class="cw-link external"> + <span class="cw-link-title">{{ currentTitle }}</span> + </div> + </a> + </div> + <div v-if="currentType === 'internal'"> + <router-link :to="'/structural_element/' + currentTarget"> + <div class="cw-link internal"> + <span class="cw-link-title"> + {{ currentTitle }} + </span> + </div> + </router-link> + </div> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Titel</translate> + <input type="text" v-model="currentTitle" /> + </label> + <label> + <translate>Art des Links</translate> + <select v-model="currentType"> + <option value="external"><translate>Extern</translate></option> + <option value="internal"><translate>Intern</translate></option> + </select> + </label> + <label v-show="currentType === 'external'"> + <translate>URL</translate> + <input type="text" v-model="currentUrl" @change="fixUrl" /> + </label> + <label v-show="currentType === 'internal'"> + <translate>Seite</translate> + <select v-model="currentTarget"> + <option v-for="(el, index) in courseware" :key="index" :value="el.id"> + {{ el.attributes.title }} + </option> + </select> + </label> + </form> + </template> + <template #info> + <p><translate>Informationen zum Link-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-link-block', + components: { + CoursewareDefaultBlock, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentType: '', + currentTarget: '', + currentUrl: '', + currentTitle: '', + }; + }, + computed: { + ...mapGetters({ + courseware: 'courseware-structural-elements/all', + }), + type() { + return this.block?.attributes?.payload?.type; + }, + target() { + return this.block?.attributes?.payload?.target; + }, + url() { + return this.block?.attributes?.payload?.url; + }, + title() { + return this.block?.attributes?.payload?.title; + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + companionWarning: 'companionWarning', + }), + initCurrentData() { + this.currentType = this.type; + this.currentTarget = this.target; + this.currentUrl = this.url; + this.currentTitle = this.title; + this.fixUrl(); + }, + fixUrl() { + if ( + this.currentUrl.indexOf('http://') !== 0 && + this.currentUrl.indexOf('https://') !== 0 && + this.currentUrl !== '' + ) { + this.currentUrl = 'https://' + this.currentUrl; + } + }, + storeBlock() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.type = this.currentType; + attributes.payload.target = this.currentTarget; + attributes.payload.url = this.currentUrl; + attributes.payload.title = this.currentTitle; + if (this.currentType === 'internal' && this.currentTarget === '') { + this.companionWarning({ + info: this.$gettext('Bitte wählen Sie eine Seite als Ziel aus') + }); + return false; + } else { + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + } + + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareListContainer.vue b/resources/vue/components/courseware/CoursewareListContainer.vue new file mode 100755 index 0000000..794231c --- /dev/null +++ b/resources/vue/components/courseware/CoursewareListContainer.vue @@ -0,0 +1,64 @@ +<template> + <courseware-default-container + :container="container" + :containerClass="'cw-container-list'" + :canEdit="canEdit" + :isTeacher="isTeacher" + @storeContainer="storeContainer" + > + <template v-slot:containerContent> + <ul class="cw-container-list-block-list"> + <li v-for="block in blocks" :key="block.id" class="cw-block-item"> + <component :is="component(block)" :block="block" :canEdit="canEdit" :isTeacher="isTeacher" /> + </li> + <li v-if="showEditMode && canEdit"><courseware-block-adder-area :container="container" :section="0" /></li> + </ul> + </template> + </courseware-default-container> +</template> + +<script> +import ContainerComponents from './container-components.js'; +import containerMixin from '../../mixins/courseware/container.js'; +import { mapGetters } from 'vuex'; + +export default { + name: 'courseware-list-container', + mixins: [containerMixin], + components: ContainerComponents, + props: { + container: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return {}; + }, + computed: { + ...mapGetters({ + blockById: 'courseware-blocks/byId', + }), + blocks() { + if (!this.container) { + return []; + } + + return this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id })); + }, + showEditMode() { + return this.$store.getters.viewMode === 'edit'; + }, + }, + methods: { + storeContainer(data) { + console.log(data); + }, + component(block) { + if (block.attributes["block-type"] !== undefined) { + return 'courseware-' + block.attributes["block-type"] + '-block'; + } + }, + }, + mounted() {}, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareManagerBlock.vue b/resources/vue/components/courseware/CoursewareManagerBlock.vue new file mode 100755 index 0000000..27d834b --- /dev/null +++ b/resources/vue/components/courseware/CoursewareManagerBlock.vue @@ -0,0 +1,44 @@ +<template> + <div :class="{ 'cw-manager-block-clickable': inserter }" class="cw-manager-block" @click="clickItem"> + <span v-if="inserter"> + <studip-icon shape="arr_2left" size="16" role="sort" /> + </span> + {{ block.attributes.title }} + <div v-if="sortBlocks" class="cw-manager-block-buttons"> + <studip-icon :class="{'cw-manager-icon-disabled' : !canMoveUp}" shape="arr_2up" size="16" role="sort" @click="moveUp" /> + <studip-icon :class="{'cw-manager-icon-disabled' : !canMoveDown}" shape="arr_2down" size="16" role="sort" @click="moveDown" /> + </div> + </div> +</template> + +<script> +export default { + name: 'courseware-manager-block', + props: { + block: Object, + inserter: Boolean, + sortBlocks: Boolean, + elementType: String, + canMoveUp: Boolean, + canMoveDown: Boolean, + sectionId: Number + }, + methods: { + clickItem() { + if (this.inserter) { + this.$emit('insertBlock', {block: this.block, source: this.elementType}); + } + }, + moveUp() { + if (this.sortBlocks) { + this.$emit('moveUp', this.block.id, this.sectionId); + } + }, + moveDown() { + if (this.sortBlocks) { + this.$emit('moveDown', this.block.id, this.sectionId); + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareManagerContainer.vue b/resources/vue/components/courseware/CoursewareManagerContainer.vue new file mode 100755 index 0000000..70f764c --- /dev/null +++ b/resources/vue/components/courseware/CoursewareManagerContainer.vue @@ -0,0 +1,260 @@ +<template> + <div class="cw-manager-container"> + <div + :class="{ 'cw-manager-container-clickable-title': inserter }" + class="cw-manager-container-title" + @click="clickItem" + > + <span v-if="inserter"> + <studip-icon shape="arr_2left" size="16" role="sort" /> + </span> + {{ container.attributes.title }} ({{container.attributes.width}}) + <div v-if="sortContainers" class="cw-manager-container-buttons"> + <studip-icon :class="{'cw-manager-icon-disabled' : !canMoveUp}" shape="arr_2up" role="sort" @click="moveUp" /> + <studip-icon :class="{'cw-manager-icon-disabled' : !canMoveDown}" shape="arr_2down" role="sort" @click="moveDown" /> + </div> + </div> + <courseware-collapsible-box :open="false" :title="$gettext('Blöcke')" class="cw-manager-container-blocks"> + <div v-if="canSortChildren"> + <button v-show="!sortBlocksActive && isCurrent" class="button sort" @click="sortBlocks"> + <translate>Blöcke sortieren</translate> + </button> + <button v-show="sortBlocksActive && isCurrent" class="button accept" @click="storeBlocksSort"> + <translate>Sortieren beenden</translate> + </button> + <button v-show="sortBlocksActive && isCurrent" class="button cancel" @click="resetBlocksSort"> + <translate>Sortieren abbrechen</translate> + </button> + </div> + <p v-if="!hasChildren"> + <translate>Dieser Abschnitt enthält keine Blöcke.</translate> + </p> + <div v-else-if="sectionsWithBlocksCurrentState.length === 1"> + <transition-group name="cw-sort-ease" tag="div"> + <courseware-manager-block + v-for="(block, blockIndex) in sectionsWithBlocksCurrentState[0].blocks" + :key="block.id" + :block="block" + :inserter="blockInserter" + :sortBlocks="sortBlocksActive" + :canMoveUp="blockIndex !== 0" + :canMoveDown="blockIndex + 1 !== sectionsWithBlocksCurrentState[0].blocks.length" + :elementType="elementType" + :sectionId="0" + @insertBlock="insertBlock" + @moveUp="moveBlockUp" + @moveDown="moveBlockDown" + /> + </transition-group> + </div> + <div v-else> + <courseware-collapsible-box + v-for="(section, index) in sectionsWithBlocksCurrentState" + :key="section.id" + :open="true" + :title="section.name" + class="cw-manager-container-blocks" + > + <transition-group name="cw-sort-ease" tag="div"> + <courseware-manager-block + v-for="(block, blockIndex) in sectionsWithBlocksCurrentState[index].blocks" + :key="block.id" + :block="block" + :inserter="blockInserter" + :sortBlocks="sortBlocksActive" + :canMoveUp="blockIndex !== 0 || index !== 0" + :canMoveDown="index + 1 !== sectionsWithBlocksCurrentState.length || blockIndex + 1 !== sectionsWithBlocksCurrentState[index].blocks.length" + :elementType="elementType" + :sectionId="index" + @insertBlock="insertBlock" + @moveUp="moveBlockUp" + @moveDown="moveBlockDown" + /> + </transition-group> + </courseware-collapsible-box> + </div> + <courseware-manager-filing + v-if="isCurrent && !sortContainers && !sortBlocksActive" + :parentId="container.id" + :parentItem="container" + itemType="block" + @deactivated="reloadContainer" + /> + </courseware-collapsible-box> + </div> +</template> + +<script> +import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue'; +import CoursewareManagerBlock from './CoursewareManagerBlock.vue'; +import CoursewareManagerFiling from './CoursewareManagerFiling.vue'; +import { mapGetters, mapActions } from 'vuex'; + +export default { + name: 'courseware-manager-container', + components: { + CoursewareCollapsibleBox, + CoursewareManagerBlock, + CoursewareManagerFiling, + }, + props: { + container: Object, + isCurrent: Boolean, + inserter: Boolean, + blockInserter: Boolean, + sortContainers: Boolean, + elementType: String, + canMoveUp: Boolean, + canMoveDown: Boolean + }, + data() { + return { + sortBlocksActive: false, + sectionsWithBlocksCurrentState: [], + }; + }, + computed: { + ...mapGetters({ + blockById: 'courseware-blocks/byId', + }), + hasChildren() { + return this.getBlocksCount >= 1; + }, + canSortChildren() { + return this.getBlocksCount > 1; + }, + containerType() { + return this.container.attributes['container-type']; + }, + hasSections() { + return this.containerType() === 'tabs' || this.containerType() === 'accordion'; + }, + getBlocksCount() { + if (this.sectionsWithBlocksCurrentState === null) { + return 0; + } else { + let blocks = 0; + + this.sectionsWithBlocksCurrentState.forEach((section) => { + blocks += section.blocks.length; + }); + + return blocks; + } + } + }, + mounted() { + this.sectionsWithBlocksCurrentState = this.getSectionsWithBlocks(); + }, + methods: { + ...mapActions({ + sortBlocksInContainer: 'sortBlocksInContainer', + updateContainer: 'updateContainer', + loadContainer: 'loadContainer', + lockObject: 'lockObject', + unlockObject: 'unlockObject' + }), + reloadContainer() { + this.loadContainer(this.container.id); + }, + clickItem() { + if (this.inserter) { + this.$emit('insertContainer', {container: this.container, source: this.elementType}); + } + }, + getSectionsWithBlocks() { + if (!this.container) { + return []; + } + if (!this.container.attributes.payload.sections) { + return []; + } + + const blockSections = JSON.parse(JSON.stringify(this.container.attributes.payload.sections)); //copy array AND objects without references + + blockSections.forEach((section) => { + section.blocks = section.blocks.flatMap( + (id) => { + return this.blockById({ id }) ?? [] //remove blocks which could not be loaded + } + ); + + section.blocks.sort((a, b) => { + return a.attributes.position > b.attributes.position; + }); + }); + + return blockSections; + }, + insertBlock(data) { + this.$emit('insertBlock', data); + }, + sortBlocks() { + this.sortBlocksActive = true; + }, + async storeBlocksSort() { + const container = this.container; + + this.sectionsWithBlocksCurrentState.forEach((section, index)=> { + container.attributes.payload.sections[index].blocks = section.blocks.map(({ id }) => ( id )); + }); + + await this.lockObject({id: container.id, type: 'courseware-containers'}); + await this.updateContainer({ container: container, structuralElementId: this.container.relationships['structural-element'].data.id }); + await this.unlockObject({id: container.id, type: 'courseware-containers'}); + + await this.sortBlocksInContainer({ container: this.container, sections: this.sectionsWithBlocksCurrentState }); + + this.sortBlocksActive = false; + }, + resetBlocksSort() { + this.sectionsWithBlocksCurrentState = this.getSectionsWithBlocks(); + this.sortBlocksActive = false; + }, + moveUp() { + if (this.sortContainers) { + this.$emit('moveUp', this.container.id); + } + }, + moveDown() { + if (this.sortContainers) { + this.$emit('moveDown', this.container.id); + } + }, + moveBlockUp(blockId, sectionId) { + let view = this; + this.sectionsWithBlocksCurrentState[sectionId].blocks.every((block, index) => { + if (block.id === blockId) { + if (index === 0) { + if (sectionId !== 0) { + view.sectionsWithBlocksCurrentState[sectionId-1].blocks.push(view.sectionsWithBlocksCurrentState[sectionId].blocks.splice(index, 1)[0]); + } + return false; + } + view.sectionsWithBlocksCurrentState[sectionId].blocks.splice(index - 1, 0, view.sectionsWithBlocksCurrentState[sectionId].blocks.splice(index, 1)[0]); + return false; + } else { + return true; + } + }); + }, + moveBlockDown(blockId, sectionId) { + let view = this; + this.sectionsWithBlocksCurrentState[sectionId].blocks.every((block, index) => { + if (block.id === blockId) { + if (index === view.sectionsWithBlocksCurrentState[sectionId].blocks.length - 1) { + if (sectionId !== view.sectionsWithBlocksCurrentState.length - 1) { + view.sectionsWithBlocksCurrentState[sectionId + 1].blocks.unshift(view.sectionsWithBlocksCurrentState[sectionId].blocks.splice(index, 1)[0]); + } + return false; + } + view.sectionsWithBlocksCurrentState[sectionId].blocks.splice(index + 1, 0, view.sectionsWithBlocksCurrentState[sectionId].blocks.splice(index, 1)[0]); + return false; + } else { + return true; + } + }); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareManagerCopySelector.vue b/resources/vue/components/courseware/CoursewareManagerCopySelector.vue new file mode 100755 index 0000000..a914cde --- /dev/null +++ b/resources/vue/components/courseware/CoursewareManagerCopySelector.vue @@ -0,0 +1,141 @@ +<template> + <div class="cw-manager-copy-selector"> + <div v-if="sourceEmpty" class="cw-manager-copy-selector-source"> + <button class="hugebutton" @click="selectSource('own'); loadOwnCourseware()"><translate>Aus meine Inhalte kopieren</translate></button> + <button class="hugebutton" @click="selectSource('remote')"><translate>Aus Veranstaltung kopieren</translate></button> + </div> + <div v-else> + <button class="button" @click="reset"><translate>Quelle auswählen</translate></button> + <div v-if="sourceRemote"> + <h2 v-if="!hasRemoteCid"><translate>Veranstaltungen</translate></h2> + <ul v-if="!hasRemoteCid"> + <li v-for="course in courses" :key="course.id" > + <button class="hugebutton" @click="loadRemoteCourseware(course.id)">{{course.attributes.title}}</button> + </li> + </ul> + <courseware-manager-element + v-if="hasRemoteCid" + type="remote" + :currentElement="remoteElement" + @selectElement="setRemoteId" + @loadSelf="loadSelf" + /> + </div> + <div v-if="sourceOwn"> + <courseware-manager-element + v-if="ownId !== ''" + type="own" + :currentElement="ownElement" + @selectElement="setOwnId" + @loadSelf="loadSelf" + /> + <courseware-companion-box + v-else + :msgCompanion="$gettext('Sie haben noch keine eigenen Inhalte angelegt')" + mood="sad" + /> + </div> + </div> + </div> +</template> + +<script> +import CoursewareManagerElement from './CoursewareManagerElement.vue'; +import { mapActions, mapGetters } from 'vuex'; +import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; + +export default { + name: 'courseware-manager-copy-selector', + components:{ + CoursewareManagerElement, + CoursewareCompanionBox, + }, + props: {}, + data() {return{ + source: '', + courses: [], + remoteCid: '', + remoteCoursewareInstance: {}, + remoteId: '', + remoteElement: {}, + ownCoursewareInstance: {}, + ownId: '', + ownElement: {}, + + }}, + computed: { + ...mapGetters({ + userId: 'userId', + structuralElementById: 'courseware-structural-elements/byId', + }), + sourceEmpty() { + return this.source === ''; + }, + sourceOwn() { + return this.source === 'own'; + }, + sourceRemote() { + return this.source === 'remote'; + }, + hasRemoteCid() { + return this.remoteCid !== ''; + }, + }, + methods: { + ...mapActions({ + loadUsersCourses: 'loadUsersCourses', + loadStructuralElement: 'loadStructuralElement', + loadRemoteCoursewareStructure: 'loadRemoteCoursewareStructure', + }), + selectSource(source) { + this.source = source; + }, + async loadRemoteCourseware(cid) { + this.remoteCid = cid; + this.remoteCoursewareInstance = await this.loadRemoteCoursewareStructure({rangeId: this.remoteCid, rangeType: 'courses'}); + if (this.remoteCoursewareInstance !== null) { + this.setRemoteId(this.remoteCoursewareInstance.relationships.root.data.id); + } else { + console.debug('can not load'); + } + + }, + async loadOwnCourseware() { + this.ownCoursewareInstance = await this.loadRemoteCoursewareStructure({rangeId: this.userId, rangeType: 'users'}); + if (this.ownCoursewareInstance !== null) { + this.setOwnId(this.ownCoursewareInstance.relationships.root.data.id); + } else { + console.debug('can not load'); + } + + }, + reset() { + this.selectSource(''); + this.remoteCid = ''; + }, + async setRemoteId(target) { + this.remoteId = target; + await this.loadStructuralElement(this.remoteId); + this.initRemote(); + }, + initRemote() { + this.remoteElement = this.structuralElementById({ id: this.remoteId }); + }, + async setOwnId(target) { + this.ownId = target; + await this.loadStructuralElement(this.ownId); + this.initOwn(); + }, + initOwn() { + this.ownElement = this.structuralElementById({ id: this.ownId }); + }, + loadSelf(data) { + this.$emit('loadSelf', data); + } + }, + async mounted() { + this.courses = await this.loadUsersCourses(this.userId); + } + +} +</script>
\ No newline at end of file diff --git a/resources/vue/components/courseware/CoursewareManagerElement.vue b/resources/vue/components/courseware/CoursewareManagerElement.vue new file mode 100755 index 0000000..3794d70 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareManagerElement.vue @@ -0,0 +1,526 @@ +<template> + <div class="cw-manager-element"> + <div v-if="currentElement"> + <div class="cw-manager-element-title"> + <div class="cw-manager-element-breadcrumb"> + <span + v-for="element in breadcrumb" + :key="element.id" + class="cw-manager-element-breadcrumb-item" + @click="selectChapter(element.id)" + > + {{ element.attributes.title }} + </span> + </div> + <header> + <span v-if="elementInserterActive && moveSelfPossible && canEdit" @click="insertElement({element: currentElement, source: type})"> + <studip-icon shape="arr_2left" size="24" role="sort" /> + </span> + {{ elementTitle }} + </header> + </div> + <courseware-collapsible-box + v-if="canRead" + :open="true" + :title="$gettext('Abschnitt')" + class="cw-manager-element-containers" + > + <div v-if="canSortContainers"> + <button v-show="!sortContainersActive && isCurrent" class="button sort" @click="sortContainers"> + <translate>Abschnitte sortieren</translate> + </button> + <button v-show="sortContainersActive && isCurrent" class="button accept" @click="storeContainersSort"> + <translate>Sortieren beenden</translate> + </button> + <button v-show="sortContainersActive && isCurrent" class="button cancel" @click="resetContainersSort"> + <translate>Sortieren abbrechen</translate> + </button> + </div> + <p v-if="!hasContainers"> + <translate>Dieses Element enthält keine Abschnitte.</translate> + </p> + <transition-group name="cw-sort-ease" tag="div"> + <courseware-manager-container + v-for="(container, index) in sortArrayContainers" + :key="container.id" + :container="container" + :isCurrent="isCurrent" + :sortContainers="sortContainersActive" + :inserter="containerInserterActive && moveSelfChildPossible" + :elementType="type" + :blockInserter="blockInserterActive" + :canMoveUp="index !== 0" + :canMoveDown="index+1 !== sortArrayContainers.length" + @insertContainer="insertContainer" + @insertBlock="insertBlock" + @moveUp="moveUpContainer" + @moveDown="moveDownContainer" + /> + </transition-group> + <courseware-manager-filing + v-if="isCurrent && !sortContainersActive && canEdit" + :parentId="currentElement.id" + :parentItem="currentElement" + itemType="container" + /> + </courseware-collapsible-box> + <courseware-collapsible-box :open="true" :title="$gettext('Seiten')" class="cw-manager-element-subchapters"> + <div v-if="canSortChildren"> + <button v-show="!sortChildrenActive && isCurrent" class="button sort" @click="sortChildren"> + <translate>Seiten sortieren</translate> + </button> + <button v-show="sortChildrenActive && isCurrent" class="button accept" @click="storeChildrenSort"> + <translate>Sortieren beenden</translate> + </button> + <button v-show="sortChildrenActive && isCurrent" class="button cancel" @click="resetChildrenSort"> + <translate>Sortieren abbrechen</translate> + </button> + </div> + <p v-if="!hasChildren"> + <translate>Dieses Element enthält keine Seiten.</translate> + </p> + <transition-group name="cw-sort-ease" tag="div"> + <courseware-manager-element-item + v-for="(child, index) in sortArrayChildren" + :key="child.id" + :element="child" + :sortChapters="sortChildrenActive" + :inserter="elementInserterActive && moveSelfChildPossible && filingData.parentItem.id !== child.id" + :type="type" + :canMoveUp="index !== 0" + :canMoveDown="index+1 !== sortArrayChildren.length" + @selectChapter="selectChapter" + @insertElement="insertElement" + @moveUp="moveUpChild" + @moveDown="moveDownChild" + /> + </transition-group> + <courseware-manager-filing + v-if="isCurrent && !sortChildrenActive && canEdit" + :parentId="currentElement.id" + :parentItem="currentElement" + itemType="element" + /> + </courseware-collapsible-box> + </div> + </div> +</template> + +<script> +import StudipIcon from '../StudipIcon.vue'; +import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue'; +import CoursewareManagerContainer from './CoursewareManagerContainer.vue'; +import CoursewareManagerElementItem from './CoursewareManagerElementItem.vue'; +import CoursewareManagerFiling from './CoursewareManagerFiling.vue'; +import { mapActions, mapGetters } from 'vuex'; +import { forEach } from 'jszip'; + +export default { + name: 'courseware-manager-element', + components: { + CoursewareCollapsibleBox, + CoursewareManagerContainer, + CoursewareManagerElementItem, + CoursewareManagerFiling, + StudipIcon, + }, + props: { + type: { + validator(value) { + return ['current', 'self', 'remote', 'own','import'].includes(value); + }, + }, + remoteCoursewareRangeId: String, + currentElement: Object, + moveSelfPossible: { + default: true + }, + moveSelfChildPossible: { + default: true + } + }, + data() { + return { + elementInserterActive: false, + containerInserterActive: false, + blockInserterActive: false, + sortChildrenActive: false, + sortContainersActive: false, + sortArrayChildren: [], + discardStateArrayChildren: [], + sortArrayContainers: [], + discardStateArrayContainers: [], + }; + }, + computed: { + ...mapGetters({ + structuralElementById: 'courseware-structural-elements/byId', + containerById: 'courseware-containers/byId', + }), + isCurrent() { + return this.type === 'current'; + }, + isSelf() { + return this.type === 'self'; + }, + isRemote() { + return this.type === 'remote'; + }, + isImport() { + return this.type === 'import'; + }, + isOwn() { + return this.type === 'own'; + }, + isSorting() { + return this.sortChildrenActive || this.sortContainersActive || this.sortBlocksActive; + }, + canEdit() { + if (this.currentElement.attributes) { + return this.currentElement.attributes['can-edit']; + } else { + return false; + } + }, + canRead() { + if (this.currentElement.attributes) { + return this.currentElement.attributes['can-read']; + } else { + return false; + } + }, + breadcrumb() { + if(this.currentElement.relationships) { + let view = this; + let ancestors = this.currentElement.relationships.ancestors.data; + let ancestorElements = []; + if(ancestors) { + ancestors.forEach((element) => { + ancestorElements.push(view.structuralElementById({ id: element.id })); + }); + } + return ancestorElements; + } else { + return []; + } + }, + elementTitle() { + if (this.currentElement.attributes) { + return this.currentElement.attributes.title + } else { + return ''; + } + }, + hasChildren() { + if (this.children === null) { + return false; + } else { + return this.children.length >= 1; + } + }, + canSortChildren() { + if (this.children === null) { + return false; + } else { + return this.children.length > 1 && this.canEdit; + } + }, + hasContainers() { + if (this.containers === null) { + return false; + } else { + return this.containers.length >= 1; + } + }, + canSortContainers() { + if (this.containers === null) { + return false; + } else { + return this.containers.length > 1 && this.canEdit; + } + }, + emptyContainers() { + if (this.containers === null) { + return true; + } else { + return this.containers.length === 0; + } + }, + containers() { + if (!this.currentElement) { + return []; + } + + const containers = this.$store.getters['courseware-containers/related']({ + parent: this.currentElement, + relationship: 'containers', + }); + + return containers; + }, + children() { + if (!this.currentElement) { + return []; + } + + if(this.currentElement.relationships) { + let view = this; + let children = this.currentElement.relationships.children.data; + let childElements = []; + children.forEach((element) => { + childElements.push(view.structuralElementById({ id: element.id })); + }); + + return childElements; + } else { + return []; + } + + }, + filingData() { + return this.$store.getters.filingData; + } + }, + methods: { + ...mapActions({ + createStructuralElement: 'createStructuralElement', + updateStructuralElement: 'updateStructuralElement', + deleteStructuralElement: 'deleteStructuralElement', + copyStructuralElement: 'copyStructuralElement', + loadStructuralElement: 'loadStructuralElement', + loadContainer: 'loadContainer', + updateContainer: 'updateContainer', + deleteContainer: 'deleteContainer', + copyContainer: 'copyContainer', + updateBlock: 'updateBlock', + deleteBlock: 'deleteBlock', + copyBlock: 'copyBlock', + lockObject: 'lockObject', + unlockObject: 'unlockObject', + sortContainersInStructualElements: 'sortContainersInStructualElements', + sortChildrenInStructualElements: 'sortChildrenInStructualElements' + }), + + selectChapter(target) { + this.$emit('selectElement', target); + }, + async insertElement(data) { + let source = data.source; + let element = data.element; + if (source === 'self') { + element.relationships.parent.data.id = this.filingData.parentItem.id; + element.attributes.position = this.filingData.parentItem.relationships.children.data.length; + await this.lockObject({ id: element.id, type: 'courseware-structural-elements' }); + await this.updateStructuralElement({ + element: element, + id: element.id, + }); + await this.unlockObject({ id: element.id, type: 'courseware-structural-elements' }); + this.loadStructuralElement(this.currentElement.id); + this.$store.dispatch('cwManagerFilingData', {}); + } else if(source === 'remote' || source === 'own') { + //create Element + let parentId = this.filingData.parentItem.id; + await this.copyStructuralElement({ + parentId: parentId, + element: element, + }); + this.$emit('loadSelf', parentId); + this.$store.dispatch('cwManagerFilingData', {}); + } else { + console.log('unreliable source:'); + console.log(source); + console.log(element); + } + + }, + async insertContainer(data) { + let source = data.source; + let container = data.container; + if (source === 'self') { + container.relationships['structural-element'].data.id = this.filingData.parentItem.id; + container.attributes.position = this.filingData.parentItem.relationships.containers.data.length; + await this.lockObject({id: container.id, type: 'courseware-containers'}); + await this.updateContainer({ + container: container, + structuralElementId: this.currentElement.id + }); + await this.unlockObject({id: container.id, type: 'courseware-containers'}); + this.$store.dispatch('cwManagerFilingData', {}); + } else if (source === 'remote' || source === 'own') { + let parentId = this.filingData.parentItem.id; + await this.copyContainer({ + parentId: parentId, + container: container, + }); + this.$emit('loadSelf', parentId); + this.$store.dispatch('cwManagerFilingData', {}); + } else { + console.log('unreliable source:'); + console.log(source); + console.log(container); + } + + }, + async insertBlock(data) { + let source = data.source; + let block = data.block; + if (source === 'self') { + let sourceContainer = await this.containerById({id: block.relationships.container.data.id}); + sourceContainer.attributes.payload.sections.forEach(section => { + let index = section.blocks.indexOf(block.id); + if(index !== -1) { + section.blocks.splice(index, 1); + } + }); + await this.lockObject({id: sourceContainer.id, type: 'courseware-containers'}); + await this.updateContainer({ + container: sourceContainer, + structuralElementId: sourceContainer.relationships['structural-element'].data.id + }); + await this.unlockObject({id: sourceContainer.id, type: 'courseware-containers'}); + + let destinationContainer = await this.containerById({id: this.filingData.parentItem.id}); + destinationContainer.attributes.payload.sections[destinationContainer.attributes.payload.sections.length-1].blocks.push(block.id); + await this.lockObject({id: destinationContainer.id, type: 'courseware-containers'}); + await this.updateContainer({ + container: destinationContainer, + structuralElementId: destinationContainer.relationships['structural-element'].data.id + }); + await this.unlockObject({id: destinationContainer.id, type: 'courseware-containers'}); + + block.relationships.container.data.id = this.filingData.parentItem.id; + block.attributes.position = this.filingData.parentItem.relationships.blocks.data.length; + await this.lockObject({id: block.id, type: 'courseware-blocks'}); + await this.updateBlock({ + block: block, + containerId: this.filingData.parentItem.id + }); + await this.unlockObject({id: block.id, type: 'courseware-blocks'}); + await this.loadContainer(sourceContainer.id); + await this.loadContainer(destinationContainer.id); + this.$emit('reloadElement'); + this.$store.dispatch('cwManagerFilingData', {}); + } else if (source === 'remote' || source === 'own') { + let parentId = this.filingData.parentItem.id; + await this.copyBlock({ + parentId: parentId, + block: block, + }); + await this.loadContainer(parentId); + this.$emit('loadSelf',this.filingData.parentItem.relationships['structural-element'].data.id); + this.$store.dispatch('cwManagerFilingData', {}); + } else { + console.debug('unreliable source:', source, block); + } + }, + + sortChildren() { + this.discardStateArrayChildren = [...this.children]; //copy array because of watcher? + this.sortChildrenActive = true; + }, + sortContainers() { + this.discardStateArrayContainers = [...this.containers]; + this.sortContainersActive = true; + }, + + storeChildrenSort() { + this.sortChildrenInStructualElements({parent: this.currentElement, children: this.sortArrayChildren}); + + this.discardStateArrayChildren = []; + this.sortChildrenActive = false; + }, + resetChildrenSort() { + this.sortArrayChildren = this.discardStateArrayChildren; + this.sortChildrenActive = false; + }, + + storeContainersSort() { + this.sortContainersInStructualElements({structuralElement: this.currentElement, containers: this.sortArrayContainers}); + + this.discardStateArrayContainers = []; + this.sortContainersActive = false; + }, + resetContainersSort() { + this.sortArrayContainers = this.discardStateArrayContainers; + this.sortContainersActive = false; + }, + + moveUpChild(childId) { + this.moveUp(childId, this.sortArrayChildren); + }, + moveDownChild(childId) { + this.moveDown(childId, this.sortArrayChildren); + }, + moveUpContainer(containerId) { + this.moveUp(containerId, this.sortArrayContainers); + }, + moveDownContainer(containerId) { + this.moveDown(containerId, this.sortArrayContainers); + }, + + moveUp(itemId, sortArray) { + sortArray.every((item, index) => { + if (item.id === itemId) { + if (index === 0) { + return false; + } + sortArray.splice(index - 1, 0, sortArray.splice(index, 1)[0]); + return false; + } else { + return true; + } + }); + }, + moveDown(itemId, sortArray) { + sortArray.every((item, index) => { + if (item.id === itemId) { + if (index === sortArray.length - 1) { + return false; + } + sortArray.splice(index + 1, 0, sortArray.splice(index, 1)[0]); + return false; + } else { + return true; + } + }); + }, + updateFilingData(data) { + if (Object.keys(data).length !== 0) { + switch (data.itemType) { + case 'element': + this.elementInserterActive = true; + break; + case 'container': + this.containerInserterActive = true; + break; + case 'block': + this.blockInserterActive = true; + break; + } + } else { + this.elementInserterActive = false; + this.containerInserterActive = false; + this.blockInserterActive = false; + } + } + }, + mounted() { + this.updateFilingData(this.filingData); + }, + watch: { + filingData(newValue) { + if (!['self', 'remote', 'own', 'import'].includes(this.type)) { + return false; + } + this.updateFilingData(newValue); + }, + containers(newContainers) { + this.sortArrayContainers = newContainers; + }, + children(newChildren) { + this.sortArrayChildren = newChildren; + } + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareManagerElementItem.vue b/resources/vue/components/courseware/CoursewareManagerElementItem.vue new file mode 100755 index 0000000..0162ca2 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareManagerElementItem.vue @@ -0,0 +1,52 @@ +<template> + <div + class="cw-manager-element-item" + :class="{ 'cw-manager-element-item-sorting': sortChapters }" + @click="clickItem" + > + <span v-if="inserter" @click="clickItem"> + <studip-icon shape="arr_2left" size="16" role="sort" /> + </span> + {{ element.attributes.title }} + <div v-if="sortChapters" class="cw-manager-element-item-buttons"> + <studip-icon :class="{'cw-manager-icon-disabled' : !canMoveUp}" shape="arr_2up" size="16" role="sort" @click="moveUp" /> + <studip-icon :class="{'cw-manager-icon-disabled' : !canMoveDown}" shape="arr_2down" size="16" role="sort" @click="moveDown" /> + </div> + </div> +</template> + +<script> +export default { + name: 'courseware-manager-element-item', + props: { + element: Object, + inserter: Boolean, + sortChapters: Boolean, + type: String, + canMoveUp: Boolean, + canMoveDown: Boolean + }, + methods: { + clickItem() { + if (this.sortChapters) { + return false; + } + if (this.inserter) { + this.$emit('insertElement', {element: this.element, source: this.type}); + } else { + this.$emit('selectChapter', this.element.id); + } + }, + moveUp() { + if (this.sortChapters) { + this.$emit('moveUp', this.element.id); + } + }, + moveDown() { + if (this.sortChapters) { + this.$emit('moveDown', this.element.id); + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareManagerFiling.vue b/resources/vue/components/courseware/CoursewareManagerFiling.vue new file mode 100755 index 0000000..ba388ee --- /dev/null +++ b/resources/vue/components/courseware/CoursewareManagerFiling.vue @@ -0,0 +1,66 @@ +<template> + <div + class="cw-manager-filing" + :class="{ 'cw-manager-filing-active': active, 'cw-manager-filing-disabled': disabled }" + @click="toggleFiling" + > + <span v-if="itemType === 'element'"><translate>Seite</translate> </span> + <span v-if="itemType === 'container'"><translate>Abschnitt</translate> </span> + <span v-if="itemType === 'block'"><translate>Block</translate> </span> + <translate>an dieser Stelle einfügen</translate> + </div> +</template> + +<script> +export default { + name: 'courseware-manager-filing', + props: { + parentId: String, + parentItem: Object, + itemType: String, // element || container || block + }, + data() { + return { + active: false, + disabled: false, + data: {}, + }; + }, + computed: { + filingData() { + return this.$store.getters.filingData; + }, + }, + methods: { + toggleFiling() { + if (this.disabled) { + return false; + } + if (this.active) { + this.$store.dispatch('cwManagerFilingData', {}); + } else { + this.$store.dispatch('cwManagerFilingData', { parentId: this.parentId, itemType: this.itemType, parentItem: this.parentItem }); + } + }, + }, + watch: { + filingData(newValue, oldValue) { + if (Object.keys(newValue).length !== 0) { + if (newValue.parentId === this.parentId && newValue.itemType === this.itemType) { + this.active = true; + } else { + this.disabled = true; + } + } else { + this.active = false; + this.disabled = false; + if (Object.keys(oldValue).length !== 0) { + if (oldValue.parentId === this.parentId && oldValue.itemType === this.itemType) { + this.$emit('deactivated'); + } + } + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareOblong.vue b/resources/vue/components/courseware/CoursewareOblong.vue new file mode 100755 index 0000000..e3a9051 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareOblong.vue @@ -0,0 +1,34 @@ +<template> + <div class="cw-oblong" :class="['cw-oblong-' + oblongSize]"> + <div class="cw-oblong-value"> + <slot name="oblongValue"></slot> + </div> + <div class="cw-oblong-description"> + <studip-icon v-if="icon" :shape="icon" :size="24"></studip-icon>{{ name }} + </div> + </div> +</template> + +<script> +export default { + name: 'courseware-oblong', + props: { + icon: String, + name: String, + color: String, + size: String, + }, + computed: { + oblongSize() { + switch (this.size) { + case 'large': + return 'large'; + case 'small': + return 'small'; + default: + return 'small'; + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareProgressCircle.vue b/resources/vue/components/courseware/CoursewareProgressCircle.vue new file mode 100755 index 0000000..729d707 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareProgressCircle.vue @@ -0,0 +1,18 @@ +<template> + <div class="cw-progress-circle" :class="['p' + value, value > 50 ? 'over50' : '']"> + <span>{{ value }}%</span> + <div class="left-half-clipper"> + <div class="first50-bar"></div> + <div class="value-bar"></div> + </div> + </div> +</template> + +<script> +export default { + name: 'courseware-progress-circle', + props: { + value: Number, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareRibbon.vue b/resources/vue/components/courseware/CoursewareRibbon.vue new file mode 100755 index 0000000..d7ec72c --- /dev/null +++ b/resources/vue/components/courseware/CoursewareRibbon.vue @@ -0,0 +1,118 @@ +<template> + <div :class="{ 'cw-ribbon-wrapper-consume': consumeMode }"> + <div v-if="stickyRibbon" class="cw-ribbon-sticky-top"></div> + <header class="cw-ribbon" :class="{ 'cw-ribbon-sticky': stickyRibbon, 'cw-ribbon-consume': consumeMode }"> + <div class="cw-ribbon-wrapper-left"> + <nav class="cw-ribbon-nav"> + <slot name="buttons" /> + </nav> + <nav class="cw-ribbon-breadcrumb"> + <ul> + <slot v-if="breadcrumbFallback" name="breadcrumbFallback" /> + <slot v-else name="breadcrumbList" /> + </ul> + </nav> + </div> + <div class="cw-ribbon-wrapper-right"> + <button class="cw-ribbon-button cw-ribbon-button-menu" @click="activeToolbar" :title="textRibbon.toolbar"></button> + <button + class="cw-ribbon-button" + :class="[consumeMode ? 'cw-ribbon-button-zoom-out' : 'cw-ribbon-button-zoom']" + :title="consumeMode ? textRibbon.fullscreen_off : textRibbon.fullscreen_on" + @click="toggleConsumeMode" + ></button> + <slot name="menu" /> + </div> + <div v-if="consumeMode" class="cw-ribbon-consume-bottom"></div> + <courseware-ribbon-toolbar + v-show="showTools" + :toolsActive="unfold" + :class="{ 'cw-ribbon-tools-sticky': stickyRibbon }" + :canEdit="canEdit" + @deactivate="deactivateToolbar" + /> + </header> + <div v-if="stickyRibbon" class="cw-ribbon-sticky-bottom"></div> + <div v-if="stickyRibbon" class="cw-ribbon-sticky-spacer"></div> + </div> +</template> + +<script> +import CoursewareRibbonToolbar from './CoursewareRibbonToolbar.vue'; + +export default { + name: 'courseware-ribbon', + components: { + CoursewareRibbonToolbar, + }, + props: { + canEdit: Boolean, + }, + data() { + return { + readModeActive: false, + stickyRibbon: false, + textRibbon: { + toolbar: this.$gettext('Inhaltsverzeichnis'), + fullscreen_on: this.$gettext('Vollbild einschalten'), + fullscreen_off: this.$gettext('Vollbild ausschalten'), + }, + unfold: false, + showTools: false, + }; + }, + computed: { + consumeMode() { + return this.$store.getters.consumeMode; + }, + toolsActive() { + return this.$store.getters.showToolbar; + }, + breadcrumbFallback() { + return window.outerWidth < 1200; + }, + }, + methods: { + toggleConsumeMode() { + if (!this.consumeMode) { + this.$store.dispatch('coursewareConsumeMode', true); + this.$store.dispatch('coursewareViewMode', 'read'); + } else { + this.$store.dispatch('coursewareConsumeMode', false); + } + }, + activeToolbar() { + this.$store.dispatch('coursewareShowToolbar', true); + }, + deactivateToolbar() { + this.$store.dispatch('coursewareShowToolbar', false); + }, + handleScroll() { + if (window.outerWidth > 767) { + this.stickyRibbon = window.scrollY > 130; + } else { + this.stickyRibbon = window.scrollY > 75; + } + }, + }, + mounted() { + window.addEventListener('scroll', this.handleScroll); + }, + watch: { + toolsActive(newState, oldState) { + let view = this; + if(newState) { + this.showTools = true; + setTimeout(() => {view.unfold = true}, 10); + } else { + this.unfold = false; + setTimeout(() => { + if(!view.activeToolbar) { + view.showTools = false; + } + }, 800); + } + } + } +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareRibbonToolbar.vue b/resources/vue/components/courseware/CoursewareRibbonToolbar.vue new file mode 100755 index 0000000..d6c6fc1 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareRibbonToolbar.vue @@ -0,0 +1,140 @@ +<template> + <div + class="cw-ribbon-tools" + :class="{ unfold: toolsActive, 'cw-ribbon-tools-consume': consumeMode }" + > + <div class="cw-ribbon-tool-content" ref="ribbonContent"> + <div class="cw-ribbon-tool-content-nav"> + <ul> + <li + tabindex="0" + ref="focusPoint" + :class="{ active: showContents }" + @click="displayTool('contents')" + > + <translate>Inhaltsverzeichnis</translate> + </li> + <li + v-if="!consumeMode && showEditMode && canEdit" + tabindex="0" + :class="{ active: showBlockAdder }" + @click="displayTool('blockadder')" + > + <translate>Elemente hinzufügen</translate> + </li> + <li + v-if="!consumeMode && displaySettings" + tabindex="0" + :class="{ active: showAdmin }" + @click="displayTool('admin')" + > + <translate>Einstellungen</translate> + </li> + </ul> + <button :title="textClose" class="cw-tools-hide-button" @click="$emit('deactivate')"></button> + </div> + <div class="cw-ribbon-tool"> + <courseware-tools-contents v-if="showContents" /> + <courseware-tools-blockadder v-if="showBlockAdder" @scrollTop="scrollTop"/> + <courseware-tools-admin v-if="showAdmin" /> + </div> + </div> + </div> +</template> +<script> +import CoursewareToolsAdmin from './CoursewareToolsAdmin.vue'; +import CoursewareToolsBlockadder from './CoursewareToolsBlockadder.vue'; +import CoursewareToolsContents from './CoursewareToolsContents.vue'; +import { mapGetters } from 'vuex'; + +export default { + name: 'courseware-ribbon-toolbar', + components: { + CoursewareToolsAdmin, + CoursewareToolsBlockadder, + CoursewareToolsContents, + }, + props: { + toolsActive: Boolean, + canEdit: Boolean, + }, + data() { + return { + showContents: true, + showAdmin: false, + showBlockAdder: false, + textClose: this.$gettext('schließen') + }; + }, + computed: { + ...mapGetters({ + userIsTeacher: 'userIsTeacher', + consumeMode: 'consumeMode', + containerAdder: 'containerAdder', + adderStorage: 'blockAdder', + viewMode: 'viewMode', + context: 'context' + }), + showEditMode() { + return this.viewMode === 'edit'; + }, + displaySettings() { + return this.context.type === 'courses' && this.isTeacher; + }, + isTeacher() { + return this.userIsTeacher; + }, + }, + methods: { + displayTool(tool) { + this.showContents = false; + this.showAdmin = false; + this.showBlockAdder = false; + + switch (tool) { + case 'contents': + this.showContents = true; + this.disableContainerAdder(); + break; + case 'admin': + this.showAdmin = true; + this.disableContainerAdder(); + break; + case 'blockadder': + this.showBlockAdder = true; + break; + } + }, + disableContainerAdder() { + if (this.containerAdder !== false) { + this.$store.dispatch('coursewareContainerAdder', false); + } + }, + scrollTop() { + this.$refs.ribbonContent.scrollTop = 0; + } + }, + watch: { + adderStorage(newValue) { + if (Object.keys(newValue).length !== 0) { + this.displayTool('blockadder'); + } + }, + consumeMode(newValue) { + if (newValue) { + this.displayTool('contents'); + } + }, + containerAdder(newValue) { + if (newValue === true) { + this.displayTool('blockadder'); + } + }, + showEditMode(newValue) { + if (!newValue) { + this.displayTool('contents'); + } + } + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue new file mode 100755 index 0000000..92eec3a --- /dev/null +++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue @@ -0,0 +1,970 @@ +<template> + <div> + <div :class="{ 'cw-structural-element-consumemode': consumeMode }" class="cw-structural-element" v-if="validContext"> + <div class="cw-structural-element-content" v-if="structuralElement"> + <courseware-ribbon :canEdit="canEdit"> + <template #buttons> + <router-link v-if="prevElement" :to="'/structural_element/' + prevElement.id"> + <button class="cw-ribbon-button cw-ribbon-button-prev" :title="textRibbon.perv" /> + </router-link> + <button v-else class="cw-ribbon-button cw-ribbon-button-prev-disabled" /> + <router-link v-if="nextElement" :to="'/structural_element/' + nextElement.id"> + <button class="cw-ribbon-button cw-ribbon-button-next" :title="textRibbon.next" /> + </router-link> + <button v-else class="cw-ribbon-button cw-ribbon-button-next-disabled" /> + </template> + <template #breadcrumbList> + <li + v-for="ancestor in ancestors" + :key="ancestor.id" + :title="ancestor.attributes.title" + class="cw-ribbon-breadcrumb-item" + > + <span> + <router-link :to="'/structural_element/' + ancestor.id"> + {{ ancestor.attributes.title }} + </router-link> + </span> + </li><li class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current" :title="structuralElement.attributes.title"> + <span>{{ structuralElement.attributes.title }}</span> + </li> + </template> + <template #breadcrumbFallback> + <li class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current" :title="structuralElement.attributes.title"> + <span>{{ structuralElement.attributes.title }}</span> + </li> + </template> + <template #menu> + <studip-action-menu + v-if="!consumeMode" + :items="menuItems" + class="cw-ribbon-action-menu" + @editCurrentElement="menuAction('editCurrentElement')" + @addElement="menuAction('addElement')" + @deleteCurrentElement="menuAction('deleteCurrentElement')" + @showInfo="menuAction('showInfo')" + @showExportOptions="menuAction('showExportOptions')" + @oerCurrentElement="menuAction('oerCurrentElement')" + @setBookmark="menuAction('setBookmark')" + /> + </template> + </courseware-ribbon> + + <div v-if="canRead" class="cw-container-wrapper" :class="{ 'cw-container-wrapper-consume': consumeMode }"> + <div v-if="structuralElementLoaded" class="cw-companion-box-wrapper"> + <courseware-empty-element-box + v-if="(empty && !isRoot && canEdit) || (empty && !canEdit) || (!noContainers && empty && isRoot && canEdit)" + :canEdit="canEdit" + :noContainers="noContainers" + /> + <courseware-wellcome-screen v-if="noContainers && isRoot && canEdit"/> + </div> + <component + v-for="container in containers" + :key="container.id" + :is="containerComponent(container)" + :container="container" + :canEdit="canEdit" + :isTeacher="isTeacher" + class="cw-container-item" + /> + </div> + <div v-else class="cw-container-wrapper" :class="{ 'cw-container-wrapper-consume': consumeMode }"> + <div v-if="structuralElementLoaded" class="cw-companion-box-wrapper"> + <courseware-companion-box mood="sad" :msgCompanion="$gettext('Diese Seite steht Ihnen leider nicht zur Verfügung')" /> + </div> + </div> + </div> + + <courseware-companion-overlay /> + + <studip-dialog + v-if="showEditDialog" + :title="textEdit.title" + :confirmText="textEdit.confirm" + :confirmClass="'accept'" + :closeText="textEdit.close" + :closeClass="'cancel'" + height="500" + width="500" + class="studip-dialog-with-tab" + @close="closeEditDialog" + @confirm="storeCurrentElement" + > + <template v-slot:dialogContent> + <courseware-tabs class="cw-tab-in-dialog"> + <courseware-tab :name="textEdit.basic" :selected="true"> + <form class="default" @submit.prevent=""> + <label> + <translate>Titel</translate> + <input type="text" v-model="currentElement.attributes.title" /> + </label> + <label> + <translate>Beschreibung</translate> + <textarea + v-model="currentElement.attributes.payload.description" + class="cw-structural-element-description" + /> + </label> + </form> + </courseware-tab> + <courseware-tab :name="textEdit.meta"> + <form class="default" @submit.prevent=""> + <label> + <translate>Farbe</translate> + <v-select + v-model="currentElement.attributes.payload.color" + :options="colors" + :reduce="color => color.class" + label="class" + class="cw-vs-select" + > + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span> + </template> + <template #no-options="{ search, searching, loading }"> + <translate>Es steht keine Auswahl zur Verfügung</translate>. + </template> + <template #selected-option="{name, hex}"> + <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span> + </template> + <template #option="{name, hex}"> + <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span> + </template> + </v-select> + </label> + <label> + <translate>Zweck</translate> + <select v-model="currentElement.attributes.purpose"> + <option value="content"><translate>Inhalt</translate></option> + <option value="template"><translate>Vorlage</translate></option> + <option value="oer"><translate>OER-Material</translate></option> + <option value="portfolio"><translate>ePortfolio</translate></option> + <option value="draft"><translate>Entwurf</translate></option> + <option value="other"><translate>Sonstiges</translate></option> + </select> + </label> + <label> + <translate>Lizenztyp</translate> + <select v-model="currentElement.attributes.payload.license_type"> + <option v-for="license in licenses" :key="license.id" :value="license.id">{{license.name}}</option> + </select> + </label> + <label> + <translate>Geschätzter zeitlicher Aufwand</translate> + <input type="text" v-model="currentElement.attributes.payload.required_time" /> + </label> + <label> + <translate>Niveau</translate><br> + <translate>von</translate> + <select v-model="currentElement.attributes.payload.difficulty_start"> + <option v-for="difficulty_start in 12" :key="difficulty_start" :value="difficulty_start">{{difficulty_start}}</option> + </select> + <translate>bis</translate> + <select v-model="currentElement.attributes.payload.difficulty_end"> + <option v-for="difficulty_end in 12" :key="difficulty_end" :value="difficulty_end">{{difficulty_end}}</option> + </select> + </label> + </form> + </courseware-tab> + <courseware-tab :name="textEdit.image"> + <form class="default" @submit.prevent=""> + <img + v-if="image" + :src="image" + class="cw-structural-element-image-preview" + :alt="$gettext('Vorschaubild')" + /> + <label v-if="image"> + <button class="button" @click="deleteImage" v-translate>Bild löschen</button> + </label> + <div v-if="uploadFileError" class="messagebox messagebox_error"> + {{ uploadFileError }} + </div> + <label v-if="!image"> + <translate>Bild hochladen</translate> + <input ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" /> + </label> + </form> + </courseware-tab> + <courseware-tab :name="textEdit.approval"> + <courseware-structural-element-permissions + v-if="inCourse" + :element="currentElement" + @updateReadApproval="updateReadApproval" + @updateWriteApproval="updateWriteApproval" + /> + <!-- <h1> + <translate>Lehrende in Stud.IP</translate> + </h1> + <label> + <input + type="checkbox" + class="default" + value="copy_approval" + v-model="currentElement.attributes['copy-approval']" + /> + <translate>Seite zum kopieren für Lehrende freigeben</translate> + </label> --> + </courseware-tab> + <courseware-tab v-if="inCourse" :name="textEdit.visible"> + <form class="default" @submit.prevent=""> + <label> + <translate>Sichtbar ab</translate> + <input type="date" v-model="currentElement.attributes['release-date']" /> + </label> + <label> + <translate>Unsichtbar ab</translate> + <input type="date" v-model="currentElement.attributes['withdraw-date']" /> + </label> + </form> + </courseware-tab> + </courseware-tabs> + </template> + </studip-dialog> + + <studip-dialog + v-if="showAddDialog" + :title="$gettext('Seite hinzufügen')" + :confirmText="'Erstellen'" + :confirmClass="'accept'" + :closeText="$gettext('Schließen')" + :closeClass="'cancel'" + class="cw-structural-element-dialog" + @close="closeAddDialog" + @confirm="createElement" + > + <template v-slot:dialogContent> + <form class="default" @submit.prevent=""> + <label> + <translate>Position der neuen Seite</translate> + <select v-model="newChapterParent"> + <option v-if="!isRoot" value="sibling"> + <translate>Neben der aktuellen Seite</translate> + </option> + <option value="descendant"><translate>Unterhalb der aktuellen Seite</translate></option> + </select> + </label> + <label> + <translate>Name der neuen Seite</translate><br /> + <input v-model="newChapterName" type="text" /> + </label> + </form> + </template> + </studip-dialog> + + <studip-dialog + v-if="showInfoDialog" + :title="textInfo.title" + :closeText="textInfo.close" + :closeClass="'cancel'" + @close="showElementInfoDialog(false)" + > + <template v-slot:dialogContent> + <table class="cw-structural-element-info"> + <tr> + <td><translate>Titel</translate>:</td> + <td>{{ structuralElement.attributes.title }}</td> + </tr> + <tr> + <td><translate>Beschreibung</translate>:</td> + <td>{{ structuralElement.attributes.payload.description }}</td> + </tr> + <tr> + <td><translate>Seite wurde erstellt von</translate>:</td> + <td>{{ owner }}</td> + </tr> + <tr> + <td><translate>Seite wurde erstellt am</translate>:</td> + <td><iso-date :date="structuralElement.attributes.mkdate" /></td> + </tr> + <tr> + <td><translate>Zuletzt bearbeitet von</translate>:</td> + <td>{{ editor }}</td> + </tr> + <tr> + <td><translate>Zuletzt bearbeitet am</translate>:</td> + <td><iso-date :date="structuralElement.attributes.chdate" /></td> + </tr> + </table> + </template> + </studip-dialog> + + <studip-dialog + v-if="showExportDialog" + :title="textExport.title" + :confirmText="textExport.confirm" + :confirmClass="'accept'" + :closeText="textExport.close" + :closeClass="'cancel'" + @close="showElementExportDialog(false)" + @confirm="exportCurrentElement" + > + <template v-slot:dialogContent> + <translate>Hiermit exportieren Sie die Seite "{{ currentElement.attributes.title }}" als ZIP-Datei.</translate> + + <div class="cw-element-export"> + <label> + <input type="checkbox" v-model="exportChildren"> + <translate>Unterseiten exportieren</translate> + </label> + </div> + + <translate v-if="exportRunning"> + Export läuft... + </translate> + </template> + + </studip-dialog> + + <studip-dialog + v-if="showOerDialog" + height="600" + width="600" + :title="textOer.title" + :confirmText="textOer.confirm" + :confirmClass="'accept'" + :closeText="textOer.close" + :closeClass="'cancel'" + @close="showElementOerDialog(false)" + @confirm="publishCurrentElement" + > + + <template v-slot:dialogContent> + <form class="default" @submit.prevent=""> + <fieldset> + <legend><translate>Grunddaten</translate></legend> + <label> + <p><translate>Vorschaubild</translate>:</p> + <img + v-if="currentElement.relationships.image.data" + :src="currentElement.relationships.image.meta['download-url']" + width="400" + /> + </label> + <label> + <p><translate>Beschreibung</translate>:</p> + <p> {{ currentElement.attributes.payload.description }}</p> + </label> + <label> + <translate>Niveau</translate>: + <p> {{ currentElement.attributes.payload.difficulty_start }} - {{ currentElement.attributes.payload.difficulty_end }}</p> + </label> + <label> + <translate>Lizenztyp</translate>: + <p>{{currentLicenseName}}</p> + </label> + <label> + <translate>Sie können diese Daten unter "Seite bearbeiten" verändern</translate>. + </label> + + </fieldset> + <fieldset> + <legend><translate>Einstellungen</translate></legend> + <label> + <translate>Unterseiten veröffentlichen</translate> + <input type="checkbox" v-model="oerChildren"> + </label> + </fieldset> + </form> + </template> + + </studip-dialog> + + <studip-dialog + v-if="showDeleteDialog" + :title="textDelete.title" + :question="textDelete.alert" + height="180" + @confirm="deleteCurrentElement" + @close="closeDeleteDialog" + ></studip-dialog> + </div> + <div v-else> + <courseware-companion-box v-if="currentElement !== ''" :msgCompanion="textCompanionWrongContext" mood="sad"/> + </div> + + </div> +</template> + +<script> +import ContainerComponents from './container-components.js'; +import CoursewareStructuralElementPermissions from './CoursewareStructuralElementPermissions.vue'; +import CoursewareAccordionContainer from './CoursewareAccordionContainer.vue'; +import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; +import CoursewareWellcomeScreen from './CoursewareWellcomeScreen.vue'; +import CoursewareEmptyElementBox from './CoursewareEmptyElementBox.vue'; +import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue'; +import CoursewareListContainer from './CoursewareListContainer.vue'; +import CoursewareTabsContainer from './CoursewareTabsContainer.vue'; +import CoursewareRibbon from './CoursewareRibbon.vue'; +import CoursewareTabs from './CoursewareTabs.vue'; +import CoursewareTab from './CoursewareTab.vue'; +import CoursewareExport from '@/vue/mixins/courseware/export.js'; +import IsoDate from './IsoDate.vue'; +import StudipDialog from '../StudipDialog.vue'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-structural-element', + components: { + CoursewareStructuralElementPermissions, + CoursewareRibbon, + CoursewareListContainer, + CoursewareAccordionContainer, + CoursewareTabsContainer, + CoursewareCompanionBox, + CoursewareCompanionOverlay, + CoursewareWellcomeScreen, + CoursewareEmptyElementBox, + CoursewareTabs, + CoursewareTab, + IsoDate, + StudipDialog, + }, + props: {}, + + mixins: [CoursewareExport], + + data() { + return { + currentId: null, + newChapterName: '', + newChapterParent: 'descendant', + currentElement: '', + uploadFileError: '', + textCompanionWrongContext: this.$gettext('Die angeforderte Seite ist nicht Teil dieser Courseware.'), + textEdit: { + title: this.$gettext('Seite bearbeiten'), + confirm: this.$gettext('Speichern'), + close: this.$gettext('Schließen'), + basic: this.$gettext('Grunddaten'), + image: this.$gettext('Bild'), + meta: this.$gettext('Metadaten'), + approval: this.$gettext('Rechte'), + visible: this.$gettext('Sichtbarkeit'), + }, + textInfo: { + title: this.$gettext('Informationen zur Seite'), + close: this.$gettext('Schließen'), + }, + textExport: { + title: this.$gettext('Seite exportieren'), + confirm: this.$gettext('Exportieren'), + close: this.$gettext('Schließen'), + }, + textAdd: { + title: this.$gettext('Seite hinzufügen'), + confirm: this.$gettext('Erstellen'), + close: this.$gettext('Schließen'), + }, + textRibbon: { + perv: this.$gettext('zurück'), + next: this.$gettext('weiter'), + }, + exportRunning: false, + exportChildren: false, + oerChildren: true, + }; + }, + + computed: { + ...mapGetters({ + courseware: 'courseware', + consumeMode: 'consumeMode', + containerById: 'courseware-containers/byId', + structuralElementById: 'courseware-structural-elements/byId', + userIsTeacher: 'userIsTeacher', + pluginManager: 'pluginManager', + showEditDialog: 'showStructuralElementEditDialog', + showAddDialog: 'showStructuralElementAddDialog', + showExportDialog: 'showStructuralElementExportDialog', + showInfoDialog: 'showStructuralElementInfoDialog', + showDeleteDialog : 'showStructuralElementDeleteDialog', + showOerDialog : 'showStructuralElementOerDialog', + oerTitle: 'oerTitle', + licenses: 'licenses' + }), + + textOer() { + return { + title: this.$gettext('Seite auf') + ' ' + this.oerTitle + ' ' + this.$gettext('veröffentlichen'), + confirm: this.$gettext('Veröffentlichen'), + close: this.$gettext('Schließen'), + } + }, + + inCourse() { + return this.$store.getters.context.type === 'courses'; + }, + + textDelete() { + let textDelete = {}; + textDelete.title = this.$gettext('Seite unwiderruflich löschen'); + textDelete.alert = this.$gettext('Möchten Sie die Seite wirklich löschen?'); + if (this.structuralElementLoaded) { + textDelete.alert = this.$gettext('Möchten Sie die Seite') +' "'+ this.structuralElement.attributes.title + '" '+ this.$gettext('wirklich löschen?'); + } + + return textDelete; + }, + + validContext() { + let valid = false; + let context = this.$store.getters.context; + if (context.type === 'courses' && this.currentElement.relationships) { + if (this.currentElement.relationships.course && context.id === this.currentElement.relationships.course.data.id) { + valid = true; + } + } + + if (context.type === 'users' && this.currentElement.relationships) { + if (this.currentElement.relationships.user && context.id === this.currentElement.relationships.user.data.id) { + valid = true; + } + } + + return valid; + }, + + image() { + return this.structuralElement.relationships?.image?.meta?.['download-url'] ?? null; + }, + + structuralElement() { + if (!this.currentId) { + return null; + } + + return this.structuralElementById({ id: this.currentId }); + }, + + structuralElementLoaded() { + return this.structuralElement !== null && this.structuralElement !== {}; + }, + + ancestors() { + if (!this.currentElement) { + return []; + } + if (this.currentElement.relationships.ancestors.data) { + return this.currentElement.relationships.ancestors.data.map(({ id }) => + this.structuralElementById({ id }) + ); + } + return []; + }, + parent() { + if (!this.structuralElement) { + return []; + } + if (this.structuralElement.relationships.parent.data) { + let id = this.structuralElement.relationships.parent.data.id; + return this.structuralElementById({ id }); + } + return []; + }, + hasSiblings() { + if (this.parent.length !== 0) { + return this.parent.relationships.children.data.length > 1; + } else { + return false; + } + }, + prevElement() { + if (this.hasSiblings) { + let view = this; + let siblings = this.parent.relationships.children.data; + let id = ''; + siblings.forEach((el, index) => { + if (el.id === view.currentId && index !== 0) { + id = siblings[index - 1].id; + } + }); + if (id === '') { + return this.parent; + } else { + return this.structuralElementById({ id }); + } + } else if (this.parent.length !== 0) { + return this.parent; + } else { + return null; + } + }, + nextElement() { + let view = this; + if (this.structuralElement.relationships.children.data.length > 0) { + let id = this.structuralElement.relationships.children.data[0].id; + return this.structuralElementById({ id }); + } else if (this.hasSiblings) { + let siblings = this.parent.relationships.children.data; + let id = ''; + siblings.forEach((el, index) => { + if (el.id === view.currentId && siblings.length > index + 1) { + id = siblings[index + 1].id; + } + }); + if (id === '') { + return this.getNextParentSibling(this.currentId); + } else { + return this.structuralElementById({ id }); + } + } else { + return this.getNextParentSibling(this.currentId); + } + }, + empty() { + if (this.containers === null) { + return true; + } else { + let noBlockFound = true; + this.containers.forEach((container) => { + if (container.relationships.blocks.data.length > 0) { + noBlockFound = false; + } + }); + return noBlockFound; + } + }, + containers() { + if (!this.structuralElement) { + return []; + } + + const containers = this.$store.getters['courseware-containers/related']({ + parent: this.structuralElement, + relationship: 'containers', + }); + + return containers; + }, + noContainers() { + if (this.containers === null) { + return true; + } else { + return this.containers.length === 0; + } + }, + + canEdit() { + if (!this.structuralElement) { + return false; + } + return this.structuralElement.attributes['can-edit']; + }, + canRead() { + if (!this.structuralElement) { + return false; + } + return this.structuralElement.attributes['can-read']; + }, + isTeacher() { + return this.userIsTeacher; + }, + + isRoot() { + return this.structuralElement.relationships.parent.data === null; + }, + + owner() { + const owner = this.$store.getters['users/related']({ + parent: this.structuralElement, + relationship: 'owner', + }); + + return owner?.attributes['formatted-name'] ?? ''; + }, + + editor() { + const editor = this.$store.getters['users/related']({ + parent: this.structuralElement, + relationship: 'editor', + }); + + return editor?.attributes['formatted-name'] ?? ''; + }, + menuItems() { + let menu = [ + { id: 3, label: this.$gettext('Informationen anzeigen'), icon: 'info', emit: 'showInfo' }, + { id: 4, label: this.$gettext('Lesezeichen setzen'), icon: 'star', emit: 'setBookmark' }, + ]; + if (this.canEdit) { + menu.push({ id: 1, label: this.$gettext('Seite bearbeiten'), icon: 'edit', emit: 'editCurrentElement' }); + menu.push({ id: 2, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' }); + menu.push({ id: 5, label: this.$gettext('Seite exportieren'), icon: 'export', emit: 'showExportOptions' }); + menu.push({ id: 6, label: this.textOer.title, icon: 'service', emit: 'oerCurrentElement' }); + } + if(!this.isRoot && this.canEdit) { + menu.push({ id: 7, label: this.$gettext('Seite löschen'), icon: 'trash', emit: 'deleteCurrentElement' }); + } + menu.sort((a, b) => a.id - b.id); + + return menu; + }, + colors() { + const colors = [ + {name: this.$gettext('Schwarz'), class: 'black', hex: '#000000', level: 100, icon: 'black', darkmode: true}, + {name: this.$gettext('Weiß'), class: 'white', hex: '#ffffff', level: 100, icon: 'white', darkmode: false}, + + {name: this.$gettext('Blau'), class: 'studip-blue', hex: '#28497c', level: 100, icon: 'blue', darkmode: true}, + {name: this.$gettext('Hellblau'), class: 'studip-lightblue', hex: '#e7ebf1', level: 40, icon: 'lightblue', darkmode: false}, + {name: this.$gettext('Rot'), class: 'studip-red', hex: '#d60000', level: 100, icon: 'red', darkmode: false}, + {name: this.$gettext('Grün'), class: 'studip-green', hex: '#008512', level: 100, icon: 'green', darkmode: true}, + {name: this.$gettext('Gelb'), class: 'studip-yellow', hex: '#ffbd33', level: 100, icon: 'yellow', darkmode: false}, + {name: this.$gettext('Grau'), class: 'studip-gray', hex: '#636a71', level: 100, icon: 'grey', darkmode: true}, + + {name: this.$gettext('Holzkohle'), class: 'charcoal', hex: '#3c454e', level: 100, icon: false, darkmode: true}, + {name: this.$gettext('Königliches Purpur'), class: 'royal-purple', hex: '#8656a2', level: 80, icon: false, darkmode: true}, + {name: this.$gettext('Leguangrün'), class: 'iguana-green', hex: '#66b570', level: 60, icon: false, darkmode: true}, + {name: this.$gettext('Königin blau'), class: 'queen-blue', hex: '#536d96', level: 80, icon: false, darkmode: true}, + {name: this.$gettext('Helles Seegrün'), class: 'verdigris', hex: '#41afaa', level: 80, icon: false, darkmode: true}, + {name: this.$gettext('Maulbeere'), class: 'mulberry', hex: '#bf5796', level: 80, icon: false, darkmode: true}, + {name: this.$gettext('Kürbis'), class: 'pumpkin', hex: '#f26e00', level: 100, icon: false, darkmode: true}, + {name: this.$gettext('Sonnenschein'), class: 'sunglow', hex: '#ffca5c', level: 80, icon: false, darkmode: false}, + {name: this.$gettext('Apfelgrün'), class: 'apple-green', hex: '#8bbd40', level: 80, icon: false, darkmode: true}, + ]; + let elementColors = []; + colors.forEach( color => { + if(color.darkmode) { + elementColors.push(color); + } + }); + + return elementColors; + }, + currentLicenseName() { + for(let i = 0; i < this.licenses.length; i++) { + if (this.licenses[i]['id'] == this.currentElement.attributes.payload.license_type) { + return this.licenses[i]['name']; + } + } + + return ''; + } + }, + + watch: { + $route(to) { + this.setCurrentId(to.params.id); + }, + }, + + async mounted() { + if (!this.currentId) { + this.setCurrentId(this.$route.params.id); + } + }, + + methods: { + ...mapActions({ + createStructuralElement: 'createStructuralElement', + updateStructuralElement: 'updateStructuralElement', + deleteStructuralElement: 'deleteStructuralElement', + loadStructuralElement: 'loadStructuralElement', + lockObject: 'lockObject', + unlockObject: 'unlockObject', + addBookmark: 'addBookmark', + companionInfo: 'companionInfo', + uploadImageForStructuralElement: 'uploadImageForStructuralElement', + deleteImageForStructuralElement: 'deleteImageForStructuralElement', + companionSuccess: 'companionSuccess', + showElementEditDialog: 'showElementEditDialog', + showElementAddDialog: 'showElementAddDialog', + showElementExportDialog: 'showElementExportDialog', + showElementInfoDialog: 'showElementInfoDialog', + showElementDeleteDialog: 'showElementDeleteDialog', + showElementOerDialog: 'showElementOerDialog', + }), + + async setCurrentId(id) { + this.currentId = id; + await this.loadStructuralElement(this.currentId); + this.initCurrent(); + }, + initCurrent() { + this.currentElement = JSON.parse(JSON.stringify(this.structuralElement)); + this.uploadFileError = ''; + }, + async menuAction(action) { + switch (action) { + case 'editCurrentElement': + await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.showElementEditDialog(true); + break; + case 'addElement': + this.newChapterName = ''; + this.newChapterParent = 'descendant'; + this.showElementAddDialog(true); + break; + case 'deleteCurrentElement': + await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.showElementDeleteDialog(true); + break; + case 'showInfo': + this.showElementInfoDialog(true); + break; + case 'showExportOptions': + this.showElementExportDialog(true); + break; + case 'oerCurrentElement': + this.showElementOerDialog(true); + break; + case 'setBookmark': + this.setBookmark(); + break; + } + }, + async closeEditDialog() { + await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.showElementEditDialog(false) + this.initCurrent(); + }, + closeAddDialog() { + this.showElementAddDialog(false); + }, + checkUploadFile() { + const file = this.$refs?.upload_image?.files[0]; + if (file.size > 2097152) { + this.uploadFileError = this.$gettext('Diese Datei ist zu groß. Bitte wählen Sie eine kleinere Datei.'); + } else if (!file.type.includes('image')) { + this.uploadFileError = this.$gettext('Diese Datei ist kein Bild. Bitte wählen Sie ein Bild aus.'); + } else { + this.uploadFileError = ''; + } + }, + deleteImage() { + this.deleteImageForStructuralElement(this.currentElement); + this.initCurrent(); + }, + async storeCurrentElement() { + const file = this.$refs?.upload_image?.files[0]; + if (file) { + if (file.size > 2097152) { + return false; + } + + this.uploadFileError = ''; + this.uploadImageForStructuralElement({ + structuralElement: this.currentElement, + file, + }).catch((error) => { + console.error(error); + this.uploadFileError = this.$gettext('Fehler beim Hochladen der Datei.'); + }); + } + + if (this.currentElement.attributes['release-date'] !== '') { + this.currentElement.attributes['release-date'] = + new Date(this.currentElement.attributes['release-date']).getTime() / 1000; + } + + if (this.currentElement.attributes['withdraw-date'] !== '') { + this.currentElement.attributes['withdraw-date'] = + new Date(this.currentElement.attributes['withdraw-date']).getTime() / 1000; + } + + await this.updateStructuralElement({ + element: this.currentElement, + id: this.currentId, + }); + await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.setCurrentId(this.$route.params.id); + this.showElementEditDialog(false); + }, + + async exportCurrentElement(data) { + if (this.exportRunning) { + return; + } + + this.exportRunning = true; + + await this.sendExportZip(this.currentElement.id, { + withChildren: this.exportChildren + }); + + this.exportRunning = false; + this.showElementExportDialog(false); + }, + + async publishCurrentElement() { + this.exportToOER(this.currentElement, {withChildren: this.oerChildren}); + }, + + async closeDeleteDialog() { + await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.showElementDeleteDialog(false); + }, + async deleteCurrentElement() { + let parent_id = this.structuralElement.relationships.parent.data.id; + await this.deleteStructuralElement({ + id: this.currentId, + parentId: this.structuralElement.relationships.parent.data.id, + }); + this.showElementDeleteDialog(false); + this.$router.push(parent_id); + }, + async createElement() { + let title = this.newChapterName; // this is the title of the new element + let parent_id = this.currentId; // new page is descandant as default + if (this.newChapterParent === 'sibling') { + parent_id = this.structuralElement.relationships.parent.data.id; + } + this.showElementAddDialog(false); + await this.createStructuralElement({ + attributes: { + title, + }, + parentId: parent_id, + currentId: this.currentId, + }); + let newElement = this.$store.getters['courseware-structural-elements/lastCreated']; + this.companionSuccess({ + info: this.$gettext('Seite') +' "' + newElement.attributes.title + '" ' + this.$gettext('wurde erfolgreich angelegt.'), + }); + }, + containerComponent(container) { + return 'courseware-' + container.attributes['container-type'] + '-container'; + }, + getNextParentSibling(element_id) { + let current = this.structuralElementById({ id: element_id }); + if (current.relationships.parent.data === null) { + return null; + } + let parent = this.structuralElementById({ id: current.relationships.parent.data.id }); + if (parent.relationships.parent.data === null) { + return null; + } + let grandParent = this.structuralElementById({ id: parent.relationships.parent.data.id }); + let parentSiblings = grandParent.relationships.children.data; + let id = ''; + parentSiblings.forEach((el, index) => { + if (parseInt(el.id, 10) === parseInt(parent.id, 10) && parentSiblings.length > index + 1) { + id = parentSiblings[index + 1].id; + } + }); + if (id === '') { + this.getNextParentSibling(parent.id); + } else { + return this.structuralElementById({ id }); + } + }, + setBookmark() { + this.addBookmark(this.structuralElement); + this.companionInfo({ info: this.$gettext('Das Lesezeichen wurde gesetzt') }); + }, + updateReadApproval(approval) { + this.currentElement.attributes['read-approval'] = approval; + }, + updateWriteApproval(approval) { + this.currentElement.attributes['write-approval'] = approval; + }, + }, + created() { + this.pluginManager.registerComponentsLocally(this); + }, + // this line provides all the components to courseware plugins + provide: () => ({ containerComponents: ContainerComponents }), +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareStructuralElementPermissions.vue b/resources/vue/components/courseware/CoursewareStructuralElementPermissions.vue new file mode 100755 index 0000000..c061ba3 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareStructuralElementPermissions.vue @@ -0,0 +1,306 @@ +<template> + <div class="cw-element-permissions"> + <label> + <input type="checkbox" class="default" v-model="userPermsReadAll" /> + <translate>Alle Teilnehmenden haben Leserechte</translate> + </label> + <label> + <input type="checkbox" class="default" v-model="userPermsWriteAll" /> + <translate>Alle Teilnehmenden haben Schreibrechte</translate> + </label> + + <table class="default" v-if="autor_members.length"> + <caption> + <translate>Studierende</translate> + </caption> + <colgroup> + <col style="width:20%" /> + <col style="width:35%" /> + <col style="width:45%" /> + </colgroup> + <thead> + <tr> + <th><translate>Lesen</translate></th> + <th><translate>Lesen und Schreiben</translate></th> + <th><translate>Name</translate></th> + </tr> + </thead> + <tbody> + <tr v-for="user in autor_members" :key="user.user_id"> + <td class="perm"> + <input + type="checkbox" + :id="user.user_id + `_read`" + :value="user.user_id" + v-model="userPermsReadUsers" + /> + </td> + <td class="perm"> + <input + type="checkbox" + :id="user.user_id + `_write`" + :value="user.user_id" + v-model="userPermsWriteUsers" + /> + </td> + + <td> + <label :for="user.user_id + `_read`"> + {{ user.formattedname }} + <i>{{ user.username }}</i> + </label> + </td> + </tr> + </tbody> + </table> + + <table class="default" v-if="user_members.length"> + <caption> + <translate>Leser/-innen</translate> + </caption> + <colgroup> + <col style="width:55%" /> + <col style="width:45%" /> + </colgroup> + <thead> + <tr> + <th><translate>Lesen</translate></th> + <th><translate>Name</translate></th> + </tr> + </thead> + <tbody> + <tr v-for="user in user_members" :key="user.user_id"> + <td> + <input + type="checkbox" + :id="user.user_id + `_read`" + :value="user.id" + v-model="userPermsReadUsers" + /> + </td> + <td> + <label :for="user.user_id + `_read`"> + {{ user.firstname }} + {{ user.lastname }} + <i>{{ user.username }}</i> + </label> + </td> + </tr> + </tbody> + </table> + + <table class="default" v-if="groups.length"> + <caption> + <translate>Gruppen</translate> + </caption> + <colgroup> + <col style="width:20%" /> + <col style="width:35%" /> + <col style="width:45%" /> + </colgroup> + <thead> + <tr> + <th><translate>Lesen</translate></th> + <th><translate>Lesen und Schreiben</translate></th> + <th><translate>Name</translate></th> + </tr> + </thead> + <tbody> + <tr v-for="group in groups" :key="group.id"> + <td class="perm"> + <input + type="checkbox" + :id="group.id + `_read`" + :value="group.id" + v-model="userPermsReadGroups" + /> + </td> + <td class="perm"> + <input + type="checkbox" + :id="group.id + `_write`" + :value="group.id" + v-model="userPermsWriteGroups" + /> + </td> + + <td> + <label :for="group.id + `_read`"> + {{ group.name }} + </label> + </td> + </tr> + </tbody> + </table> + </div> +</template> +<script> +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-structural-element-permissions', + props: { + element: Object, + }, + data() { + return { + user_perms: {}, + userPermsReadUsers: [], + userPermsReadGroups: [], + userPermsReadAll: Boolean, + userPermsWriteUsers: [], + userPermsWriteGroups: [], + userPermsWriteAll: Boolean, + }; + }, + + mounted() { + if (this.element.attributes['read-approval'].users !== undefined) { + this.userPermsReadUsers = this.element.attributes['read-approval'].users; + } + if (this.element.attributes['read-approval'].groups !== undefined) { + this.userPermsReadGroups = this.element.attributes['read-approval'].groups; + } + if (this.element.attributes['read-approval'].all !== undefined) { + this.userPermsReadAll = this.element.attributes['read-approval'].all; + } else { + this.userPermsReadAll = true; + } + if (this.element.attributes['write-approval'].users !== undefined) { + this.userPermsWriteUsers = this.element.attributes['write-approval'].users; + } + if (this.element.attributes['write-approval'].groups !== undefined) { + this.userPermsWriteGroups = this.element.attributes['write-approval'].groups; + } + if (this.element.attributes['write-approval'].all !== undefined) { + this.userPermsWriteAll = this.element.attributes['write-approval'].all; + } else { + this.userPermsWriteAll = false; + } + + // load memberships for coursewares in a course context + if (this.context.type === 'courses') { + const parent = { type: 'courses', id: this.context.id }; + this.loadCourseMemberships({ parent, relationship: 'memberships', options: { include: 'user' } }); + this.loadCourseStatusGroups({ parent, relationship: 'status-groups' }); + } + }, + + computed: { + ...mapGetters({ + context: 'context', + courseware: 'courseware', + course: 'courses/related', + relatedCourseMemberships: 'course-memberships/related', + relatedCourseStatusGroups: 'status-groups/related', + relatedUser: 'users/related', + }), + users() { + const parent = { type: 'courses', id: this.context.id }; + const relationship = 'memberships'; + const memberships = this.relatedCourseMemberships({ parent, relationship }); + + return ( + memberships?.map((membership) => { + const parent = { type: membership.type, id: membership.id }; + const member = this.relatedUser({ parent, relationship: 'user' }); + + return { + user_id: member.id, + formattedname: member.attributes['formatted-name'], + username: member.attributes['username'], + perm: membership.attributes['permission'], + }; + }) ?? [] + ); + }, + groups() { + const parent = { type: 'courses', id: this.context.id }; + const relationship = 'status-groups'; + const statusGroups = this.relatedCourseStatusGroups({ parent, relationship }); + + return ( + statusGroups?.map((statusGroup) => { + return { + id: statusGroup.id, + name: statusGroup.attributes['name'], + }; + }) ?? [] + ); + }, + autor_members() { + if (Object.keys(this.users).length === 0 && this.users.constructor === Object) { + return []; + } + + let members = this.users.filter(function (user) { + return user.perm === 'autor'; + }); + + return members; + }, + + user_members() { + if (Object.keys(this.users).length === 0 && this.users.constructor === Object) { + return []; + } + + let members = this.users.filter(function (user) { + return user.perm === 'user'; + }); + + return members; + }, + + readApproval() { + return { + all: this.userPermsReadAll, + users: this.userPermsReadUsers, + groups: this.userPermsReadGroups, + }; + }, + + writeApproval() { + return { + all: this.userPermsWriteAll, + users: this.userPermsWriteUsers, + groups: this.userPermsWriteGroups, + }; + }, + }, + + methods: { + ...mapActions({ + loadCourseMemberships: 'course-memberships/loadRelated', + loadCourseStatusGroups: 'status-groups/loadRelated', + }), + }, + + watch: { + userPermsReadUsers(newVal, oldVal) { + this.$emit('updateReadApproval', this.readApproval); + }, + userPermsReadGroups(newVal, oldVal) { + this.$emit('updateReadApproval', this.readApproval); + }, + userPermsReadAll(newVal, oldVal) { + this.$emit('updateReadApproval', this.readApproval); + if (newVal === true) { + this.userPermsWriteAll = false; + } + }, + userPermsWriteUsers(newVal, oldVal) { + this.$emit('updateWriteApproval', this.writeApproval); + }, + userPermsWriteGroups(newVal, oldVal) { + this.$emit('updateWriteApproval', this.writeApproval); + }, + userPermsWriteAll(newVal, oldVal) { + this.$emit('updateWriteApproval', this.writeApproval); + if (newVal === true) { + this.userPermsReadAll = false; + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareTab.vue b/resources/vue/components/courseware/CoursewareTab.vue new file mode 100755 index 0000000..c11a552 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareTab.vue @@ -0,0 +1,30 @@ +<template> + <div class="cw-tab" :class="{ 'cw-tab-active': isActive }"> + <slot></slot> + </div> +</template> + +<script> +export default { + name: 'courseware-tab', + props: { + name: {type: String, required: true }, + selected: { type: Boolean, default: false }, + index: {type: Number, default: 0 }, + icon: {type: String, default: ''}, + }, + data() { + return { + isActive: false, + }; + }, + computed: { + href() { + return '#' +this.index + '-' +this.name.toLowerCase().replace(/ /g, '-'); + }, + }, + mounted() { + this.isActive = this.selected; + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue b/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue new file mode 100755 index 0000000..0a0ea00 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue @@ -0,0 +1,179 @@ +<template> + <div class="cw-block cw-block-table-of-contents"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeText" + @closeEdit="closeEdit" + > + <template #content> + <div v-if="currentStyle !== 'tiles' && currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div> + <ul + v-if="currentStyle === 'list-details' || currentStyle === 'list'" + :class="['cw-block-table-of-contents-' + currentStyle]" + > + <li v-for="child in childElements" :key="child.id"> + <router-link :to="'/structural_element/' + child.id"> + <div class="cw-block-table-of-contents-title-box" :class="[child.attributes.payload.color]"> + {{ child.attributes.title }} + <p v-if="currentStyle === 'list-details'">{{ child.attributes.payload.description }}</p> + </div> + </router-link> + </li> + </ul> + <ul + v-if="currentStyle === 'tiles'" + class="cw-block-table-of-contents-tiles cw-tiles" + :class="[childElements.length > 3 ? 'cw-tiles-space-between' : '']" + > + <li + v-for="child in childElements" + :key="child.id" + class="tile" + :class="[child.attributes.payload.color, childElements.length > 3 ? '': 'cw-tile-margin']" + > + <router-link :to="'/structural_element/' + child.id" :title="child.attributes.title"> + <div + class="preview-image" + :style="getChildStyle(child)" + ></div> + <div class="description"> + <header>{{ child.attributes.title }}</header> + <div class="description-text-wrapper"> + <p>{{ child.attributes.payload.description }}</p> + </div> + <footer> + {{ child.relationships.children.data.length }} + <translate + :translate-n="child.relationships.children.data.length" + translate-plural="Seiten" + > + Seite + </translate> + </footer> + </div> + </router-link> + </li> + </ul> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Überschrift</translate> + <input type="text" v-model="currentTitle" /> + </label> + <label> + <translate>Layout</translate> + <select v-model="currentStyle"> + <option value="list"><translate>Liste</translate></option> + <option value="list-details"><translate>Liste mit Beschreibung</translate></option> + <option value="tiles"><translate>Kacheln</translate></option> + </select> + </label> + </form> + </template> + <template #info><translate>Informationen zum Inhaltsverzeichnis-Block</translate></template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-table-of-contents-block', + components: { + CoursewareDefaultBlock, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentTitle: '', + currentStyle: '', + }; + }, + computed: { + ...mapGetters({ + structuralElementById: 'courseware-structural-elements/byId', + }), + structuralElement() { + return this.structuralElementById({ id: this.$route.params.id }); + }, + childElements() { + let view = this; + let children = this.structuralElement.relationships.children.data; + let childElements = []; + children.forEach((element) => { + childElements.push(view.structuralElementById({ id: element.id })); + }); + + return childElements; + }, + title() { + return this.block?.attributes?.payload?.title; + }, + style() { + return this.block?.attributes?.payload?.style; + }, + childSets() { + let childSets = []; + let childElements = this.childElements; + while (childElements.length > 0) { + let set = []; + for (let i = 0; i < 4; i++) { + let elem = childElements.shift(); + if (elem !== undefined) { + set.push(elem); + } + } + childSets.push(set); + } + + return childSets; + } + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + this.currentTitle = this.title; + this.currentStyle = this.style; + }, + closeEdit() { + this.initCurrentData(); + }, + storeText() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.title = this.currentTitle; + attributes.payload.style = this.currentStyle; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + getChildStyle(child) { + let url = child.relationships?.image?.meta?.['download-url']; + + if(url) { + return {'background-image': 'url(' + url + ')'}; + } else { + return {}; + } + } + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareTabs.vue b/resources/vue/components/courseware/CoursewareTabs.vue new file mode 100755 index 0000000..4cae1b6 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareTabs.vue @@ -0,0 +1,45 @@ +<template> + <div class="cw-tabs"> + <ul class="cw-tabs-nav"> + <li + v-for="(tab, index) in tabs" + :key="index" + :class="[ + tab.isActive ? 'is-active' : '', + tab.icon !== '' && tab.name !== '' ? 'cw-tabs-nav-icon-text-' + tab.icon : '', + tab.icon !== '' && tab.name === '' ? 'cw-tabs-nav-icon-solo-' + tab.icon : '', + ]" + :href="tab.href" + :tabindex="index" + @click="selectTab(tab)" + @keydown.enter="selectTab(tab)" + @keydown.space="selectTab(tab)" + > + {{ tab.name }} + </li> + </ul> + <div class="cw-tabs-content"> + <slot></slot> + </div> + </div> +</template> + +<script> +export default { + name: 'courseware-tabs', + data() { + return { tabs: [] }; + }, + created() { + this.tabs = this.$children; + }, + methods: { + selectTab(selectedTab) { + this.tabs.forEach((tab) => { + tab.isActive = tab.index + '-' + tab.name === selectedTab.index + '-' + selectedTab.name; + }); + this.$emit('selectTab', selectedTab.name); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareTabsContainer.vue b/resources/vue/components/courseware/CoursewareTabsContainer.vue new file mode 100755 index 0000000..950cabb --- /dev/null +++ b/resources/vue/components/courseware/CoursewareTabsContainer.vue @@ -0,0 +1,176 @@ +<template> + <courseware-default-container + :container="container" + :containerClass="'cw-container-tabs'" + :canEdit="canEdit" + :isTeacher="isTeacher" + @storeContainer="storeContainer" + @closeEdit="initCurrentData" + > + <template v-slot:containerContent> + <courseware-tabs> + <courseware-tab + v-for="(section, index) in container.attributes.payload.sections" + :key="index" + :index="index" + :name="section.name" + :icon="section.icon" + :selected="index === 0" + > + <ul class="cw-container-tabs-block-list"> + <li v-for="block in blocks" :key="block.id" class="cw-block-item"> + <component + v-if="section.blocks.includes(block.id)" + :is="component(block)" + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + /> + </li> + <li v-if="showEditMode"> + <courseware-block-adder-area :container="container" :section="index" @updateContainerContent="updateContent"/> + </li> + </ul> + </courseware-tab> + </courseware-tabs> + </template> + <template v-slot:containerEditDialog> + <form class="default cw-container-dialog-edit-form" @submit.prevent=""> + <fieldset v-for="(section, index) in currentContainer.attributes.payload.sections" :key="index"> + <label> + <translate>Title</translate> + <input type="text" v-model="section.name" /> + </label> + <label> + <translate>Icon</translate> + <v-select :options="icons" v-model="section.icon" class="cw-vs-select"> + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span> + </template> + <template #no-options="{ search, searching, loading }"> + <translate>Es steht keine Auswahl zur Verfügung</translate>. + </template> + <template #selected-option="option"> + <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span> + </template> + <template #option="option"> + <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span> + </template> + </v-select> + </label> + <label + class="cw-container-section-delete" + v-if="currentContainer.attributes.payload.sections.length > 1" + > + <button class="button trash" @click="deleteSection(index)"><translate>Tab löschen</translate></button> + </label> + </fieldset> + </form> + <button class="button add" @click="addSection"><translate>Tab hinzufügen</translate></button> + </template> + </courseware-default-container> +</template> + +<script> +import ContainerComponents from './container-components.js'; +import containerMixin from '../../mixins/courseware/container.js'; +import contentIcons from './content-icons.js'; +import CoursewareTabs from './CoursewareTabs.vue'; +import CoursewareTab from './CoursewareTab.vue'; +import StudipIcon from './../StudipIcon.vue'; + +import { mapGetters, mapActions } from 'vuex'; + +export default { + name: 'courseware-tabs-container', + mixins: [containerMixin], + components: Object.assign(ContainerComponents, { + CoursewareTabs, + CoursewareTab, + StudipIcon, + }), + props: { + container: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentContainer: {}, + textDeleteSection: this.$gettext('Sektion entfernen'), + selectAttributes: {'ref': 'openIndicator', 'role': 'presentation', 'class': 'vs__open-indicator'} + }; + }, + computed: { + ...mapGetters({ + blockById: 'courseware-blocks/byId', + }), + blocks() { + if (!this.container) { + return []; + } + + return this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id })); + }, + showEditMode() { + return this.$store.getters.viewMode === 'edit'; + }, + icons() { + return contentIcons; + }, + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateContainer: 'updateContainer', + unlockObject: 'unlockObject', + }), + initCurrentData() { + // clone container to make edit reversible + this.currentContainer = JSON.parse(JSON.stringify(this.container)); + }, + addSection() { + this.currentContainer.attributes.payload.sections.push({ name: '', icon: '', blocks: [] }); + }, + deleteSection(index) { + if (this.currentContainer.attributes.payload.sections.length === 1) { + return; + } + if (this.currentContainer.attributes.payload.sections[index].blocks.length > 0) { + if (index === 0) { + this.currentContainer.attributes.payload.sections[ + index + 1 + ].blocks = this.currentContainer.attributes.payload.sections[index + 1].blocks.concat( + this.currentContainer.attributes.payload.sections[index].blocks + ); + } else { + this.currentContainer.attributes.payload.sections[ + index - 1 + ].blocks = this.currentContainer.attributes.payload.sections[index - 1].blocks.concat( + this.currentContainer.attributes.payload.sections[index].blocks + ); + } + } + this.currentContainer.attributes.payload.sections.splice(index, 1); + }, + async storeContainer() { + await this.updateContainer({ + container: this.currentContainer, + structuralElementId: this.currentContainer.relationships['structural-element'].data.id, + }); + await this.unlockObject({ id: this.container.id, type: 'courseware-containers' }); + this.initCurrentData(); + }, + component(block) { + return 'courseware-' + block.attributes["block-type"] + '-block'; + }, + updateContent(blockAdder) { + if(blockAdder.container.id === this.container.id) { + this.initCurrentData(); + } + } + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareTalkBubble.vue b/resources/vue/components/courseware/CoursewareTalkBubble.vue new file mode 100755 index 0000000..72a240a --- /dev/null +++ b/resources/vue/components/courseware/CoursewareTalkBubble.vue @@ -0,0 +1,26 @@ +<template> + <div :class="{ 'cw-talk-bubble-own-post': payload.own }" class="cw-talk-bubble"> + <div class="cw-talk-bubble-user" v-if="!payload.own"> + <div class="cw-talk-bubble-avatar"> + <img :src="payload.user_avatar" /> + </div> + <span>{{ payload.user_name }}</span> + </div> + <div class="cw-talk-bubble-talktext"> + <p>{{ payload.content }}</p> + <p class="cw-talk-bubble-talktext-time"><iso-date :date="payload.chdate" /></p> + </div> + </div> +</template> + +<script> +import IsoDate from './IsoDate.vue'; + +export default { + name: 'courseware-talk-bubble', + components: { IsoDate }, + props: { + payload: Object, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareTextBlock.vue b/resources/vue/components/courseware/CoursewareTextBlock.vue new file mode 100755 index 0000000..12cc546 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareTextBlock.vue @@ -0,0 +1,71 @@ +<template> + <div class="cw-block cw-block-text"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="false" + ref="defaultBlock" + @storeEdit="storeText" + @closeEdit="closeEdit" + > + <template #content> + <section class="cw-block-content" v-html="currentText"></section> + </template> + <template v-if="canEdit" #edit> + <studip-wysiwyg v-model="currentText"></studip-wysiwyg> + </template> + <template #info><translate>Informationen zum Text-Block</translate></template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import StudipWysiwyg from '../StudipWysiwyg.vue'; +import { mapActions } from 'vuex'; + +export default { + name: 'courseware-text-block', + components: { + CoursewareDefaultBlock, + StudipWysiwyg, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentText: '', + }; + }, + computed: { + text() { + return this.block?.attributes?.payload?.text; + }, + }, + mounted() { + this.currentText = this.text; + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + closeEdit() { + this.currentText = this.text; + }, + async storeText() { + let attributes = this.block.attributes; + attributes.payload.text = this.currentText; + await this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + this.$refs.defaultBlock.displayFeature(false); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareToolsAdmin.vue b/resources/vue/components/courseware/CoursewareToolsAdmin.vue new file mode 100755 index 0000000..2c41440 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareToolsAdmin.vue @@ -0,0 +1,67 @@ +<template> + <div class="cw-tools cw-tools-admin"> + <form class="default" @submit.prevent=""> + <fieldset> + <legend><translate>Allgemeine Einstellungen</translate></legend> + <label> + <span><translate>Art der Kapitelabfolge</translate></span> + <select class="size-s" v-model="currentProgression"> + <option value="false"><translate>Frei</translate></option> + <option value="true"><translate>Sequentiell</translate></option> + </select> + </label> + + <label> + <span><translate>Editierberechtigung für Tutor/-innen</translate></span> + <select class="size-s" v-model="currentPermissionLevel"> + <option value="dozent"><translate>Nein</translate></option> + <option value="tutor"><translate>Ja</translate></option> + </select> + </label> + </fieldset> + </form> + <button class="button" @click="store"><translate>Übernehmen</translate></button> + </div> +</template> + +<script> +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'cw-tools-admin', + data() { + return { + currentPermissionLevel: '', + currentProgression: '', + }; + }, + computed: { + ...mapGetters({ + courseware: 'courseware', + }), + }, + methods: { + ...mapActions({ + storeCoursewareSettings: 'storeCoursewareSettings', + companionSuccess: 'companionSuccess', + }), + initData() { + this.currentPermissionLevel = this.courseware.attributes['editing-permission-level']; + this.currentProgression = this.courseware.attributes['sequential-progression']; + }, + store() { + this.companionSuccess({ + info: this.$gettext('Einstellungen wurden übernommen'), + }) + this.storeCoursewareSettings({ + permission: this.currentPermissionLevel, + progression: this.currentProgression, + }); +; + }, + }, + mounted() { + this.initData(); + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareToolsBlockadder.vue b/resources/vue/components/courseware/CoursewareToolsBlockadder.vue new file mode 100755 index 0000000..7baa986 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareToolsBlockadder.vue @@ -0,0 +1,244 @@ +<template> + <div class="cw-tools-element-adder"> + <ul class="cw-tools-element-adder-tabs"> + <li + :class="{ 'active': showBlockadder }" + class="cw-tools-element-adder-tab" + @click="displayBlockAdder" + > + <translate>Blöcke</translate> + </li> + <li + :class="{ 'active': showContaineradder }" + class="cw-tools-element-adder-tab" + @click="displayContainerAdder" + > + <translate>Abschnitte</translate> + </li> + </ul> + + <div v-show="showBlockadder" class="cw-tools cw-tools-blockadder"> + <courseware-collapsible-box :title="textBlockHelper"> + <courseware-block-helper :blockTypes="blockTypes" /> + </courseware-collapsible-box> + + <courseware-collapsible-box :title="textAdderFavs" :open="favoriteBlockTypes.length > 0"> + <div class="cw-element-adder-wrapper" v-if="!showEditFavs"> + <courseware-companion-box + v-if="favoriteBlockTypes.length === 0" + mood="sad" + :msgCompanion="textFavsEmpty" + /> + <courseware-blockadder-item + v-for="(block, index) in favoriteBlockTypes" + :key="index" + :title="block.title" + :icon="block.icon" + :type="block.type" + :description="block.description" + /> + </div> + + <div class="cw-element-adder-favs-wrapper" v-if="showEditFavs"> + <div class="cw-element-adder-all-blocks" :class="{ 'fav-edit-active': showEditFavs }"> + <courseware-blockadder-item + v-for="(block, index) in blockTypes" + :key="index" + :title="block.title" + :type="block.type" + :description="block.description" + /> + </div> + <div class="cw-element-adder-favs"> + <div + v-for="(block, index) in blockTypes" + :key="'fav-item-' + index" + class="cw-block-fav-item" + :class="[isBlockFav(block) ? 'cw-block-fav-item-active' : '']" + @click="toggleFavItem(block)" + ></div> + </div> + </div> + + <button v-show="!showEditFavs" class="button" @click="showEditFavs = true"> + <translate>Favoriten bearbeiten</translate> + </button> + <button v-show="showEditFavs" class="button" @click="endEditFavs"> + <translate>Favoriten bearbeiten schließen</translate> + </button> + </courseware-collapsible-box> + + <courseware-collapsible-box :title="textAdderAll"> + <div class="cw-element-adder-all-blocks" :class="{ 'fav-edit-active': showEditFavs }"> + <courseware-blockadder-item + v-for="(block, index) in blockTypes" + :key="index" + :title="block.title" + :type="block.type" + :description="block.description" + /> + </div> + </courseware-collapsible-box> + + <courseware-collapsible-box + v-for="(category, index) in blockCategories" + :key="index" + :title="category.title" + :open="category.type === 'basis' && favoriteBlockTypes.length === 0" + > + <div v-for="(block, index) in blockTypes" :key="index"> + <courseware-blockadder-item + v-if="block.categories.includes(category.type)" + :title="block.title" + :icon="block.icon" + :type="block.type" + :description="block.description" + /> + </div> + </courseware-collapsible-box> + </div> + + <div v-show="showContaineradder" class="cw-tools cw-tools-containeradder"> + <courseware-collapsible-box + v-for="(style, index) in containerStyles" + :key="index" + :title="style.title" + :open="index === 0" + > + <courseware-container-adder-item + v-for="(container, index) in containerTypes" + :key="index" + :title="container.title" + :type="container.type" + :colspan="style.colspan" + :description="container.description" + :firstSection="$gettext('erstes Element')" + :secondSection="$gettext('zweites Element')" + ></courseware-container-adder-item> + </courseware-collapsible-box> + </div> + </div> +</template> + +<script> +import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue'; +import CoursewareBlockadderItem from './CoursewareBlockadderItem.vue'; +import CoursewareContainerAdderItem from './CoursewareContainerAdderItem.vue'; +import CoursewareBlockHelper from './CoursewareBlockHelper.vue'; +import { mapGetters } from 'vuex'; +import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; + +export default { + name: 'cw-tools-blockadder', + components: { + CoursewareCollapsibleBox, + CoursewareBlockadderItem, + CoursewareContainerAdderItem, + CoursewareBlockHelper, + CoursewareCompanionBox, + }, + data() { + return { + showBlockadder: true, + showContaineradder: false, + showEditFavs: false, + textAdderFavs: this.$gettext('Favoriten'), + textAdderAll: this.$gettext('Alle Blöcke'), + textBlockHelper: this.$gettext('Blockassistent'), + textFavsEmpty: this.$gettext('Sie haben noch keine Lieblingsblöcke ausgewählt.'), + }; + }, + computed: { + ...mapGetters({ + adderStorage: 'blockAdder', + containerAdder: 'containerAdder', + unorderedBlockTypes: 'blockTypes', + containerTypes: 'containerTypes', + favoriteBlockTypes: 'favoriteBlockTypes', + showToolbar: 'showToolbar', + }), + blockTypes() { + let blockTypes = JSON.parse(JSON.stringify(this.unorderedBlockTypes)); + blockTypes.sort((a, b) => { + return a.title > b.title ? 1 : b.title > a.title ? -1 : 0; + }); + return blockTypes; + }, + containerStyles() { + return [ + { title: this.$gettext('Standard'), colspan: 'full'}, + { title: this.$gettext('Halbe Breite'), colspan: 'half' }, + { title: this.$gettext('Halbe Breite (zentriert)'), colspan: 'half-center' }, + ]; + }, + blockCategories() { + return [ + { title: this.$gettext('Standard'), type: 'basis' }, + { title: this.$gettext('Texte'), type: 'text' }, + { title: this.$gettext('Multimedia'), type: 'multimedia' }, + { title: this.$gettext('Aufgaben & Interaktion'), type: 'interaction' }, + { title: this.$gettext('Gestaltung'), type: 'layout' }, + { title: this.$gettext('Dateien'), type: 'files' }, + { title: this.$gettext('Externe Inhalte'), type: 'external' }, + ]; + } + }, + methods: { + displayContainerAdder() { + this.showContaineradder = true; + this.showBlockadder = false; + }, + displayBlockAdder() { + this.showContaineradder = false; + this.showBlockadder = true; + this.disableContainerAdder(); + }, + toggleFavItem(block) { + if (this.isBlockFav(block)) { + this.$store.dispatch('removeFavoriteBlockType', block.type); + } else { + this.$store.dispatch('addFavoriteBlockType', block.type); + } + }, + isBlockFav(block) { + let isFav = false; + this.favoriteBlockTypes.forEach((type) => { + if (type.type === block.type) { + isFav = true; + } + }); + + return isFav; + }, + disableContainerAdder() { + this.$store.dispatch('coursewareContainerAdder', false); + }, + endEditFavs() { + this.showEditFavs = false; + this.$emit('scrollTop'); + } + }, + mounted() { + if (this.containerAdder === true) { + this.displayContainerAdder(); + } + }, + watch: { + adderStorage(newValue) { + if (Object.keys(newValue).length !== 0) { + this.displayBlockAdder(); + } + }, + containerAdder(newValue) { + if (newValue === true) { + this.displayContainerAdder(); + } + }, + showToolbar(newValue, oldValue) { + if (oldValue === true && newValue === false) { + this.disableContainerAdder(); + } + } + } +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareToolsContents.vue b/resources/vue/components/courseware/CoursewareToolsContents.vue new file mode 100755 index 0000000..071903e --- /dev/null +++ b/resources/vue/components/courseware/CoursewareToolsContents.vue @@ -0,0 +1,72 @@ +<template> + <div class="cw-tools cw-tools-contents"> + <courseware-tree :treeData="treeData" v-if="courseware.length" /> + </div> +</template> + +<script> +import CoursewareTree from './CoursewareTree.vue'; +import { mapGetters } from 'vuex'; + +export default { + name: 'courseware-tools-contents', + components: { + CoursewareTree, + }, + + computed: { + ...mapGetters({ + courseware: 'courseware-structural-elements/all', + }), + + currentElementId() { + return this.$route.params.id; + }, + + treeData() { + let treeData = { + name: 'Courseware', + }; + if (this.courseware !== []) { + let children = this.loadChildren(null, this.courseware, 0); + + if (children.length) { + treeData.children = children; + } + } + + if (treeData.children !== undefined && treeData.children.length) { + return treeData.children[0]; + } + + return treeData; + }, + }, + + methods: { + loadChildren(parentId, data, depth) { + let children = []; + + for (var i = 0; i < data.length; i++) { + if (data[i].relationships.parent.data?.id == parentId) { + let new_childs = this.loadChildren(data[i].id, data, depth + 1); + children.push({ + name: data[i].attributes.title, + position: data[i].attributes.position, + element_id: data[i].id, + children: new_childs, + depth: depth, + current: this.currentElementId === data[i].id + }); + } + } + + children.sort((a, b) => { + return a.position > b.position ? 1 : b.position > a.position ? -1 : 0; + }); + + return children; + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareTree.vue b/resources/vue/components/courseware/CoursewareTree.vue new file mode 100755 index 0000000..992f3eb --- /dev/null +++ b/resources/vue/components/courseware/CoursewareTree.vue @@ -0,0 +1,18 @@ +<template> + <div class="cw-tree"> + <ul class="cw-tree-root-list"> + <courseware-tree-item class="cw-tree-item" :item="treeData"></courseware-tree-item> + </ul> + </div> +</template> + +<script> +import CoursewareTreeItem from './CoursewareTreeItem.vue'; +export default { + components: { CoursewareTreeItem }, + name: 'courseware-tree', + props: { + treeData: Object, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareTreeItem.vue b/resources/vue/components/courseware/CoursewareTreeItem.vue new file mode 100755 index 0000000..d980d7f --- /dev/null +++ b/resources/vue/components/courseware/CoursewareTreeItem.vue @@ -0,0 +1,41 @@ +<template> + <li> + <div :class="{'cw-tree-item-is-root': isRoot, 'cw-tree-item-first-level': isFirstLevel}"> + <router-link + :to="'/structural_element/' + item.element_id" + class="cw-tree-item-link" + :class="{'cw-tree-item-link-current': item.current}" + > + {{ item.name }} + </router-link> + </div> + <ul v-if="hasChildren" :class="{'cw-tree-chapter-list': isRoot}"> + <courseware-tree-item + v-for="(child, index) in item.children" + :key="index" + :item="child" + class="cw-tree-item" + /> + </ul> + </li> +</template> + +<script> +export default { + name: 'courseware-tree-item', + props: { + item: Object, + }, + computed: { + hasChildren() { + return this.item.children && this.item.children.length; + }, + isRoot() { + return this.item.depth === 0; + }, + isFirstLevel() { + return this.item.depth === 1; + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareTypewriterBlock.vue b/resources/vue/components/courseware/CoursewareTypewriterBlock.vue new file mode 100755 index 0000000..abdc8eb --- /dev/null +++ b/resources/vue/components/courseware/CoursewareTypewriterBlock.vue @@ -0,0 +1,156 @@ +<template> + <div class="cw-block cw-block-typewriter"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeText" + @closeEdit="closeEdit" + > + <template #content> + <div class="cw-typewriter-content"> + <vue-typer + :text="currentText" + initial-action="typing" + :repeat="0" + :type-delay="typeDelay" + caret-animation="smooth" + :class="[currentFont, currentSize]" + ></vue-typer> + </div> + </template> + <template v-if="canEdit" #edit> + <label class="cw-typewriter-content-label"> + <translate>Text</translate> + <textarea v-model="currentText" name="cw-typewriter-content" class="cw-typewriter-content"></textarea> + </label> + + <label class="cw-typewriter-speed-label"> + <translate>Geschwindigkeit</translate> + <select v-model="currentSpeed" class="cw-typewriter-speed" name="cw-typewriter-speed" @change="restartTyping"> + <option value="0"><translate>Langsam</translate></option> + <option value="1"><translate>Normal</translate></option> + <option value="2"><translate>Schnell</translate></option> + <option value="3"><translate>Sehr schnell</translate></option> + </select> + </label> + + <label class="cw-typewriter-font-label"> + <translate>Schriftart</translate> + <select v-model="currentFont" class="cw-typewriter-font" name="cw-typewriter-font"> + <option value="font-default"><translate>Standard</translate></option> + <option value="font-typewriter">Lucida Sans Typewriter</option> + <option value="font-trebuchet">Trebuchet MS</option> + <option value="font-tahoma">Tahoma</option> + <option value="font-georgia">Georgia</option> + <option value="font-narrow">Arial Narrow</option> + </select> + </label> + + <label class="cw-typewriter-size-label"> + <translate>Schriftgröße</translate> + <select v-model="currentSize" class="cw-typewriter-size" name="cw-typewriter-size"> + <option value="size-default">100%</option> + <option value="size-tall">125%</option> + <option value="size-grande">150%</option> + <option value="size-huge">200%</option> + </select> + </label> + </template> + <template #info> + <p><translate>Informationen zum Schreibmaschinen-Block</translate></p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import { VueTyper } from 'vue-typer'; +import { mapActions } from 'vuex'; + +export default { + name: 'courseware-typewriter-block', + components: { + CoursewareDefaultBlock, + VueTyper, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + speeds: [200, 100, 50, 25], + typing: false, + speedClasses: [ + 'cw-typewriter-letter-fadein-slow', + 'cw-typewriter-letter-fadein-normal', + 'cw-typewriter-letter-fadein-fast', + 'cw-typewriter-letter-fadein-veryfast', + ], + currentText: ' ', + currentSpeed: '', + currentFont: '', + currentSize: '', + }; + }, + computed: { + text() { + return this.block?.attributes?.payload?.text; + }, + speed() { + return this.block?.attributes?.payload?.speed; + }, + typeDelay() { + return this.speeds[this.currentSpeed]; + }, + font() { + return this.block?.attributes?.payload?.font; + }, + size() { + return this.block?.attributes?.payload?.size; + } + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + }), + initCurrentData() { + this.currentText = this.text; + this.currentSpeed = this.speed; + this.currentFont = this.font; + this.currentSize = this.size; + }, + restartTyping() { + let text = this.currentText; + this.currentText = ' '; + this.$nextTick(()=> { + this.currentText = text; + }); + }, + closeEdit() { + this.initCurrentData(); + }, + storeText() { + let attributes = {}; + attributes.payload = {}; + attributes.payload.text = this.currentText; + attributes.payload.speed = this.currentSpeed; + attributes.payload.font = this.currentFont; + attributes.payload.size = this.currentSize; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + } + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareVideoBlock.vue b/resources/vue/components/courseware/CoursewareVideoBlock.vue new file mode 100755 index 0000000..13841c8 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareVideoBlock.vue @@ -0,0 +1,219 @@ +<template> + <div class="cw-block cw-block-video"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="true" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div v-if="currentTitle !== '' && currentURL" class="cw-block-title">{{ currentTitle }}</div> + <video + v-if="currentURL" + :src="currentURL" + :type="currentFile !== '' ? currentFile.mime_type : ''" + controls + :autoplay="currentAutoplay === 'enabled'" + @contextmenu="contextHandler" + /> + </template> + <template v-if="canEdit" #edit> + <form class="default" @submit.prevent=""> + <label> + <translate>Überschrift</translate> + <input type="text" v-model="currentTitle" /> + </label> + <label> + <translate>Quelle</translate> + <select v-model="currentSource"> + <option value="studip"><translate>Dateibereich</translate></option> + <option value="web"><translate>Web-Adresse</translate></option> + </select> + </label> + <label v-if="currentSource === 'web'"> + <translate>URL</translate> + <input type="text" v-model="currentWebUrl" /> + </label> + <label v-if="currentSource === 'studip'"> + <translate>Datei</translate> + <courseware-file-chooser + v-model="currentFileId" + :isVideo="true" + @selectFile="updateCurrentFile" + /> + </label> + <label> + <translate>Seitenverhältnis</translate> + <select v-model="currentAspect"> + <option value="169">16:9</option> + <option value="43">4:3</option> + </select> + </label> + <label> + <translate>Video startet automatisch</translate> + <select v-model="currentAutoplay"> + <option value="disabled"><translate>Nein</translate></option> + <option value="enabled"><translate>Ja</translate></option> + </select> + </label> + <label> + <translate>Contextmenü</translate> + <select v-model="currentContextMenu"> + <option value="enabled"><translate>Erlauben</translate></option> + <option value="disabled"><translate>Verhindern</translate></option> + </select> + </label> + </form> + </template> + <template #info><translate>Informationen zum Video-Block</translate></template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareFileChooser from './CoursewareFileChooser.vue'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-video-block', + components: { + CoursewareDefaultBlock, + CoursewareFileChooser, + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentSource: '', + currentTitle: '', + currentFile: {}, + currentFileId: '', + currentAspect: '', + currentContextMenu: '', + currentAutoplay: '', + currentWebUrl: '', + }; + }, + computed: { + ...mapGetters({ + fileRefById: 'file-refs/byId', + urlHelper: 'urlHelper', + }), + title() { + return this.block?.attributes?.payload?.title; + }, + source() { + return this.block?.attributes?.payload?.source; + }, + fileId() { + return this.block?.attributes?.payload?.file_id; + }, + webUrl() { + return this.block?.attributes?.payload?.web_url; + }, + aspect() { + return this.block?.attributes?.payload?.aspect; + }, + contextMenu() { + return this.block?.attributes?.payload?.context_menu; + }, + autoplay() { + return this.block?.attributes?.payload?.autoplay; + }, + currentURL() { + if (this.currentSource === 'studip' && this.currentFile) { + return this.currentFile.download_url; + } + if (this.currentSource === 'web') { + return this.currentWebUrl; + } + return false; + }, + + }, + mounted() { + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + loadFileRef: 'file-refs/loadById', + companionWarning: 'companionWarning', + }), + storeBlock() { + let cmpInfo = false; + let attributes = {}; + attributes.payload = {}; + attributes.payload.title = this.currentTitle; + attributes.payload.source = this.currentSource; + if (this.currentSource === 'studip' && this.currentFile !== undefined) { + attributes.payload.file_id = this.currentFile.id; + attributes.payload.web_url = ''; + } else if (this.currentSource === 'web' && this.currentWebUrl !== '') { + attributes.payload.file_id = ''; + attributes.payload.web_url = this.currentWebUrl; + } else { + cmpInfo = this.$gettext('Bitte wählen Sie ein Video aus'); + } + attributes.payload.aspect = this.currentAspect; + attributes.payload.context_menu = this.currentContextMenu; + attributes.payload.autoplay = this.currentAutoplay; + + if (cmpInfo) { + this.companionWarning({ info: cmpInfo }); + return false; + } + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + initCurrentData() { + this.currentSource = this.source; + this.currentTitle = this.title; + this.currentWebUrl = this.webUrl; + this.currentFileId = this.fileId; + this.currentAspect = this.aspect; + this.currentContextMenu = this.contextMenu; + this.currentAutoplay = this.autoplay; + this.loadFile(); + + }, + async loadFile() { + const id = this.currentFileId; + await this.loadFileRef({ id }); + const fileRef = this.fileRefById({ id }); + + if (fileRef) { + this.updateCurrentFile({ + id: fileRef.id, + name: fileRef.attributes.name, + download_url: this.urlHelper.getURL( + 'sendfile.php', + { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name }, + true + ), + }); + } + }, + updateCurrentFile(file) { + this.currentFile = file; + this.currentFileId = file.id; + }, + contextHandler(e) { + if (this.currentContextMenu === '0') { + e.preventDefault(); + console.log('context menu disabled'); + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareViewWidget.vue b/resources/vue/components/courseware/CoursewareViewWidget.vue new file mode 100755 index 0000000..ff18ff5 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareViewWidget.vue @@ -0,0 +1,34 @@ +<template> + <ul class="widget-list widget-links sidebar-views cw-view-widget"> + <li :class="{ active: readView }" @click="setReadView"><translate>Lesen</translate></li> + <li :class="{ active: editView }" @click="setEditView"><translate>Bearbeiten</translate></li> + </ul> +</template> + +<script> +import { mapActions } from 'vuex'; + +export default { + name: 'courseware-view-widget', + computed: { + readView() { + return this.$store.getters.viewMode === 'read'; + }, + editView() { + return this.$store.getters.viewMode === 'edit'; + }, + }, + methods: { + ...mapActions( + ['coursewareBlockAdder'] + ), + setReadView() { + this.$store.dispatch('coursewareViewMode', 'read'); + this.coursewareBlockAdder({}); + }, + setEditView() { + this.$store.dispatch('coursewareViewMode', 'edit'); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareWellcomeScreen.vue b/resources/vue/components/courseware/CoursewareWellcomeScreen.vue new file mode 100755 index 0000000..8741fd0 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareWellcomeScreen.vue @@ -0,0 +1,81 @@ +<template> + <div class="cw-wellcome-screen"> + <div class="cw-wellcome-screen-keyvisual"></div> + <header> + <translate>Willkommen bei Courseware</translate> + </header> + <div class="cw-wellcome-screen-actions"> + <a href="https://hilfe.studip.de/help/5.0/de/Basis.Courseware" target="_blank"> + <button class="button"><translate>Mehr über Courseware erfahren</translate></button> + </a> + <button class="button" :title="$gettext('Fügt einen Standard-Abschnitt mit einem Text-Block hinzu')" @click="addDefault"><translate>Ersten Inhalt erstellen</translate></button> + <button class="button" @click="addContainer"><translate>Einen Abschnitt auswählen</translate></button> + + </div> + </div> +</template> + +<script> +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-wellcome-screen', + components: { + }, + props: {}, + data() { + return{} + }, + computed: { + ...mapGetters({ + consumeMode: 'consumeMode' + }), + }, + methods: { + ...mapActions({ + createContainer: 'createContainer', + createBlock: 'createBlockInContainer', + coursewareBlockAdder: 'coursewareBlockAdder', + companionSuccess: 'companionSuccess', + updateContainer: 'updateContainer', + lockObject: 'lockObject', + unlockObject: 'unlockObject', + }), + addContainer() { + this.$store.dispatch('coursewareConsumeMode', false); + this.$store.dispatch('coursewareViewMode', 'edit'); + this.$store.dispatch('coursewareContainerAdder', true); + this.$store.dispatch('coursewareShowToolbar', true); + }, + async addDefault() { + let attributes = {}; + attributes["container-type"] = 'list'; + attributes.payload = { + colspan: 'full', + sections: [{ name: 'Liste', icon: '', blocks: [] }], + }; + await this.createContainer({ structuralElementId: this.$route.params.id, attributes: attributes }); + let newContainer = this.$store.getters['courseware-containers/lastCreated']; + await this.lockObject({ id: newContainer.id, type: 'courseware-containers' }); + await this.createBlock({ + container: newContainer, + section: 0, + blockType: 'text', + }); + this.$store.dispatch('coursewareViewMode', 'edit'); + this.$store.dispatch('coursewareConsumeMode', false); + this.companionSuccess({ + info: this.$gettext('Elemente für Ihren ersten Inhalt wurden angelegt'), + }); + const newBlock = this.$store.getters['courseware-blocks/lastCreated']; + newContainer.attributes.payload.sections[0].blocks.push(newBlock.id); + const structuralElementId = this.$route.params.id + await this.updateContainer({ container: newContainer, structuralElementId: structuralElementId }); + await this.unlockObject({ id: newContainer.id, type: 'courseware-containers' }); + + + } + } + +} +</script>
\ No newline at end of file diff --git a/resources/vue/components/courseware/DashboardApp.vue b/resources/vue/components/courseware/DashboardApp.vue new file mode 100755 index 0000000..4338be2 --- /dev/null +++ b/resources/vue/components/courseware/DashboardApp.vue @@ -0,0 +1,11 @@ +<template> + <courseware-course-dashboard></courseware-course-dashboard> +</template> + +<script> +import CoursewareCourseDashboard from './CoursewareCourseDashboard.vue'; + +export default { + components: { CoursewareCourseDashboard }, +}; +</script> diff --git a/resources/vue/components/courseware/IndexApp.vue b/resources/vue/components/courseware/IndexApp.vue new file mode 100755 index 0000000..2dfe493 --- /dev/null +++ b/resources/vue/components/courseware/IndexApp.vue @@ -0,0 +1,45 @@ +<template> + <div v-if="courseware"> + <courseware-structural-element></courseware-structural-element> + <MountingPortal mountTo="#courseware-action-widget" name="sidebar-actions"> + <courseware-action-widget></courseware-action-widget> + </MountingPortal> + <MountingPortal mountTo="#courseware-view-widget" name="sidebar-views"> + <courseware-view-widget></courseware-view-widget> + </MountingPortal> + </div> + <div v-else> + <translate>Inhalte werden geladen</translate>... + </div> +</template> + +<script> +import CoursewareStructuralElement from './CoursewareStructuralElement.vue'; +import CoursewareViewWidget from './CoursewareViewWidget.vue'; +import CoursewareActionWidget from './CoursewareActionWidget.vue'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + components: { + CoursewareStructuralElement, + CoursewareViewWidget, + CoursewareActionWidget, + }, + computed: { + ...mapGetters(['courseware', 'userId', 'blockAdder']), + }, + methods: { + ...mapActions(['loadCoursewareStructure', 'loadTeacherStatus', 'coursewareBlockAdder']), + }, + async mounted() { + await this.loadCoursewareStructure(); + await this.loadTeacherStatus(this.userId); + // console.debug('IndexApp mounted for courseware:', this.courseware, this.$store); + }, + watch: { + $route() { + this.coursewareBlockAdder({}); //reset block adder on navigate + } + } +}; +</script> diff --git a/resources/vue/components/courseware/IsoDate.vue b/resources/vue/components/courseware/IsoDate.vue new file mode 100755 index 0000000..4ab6840 --- /dev/null +++ b/resources/vue/components/courseware/IsoDate.vue @@ -0,0 +1,17 @@ +<template> + <studip-date-time :timestamp="unixTimestamp" /> +</template> + +<script> +export default { + name: 'courseware-iso-date', + props: { + date: String, + }, + computed: { + unixTimestamp() { + return +new Date(this.date) / 1000; + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/ManagerApp.vue b/resources/vue/components/courseware/ManagerApp.vue new file mode 100755 index 0000000..0369522 --- /dev/null +++ b/resources/vue/components/courseware/ManagerApp.vue @@ -0,0 +1,21 @@ +<template> + <courseware-course-manager></courseware-course-manager> +</template> + +<script> +import CoursewareCourseManager from './CoursewareCourseManager.vue'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + components: { CoursewareCourseManager }, + computed: { + ...mapGetters(['courseware']), + }, + methods: { + ...mapActions(['loadCoursewareStructure']), + }, + async mounted() { + await this.loadCoursewareStructure(); + }, +}; +</script> diff --git a/resources/vue/components/courseware/block-mixin.js b/resources/vue/components/courseware/block-mixin.js new file mode 100755 index 0000000..2e084ba --- /dev/null +++ b/resources/vue/components/courseware/block-mixin.js @@ -0,0 +1,24 @@ +import { mapActions, mapGetters } from 'vuex'; + +export const blockMixin = { + computed: { + ...mapGetters({ + getUserProgress: 'courseware-user-progresses/related', + }), + userProgress: { + get: function () { + return this.getUserProgress({ parent: this.block, relationship: 'user-progress' }); + }, + set: function (grade) { + this.userProgress.attributes.grade = grade; + + return this.updateUserProgress(this.userProgress); + }, + }, + }, + methods: { + ...mapActions({ + updateUserProgress: 'courseware-user-progresses/update', + }), + }, +}; diff --git a/resources/vue/components/courseware/container-components.js b/resources/vue/components/courseware/container-components.js new file mode 100755 index 0000000..d3a4cf9 --- /dev/null +++ b/resources/vue/components/courseware/container-components.js @@ -0,0 +1,57 @@ +import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue'; +import CoursewareDefaultContainer from './CoursewareDefaultContainer.vue'; +import CoursewareBlockAdderArea from './CoursewareBlockAdderArea.vue'; +// blocks +import CoursewareAudioBlock from './CoursewareAudioBlock.vue'; +import CoursewareBeforeAfterBlock from './CoursewareBeforeAfterBlock.vue'; +import CoursewareCanvasBlock from './CoursewareCanvasBlock.vue'; +import CoursewareChartBlock from './CoursewareChartBlock.vue'; +import CoursewareCodeBlock from './CoursewareCodeBlock.vue'; +import CoursewareConfirmBlock from './CoursewareConfirmBlock.vue'; +import CoursewareDateBlock from './CoursewareDateBlock.vue'; +import CoursewareDialogCardsBlock from './CoursewareDialogCardsBlock.vue'; +import CoursewareDocumentBlock from './CoursewareDocumentBlock.vue'; +import CoursewareDownloadBlock from './CoursewareDownloadBlock.vue'; +import CoursewareEmbedBlock from './CoursewareEmbedBlock.vue'; +import CoursewareFolderBlock from './CoursewareFolderBlock.vue'; +import CoursewareGalleryBlock from './CoursewareGalleryBlock.vue'; +import CoursewareHeadlineBlock from './CoursewareHeadlineBlock.vue'; +import CoursewareIframeBlock from './CoursewareIframeBlock.vue'; +import CoursewareImageMapBlock from './CoursewareImageMapBlock.vue'; +import CoursewareKeyPointBlock from './CoursewareKeyPointBlock.vue'; +import CoursewareLinkBlock from './CoursewareLinkBlock.vue'; +import CoursewareTableOfContentsBlock from './CoursewareTableOfContentsBlock.vue'; +import CoursewareTextBlock from './CoursewareTextBlock.vue'; +import CoursewareTypewriterBlock from './CoursewareTypewriterBlock.vue'; +import CoursewareVideoBlock from './CoursewareVideoBlock.vue'; + +const ContainerComponents = { + CoursewareDefaultBlock, + CoursewareDefaultContainer, + CoursewareBlockAdderArea, + // blocks + CoursewareAudioBlock, + CoursewareBeforeAfterBlock, + CoursewareCanvasBlock, + CoursewareChartBlock, + CoursewareCodeBlock, + CoursewareConfirmBlock, + CoursewareDateBlock, + CoursewareDialogCardsBlock, + CoursewareDocumentBlock, + CoursewareDownloadBlock, + CoursewareEmbedBlock, + CoursewareFolderBlock, + CoursewareGalleryBlock, + CoursewareHeadlineBlock, + CoursewareIframeBlock, + CoursewareImageMapBlock, + CoursewareKeyPointBlock, + CoursewareLinkBlock, + CoursewareTableOfContentsBlock, + CoursewareTextBlock, + CoursewareTypewriterBlock, + CoursewareVideoBlock, +}; + +export default ContainerComponents; diff --git a/resources/vue/components/courseware/content-icons.js b/resources/vue/components/courseware/content-icons.js new file mode 100755 index 0000000..ce5464c --- /dev/null +++ b/resources/vue/components/courseware/content-icons.js @@ -0,0 +1,114 @@ +const contentIcons = [ + 'accept', + 'add', + 'add-circle', + 'admin', + 'aladdin', + 'arr_1down', + 'arr_1left', + 'arr_1right', + 'arr_1up', + 'arr_2down', + 'arr_2left', + 'arr_2right', + 'arr_2up', + 'audio', + 'audio3', + 'billboard', + 'block-canvas', + 'block-comparison', + 'block-eyecatcher', + 'block-gallery', + 'block-gallery2', + 'block-imagemap', + 'brainstorm', + 'campusnavi', + 'chat2', + 'code', + 'community2', + 'computer', + 'consultation', + 'content', + 'courseware', + 'crown', + 'date-single', + 'decline', + 'decline-circle', + 'doctoral_cap', + 'download', + 'dropbox', + 'edit', + 'exclaim', + 'exclaim-circle', + 'export', + 'favorite', + 'filter', + 'globe', + 'graph', + 'group2', + 'group3', + 'group4', + 'home2', + 'info', + 'info-circle', + 'install', + 'institute', + 'key', + 'knife', + 'learnmodule', + 'lightbulb', + 'lightbulb2', + 'link2', + 'link3', + 'link-extern', + 'link-intern', + 'literature', + 'lock-locked', + 'lock-unlocked', + 'mail2', + 'medal', + 'metro', + 'microphone', + 'module', + 'network', + 'notification', + 'notification2', + 'opencast', + 'outer-space', + 'permalink', + 'person', + 'phone', + 'picture', + 'place', + 'plugin', + 'question', + 'question-circle', + 'ranking', + 'remove', + 'remove-circle', + 'resources', + 'roles', + 'schedule2', + 'search', + 'settings', + 'span-empty', + 'span-1quarter', + 'span-2quarter', + 'span-3quarter', + 'span-full', + 'spiral', + 'sport', + 'staple', + 'star', + 'star-empty', + 'star-halffull', + 'test', + 'tools', + 'topic', + 'ufo', + 'video2', + 'visibility-visible', + 'wizard' +]; + +export default contentIcons; diff --git a/resources/vue/components/courseware/plugin-manager.js b/resources/vue/components/courseware/plugin-manager.js new file mode 100755 index 0000000..26ee566 --- /dev/null +++ b/resources/vue/components/courseware/plugin-manager.js @@ -0,0 +1,24 @@ +class PluginManager { + constructor() { + this.blocks = []; + this.containers = []; + } + + addBlock(name, block) { + this.blocks[name] = block; + } + addContainer(name, container) { + this.containers[name] = container; + } + + registerComponentsLocally(component) { + for (const [name, block] of Object.entries(this.blocks)) { + component.$options.components[name] = block; + } + for (const [name, container] of Object.entries(this.containers)) { + component.$options.components[name] = container; + } + } +} + +export default PluginManager; diff --git a/resources/vue/courseware-dashboard-app.js b/resources/vue/courseware-dashboard-app.js new file mode 100755 index 0000000..3a67756 --- /dev/null +++ b/resources/vue/courseware-dashboard-app.js @@ -0,0 +1,13 @@ +import DashboardApp from './components/courseware/DashboardApp.vue'; + +const mountApp = (STUDIP, createApp, element) => { + const app = createApp({ + render: (h) => h(DashboardApp), + }); + + app.$mount(element); + + return app; +}; + +export default mountApp; diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js new file mode 100755 index 0000000..1294de2 --- /dev/null +++ b/resources/vue/courseware-index-app.js @@ -0,0 +1,137 @@ +import CoursewareModule from './store/courseware/courseware.module'; +import CoursewareStructuralElement from './components/courseware/CoursewareStructuralElement.vue'; +import IndexApp from './components/courseware/IndexApp.vue'; +import PluginManager from './components/courseware/plugin-manager.js'; +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import Vuex from 'vuex'; +import axios from 'axios'; +import { mapResourceModules } from '@elan-ev/reststate-vuex'; +import vSelect from 'vue-select'; +import 'vue-select/dist/vue-select.css' + +Vue.component('v-select', vSelect); + +const mountApp = (STUDIP, createApp, element) => { + const getHttpClient = () => + axios.create({ + baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true), + headers: { + 'Content-Type': 'application/vnd.api+json', + }, + }); + + // get id of parent structural element + let elem_id = null; + let entry_id = null; + let entry_type = null; + let oer_title = null; + let licenses = null; + let elem; + + if ((elem = document.getElementById(element.substring(1))) !== undefined) { + if (elem.attributes !== undefined) { + if (elem.attributes['entry-element-id'] !== undefined) { + elem_id = elem.attributes['entry-element-id'].value; + } + + if (elem.attributes['entry-type'] !== undefined) { + entry_type = elem.attributes['entry-type'].value; + } + + if (elem.attributes['entry-id'] !== undefined) { + entry_id = elem.attributes['entry-id'].value; + } + + if (elem.attributes['oer-title'] !== undefined) { + oer_title = elem.attributes['oer-title'].value; + } + // we need a route for License SORM + if (elem.attributes['licenses'] !== undefined) { + licenses = JSON.parse(elem.attributes['licenses'].value); + } + } + } + + const routes = [ + { + path: '/', + redirect: '/structural_element/' + elem_id, + }, + { + path: '/structural_element/:id', + name: 'CoursewareStructuralElement', + component: CoursewareStructuralElement, + }, + ]; + + const base = `${STUDIP.ABSOLUTE_URI_STUDIP}dispatch.php/course/courseware/?cid=${STUDIP.URLHelper.parameters.cid}`; + const router = new VueRouter({ + base, + routes, + }); + + const httpClient = getHttpClient(); + + const store = new Vuex.Store({ + modules: { + courseware: CoursewareModule, + ...mapResourceModules({ + names: [ + 'courses', + 'course-memberships', + 'courseware-blocks', + 'courseware-block-comments', + 'courseware-block-feedback', + 'courseware-containers', + 'courseware-instances', + 'courseware-structural-elements', + 'courseware-user-data-fields', + 'courseware-user-progresses', + 'files', + 'file-refs', + 'folders', + 'status-groups', + 'users', + 'institutes', + 'semesters', + 'sem-classes', + 'sem-types', + 'terms-of-use' + ], + httpClient, + }), + }, + }); + + store.dispatch('setUrlHelper', STUDIP.URLHelper); + store.dispatch('setUserId', STUDIP.USER_ID); + store.dispatch('users/loadById', {id: STUDIP.USER_ID}); + store.dispatch('setHttpClient', httpClient); + + store.dispatch('coursewareContext', { + id: entry_id, + type: entry_type, + }); + + store.dispatch('coursewareCurrentElement', elem_id); + + store.dispatch('oerTitle', oer_title); + store.dispatch('licenses', licenses); + + const pluginManager = new PluginManager(); + store.dispatch('setPluginManager', pluginManager); + STUDIP.eventBus.emit('courseware:init-plugin-manager', pluginManager); + + const app = createApp({ + render: (h) => h(IndexApp), + router, + store, + }); + + app.$mount(element); + + return app; +}; + +export default mountApp; diff --git a/resources/vue/courseware-manager-app.js b/resources/vue/courseware-manager-app.js new file mode 100755 index 0000000..fc6f98a --- /dev/null +++ b/resources/vue/courseware-manager-app.js @@ -0,0 +1,84 @@ +import CoursewareModule from './store/courseware/courseware.module'; +import ManagerApp from './components/courseware/ManagerApp.vue'; +import Vuex from 'vuex'; +import axios from 'axios'; +import { mapResourceModules } from '@elan-ev/reststate-vuex'; + + +const mountApp = (STUDIP, createApp, element) => { + const getHttpClient = () => + axios.create({ + baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true), + headers: { + 'Content-Type': 'application/vnd.api+json', + }, + }); + + const httpClient = getHttpClient(); + + const store = new Vuex.Store({ + modules: { + courseware: CoursewareModule, + ...mapResourceModules({ + names: [ + 'courses', + 'course-memberships', + 'courseware-blocks', + 'courseware-block-comments', + 'courseware-block-feedback', + 'courseware-containers', + 'courseware-instances', + 'courseware-structural-elements', + 'courseware-user-data-fields', + 'courseware-user-progresses', + 'files', + 'file-refs', + 'folders', + 'status-groups', + 'users', + 'institutes', + 'semesters', + 'sem-classes', + 'sem-types', + ], + httpClient: httpClient, + }), + }, + }); + + // get id of parent structural element + let entry_id = null; + let entry_type = null; + let elem; + + if ((elem = document.getElementById(element.substring(1))) !== undefined) { + if (elem.attributes !== undefined) { + if (elem.attributes['entry-type'] !== undefined) { + entry_type = elem.attributes['entry-type'].value; + } + + if (elem.attributes['entry-id'] !== undefined) { + entry_id = elem.attributes['entry-id'].value; + } + } + } + + store.dispatch('setUrlHelper', STUDIP.URLHelper); + store.dispatch('setUserId', STUDIP.USER_ID); + store.dispatch('setHttpClient', httpClient); + store.dispatch('coursewareContext', { + id: entry_id, + type: entry_type, + }); + + const app = createApp({ + render: (h) => h(ManagerApp), + store, + }); + + app.$mount(element); + + return app; +}; + +export default mountApp; diff --git a/resources/vue/mixins/MyCoursesMixin.js b/resources/vue/mixins/MyCoursesMixin.js new file mode 100644 index 0000000..99db7de --- /dev/null +++ b/resources/vue/mixins/MyCoursesMixin.js @@ -0,0 +1,174 @@ +import Responsive from '../../assets/javascripts/lib/responsive.js'; + +import { mapState, mapActions, mapGetters } from 'vuex'; +import MyCoursesNavigation from '../components/MyCoursesNavigation.vue'; + +export default { + components: { MyCoursesNavigation }, + data () { + return { + responsiveDisplay: false, + }; + }, + methods: { + ...mapActions('mycourses', [ + 'toggleOpenGroup', + 'updateConfigValue', + ]), + + getCourseName(course, include_number = false) { + let name = course.name; + if (include_number) { + name = `${course.number} ${course.name}`; + } + return name.trim(); + }, + + urlFor(url, parameters, ignore_params) { + return STUDIP.URLHelper.getURL(url, parameters, ignore_params); + }, + + getCourses (ids) { + return ids.map(id => this.courses[id]); + }, + + isParent (course) { + return course.children.length > 0 && course.children.every(childId => { + return this.courses[childId] !== undefined; + }); + }, + isChild (course) { + return course.parent !== null && this.courses[course.parent] !== undefined; + }, + + getHiddenTooltip(course) { + let infotext = this.$gettext('Versteckte Veranstaltungen können über die Suchfunktionen nicht gefunden werden.'); + infotext += ' '; + if (course.is_teacher && this.getConfig('allow_dozent_visibility')) { + infotext += this.$gettext('Um die Veranstaltung sichtbar zu machen, wählen Sie den Punkt "Sichtbarkeit" im Administrationsbereich der Veranstaltung.'); + } else { + infotext += this.$gettext('Um die Veranstaltung sichtbar zu machen, wenden Sie sich an Administrierende.'); + } + return infotext; + }, + + getActionMenuForCourse(course, withColorPicker = false) { + let menu = []; + + if (!course.is_studygroup) { + menu.push({ + url: this.urlFor(`dispatch.php/course/details/index/${course.id}`, {from: this.urlFor('dispatch.php/my_courses/index')}), + label: this.$gettext('Veranstaltungsdetails'), + icon: 'info-circle', + attributes: { + 'data-dialog': '' + }, + }); + } + + if (withColorPicker) { + // Color grouping + menu.push({ + emit: 'show-color-picker', + emitArguments: [course], + label: this.$gettext('Farbgruppierung ändern'), + icon: 'group4' + }); + } + + // Extra navigation? + if (!course.is_group) { + if (course.extra_navigation) { + menu.push(course.extra_navigation); + } else if (course.admission_binding) { + menu.push({ + url: this.urlFor('dispatch.php/my_courses/decline_binding'), + label: this.$gettext('Aus der Veranstaltung austragen'), + icon: 'decline/door-leave', + attributes: { + title: this.$gettext('Die Teilnahme ist bindend. Bitte wenden Sie sich an die Lehrenden.'), + }, + disabled: true + }); + } else { + menu.push({ + url: this.urlFor(`dispatch.php/my_courses/decline/${course.id}`, {cmd: 'suppose_to_kill'}), + label: this.$gettext('Aus der Veranstaltung austragen'), + icon: 'door-leave' + }); + } + } + + return menu; + }, + + getNavigationForCourse(course, gaps = false) { + let navigation = {}; + + Object.entries(course.navigation).forEach(([key, nav]) => { + if (!nav && !gaps) { + return; + } + + if (this.getConfig('navigation_show_only_new') && !nav.important) { + return; + } + + let result = nav ? Object.assign({}, nav) : false; + if (nav) { + if (nav.important) { + result.class = 'my-courses-navigation-important'; + result.icon.role = 'attention'; + result.icon.shape = result.icon.shape.replace(/^new\//, ''); + } else { + result.class = false; + result.icon.role = 'clickable'; + } + + result.url = this.urlFor('seminar_main.php', { + auswahl: course.id, + redirect_to: result.url, + }); + } + + navigation[key] = result; + }); + + return navigation; + }, + }, + + computed: { + ...mapState('mycourses', [ + 'courses', + 'groups', + 'userid', + 'config', + ]), + ...mapGetters('mycourses', [ + 'isGroupOpen', + 'getConfig', + ]), + + viewConfig () { + return this.responsiveDisplay ? 'responsive_type' : 'display_type'; + }, + numberOfNavElements () { + return Math.max( + ...Object.values(this.courses).map(course => { + const navigation = this.getNavigationForCourse(course, true); + return Object.values(navigation).length; + }) + ); + } + }, + + created () { + this.responsiveDisplay = Responsive.media_query.matches; + Responsive.media_query.addListener(() => { + console.log('changing responsive display', Responsive.media_query.matches); + this.responsiveDisplay = Responsive.media_query.matches; + console.log('changed responsive display', this.responsiveDisplay); + }) + } +} diff --git a/resources/vue/mixins/courseware/container.js b/resources/vue/mixins/courseware/container.js new file mode 100755 index 0000000..fac1ea6 --- /dev/null +++ b/resources/vue/mixins/courseware/container.js @@ -0,0 +1,12 @@ +import { mapGetters } from 'vuex'; + +const containerMixin = { + computed: { + ...mapGetters(['pluginManager']), + }, + created: function () { + this.pluginManager.registerComponentsLocally(this); + }, +}; + +export default containerMixin; diff --git a/resources/vue/mixins/courseware/export.js b/resources/vue/mixins/courseware/export.js new file mode 100755 index 0000000..7a1a8e2 --- /dev/null +++ b/resources/vue/mixins/courseware/export.js @@ -0,0 +1,261 @@ +import { mapActions, mapGetters } from 'vuex'; +import JSZip from 'jszip'; +import FileSaver from 'file-saver'; +import axios from 'axios'; + +export default { + computed: { + ...mapGetters({ + courseware: 'courseware', + containerById: 'courseware-containers/byId', + folderById: 'folders/byId', + filesById: 'files/byId', + structuralElementById: 'courseware-structural-elements/byId', + }), + }, + + data() { + return { + exportFiles: { + json: [], + download: [], + }, + }; + }, + + methods: { + async sendExportZip(root_id = null, options) { + let zip = await this.createExportFile(root_id, options); + await zip.generateAsync({ type: 'blob' }).then(function (content) { + FileSaver.saveAs(content, 'courseware-export-' + new Date().toISOString().slice(0, 10) + '.zip'); + }); + }, + + async createExportFile(root_id = null, options) { + let completeExport = false; + + if (!root_id) { + root_id = this.courseware.relationships.root.data.id; + completeExport = true; + } + + let exportData = await this.exportCourseware(root_id, options); + + let zip = new JSZip(); + zip.file('courseware.json', JSON.stringify(exportData.json)); + zip.file('files.json', JSON.stringify(exportData.files.json)); + + if (completeExport) { + zip.file('settings.json', JSON.stringify(exportData.settings)); + } + + // add all additional files from blocks + for (let id in exportData.files.download) { + zip.file( + id, + await fetch(exportData.files.download[id].url) + .then((response) => response.blob()) + .then((textString) => { + return textString; + }) + ); + } + + return zip; + }, + + async exportCourseware(root_id, options) { + let withChildren = false; + + if (options && options.withChildren === true) { + withChildren = true; + } + + let root_element = await this.structuralElementById({id: root_id}); + + //prevent loss of data + root_element = JSON.parse(JSON.stringify(root_element)); + + // load whole courseware nonetheless, only export relevant elements + let elements = await this.$store.getters['courseware-structural-elements/all']; + + root_element.containers = []; + if (root_element.relationships.containers?.data?.length) { + for (var j = 0; j < root_element.relationships.containers.data.length; j++) { + root_element.containers.push( + await this.exportContainer( + this.containerById({ + id: root_element.relationships.containers.data[j].id, + }) + ) + ); + } + } + + if (withChildren && elements !== []) { + let children = await this.exportStructuralElement(root_id, elements); + + if (children.length) { + root_element.children = children; + } + } + + delete root_element.relationships; + delete root_element.links; + + let settings = { + 'editing-permission-level': this.courseware.attributes['editing-permission-level'], + 'sequential-progression': this.courseware.attributes['sequential-progression'] + }; + + return { + json: root_element, + files: this.exportFiles, + settings: settings + }; + }, + + async exportToOER(element, options) { + let formData = new FormData(); + + let exportZip = await this.createExportFile(element.id, options); + let zip = await exportZip.generateAsync({ type: 'blob' }); + + let description = element.attributes.payload.description ? element.attributes.payload.description : ''; + let difficulty_start = element.attributes.payload.difficulty_start ? element.attributes.payload.difficulty_start : '1'; + let difficulty_end = element.attributes.payload.difficulty_end ? element.attributes.payload.difficulty_end : '12'; + + if (element.relationships.image.data !== null) { + let image = {}; + await axios.get(element.relationships.image.meta['download-url'] , {responseType: 'blob'}).then(response => { image = response.data }); + formData.append("image", image); + } + + formData.append("data[name]", element.attributes.title); + formData.append("tags[]", "Lernmaterial"); + formData.append("file", zip, (element.attributes.title).replace(/\s+/g, '_') + '.zip'); + formData.append("data[description]", description); + formData.append("data[difficulty_start]", difficulty_start); + formData.append("data[difficulty_end]", difficulty_end); + formData.append("data[category]", 'elearning'); + + axios({ + method: 'post', + url: STUDIP.URLHelper.getURL('dispatch.php/oer/mymaterial/edit/'), + data: formData, + headers: { "Content-Type": "multipart/form-data"} + }).then( () => { + this.companionInfo({ info: this.$gettext('Seite wurde an OER Campus gesendet.') }); + }); + }, + + async exportStructuralElement(parentId, data) { + let children = []; + + for (var i = 0; i < data.length; i++) { + if (data[i].relationships.parent.data?.id === parentId) { + let new_childs = await this.exportStructuralElement(data[i].id, data); + let content = { ...data[i] }; + content.containers = []; + + await this.loadStructuralElement(content.id); + + let element = this.structuralElementById({ id: content.id }); + + // load containers, if there are any for this struct + if (element.relationships.containers?.data?.length) { + for (var j = 0; j < element.relationships.containers.data.length; j++) { + content.containers.push( + await this.exportContainer( + this.containerById({ + id: element.relationships.containers.data[j].id, + }) + ) + ); + } + } + + delete content.relationships; + content.children = new_childs; + + children.push(content); + } + } + + return children; + }, + + async exportContainer(container_ref) { + // make a local copy of the container + let container = { ...container_ref }; + + container.blocks = []; + + let blocks = this.$store.getters['courseware-blocks/all']; + + // now, load the blocks for this container, if there are any + if (blocks.length) { + for (var k = 0; k < blocks.length; k++) { + if (blocks[k].relationships.container?.data.id === container.id) { + container.blocks.push(await this.exportBlock(blocks[k])); + } + } + } + + delete container.relationships; + + return container; + }, + + async exportBlock(block_ref) { + // make a local copy of the block + let block = { ...block_ref }; + + // export file data (if any) + if (block_ref.relationships['file-refs']?.links?.related) { + await this.exportFileRefs(block_ref.id); + } + + delete block.relationships; + + return block; + }, + + async exportFileRefs(block_id) { + // load file-ref data + let refs = await this.loadFileRefs(block_id); + + // add infos to exportFiles JSON + for (let ref_id in refs) { + let fileref = {}; + let folderId = refs[ref_id].relationships.parent.data.id; + await this.loadFolder(folderId); + let folder = this.folderById({id: folderId}); + + fileref.attributes = refs[ref_id].attributes; + fileref.related_block_id = block_id; + fileref.id = refs[ref_id].id; + fileref.folder = { + id: folder.id, + name: folder.attributes.name, + type: folder.attributes['folder-type'] + } + + this.exportFiles.json.push(fileref); + + // prevent multiple downloads of the same file + this.exportFiles.download[refs[ref_id].id] = { + folder: folderId, + url: refs[ref_id].meta['download-url'] + }; + } + }, + + ...mapActions([ + 'loadStructuralElement', + 'loadFileRefs', + 'loadFolder', + 'companionInfo' + ]), + }, +}; diff --git a/resources/vue/mixins/courseware/import.js b/resources/vue/mixins/courseware/import.js new file mode 100755 index 0000000..c2b83c7 --- /dev/null +++ b/resources/vue/mixins/courseware/import.js @@ -0,0 +1,185 @@ +import { mapActions, mapGetters } from 'vuex'; + +export default { + data() { + return { + importFolder: null, + file_mapping: {} + }; + }, + + computed: { + ...mapGetters({ + context: 'context', + courseware: 'courseware-instances/all' + }), + }, + + methods: { + animateImport() {}, + + async importCourseware(element, parent_id, files) + { + // import all files + await this.uploadAllFiles(files); + + this.animateImport(); + + await this.importStructuralElement([element], parent_id, files); + + }, + + async importStructuralElement(element, parent_id, files) { + if (element.length) { + for (var i = 0; i < element.length; i++) { + // TODO: create element on server and fetch new id + await this.createStructuralElement({ + attributes: element[i].attributes, + parentId: parent_id, + currentId: parent_id, + }); + + this.animateImport(); + + let new_element = this.$store.getters['courseware-structural-elements/lastCreated']; + if (element[i].children?.length > 0) { + await this.importStructuralElement(element[i].children, new_element.id, files); + } + + if (element[i].containers?.length > 0) { + for (var j = 0; j < element[i].containers.length; j++) { + let container = element[i].containers[j]; + // TODO: create element on server and fetch new id + await this.createContainer({ + attributes: container.attributes, + structuralElementId: new_element.id, + }); + + this.animateImport(); + + let new_container = this.$store.getters['courseware-containers/lastCreated']; + + if (container.blocks?.length) { + for (var k = 0; k < container.blocks.length; k++) { + await this.importBlock(container.blocks[k], new_container, files); + } + } + } + } + } + } + }, + + async importBlock(block, block_container, files) { + // TODO: create element + await this.createBlockInContainer({ + container: {type: block_container.type, id: block_container.id}, + blockType: block.attributes['block-type'], + }); + + this.animateImport(); + + let new_block = this.$store.getters['courseware-blocks/lastCreated']; + + // update old id ids in payload part + for (var i = 0; i < files.length; i++) { + if (files[i].related_block_id === block.id) { + let old_file = this.file_mapping[files[i].id].old; + let new_file = this.file_mapping[files[i].id].new; + let payload = JSON.stringify(block.attributes.payload); + + payload = payload.replaceAll(old_file.id, new_file.id); + payload = payload.replaceAll(old_file.folder.id, new_file.relationships.parent.data.id); + + block.attributes.payload = JSON.parse(payload); + } + } + + await this.updateBlockInContainer({ + attributes: block.attributes, + blockId: new_block.id, + containerId: block_container.id, + }); + + this.animateImport(); + }, + + + async uploadAllFiles(files) { + // create folder for importing the files into + let now = new Date(); + let main_folder = await this.createRootFolder({ + context: this.context, + folder: { + type: 'StandardFolder', + name: ' CoursewareImport ' + + now.toLocaleString('de-DE', { timeZone: 'UTC' }) + + ' ' + now.getMilliseconds(), + } + }); + + this.animateImport(); + + let folders = {}; + + // upload all files to the newly created folder + if (main_folder) { + for (var i = 0; i < files.length; i++) { + + // if the subfolder with the referenced id does not exist yet, create it + if (!folders[files[i].folder.id]) { + folders[files[i].folder.id] = await this.createFolder({ + context: this.context, + parent: { + data: { + id: main_folder.id, + type: 'folders' + } + }, + folder: { + type: files[i].folder.type, + name: files[i].folder.name + } + }); + } + + // only upload files with the same id once + if (this.file_mapping[files[i].id] === undefined) { + let zip_filedata = await this.zip.file(files[i].id).async('blob'); + + // create new blob with correct type + let filedata = zip_filedata.slice(0, zip_filedata.size, files[i].attributes['mime-type']); + + let file = await this.createFile({ + file: files[i], + filedata: filedata, + folder: folders[files[i].folder.id] + }); + + this.animateImport(); + + //file mapping + this.file_mapping[files[i].id] = { + old: files[i], + new: file + }; + } + } + } else { + return false; + } + + return true; + }, + + ...mapActions([ + 'createBlockInContainer', + 'createContainer', + 'createStructuralElement', + 'updateBlockInContainer', + 'createFolder', + 'createRootFolder', + 'createFile' + ]), + }, +}; diff --git a/resources/vue/store/MyCoursesStore.js b/resources/vue/store/MyCoursesStore.js new file mode 100644 index 0000000..a090040 --- /dev/null +++ b/resources/vue/store/MyCoursesStore.js @@ -0,0 +1,93 @@ +const configMapping = { + display_type: value => { + return { + MY_COURSES_TILED_DISPLAY: value === 'tiles', + } + }, + responsive_type: value => { + return { + MY_COURSES_TILED_DISPLAY_RESPONSIVE: value === 'tiles', + } + }, + navigation_show_only_new: value => { + return { + MY_COURSES_SHOW_NEW_ICONS_ONLY: value, + }; + }, + open_groups: value => { + return { + MY_COURSES_OPEN_GROUPS: value, + }; + }, + +}; + +export default { + namespaced: true, + + state: () => ({ + courses: {}, + groups: {}, + userid: null, + config: {}, + }), + + getters: { + isGroupOpen: (state) => (group) => { + if (state.config.group_by === 'sem_number') { + return true; + } + return state.config.open_groups.includes(group.id); + }, + getConfig: (state) => (key) => { + return state.config[key]; + }, + }, + + mutations: { + setCourses (state, courses) { + state.courses = courses; + }, + setGroups (state, groups) { + state.groups = groups; + }, + setUserId (state, userid) { + state.userid = userid; + }, + setConfig (state, config) { + state.config = config; + }, + }, + + actions: { + updateConfigValue({ commit, state }, { key, value }) { + commit('setConfig', { ...state.config, [key]: value }); + + // do we have to store this on the server? + if (!configMapping[key]) { + return Promise.resolve(null); + } + + const configValue = configMapping[key](value); + const configKey = Object.keys(configValue)[0]; + const documentId = `${state.userid}_${configKey}`; + + const data = { + id: documentId, + type: 'config-values', + attributes: { value: configValue[configKey] } + }; + + return STUDIP.jsonapi.PATCH(`config-values/${documentId}`, { data: { data } }) + }, + toggleOpenGroup ({ state, dispatch }, group) { + let open_groups = [ ...state.config.open_groups ]; + if (open_groups.includes(group.id)) { + open_groups = open_groups.filter(item => item != group.id); + } else { + open_groups.push(group.id); + } + return dispatch('updateConfigValue', { key: 'open_groups', value: open_groups }); + } + } +} diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js new file mode 100755 index 0000000..04ff970 --- /dev/null +++ b/resources/vue/store/courseware/courseware.module.js @@ -0,0 +1,988 @@ +import axios from 'axios'; + +const getDefaultState = () => { + return { + blockAdder: {}, + containerAdder: false, + consumeMode: false, + context: {}, + courseware: {}, + currentElement: {}, + oerTitle: null, + licenses: null, // we need a route for License SORM + httpClient: null, + lastElement: null, + msg: 'Dehydrated', + msgCompanionOverlay: + 'Hallo! Ich bin Ihr persönlicher Companion. Wussten Sie schon, dass Courseware jetzt noch einfacher zu bedienen ist?', + styleCompanionOverlay: 'default', + pluginManager: null, + showCompanionOverlay: false, + showToolbar: false, + urlHelper: null, + userId: null, + viewMode: 'read', + filingData: {}, + userIsTeacher: false, + + showStructuralElementEditDialog: false, + showStructuralElementAddDialog: false, + showStructuralElementExportDialog: false, + showStructuralElementInfoDialog: false, + showStructuralElementDeleteDialog: false, + showStructuralElementOerDialog: false, + }; +}; + +const initialState = getDefaultState(); + +const getters = { + msg(state) { + return state.msg; + }, + lastElement(state) { + return state.lastElement; + }, + courseware(state) { + return state.courseware; + }, + currentElement(state) { + return state.currentElement; + }, + oerTitle(state) { + return state.oerTitle; + }, + licenses(state) { + return state.licenses; + }, + context(state) { + return state.context; + }, + blockTypes(state) { + return state.courseware?.attributes?.['block-types'] ?? []; + }, + containerTypes(state) { + return state.courseware?.attributes?.['container-types'] ?? []; + }, + favoriteBlockTypes(state) { + const allBlockTypes = state.courseware?.attributes?.['block-types'] ?? []; + const favorites = state.courseware?.attributes?.['favorite-block-types'] ?? []; + + return allBlockTypes.filter(({ type }) => favorites.includes(type)); + }, + viewMode(state) { + return state.viewMode; + }, + showToolbar(state) { + return state.showToolbar; + }, + blockAdder(state) { + return state.blockAdder; + }, + containerAdder(state) { + return state.containerAdder; + }, + showCompanionOverlay(state) { + return state.showCompanionOverlay; + }, + msgCompanionOverlay(state) { + return state.msgCompanionOverlay; + }, + styleCompanionOverlay(state) { + return state.styleCompanionOverlay; + }, + consumeMode(state) { + return state.consumeMode; + }, + httpClient(state) { + return state.httpClient; + }, + urlHelper(state) { + return state.urlHelper; + }, + userId(state) { + return state.userId; + }, + userIsTeacher(state) { + return state.userIsTeacher; + }, + pluginManager(state) { + return state.pluginManager; + }, + filingData(state) { + return state.filingData; + }, + showStructuralElementEditDialog(state) { + return state.showStructuralElementEditDialog; + }, + showStructuralElementAddDialog(state) { + return state.showStructuralElementAddDialog; + }, + showStructuralElementExportDialog(state) { + return state.showStructuralElementExportDialog; + }, + showStructuralElementInfoDialog(state) { + return state.showStructuralElementInfoDialog; + }, + showStructuralElementOerDialog(state) { + return state.showStructuralElementOerDialog; + }, + showStructuralElementDeleteDialog(state) { + return state.showStructuralElementDeleteDialog; + } +}; + +export const state = { ...initialState }; + +export const actions = { + async loadCoursewareStructure({ commit, dispatch, state, rootGetters }) { + const parent = state.context; + const relationship = 'courseware'; + const options = { + include: 'bookmarks,root,root.descendants', + }; + + await dispatch(`courseware-instances/loadRelated`, { parent, relationship, options }, { root: true }); + + return commit('coursewareSet', rootGetters['courseware-instances/all'][0]); + }, + + loadContainer({ dispatch }, containerId) { + const options = { + include: 'blocks', + }; + + return dispatch('courseware-containers/loadById', { id: containerId, options }, { root: true }); + }, + + loadStructuralElement({ dispatch }, structuralElementId) { + const options = { + include: + 'ancestors,containers,containers.blocks,containers.blocks.editor,containers.blocks.owner,containers.blocks.user-data-field,containers.blocks.user-progress,descendants,editor,owner', + 'fields[users]': 'formatted-name', + }; + + return dispatch( + 'courseware-structural-elements/loadById', + { id: structuralElementId, options }, + { root: true } + ); + }, + + loadFileRefs({ dispatch, rootGetters }, block_id) { + const parent = { + type: 'courseware-blocks', + id: block_id, + }; + + const relationship = 'file-refs'; + + return dispatch('courseware-blocks/loadRelated', { parent, relationship }, { root: true }).then(() => { + const refs = rootGetters['courseware-blocks/related']({ + parent, + relationship, + }); + return refs; + }); + }, + + async createFile(context, { file, filedata, folder }) { + const formData = new FormData(); + formData.append('file', filedata, file.attributes.name); + + const url = `folders/${folder.id}/file-refs`; + let request = await state.httpClient.post(url, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return state.httpClient.get(request.headers.location).then((response) => { + return response.data.data; + }); + }, + + async createRootFolder({ dispatch, rootGetters }, { context, folder }) { + // get root folder for this context + await dispatch( + 'courses/loadRelated', + { + parent: context, + relationship: 'folders', + }, + { root: true } + ); + + let folders = await rootGetters['courses/related']({ + parent: context, + relationship: 'folders', + }); + + let rootFolder = null; + + for (let i = 0; i < folders.length; i++) { + if (folders[i].attributes['folder-type'] === 'RootFolder') { + rootFolder = folders[i]; + } + } + + const newFolder = { + data: { + type: 'folders', + attributes: { + name: folder.name, + 'folder-type': 'StandardFolder', + }, + relationships: { + parent: { + data: { + type: 'folders', + id: rootFolder.id, + }, + }, + }, + }, + }; + + return state.httpClient.post(`courses/${context.id}/folders`, newFolder).then((response) => { + return response.data.data; + }); + }, + + async createFolder(store, { context, parent, folder }) { + const newFolder = { + data: { + type: 'folders', + attributes: { + name: folder.name, + 'folder-type': folder.type, + }, + relationships: { + parent: parent, + }, + }, + }; + + return state.httpClient.post(`courses/${context.id}/folders`, newFolder).then((response) => { + return response.data.data; + }); + }, + + loadFolder({ dispatch }, folderId) { + const options = {}; + + return dispatch('folders/loadById', { id: folderId, options }, { root: true }); + }, + + copyBlock({ getters }, { parentId, block }) { + const copy = { + data: { + block: block, + parent_id: parentId, + }, + }; + + return state.httpClient.post(`courseware-blocks/${block.id}/copy`, copy).then((resp) => { + // console.log(resp); + }); + }, + copyContainer({ getters }, { parentId, container }) { + const copy = { + data: { + container: container, + parent_id: parentId, + }, + }; + + return state.httpClient.post(`courseware-containers/${container.id}/copy`, copy).then((resp) => { + // console.log(resp); + }); + }, + copyStructuralElement({ getters }, { parentId, element }) { + const copy = { + data: { + element: element, + parent_id: parentId, + }, + }; + + return state.httpClient.post(`courseware-structural-elements/${element.id}/copy`, copy).then((resp) => { + // console.log(resp); + }); + }, + + lockObject({ dispatch, getters }, { id, type }) { + return dispatch(`${type}/setRelated`, { + parent: { id, type }, + relationship: 'edit-blocker', + data: { + type: 'users', + id: getters.userId, + }, + }); + }, + + async createBlockInContainer({ dispatch }, { container, blockType }) { + const block = { + attributes: { + 'block-type': blockType, + payload: null, + }, + relationships: { + container: { + data: { type: container.type, id: container.id }, + }, + }, + }; + await dispatch('courseware-blocks/create', block, { root: true }); + + return dispatch('loadContainer', container.id); + }, + + async deleteBlockInContainer({ dispatch }, { containerId, blockId }) { + const data = { + id: blockId, + }; + await dispatch('courseware-blocks/delete', data, { root: true }); + //TODO throws TypeError: block is undefined after delete + return dispatch('loadContainer', containerId); + }, + + async updateBlockInContainer({ dispatch }, { attributes, blockId, containerId }) { + const container = { + type: 'courseware-containers', + id: containerId, + }; + const block = { + type: 'courseware-blocks', + attributes: attributes, + id: blockId, + relationships: { + container: { + data: { type: container.type, id: container.id }, + }, + }, + }; + + await dispatch('courseware-blocks/update', block, { root: true }); + await dispatch('unlockObject', { id: blockId, type: 'courseware-blocks' }); + + return dispatch('loadContainer', containerId); + }, + + async updateBlock({ dispatch }, { block, containerId }) { + const container = { + type: 'courseware-containers', + id: containerId, + }; + const updateBlock = { + type: 'courseware-blocks', + attributes: block.attributes, + id: block.id, + relationships: { + container: { + data: { type: container.type, id: container.id }, + }, + }, + }; + await dispatch('courseware-blocks/update', updateBlock, { root: true }); + + return dispatch('loadContainer', containerId); + }, + + async deleteBlock({ dispatch }, { containerId, blockId }) { + const data = { + id: blockId, + }; + await dispatch('courseware-blocks/delete', data, { root: true }); + //TODO throws TypeError: block is undefined after delete + return dispatch('loadContainer', containerId); + }, + + async storeCoursewareSettings({ dispatch, getters }, { permission, progression }) { + const courseware = getters.courseware; + courseware.attributes['editing-permission-level'] = permission; + courseware.attributes['sequential-progression'] = progression; + + return dispatch('courseware-instances/update', courseware, { root: true }); + }, + + sortChildrenInStructualElements({ dispatch }, { parent, children }) { + const childrenResourceIdentifiers = children.map(({ type, id }) => ({ type, id })); + + return dispatch( + `courseware-structural-elements/setRelated`, + { + parent: { type: parent.type, id: parent.id }, + relationship: 'children', + data: childrenResourceIdentifiers, + }, + { root: true } + ); + }, + + async createStructuralElement({ dispatch }, { attributes, parentId, currentId }) { + const data = { + attributes, + relationships: { + parent: { + data: { + type: 'courseware-structural-elements', + id: parentId, + }, + }, + }, + }; + await dispatch('courseware-structural-elements/create', data, { root: true }); + + return dispatch('loadStructuralElement', currentId); + }, + + async deleteStructuralElement({ dispatch }, { id, parentId }) { + const data = { + id: id, + }; + await dispatch('courseware-structural-elements/delete', data, { root: true }); + return dispatch('loadStructuralElement', parentId); + }, + + async updateStructuralElement({ dispatch }, { element, id }) { + await dispatch('courseware-structural-elements/update', element, { root: true }); + + return dispatch('loadStructuralElement', id); + }, + + sortContainersInStructualElements({ dispatch }, { structuralElement, containers }) { + const containerResourceIdentifiers = containers.map(({ type, id }) => ({ type, id })); + + return dispatch( + `courseware-structural-elements/setRelated`, + { + parent: { type: structuralElement.type, id: structuralElement.id }, + relationship: 'containers', + data: containerResourceIdentifiers, + }, + { root: true } + ); + }, + + async createContainer({ dispatch }, { attributes, structuralElementId }) { + const data = { + attributes, + relationships: { + 'structural-element': { + data: { + type: 'courseware-structural-elements', + id: structuralElementId, + }, + }, + }, + }; + await dispatch('courseware-containers/create', data, { root: true }); + + return dispatch('loadStructuralElement', structuralElementId); + }, + + async deleteContainer({ dispatch }, { containerId, structuralElementId }) { + const data = { + id: containerId, + }; + await dispatch('courseware-containers/delete', data, { root: true }); + //TODO throws TypeError: container is undefined after delete + return dispatch('loadStructuralElement', structuralElementId); + }, + + async updateContainer({ dispatch }, { container, structuralElementId }) { + await dispatch('courseware-containers/update', container, { root: true }); + + return dispatch('loadStructuralElement', structuralElementId); + }, + + sortBlocksInContainer({ dispatch }, { container, sections }) { + let blockResourceIdentifiers = []; + + sections.forEach((section) => { + blockResourceIdentifiers.push(...section.blocks.map(({ type, id }) => ({ type, id }))); + }); + + return dispatch( + `courseware-containers/setRelated`, + { + parent: { type: container.type, id: container.id }, + relationship: 'blocks', + data: blockResourceIdentifiers, + }, + { root: true } + ); + }, + + lockObject({ dispatch, getters }, { id, type }) { + return dispatch(`${type}/setRelated`, { + parent: { id, type }, + relationship: 'edit-blocker', + data: { + type: 'users', + id: getters.userId, + }, + }); + }, + + unlockObject({ dispatch }, { id, type }) { + return dispatch(`${type}/setRelated`, { + parent: { id, type }, + relationship: 'edit-blocker', + data: null, + }); + }, + + async companionInfo({ dispatch }, { info }) { + await dispatch('coursewareStyleCompanionOverlay', 'default'); + await dispatch('coursewareMsgCompanionOverlay', info); + return dispatch('coursewareShowCompanionOverlay', true); + }, + + async companionSuccess({ dispatch }, { info }) { + await dispatch('coursewareStyleCompanionOverlay', 'happy'); + await dispatch('coursewareMsgCompanionOverlay', info); + return dispatch('coursewareShowCompanionOverlay', true); + }, + + async companionError({ dispatch }, { info }) { + await dispatch('coursewareStyleCompanionOverlay', 'sad'); + await dispatch('coursewareMsgCompanionOverlay', info); + return dispatch('coursewareShowCompanionOverlay', true); + }, + + async companionWarning({ dispatch }, { info }) { + await dispatch('coursewareStyleCompanionOverlay', 'alert'); + await dispatch('coursewareMsgCompanionOverlay', info); + return dispatch('coursewareShowCompanionOverlay', true); + }, + + async companionSpecial({ dispatch }, { info }) { + await dispatch('coursewareStyleCompanionOverlay', 'special'); + await dispatch('coursewareMsgCompanionOverlay', info); + return dispatch('coursewareShowCompanionOverlay', true); + }, + + // adds a favorite block type using the `type` of the BlockType + async addFavoriteBlockType({ dispatch, getters }, blockType) { + const blockTypes = new Set(getters.favoriteBlockTypes.map(({ type }) => type)); + blockTypes.add(blockType); + + return dispatch('storeFavoriteBlockTypes', [...blockTypes]); + }, + + // removes a favorite block type using the `type` of the BlockType + async removeFavoriteBlockType({ dispatch, getters }, blockType) { + const blockTypes = new Set(getters.favoriteBlockTypes.map(({ type }) => type)); + blockTypes.delete(blockType); + + return dispatch('storeFavoriteBlockTypes', [...blockTypes]); + }, + + // sets the favorite block types using an array of the `type`s of those BlockTypes + async storeFavoriteBlockTypes({ dispatch, getters }, favoriteBlockTypes) { + const courseware = getters.courseware; + courseware.attributes['favorite-block-types'] = favoriteBlockTypes; + + return dispatch('courseware-instances/update', courseware, { root: true }); + }, + + coursewareCurrentElement(context, id) { + context.commit('coursewareCurrentElementSet', id); + }, + + coursewareContext(context, id) { + context.commit('coursewareContextSet', id); + }, + + oerTitle(context, title) { + context.commit('oerTitleSet', title); + }, + + licenses(context, licenses) { + context.commit('licensesSet', licenses); + }, + + coursewareViewMode(context, view) { + context.commit('coursewareViewModeSet', view); + }, + + coursewareShowToolbar(context, toolbar) { + context.commit('coursewareShowToolbarSet', toolbar); + }, + + coursewareBlockAdder(context, adder) { + context.commit('coursewareBlockAdderSet', adder); + }, + + coursewareContainerAdder(context, adder) { + context.commit('coursewareContainerAdderSet', adder); + }, + + coursewareShowCompanionOverlay(context, companion_overlay) { + context.commit('coursewareShowCompanionOverlaySet', companion_overlay); + }, + + coursewareMsgCompanionOverlay(context, companion_overlay_msg) { + context.commit('coursewareMsgCompanionOverlaySet', companion_overlay_msg); + }, + + coursewareStyleCompanionOverlay(context, companion_overlay_style) { + context.commit('coursewareStyleCompanionOverlaySet', companion_overlay_style); + }, + + coursewareConsumeMode(context, mode) { + context.commit('coursewareConsumeModeSet', mode); + }, + + setHttpClient({ commit }, httpClient) { + commit('setHttpClient', httpClient); + }, + + setUrlHelper({ commit }, urlHelper) { + commit('setUrlHelper', urlHelper); + }, + + setUserId({ commit }, userId) { + commit('setUserId', userId); + }, + + showElementEditDialog(context, bool) { + context.commit('setShowStructuralElementEditDialog', bool) + }, + + showElementAddDialog(context, bool) { + context.commit('setShowStructuralElementAddDialog', bool) + }, + + showElementExportDialog(context, bool) { + context.commit('setShowStructuralElementExportDialog', bool) + }, + + showElementInfoDialog(context, bool) { + context.commit('setShowStructuralElementInfoDialog', bool) + }, + + showElementOerDialog(context, bool) { + context.commit('setShowStructuralElementOerDialog', bool) + }, + + showElementDeleteDialog(context, bool) { + context.commit('setShowStructuralElementDeleteDialog', bool) + }, + + addBookmark({ dispatch, rootGetters }, structuralElement) { + const cw = rootGetters['courseware']; + + // get existing bookmarks + const bookmarks = + rootGetters['courseware-structural-elements/related']({ + parent: cw, + relationship: 'bookmarks', + })?.map(({ type, id }) => ({ type, id })) ?? []; + + // add a new bookmark + const data = [...bookmarks, { type: structuralElement.type, id: structuralElement.id }]; + + // send them home + return dispatch( + `courseware-structural-elements/setRelated`, + { + parent: { type: cw.type, id: cw.id }, + relationship: 'bookmarks', + data, + }, + { root: true } + ); + }, + + removeBookmark({ dispatch, rootGetters }, structuralElement) { + const cw = rootGetters['courseware']; + + // get existing bookmarks + const bookmarks = + rootGetters['courseware-structural-elements/related']({ + parent: cw, + relationship: 'bookmarks', + })?.map(({ type, id }) => ({ type, id })) ?? []; + + // filter bookmark that must be removed + const data = bookmarks.filter(({ id }) => id !== structuralElement.id); + + // send them home + return dispatch( + `courseware-structural-elements/setRelated`, + { + parent: { type: cw.type, id: cw.id }, + relationship: 'bookmarks', + data, + }, + { root: true } + ); + }, + + setPluginManager({ commit }, pluginManager) { + commit('setPluginManager', pluginManager); + }, + + uploadImageForStructuralElement({ dispatch, state }, { structuralElement, file }) { + const formData = new FormData(); + formData.append('image', file); + + const url = `courseware-structural-elements/${structuralElement.id}/image`; + return state.httpClient.post(url, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + }, + + async deleteImageForStructuralElement({ dispatch, state }, structuralElement) { + const url = `courseware-structural-elements/${structuralElement.id}/image`; + await state.httpClient.delete(url); + + return dispatch('loadStructuralElement', structuralElement.id); + }, + + cwManagerFilingData(context, msg) { + context.commit('cwManagerFilingDataSet', msg); + }, + + loadUsersCourses({ dispatch, rootGetters, state }, userId) { + const parent = { + type: 'users', + id: userId, + }; + const relationship = 'course-memberships'; + + const options = { + include: 'course', + }; + + return dispatch('course-memberships/loadRelated', { parent, relationship, options }, { root: true }).then( + () => { + const memberships = rootGetters['course-memberships/related']({ + parent, + relationship, + }); + let courses = []; + memberships.forEach((membership) => { + if ( + membership.attributes.permission === 'dozent' && + state.context.id !== membership.relationships.course.data.id + ) { + courses.push(rootGetters['courses/related']({ parent: membership, relationship: 'course' })); + } + }); + return courses; + } + ); + }, + + loadRemoteCoursewareStructure({ dispatch, rootGetters }, { rangeId, rangeType }) { + const parent = { + id: rangeId, + type: rangeType, + }; + + const relationship = 'courseware'; + + return dispatch(`courseware-instances/loadRelated`, { parent, relationship }, { root: true }).then( + (response) => { + const instance = rootGetters['courseware-instances/related']({ + parent: parent, + relationship: relationship, + }); + + return instance; + }, + (error) => { + return null; + } + ); + }, + + loadTeacherStatus({ dispatch, rootGetters, state, commit, getters }, userId) { + const parent = { + type: 'users', + id: userId, + }; + const relationship = 'course-memberships'; + + const options = { + include: 'course', + }; + + return dispatch('course-memberships/loadRelated', { parent, relationship, options }, { root: true }).then( + () => { + const memberships = rootGetters['course-memberships/related']({ + parent, + relationship, + }); + let isTeacher = false; + memberships.forEach((membership) => { + if (getters.courseware.attributes['editing-permission-level'] === 'dozent') { + if ( + membership.attributes.permission === 'dozent' && + state.context.id === membership.relationships.course.data.id + ) { + isTeacher = true; + } + } + if (getters.courseware.attributes['editing-permission-level'] === 'tutor') { + if ( + (membership.attributes.permission === 'dozent' || + membership.attributes.permission === 'tutor') && + state.context.id === membership.relationships.course.data.id + ) { + isTeacher = true; + } + } + }); + return commit('setUserIsTeacher', isTeacher); + } + ); + }, + + loadFeedback({ dispatch }, blockId) { + const parent = { type: 'courseware-blocks', id: `${blockId}` }; + const relationship = 'feedback'; + const options = { + include: 'user', + }; + + return dispatch('courseware-block-feedback/loadRelated', { parent, relationship, options }, { root: true }); + }, + + async createFeedback({ dispatch }, { blockId, feedback }) { + const data = { + attributes: { + feedback, + }, + relationships: { + block: { + data: { + type: 'courseware-blocks', + id: blockId, + }, + }, + }, + }; + await dispatch('courseware-block-feedback/create', data, { root: true }); + + return dispatch('loadFeedback', blockId); + }, +}; + +/* eslint no-param-reassign: ["error", { "props": false }] */ +export const mutations = { + coursewareSet(state, data) { + state.courseware = data; + }, + + coursewareCurrentElementSet(state, data) { + state.lastElement = state.currentElement; + state.currentElement = data; + }, + + coursewareContextSet(state, data) { + state.context = data; + }, + + oerTitleSet(state, data) { + state.oerTitle = data; + }, + + licensesSet(state, data) { + state.licenses = data; + }, + + coursewareViewModeSet(state, data) { + state.viewMode = data; + }, + + coursewareShowToolbarSet(state, data) { + state.showToolbar = data; + }, + + coursewareBlockAdderSet(state, data) { + state.blockAdder = data; + }, + + coursewareContainerAdderSet(state, data) { + state.containerAdder = data; + }, + + coursewareShowCompanionOverlaySet(state, data) { + state.showCompanionOverlay = data; + }, + + coursewareMsgCompanionOverlaySet(state, data) { + state.msgCompanionOverlay = data; + }, + + coursewareStyleCompanionOverlaySet(state, data) { + state.styleCompanionOverlay = data; + }, + + coursewareConsumeModeSet(state, data) { + state.consumeMode = data; + }, + + setHttpClient(state, httpClient) { + state.httpClient = httpClient; + }, + + setUrlHelper(state, urlHelper) { + state.urlHelper = urlHelper; + }, + + setUserId(state, userId) { + state.userId = userId; + }, + + setUserIsTeacher(state, isTeacher) { + state.userIsTeacher = isTeacher; + }, + + setPluginManager(state, pluginManager) { + state.pluginManager = pluginManager; + }, + + cwManagerFilingDataSet(state, data) { + state.filingData = data; + }, + + setShowStructuralElementEditDialog(state, showEdit) { + state.showStructuralElementEditDialog = showEdit; + }, + + setShowStructuralElementAddDialog(state, showAdd) { + state.showStructuralElementAddDialog = showAdd; + }, + + setShowStructuralElementExportDialog(state, showExport) { + state.showStructuralElementExportDialog = showExport; + }, + + setShowStructuralElementInfoDialog(state, showInfo) { + state.showStructuralElementInfoDialog = showInfo; + }, + + setShowStructuralElementOerDialog(state, showOer) { + state.showStructuralElementOerDialog = showOer; + }, + + setShowStructuralElementDeleteDialog(state, showDelete) { + state.showStructuralElementDeleteDialog = showDelete; + } +}; + +export default { + state, + actions, + mutations, + getters, +}; |
