aboutsummaryrefslogtreecommitdiff
path: root/resources/assets/javascripts/lib
diff options
context:
space:
mode:
authorPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
committerPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
commit4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch)
tree5c07151ae61276d334e88f6309c30d439a85c12e /resources/assets/javascripts/lib
parentda0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff)
parent97a188592c679890a25c37ab78463add76a52ff7 (diff)
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'resources/assets/javascripts/lib')
-rw-r--r--resources/assets/javascripts/lib/RestrictedDatesHelper.ts89
-rw-r--r--resources/assets/javascripts/lib/abstract-api.js69
-rw-r--r--resources/assets/javascripts/lib/activityfeed.js53
-rw-r--r--resources/assets/javascripts/lib/blubber.js1
-rw-r--r--resources/assets/javascripts/lib/clipboard.js145
-rw-r--r--resources/assets/javascripts/lib/dialog.js7
-rw-r--r--resources/assets/javascripts/lib/extract_callback.js5
-rw-r--r--resources/assets/javascripts/lib/fullcalendar.js32
-rw-r--r--resources/assets/javascripts/lib/global_search.js16
-rw-r--r--resources/assets/javascripts/lib/header_magic.js2
-rw-r--r--resources/assets/javascripts/lib/jsonapi.ts (renamed from resources/assets/javascripts/lib/jsonapi.js)8
-rw-r--r--resources/assets/javascripts/lib/messages.js2
-rw-r--r--resources/assets/javascripts/lib/personal_notifications.js5
-rw-r--r--resources/assets/javascripts/lib/questionnaire.js4
-rw-r--r--resources/assets/javascripts/lib/resources.js45
-rw-r--r--resources/assets/javascripts/lib/restapi.js12
-rw-r--r--resources/assets/javascripts/lib/scroll.js59
-rw-r--r--resources/assets/javascripts/lib/scroll_to_top.js22
-rw-r--r--resources/assets/javascripts/lib/search.js12
-rw-r--r--resources/assets/javascripts/lib/tooltip.js227
-rw-r--r--resources/assets/javascripts/lib/wysiwyg.js6
21 files changed, 379 insertions, 442 deletions
diff --git a/resources/assets/javascripts/lib/RestrictedDatesHelper.ts b/resources/assets/javascripts/lib/RestrictedDatesHelper.ts
new file mode 100644
index 0000000..bcc0af2
--- /dev/null
+++ b/resources/assets/javascripts/lib/RestrictedDatesHelper.ts
@@ -0,0 +1,89 @@
+import { jsonapi } from "./jsonapi";
+
+type RestrictedDate = {
+ year: Number,
+ month: Number,
+ day: Number,
+
+ reason: string | null,
+ lock: boolean
+}
+
+class RestrictedDatesHelper
+{
+ static #loadedYears : Number[] = [];
+ static #restrictedDates: RestrictedDate[] = [];
+
+ static isDateRestricted(date: Date, returnBoolean: Boolean = false): RestrictedDate | Boolean {
+ const restrictedDate : RestrictedDate | undefined = this.#restrictedDates.find(item => {
+ return item.year === date.getFullYear()
+ && item.month === date.getMonth() + 1
+ && item.day === date.getDate();
+ });
+
+ if (returnBoolean) {
+ return !!restrictedDate;
+ }
+
+ return restrictedDate ?? this.#convertDate(date, null, false);
+ }
+
+ static async loadRestrictedDatesByYear(year: Number): Promise<void> {
+ if (this.#loadedYears.includes(year)) {
+ return Promise.reject();
+ }
+
+ this.#loadedYears.push(year);
+
+ jsonapi.withPromises().request('holidays', {data: {
+ 'filter[year]': year
+ }}).then((response: [] | Object) => {
+ // Since PHP will return an empty object as an array,
+ // we need to check
+ if (Array.isArray(response)) {
+ return;
+ }
+
+ for (const [date, data] of Object.entries(response)) {
+ this.#addRestrictedDate(
+ new Date(date),
+ data.holiday,
+ data.mandatory
+ );
+ }
+ });
+ }
+
+ static #addRestrictedDate(date: Date, reason: string, lock: boolean = true): void {
+ const restricted = this.#convertDate(date, reason, lock);
+
+ this.#restrictedDates = this.#restrictedDates.filter(item => {
+ return item.year !== restricted.year
+ || item.month !== restricted.month
+ || item.day !== restricted.day;
+ });
+
+ this.#restrictedDates.push(restricted);
+ }
+
+ static removeRestrictedDate(date: Date): void {
+ this.#restrictedDates = this.#restrictedDates.filter(item => {
+ return item.year !== date.getFullYear()
+ || item.month !== date.getMonth() + 1
+ || item.day !== date.getDate();
+ });
+ }
+
+ static #convertDate(date: Date, reason: string | null, lock: boolean): RestrictedDate {
+ return {
+ year: date.getFullYear(),
+ month: date.getMonth() + 1,
+ day: date.getDate(),
+
+ reason,
+ lock
+ };
+ }
+}
+
+export default RestrictedDatesHelper;
diff --git a/resources/assets/javascripts/lib/abstract-api.js b/resources/assets/javascripts/lib/abstract-api.js
index eafca85..95ae015 100644
--- a/resources/assets/javascripts/lib/abstract-api.js
+++ b/resources/assets/javascripts/lib/abstract-api.js
@@ -1,5 +1,20 @@
import Overlay from './overlay.js';
+class APIError extends Error
+{
+ static createWithJqXhr(message, jqXhr) {
+ const error = new APIError(message);
+ error.setJqXhr(jqXhr);
+ return error;
+ }
+
+ jqXhr = null;
+
+ setJqXhr(jqXhr) {
+ this.jqXhr = jqXhr;
+ }
+}
+
class AbstractAPI
{
static get supportedMethods() {
@@ -52,6 +67,8 @@ class AbstractAPI
var deferred;
+ const request = this.#createRequest(url, options);
+
if (options.async && this.request_count > 0) {
// Request should be sent asynchronous after every other request
// is finished. The configuration for this particular request is
@@ -73,10 +90,10 @@ class AbstractAPI
this.total_requests += 1;
// Actual request
- deferred = $.ajax(STUDIP.URLHelper.getURL(`${this.base_url}/${url}`, {}, true), {
+ deferred = $.ajax(request.url, {
contentType: options.contentType || 'application/x-www-form-urlencoded; charset=UTF-8',
method: options.method.toUpperCase(),
- data: this.encodeData(options.data, options.method.toUpperCase()),
+ data: this.encodeData(request.data, options.method.toUpperCase()),
headers: options.headers
}).always(() => {
// Decrease request counter, remove overlay if neccessary
@@ -93,6 +110,54 @@ class AbstractAPI
}
}).promise();
}
+
+ #createRequest(url, options) {
+ const hasBody = ['post', 'put', 'patch'].includes(options.method.toLowerCase());
+ const query = hasBody ? '' : `?${this.convertDataToRequestParameters(options.data)}`;
+
+ return {
+ url: STUDIP.URLHelper.getURL(`${this.base_url}/${url}${query}`, {}, true),
+ data: hasBody ? options.data : {},
+ };
+ }
+
+ convertDataToRequestParameters(data, prefix = '') {
+ return Object.entries(data).filter(([key, value]) => {
+ return value !== null;
+ }).map(([key, value]) => {
+ const name = prefix ? `${prefix}[${key}]` : `${key}`;
+ if (value.constructor?.name === 'Object') {
+ return this.convertDataToRequestParameters(value, name);
+ } else {
+ return `${name}=${value}`;
+ }
+ }).join('&');
+ }
+
+ withPromises() {
+ return new Proxy(this, {
+ get(target, prop, receiver) {
+ // This will allow http methods to be written as lowercase when called as methods
+ // (e.g. api.patch() instead of api.PATCH())
+ if (target[prop] === undefined && AbstractAPI.supportedMethods.includes(prop.toUpperCase())) {
+ prop = prop.toUpperCase();
+ }
+
+ // Only handle calls to request methods
+ if (prop !== 'request') {
+ return Reflect.get(target, prop, receiver);
+ }
+
+ // Return a wrapped promise that handles the deferred
+ return (url, options = {}) => new Promise((resolve, reject) => {
+ target[prop].apply(target, [url, options]).then(
+ (response) => resolve(response),
+ (jqXhr, textStatus, errorThrown) => reject(APIError.createWithJqXhr(errorThrown || textStatus, jqXhr))
+ );
+ });
+ }
+ })
+ }
}
// Create shortcut methods for easier access by method
diff --git a/resources/assets/javascripts/lib/activityfeed.js b/resources/assets/javascripts/lib/activityfeed.js
index 74c27f9..12f0bac 100644
--- a/resources/assets/javascripts/lib/activityfeed.js
+++ b/resources/assets/javascripts/lib/activityfeed.js
@@ -6,13 +6,13 @@ const ActivityFeed = {
maxheight: null,
filter: null,
- init: function() {
+ init() {
STUDIP.ActivityFeed.maxheight = parseInt($('#stream-container').css('max-height').replace(/[^-\d.]/g, ''));
STUDIP.ActivityFeed.loadFeed(STUDIP.ActivityFeed.filter);
- $('#stream-container').scroll(function () {
- var scrollBottom = $('#stream-container').scrollTop() + $('#stream-container').height() + 250;
+ $('#stream-container').scroll(() => {
+ const scrollBottom = $('#stream-container').scrollTop() + $('#stream-container').height() + 250;
if ($('#stream-container').prop('scrollHeight') < scrollBottom) {
STUDIP.ActivityFeed.loadFeed(STUDIP.ActivityFeed.filter);
@@ -23,7 +23,7 @@ const ActivityFeed = {
$(document).on('click', '.provider_circle', function () {
$(this).parent().parent().children('.activity-content').toggle();
}).on('click', '#toggle-all-activities,#toggle-user-activities', function () {
- var toggled = $(this).is(':not(.toggled)');
+ const toggled = $(this).is(':not(.toggled)');
$(this).toggleClass('toggled', toggled);
STUDIP.ActivityFeed.setToggleStatus();
@@ -32,11 +32,11 @@ const ActivityFeed = {
});
},
- getTemplate: _.memoize(function(name) {
- return _.template($("script." + name).html());
+ getTemplate: _.memoize(name => {
+ return _.template($(`script.${name}`).html());
}),
- loadFeed: function(filtertype) {
+ loadFeed(filtertype) {
if (STUDIP.ActivityFeed.user_id === null) {
console.log('Could not retrieve activities, no valid user id found!');
return false;
@@ -48,17 +48,18 @@ const ActivityFeed = {
STUDIP.ActivityFeed.polling = true;
- STUDIP.api.GET(['user', STUDIP.ActivityFeed.user_id, 'activitystream'], {
- data: {
- filtertype: JSON.stringify(filtertype),
- scrollfrom: STUDIP.ActivityFeed.scrolledfrom
- }
- }).done(function (activities) {
- var stream = STUDIP.ActivityFeed.getTemplate('activity_stream');
- var activity = STUDIP.ActivityFeed.getTemplate('activity');
- var activity_urls = STUDIP.ActivityFeed.getTemplate('activity-urls');
- var num_entries = Object.keys(activities).length;
- var lastelem = $(activities).last();
+ const url = STUDIP.URLHelper.getURL('dispatch.php/activityfeed/load', {
+ filtertype: JSON.stringify(filtertype),
+ scrollfrom: STUDIP.ActivityFeed.scrolledfrom,
+ });
+ fetch(url).then(
+ response => response.json(),
+ ).then(activities => {
+ const stream = STUDIP.ActivityFeed.getTemplate('activity_stream');
+ const activity = STUDIP.ActivityFeed.getTemplate('activity');
+ const activity_urls = STUDIP.ActivityFeed.getTemplate('activity-urls');
+ const num_entries = Object.keys(activities).length;
+ const lastelem = $(activities).last();
if (lastelem[0]) {
STUDIP.ActivityFeed.scrolledfrom = lastelem[0].mkdate;
@@ -79,15 +80,15 @@ const ActivityFeed = {
if ($('#stream-container').height() < STUDIP.ActivityFeed.maxheight) {
STUDIP.ActivityFeed.loadFeed('');
}
- }).fail(function () {
- var template = STUDIP.ActivityFeed.getTemplate('activity-load-error');
+ }).catch(() => {
+ const template = STUDIP.ActivityFeed.getTemplate('activity-load-error');
STUDIP.ActivityFeed.writeToStream(template());
- }).always(function () {
+ }).finally(() => {
STUDIP.ActivityFeed.polling = false;
});
},
- writeToStream: function (html) {
+ writeToStream(html) {
if (STUDIP.ActivityFeed.initial) {
// replace data in DOM
$('#stream-container').html('');
@@ -98,9 +99,9 @@ const ActivityFeed = {
$('#stream-container').append(html);
},
- setToggleStatus: function() {
- var show_details = $('#toggle-all-activities').is('.toggled'),
- show_own = $('#toggle-user-activities').is('.toggled');
+ setToggleStatus() {
+ const show_details = $('#toggle-all-activities').is('.toggled');
+ const show_own = $('#toggle-user-activities').is('.toggled');
// update toggle status fir activity contents
$('.activity-content').toggle(show_details);
@@ -109,7 +110,7 @@ const ActivityFeed = {
$('.activity:has(.provider_circle.right)').toggle(show_own);
},
- updateFilter: function(filter) {
+ updateFilter(filter) {
STUDIP.ActivityFeed.filter = filter;
STUDIP.ActivityFeed.initial = true;
STUDIP.ActivityFeed.scrolledfrom = Math.floor(Date.now() / 1000);
diff --git a/resources/assets/javascripts/lib/blubber.js b/resources/assets/javascripts/lib/blubber.js
index 3e93985..4e724b4 100644
--- a/resources/assets/javascripts/lib/blubber.js
+++ b/resources/assets/javascripts/lib/blubber.js
@@ -47,6 +47,7 @@ const Blubber = {
subscribe: follow,
});
}).then(() => {
+ elements.attr('aria-pressed', follow ? 'true' : 'false');
elements.toggleClass('unfollowed', !follow);
}).finally(() => {
elements.removeClass('loading');
diff --git a/resources/assets/javascripts/lib/clipboard.js b/resources/assets/javascripts/lib/clipboard.js
index e5890ab..6fd1489 100644
--- a/resources/assets/javascripts/lib/clipboard.js
+++ b/resources/assets/javascripts/lib/clipboard.js
@@ -1,4 +1,14 @@
-import {$gettext} from './gettext';
+function extractAttribute(node, attribute) {
+ return node.querySelector(`input[name="${attribute}"]`)?.value.trim();
+}
+
+function extractAttributes(node, attributes) {
+ const result = {};
+ for (const key of attributes) {
+ result[key] = extractAttribute(node, key);
+ }
+ return result;
+}
const Clipboard = {
switchClipboard: function(event) {
@@ -32,32 +42,30 @@ const Clipboard = {
}
},
- handleAddForm: function(event) {
- if (!event) {
- return false;
- }
-
+ handleAddForm(event) {
+ event.preventDefault();
+ const attributes = extractAttributes(event.target, ['name', 'allowed_item_class']);
//Check if a name is entered in the form:
- let name_input = jQuery(event.target).find('input[type="text"][name="name"]');
+ const name_input = event.target.querySelector('input[name="name"]');
if (!name_input) {
//Something is wrong with the HTML:
return false;
}
- let name = jQuery(name_input).val().trim();
- if (!name) {
+ if (!attributes.name) {
//The name field is empty. Why send an empty field?
return false;
}
- //Submit the form via AJAX:
- STUDIP.api.POST(
- 'clipboard/add',
- {
- data: jQuery(event.target).serialize()
- }
- ).done(STUDIP.Clipboard.add);
+ // Submit the form via AJAX:
+ STUDIP.jsonapi.POST('clipboards', {data: {data: {attributes}}}).done(({data}) => {
+ STUDIP.Clipboard.add({
+ id: data.id,
+ name: data.attributes.name,
+ widget_id: extractAttribute(event.target, 'widget_id')
+ });
+ });
},
add: function(data) {
@@ -134,11 +142,9 @@ const Clipboard = {
jQuery(widget_node).find('#clipboard-group-container').removeClass('invisible');
//Call the droppable jQuery method on the new clipboard area:
- jQuery(clipboard_node).droppable(
- {
- drop: STUDIP.Clipboard.handleItemDrop
- }
- );
+ jQuery(clipboard_node).droppable({
+ drop: STUDIP.Clipboard.handleItemDrop
+ });
//Clear the text input in the "add clipboard" form:
jQuery(widget_node).find(
@@ -238,17 +244,19 @@ const Clipboard = {
}
//Add the item to the clipboard via AJAX:
- STUDIP.api.POST(
- 'clipboard/' + clipboard_id + '/item',
- {
+ STUDIP.jsonapi.POST(`clipboards/${clipboard_id}/items`, {
+ data: {
data: {
- 'range_id': range_id,
- 'range_type': range_type,
- 'widget_id': widget_id
+ attributes: { range_id, range_type }
}
}
- ).done(function(data) {
- STUDIP.Clipboard.addDroppedItem(data);
+ }).done(({data}) => {
+ STUDIP.Clipboard.addDroppedItem({
+ id: data.id,
+ name: data.attributes.name,
+ range_id: data.attributes.range_id,
+ widget_id
+ });
});
},
@@ -263,6 +271,7 @@ const Clipboard = {
let widget = jQuery('#ClipboardWidget_' + response_data['widget_id']);
let clipboard_id = jQuery(widget).find(".clipboard-selector").val();
+
if (!widget) {
//The widget with the speicified widget-ID
//is not present on the current page.
@@ -302,7 +311,6 @@ const Clipboard = {
jQuery(new_item_node).removeClass('invisible');
let name_column = jQuery(new_item_node).find('td.item-name');
- console.log(name_column);
jQuery('<span/>').text(response_data['name']).appendTo(name_column)
let id_field = jQuery(new_item_node).find("input[name='selected_clipboard_items[]']");
jQuery(id_field).val(checkbox_id);
@@ -325,25 +333,16 @@ const Clipboard = {
);
},
- rename: function(widget_id) {
- if (!widget_id) {
- //Required data are missing!
- return;
- }
+ rename(widget_id) {
+ const widget = jQuery('#ClipboardWidget_' + widget_id);
+ const clipboard_id = widget.find('.clipboard-selector').val();
+ const name = widget.find('input.clipboard-name').val();
- let widget = jQuery('#ClipboardWidget_' + widget_id);
- let clipboard_id = jQuery(widget).find(".clipboard-selector").val();
- let namer = jQuery(widget).find("input.clipboard-name");
-
- STUDIP.api.PUT(
- 'clipboard/' + clipboard_id,
- {
- data: {
- name: namer.val()
- }
- }
- ).done(function(data) {
- STUDIP.Clipboard.update(data, widget_id)
+ STUDIP.jsonapi.PATCH(`clipboards/${clipboard_id}`, {data: {data: {attributes: {name}}}}).done(({data}) => {
+ STUDIP.Clipboard.update({
+ id: data.id,
+ name: data.attributes.name,
+ }, widget_id)
});
},
@@ -358,7 +357,7 @@ const Clipboard = {
STUDIP.Clipboard.toggleEditButtons(widget_id);
},
- remove: function(clipboard_id, widget_id) {
+ remove(clipboard_id, widget_id) {
if (!clipboard_id || !widget_id) {
//Required data are missing!
return;
@@ -427,10 +426,6 @@ const Clipboard = {
},
handleRemoveClick: function(delete_icon) {
- if (!delete_icon) {
- return;
- }
-
//Get the data of the clipboard:
let clipboard_select = jQuery(delete_icon).siblings('.clipboard-selector')[0];
if (!clipboard_select) {
@@ -444,52 +439,42 @@ const Clipboard = {
//Another case where something is wrong with the HTML.
return;
}
- let widget_id = jQuery(widget).data('widget_id');
- STUDIP.api.DELETE(
- 'clipboard/' + clipboard_id,
- {
- data: {
- widget_id: widget_id
- }
- }
- ).done(function() {
+ const widget_id = jQuery(widget).data('widget_id');
+
+ STUDIP.jsonapi.DELETE(`clipboards/${clipboard_id}`).done(() => {
STUDIP.Clipboard.remove(clipboard_id, widget_id);
});
},
- removeItem: function(delete_icon) {
- if (!delete_icon) {
- return;
- }
-
- //Get the item-ID:
- let item_html = jQuery(delete_icon).parents('tr');
- let range_id = jQuery(item_html).data('range_id');
- let clipboard_element = jQuery(item_html).parents('table');
- let clipboard_id = jQuery(clipboard_element).data('id');
+ removeItem(delete_icon) {
+ // Get the item-ID:
+ const item_element = jQuery(delete_icon).parents('tr');
+ const range_id = jQuery(item_element).data('range_id');
+ const clipboard_element = jQuery(item_element).parents('table');
+ const clipboard_id = jQuery(clipboard_element).data('id');
if (!range_id || !clipboard_id) {
//We cannot proceed without the item-ID and the clipboard-ID!
return;
}
- STUDIP.api.DELETE(
- 'clipboard/' + clipboard_id + '/item/' + range_id
- ).done(function() {
+ STUDIP.jsonapi.DELETE(`clipboards/${clipboard_id}/items`, {
+ data: {
+ filter: { range_id }
+ }
+ }).done(() => {
//Check if the item has siblings:
- let siblings = jQuery(item_html).siblings();
+ let siblings = item_element.siblings();
if (siblings.length < 3) {
//Only the "no items" element and the template
//are siblings of the item.
//We must display the "no items" element:
- jQuery(item_html).siblings(
- '.empty-clipboard-message'
- ).removeClass('invisible');
+ item_element.siblings('.empty-clipboard-message').removeClass('invisible');
jQuery("#clipboard-group-container").find('.widget-links').addClass('invisible');
}
//Finally remove the item:
- jQuery(item_html).remove();
+ item_element.remove();
});
},
diff --git a/resources/assets/javascripts/lib/dialog.js b/resources/assets/javascripts/lib/dialog.js
index b5cab54..8f6e50a 100644
--- a/resources/assets/javascripts/lib/dialog.js
+++ b/resources/assets/javascripts/lib/dialog.js
@@ -386,7 +386,12 @@ Dialog.show = function(content, options = {}) {
.before(element);
}
- $(this).parent().find('.ui-dialog-title').attr('title', options.title);
+ $(this).parent().find('.ui-dialog-title').attr({
+ title: options.title,
+ role: 'heading',
+ 'aria-level': 2
+ });
+ $(this).parents('.studip-dialog').attr('aria-modal', 'true');
instance.open = true;
// Execute scripts
diff --git a/resources/assets/javascripts/lib/extract_callback.js b/resources/assets/javascripts/lib/extract_callback.js
index bf7ac79..a630275 100644
--- a/resources/assets/javascripts/lib/extract_callback.js
+++ b/resources/assets/javascripts/lib/extract_callback.js
@@ -56,8 +56,9 @@ export default function extractCallback(cmd, payload, root = window) {
}
}
- if (callback[chunk] === undefined) {
- throw 'Error: Undefined callback ' + cmd;
+ if (callback === null || callback[chunk] === undefined) {
+ console.log('Error: Undefined callback ' + cmd);
+ return;
}
if (typeof callback[chunk] === 'function' && parameters !== null) {
diff --git a/resources/assets/javascripts/lib/fullcalendar.js b/resources/assets/javascripts/lib/fullcalendar.js
index 49274f4..5b7d032 100644
--- a/resources/assets/javascripts/lib/fullcalendar.js
+++ b/resources/assets/javascripts/lib/fullcalendar.js
@@ -622,8 +622,13 @@ class Fullcalendar
$('.fc-slats tr:odd .fc-widget-content:not(.fc-axis)').remove();
}
- STUDIP.api.GET(`semester/${timestamp}/week`).done((data) => {
+ if (document.getElementById('booking-plan-header-semname') === null) {
+ return;
+ }
+ $.getJSON(
+ STUDIP.URLHelper.getURL(`dispatch.php/resources/ajax/semester_week/${timestamp}`)
+ ).done((data) => {
if (data) {
$('#booking-plan-header-semname').text(data.semester_name);
if (data.sem_week) {
@@ -640,7 +645,7 @@ class Fullcalendar
$('#booking-plan-header-semrow').hide();
$('#booking-plan-header-semweek-part').hide();
}
- })
+ });
},
resourceRender (renderInfo) {
if ($(renderInfo.view.context.calendar.el).hasClass('room-group-booking-plan')) {
@@ -731,12 +736,31 @@ class Fullcalendar
//Get the timestamp:
let timestamp = changedMoment.getTime() / 1000;
- jQuery('a.resource-bookings-actions').each(function () {
+ jQuery('a.resource-bookings-actions, a.calendar-action').each(function () {
const url = new URL(this.href);
- url.searchParams.set('timestamp', timestamp)
+ url.searchParams.set('timestamp', timestamp.toString())
url.searchParams.set('defaultDate', changed_date)
this.href = url.toString();
});
+ jQuery('.sidebar-widget.calendar-action').each(function() {
+ //Each sidebar widget is different. The placement of the defaultDate URL parameter
+ //has to reflect that.
+ jQuery(this).find('button[formaction]').each(function() {
+ //Modify the formaction attribute:
+ let url = new URL(jQuery(this).attr('formaction'));
+ url.searchParams.set('defaultDate', changed_date);
+ jQuery(this).attr('formaction', url.toString());
+ });
+ jQuery(this).find('form[action]').each(function() {
+ //Add a hidden input with the defaultDate:
+ let hidden_input = jQuery(this).find('input[name="defaultDate"]')[0];
+ if (!hidden_input) {
+ hidden_input = jQuery('<input type="hidden" name="defaultDate">');
+ jQuery(this).append(hidden_input);
+ }
+ jQuery(hidden_input).val(changed_date);
+ });
+ });
// Now change the URL of the window.
const url = new URL(window.location.href);
diff --git a/resources/assets/javascripts/lib/global_search.js b/resources/assets/javascripts/lib/global_search.js
index dbd045b..394b7e3 100644
--- a/resources/assets/javascripts/lib/global_search.js
+++ b/resources/assets/javascripts/lib/global_search.js
@@ -9,6 +9,7 @@ const GlobalSearch = {
*/
toggleSearchBar: function(visible, cleanup) {
$('#globalsearch-searchbar').toggleClass('is-visible', visible);
+ $('#globalsearch-input').attr('aria-expanded', visible ? 'true' : 'false');
$('#globalsearch-input').toggleClass('hidden-small-down', !visible);
$('#globalsearch-icon').toggleClass('hidden-small-down', visible);
$('#globalsearch-clear').toggleClass('hidden-small-down', !visible);
@@ -70,7 +71,7 @@ const GlobalSearch = {
// Iterate over each result category.
$.each(json, function(name, value) {
// Create an <article> for category.
- var category = $(`<article id="globalsearch-${name}">`),
+ var category = $(`<article id="globalsearch-${name}" role="list">`),
header = $('<header>').appendTo(category),
counter = 0;
@@ -96,7 +97,7 @@ const GlobalSearch = {
// Process results and create corresponding entries.
$.each(value.content, function(index, result) {
// Create single result entry.
- var single = $('<section>'),
+ var single = $(`<a href="${result.url}" role="listitem" ${dataDialog}>`),
data = $('<div class="globalsearch-result-data">'),
details = $('<div class="globalsearch-result-details">');
@@ -107,17 +108,17 @@ const GlobalSearch = {
// Which result types should be opened via dialog?
const openInDialog = ['GlobalSearchFiles', 'GlobalSearchMessages'];
var dataDialog = (openInDialog.indexOf(name) >= 0 ? dataDialog = 'data-dialog' : dataDialog = '');
- var link = $(`<a href="${result.url}" ${dataDialog}>`).appendTo(single);
+ //var link = $(`<a href="${result.url}" ${dataDialog}>`).appendTo(single);
// Optional image...
if (result.img !== null) {
- $(`<img src="${result.img}">`)
+ $(`<img src="${result.img}" alt="">`)
.wrap('<div class="globalsearch-result-img">')
.parent() // Element is now the wrapper
- .appendTo(link);
+ .appendTo(single);
}
- link.append(data);
+ single.append(data);
// Name/title
$('<div class="globalsearch-result-title">')
@@ -144,7 +145,7 @@ const GlobalSearch = {
if (result.date !== null) {
$('<div class="globalsearch-result-time">')
.html(result.date)
- .appendTo(link);
+ .appendTo(single);
}
// "Expand" attribute for further, result-related search
@@ -178,6 +179,7 @@ const GlobalSearch = {
GlobalSearch.lastSearch = null;
$('#globalsearch-searchbar').removeClass('is-visible has-value');
+ $('#globalsearch-input').attr('aria-expanded', 'false');
$('#globalsearch-input').val('');
$('#globalsearch-results').html('');
$('#globalsearch-input').focus();
diff --git a/resources/assets/javascripts/lib/header_magic.js b/resources/assets/javascripts/lib/header_magic.js
index f465e7e..a581107 100644
--- a/resources/assets/javascripts/lib/header_magic.js
+++ b/resources/assets/javascripts/lib/header_magic.js
@@ -17,7 +17,7 @@ const scroll = function(scrolltop) {
const HeaderMagic = {
enable() {
fold = $('#navigation-level-1').height();
- Scroll.addHandler('header', scroll);
+ Scroll.addHandler('header', scroll, true);
},
disable() {
Scroll.removeHandler('header');
diff --git a/resources/assets/javascripts/lib/jsonapi.js b/resources/assets/javascripts/lib/jsonapi.ts
index f3217bc..80176cc 100644
--- a/resources/assets/javascripts/lib/jsonapi.js
+++ b/resources/assets/javascripts/lib/jsonapi.ts
@@ -3,11 +3,11 @@ import AbstractAPI from './abstract-api.js';
// Actual JSONAPI object
class JSONAPI extends AbstractAPI
{
- constructor(version = 1) {
+ constructor(version: number = 1) {
super(`jsonapi.php/v${version}`);
}
- encodeData (data, method) {
+ encodeData (data: any, method: string): any {
data = super.encodeData(data);
if (['DELETE', 'GET', 'HEAD'].includes(method)) {
@@ -21,11 +21,11 @@ class JSONAPI extends AbstractAPI
return JSON.stringify(data);
}
- request (url, options = {}) {
+ request (url: string, options: any = {}) {
options.contentType = 'application/vnd.api+json';
return super.request(url, options);
}
}
export default JSONAPI;
-export const jsonapi = new JSONAPI();
+export const jsonapi: JSONAPI = new JSONAPI();
diff --git a/resources/assets/javascripts/lib/messages.js b/resources/assets/javascripts/lib/messages.js
index 7ce5328..8e27f8f 100644
--- a/resources/assets/javascripts/lib/messages.js
+++ b/resources/assets/javascripts/lib/messages.js
@@ -252,6 +252,8 @@ const Messages = {
if (jQuery('#' + name).is(':visible')) {
jQuery('#' + name)[0].scrollIntoView(false);
}
+ jQuery('#toggle-' + name)
+ .attr('aria-expanded', jQuery('#toggle-' + name).attr('aria-expanded') !== 'true');
}
};
diff --git a/resources/assets/javascripts/lib/personal_notifications.js b/resources/assets/javascripts/lib/personal_notifications.js
index 90f1053..392e8b0 100644
--- a/resources/assets/javascripts/lib/personal_notifications.js
+++ b/resources/assets/javascripts/lib/personal_notifications.js
@@ -116,6 +116,11 @@ const PersonalNotifications = {
.click(STUDIP.PersonalNotifications.activate);
}
}
+
+ // Special handling for personal notifications:
+ $('#notification-container').on('mouseover mouseout', function (event) {
+ $(this).attr('aria-expanded', $(this).attr('aria-expanded') === 'true' ? 'false' : 'true');
+ });
},
activate () {
Promise.resolve(Notification.requestPermission()).then(permission => {
diff --git a/resources/assets/javascripts/lib/questionnaire.js b/resources/assets/javascripts/lib/questionnaire.js
index 2bca8c6..9a89348 100644
--- a/resources/assets/javascripts/lib/questionnaire.js
+++ b/resources/assets/javascripts/lib/questionnaire.js
@@ -86,7 +86,7 @@ const Questionnaire = {
}
$.post(STUDIP.URLHelper.getURL('dispatch.php/questionnaire/store/' + (this.data.id || '')), {
questionnaire: data,
- questions_data: questions,
+ questions_data: JSON.stringify(questions),
range_type: this.range_type,
range_id: this.range_id
}).done(() => {
@@ -112,7 +112,7 @@ const Questionnaire = {
id: id,
questiontype: this.questions[i].questiontype,
internal_name: this.questions[i].internal_name,
- questiondata: Object.assign({}, this.questions[i].questiondata)
+ questiondata: JSON.parse(JSON.stringify(this.questions[i].questiondata)),
});
this.activeTab = id;
},
diff --git a/resources/assets/javascripts/lib/resources.js b/resources/assets/javascripts/lib/resources.js
index 3287b42..6ff4156 100644
--- a/resources/assets/javascripts/lib/resources.js
+++ b/resources/assets/javascripts/lib/resources.js
@@ -50,7 +50,7 @@ class Resources
jQuery(row_tds[user_td_index]).children('input').removeAttr('disabled');
if (username) {
- jQuery(row_tds[user_td_index]).append(username);
+ jQuery('<span>').text(username).appendTo(row_tds[user_td_index]);
} else {
jQuery(row_tds[user_td_index]).append('ID ' + user_id);
}
@@ -60,8 +60,6 @@ class Resources
}
jQuery(user_id_input).val(user_id);
- var perm_select = jQuery(row_tds[user_td_index + 1]).children()[0];
-
if (temp_perms_row) {
//Set the time input fields to useful values:
@@ -134,22 +132,19 @@ class Resources
jQuery(table_element).trigger('update');
};
- STUDIP.api.GET(
- `user/${user_id}`
- ).done(function (data) {
- var username = data.name.family
- + ', '
- + data.name.given;
- if (data.name.prefix) {
- username += ', ' + data.name.prefix;
+ STUDIP.jsonapi.GET(`users/${user_id}`).done(data => {
+ const attributes = data.data.attributes;
+
+ let username = `${attributes['family-name']}, ${attributes['given-name']}`;
+ if (attributes['name-prefix']) {
+ username += `, ${attributes['name-prefix']}`;
}
- if (data.name.suffix) {
- username += ' ' + data.name.suffix;
+ if (attributes['name-suffix']) {
+ username += ` ${attributes['name-suffix']}`;
}
- username += ' (' + data.name.username + ')'
- + ' (' + data.perms + ')';
+ username += ` (${attributes.username}) (${attributes.permission})`;
insert_function(user_id, username);
- }).fail(function () {
+ }).fail(() => {
insert_function(user_id);
});
}
@@ -160,23 +155,13 @@ class Resources
return;
}
- STUDIP.api.GET(
- `course/${course_id}/members`,
- {
- data: {
- //The limit '0' results in a division by zero.
- //Hopefully, the limit is set to a value high enough:
- limit: 1000000
- }
- }
- ).done(function (data) {
- for (var attribute in data.collection) {
- var user_id = data.collection[attribute].member.id;
+ STUDIP.jsonapi.GET(`courses/${course_id}/memberships`, {data: {page: {limit: 1000000}}}).done(data => {
+ data.data.forEach(membership => {
STUDIP.Resources.addUserToPermissionList(
- user_id,
+ membership.relationships.user.data.id,
table_element
);
- }
+ });
});
}
diff --git a/resources/assets/javascripts/lib/restapi.js b/resources/assets/javascripts/lib/restapi.js
deleted file mode 100644
index b6e31df..0000000
--- a/resources/assets/javascripts/lib/restapi.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import AbstractAPI from './abstract-api.js';
-
-// Actual RESTAPI object
-class RESTAPI extends AbstractAPI
-{
- constructor() {
- super('api.php');
- }
-}
-
-export default RESTAPI;
-export const api = new RESTAPI();
diff --git a/resources/assets/javascripts/lib/scroll.js b/resources/assets/javascripts/lib/scroll.js
index a4d24d5..f4ffc66 100644
--- a/resources/assets/javascripts/lib/scroll.js
+++ b/resources/assets/javascripts/lib/scroll.js
@@ -6,49 +6,54 @@
* Updates/calls to the callback are synchronized to screen refresh by using
* the animation frame method (which will fallback to a timer based solution).
*/
-var handlers = {};
-var animId = false;
+const handlers = {};
+let animId = false;
-var lastTop = null;
-var lastLeft = null;
-
-function scrollHandler() {
- var scrollTop = $(document).scrollTop();
- var scrollLeft = $(document).scrollLeft();
-
- if (scrollTop !== lastTop || scrollLeft !== lastLeft) {
- $.each(handlers, function(index, handler) {
- handler(scrollTop, scrollLeft);
- });
-
- lastTop = scrollTop;
- lastLeft = scrollLeft;
- }
-
- animId = false;
-
- engageScrollTrigger();
-}
+let lastTop = null;
+let lastLeft = null;
function refresh() {
- var hasHandlers = !$.isEmptyObject(handlers);
+ const hasHandlers = Object.keys(handlers).length > 0;
if (!hasHandlers && animId !== false) {
window.cancelAnimationFrame(animId);
animId = false;
} else if (hasHandlers && animId === false) {
- animId = window.requestAnimationFrame(scrollHandler);
+ animId = window.requestAnimationFrame(() => Scroll.executeHandlers());
}
}
function engageScrollTrigger() {
- $(window).off('scroll.studip-handler');
- $(window).one('scroll.studip-handler', refresh);
+ window.removeEventListener('scroll', refresh);
+ window.addEventListener('scroll', refresh, {once: true});
}
const Scroll = {
- addHandler(index, handler) {
+ executeHandlers(only_these = []) {
+ const scrollTop = document.scrollingElement.scrollTop;
+ const scrollLeft = document.scrollingElement.scrollLeft;
+
+ if (scrollTop !== lastTop || scrollLeft !== lastLeft) {
+ for (const [index, handler] of Object.entries(handlers)) {
+ if (only_these.length === 0 || only_these.includes(index)) {
+ handler(scrollTop, scrollLeft);
+ }
+ }
+
+ lastTop = scrollTop;
+ lastLeft = scrollLeft;
+ }
+
+ animId = false;
+
+ engageScrollTrigger();
+ },
+ addHandler(index, handler, immediate = false) {
handlers[index] = handler;
engageScrollTrigger();
+
+ if (immediate) {
+ Scroll.executeHandlers([index]);
+ }
},
removeHandler(index) {
delete handlers[index];
diff --git a/resources/assets/javascripts/lib/scroll_to_top.js b/resources/assets/javascripts/lib/scroll_to_top.js
index 2a75402..9b0d3ee 100644
--- a/resources/assets/javascripts/lib/scroll_to_top.js
+++ b/resources/assets/javascripts/lib/scroll_to_top.js
@@ -4,9 +4,9 @@ let fold;
let was_below_the_fold = false;
const back_to_top = function(scrolltop) {
- var is_below_the_fold = scrolltop > fold;
+ let is_below_the_fold = scrolltop > fold;
if (is_below_the_fold !== was_below_the_fold) {
- $('#scroll-to-top').toggleClass('hide', !is_below_the_fold);
+ document.getElementById('scroll-to-top').classList.toggle('hide', !is_below_the_fold);
was_below_the_fold = is_below_the_fold;
}
};
@@ -23,15 +23,21 @@ const ScrollToTop = {
},
disable() {
Scroll.removeHandler('header');
- $('#scroll-to-top').addClass('hide');
+ document.getElementById('scroll-to-top').classList.add('hide');
},
moveBack() {
- $('#scroll-to-top').on('click', function(e) {
- $('html, body').stop().animate({
- scrollTop: (0)
- }, 1000, 'easeInOutExpo');
- e.preventDefault();
+ document.getElementById('scroll-to-top').addEventListener('click', (evt) => {
+ evt.preventDefault();
+ this.toTop();
});
+ document.getElementById('scroll-to-top').addEventListener('keypress', (evt) => {
+ if (evt.code === 'Space') {
+ this.toTop();
+ }
+ });
+ },
+ toTop() {
+ window.scroll({top: 0, left: 0, behavior: 'smooth'});
}
};
diff --git a/resources/assets/javascripts/lib/search.js b/resources/assets/javascripts/lib/search.js
index 5d39f43..f8108cd 100644
--- a/resources/assets/javascripts/lib/search.js
+++ b/resources/assets/javascripts/lib/search.js
@@ -200,7 +200,7 @@ const Search = {
// Optional image...
if (result.img !== null) {
$('<div class="search-result-img hidden-tiny-down">')
- .append(`<img src="${result.img}">`)
+ .append(`<img src="${result.img}" alt="">`)
.appendTo(link);
}
@@ -339,7 +339,8 @@ const Search = {
* Hide all select filters in the sidebar.
*/
hideAllFilters: function () {
- $('div[id$="_filter"]').hide();
+ $('#filter_widget').hide();
+ $('#filter_widget *[id$="_filter"]').hide();
},
/**
@@ -350,12 +351,11 @@ const Search = {
showFilter: function (category) {
var filters = $('#search-results').data('filters');
STUDIP.Search.hideAllFilters();
- if (filters && filters[category] !== undefined && category != 'show_all_categories') {
+ if (filters && filters[category] !== undefined && filters[category].length > 0) {
+ $('#filter_widget').show();
for (let i = 0; i < filters[category].length; i++) {
$(`#${filters[category][i]}_filter`).show();
}
- } else if (category === 'show_all_categories') {
- $('#semester_filter').show();
}
},
@@ -547,7 +547,7 @@ const Search = {
if (item != 'category') {
var value = filter[item];
if (value.trim()) {
- var name = $(`#${item}_filter .sidebar-widget-header`).text().trim();
+ var name = $(`#${item}_filter .label-text`).text().trim();
var value_text = $(`#${item}_select option:selected`).text().trim();
var filterItem = $('<button></button>').addClass('button remove-filter').text(name + ': ' + value_text).attr('data-filter-name', item);
filterItem.on('click', function () {
diff --git a/resources/assets/javascripts/lib/tooltip.js b/resources/assets/javascripts/lib/tooltip.js
deleted file mode 100644
index 2cdac27..0000000
--- a/resources/assets/javascripts/lib/tooltip.js
+++ /dev/null
@@ -1,227 +0,0 @@
-import CSS from './css.js';
-
-/**
- * Tooltip library for Stud.IP
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @copyright Stud.IP Core Group 2014
- * @license GPL2 or any later version
- * @since Stud.IP 3.1
- */
-
-let count = 0;
-let threshold = 0;
-
-class Tooltip {
- static get count() {
- return count;
- }
-
- static set count(value) {
- count = value;
- }
-
- // Threshold used for "edge detection" (imagine a padding along the edges)
- static get threshold() {
- return threshold;
- }
-
- static set threshold(value) {
- threshold = value;
- }
-
- /**
- * Returns a new unique id of a tooltip.
- *
- * @return {string} Unique id
- * @static
- */
- static getId() {
- const id = `studip-tooltip-${Tooltip.count}`;
- Tooltip.count += 1;
- return id;
- }
-
- /**
- * Constructs a new tooltip at given location with given content.
- * The applied css class may be changed by the fourth parameter.
- *
- * @class
- * @classdesc Stud.IP tooltips provide an improved layout and handling
- * of contents (including html) than the browser's default
- * tooltip through title attribute would
- *
- * @param {int} x - Horizontal position of the tooltip
- * @param {int} y - Vertical position of the tooltip
- * @param {string} content - Content of the tooltip (may be html)
- * @param {string} css_class - Optional name of the applied css class /
- * defaults to 'studip-tooltip'
- */
- constructor(x, y, content, css_class) {
- // Obtain unique id of the tooltip
- this.id = Tooltip.getId();
-
- // Create dom element of the tooltip, apply id and class and attach
- // to dom
- this.element = $('<div>');
- this.element.addClass(css_class || 'studip-tooltip');
- this.element.attr('id', this.id);
- this.element.attr('role', 'tooltip');
- this.element.appendTo('body');
-
- // Set position and content and paint the tooltip
- this.position(x, y);
- this.update(content);
- this.paint();
- }
-
- /**
- * Translates the arrow(s) under a tooltip using css3 translate
- * transforms. This is needed at the edges of the screen.
- * This implies that a current browser is used. The translation could
- * also be achieved by adjusting margins but that way we would need
- * to hardcode values into this function since it's a struggle to
- * obtain the neccessary values from the CSS pseudo selectors in JS.
- *
- * Internal, css rules are dynamically created and applied to the current
- * document by using the methods provided in the file studip-css.js.
- *
- * @param {int} x - Horizontal offset
- * @param {int} y - Vertical offset
- */
- translateArrows(x, y, left_arrow = false) {
- CSS.removeRule(`#${this.id}::before`);
- CSS.removeRule(`#${this.id}::after`);
-
- if (x !== 0 || y !== 0) {
- let before_rule = {
- transform: `translate(${x}px, ${y}px);`
- };
- if (left_arrow) {
- before_rule.transform = `translate(${x}px, ${y}px) rotate(90deg);`;
- }
- let after_rule = before_rule;
- if (left_arrow) {
- after_rule['border-width'] = '9px';
- }
- CSS.addRule(`#${this.id}::before`, before_rule, ['-ms-', '-webkit-']);
- CSS.addRule(`#${this.id}::after`, after_rule, ['-ms-', '-webkit-']);
- }
- }
-
- /**
- * Updates the position of the tooltip.
- *
- * @param {int} x - Horizontal position of the tooltip
- * @param {int} y - Vertical position of the tooltip
- */
- position(x, y) {
- this.x = x;
- this.y = y;
- }
-
- /**
- * Updates the contents of the tooltip.
- *
- * @param {string} content - Content of the tooltip (may be html)
- */
- update(content) {
- this.element.html(content);
- }
-
- /**
- * "Paints" the tooltip. This method actually computes the dimensions of
- * the tooltips, checks for screen edges and calculates the actual offset
- * in the current document.
- * This method is neccessary due to the fact that position and content
- * can be changed apart from each other.
- * Thus: Don't forget to repaint after adjusting any of the two.
- */
- paint() {
- const width = this.element.outerWidth(true);
- const height = this.element.outerHeight(true);
- const maxWidth = $(document).width();
- const maxHeight = $(document).height();
- let x = this.x - width / 2;
- let y = this.y - height;
- //The arrow offset is the offset from the bottom right corner of
- //the tooltip "frame".
- let arrow_offset_x = 0;
- let arrow_offset_y = 0;
- let left_arrow = false;
-
- if (y < 0) {
- y = 0;
- x = this.x + 20;
- //Put the arrow on the left side and move the tooltip,
- //if there is still enough place left on the right.
- left_arrow = true;
- arrow_offset_y = -height + this.y + 10;
- if (arrow_offset_y > -20) {
- y+= arrow_offset_y + 20;
- arrow_offset_y = -20;
- }
- arrow_offset_x = -width / 2 - 8;
- } else if (y + height > maxHeight) {
- y = maxHeight - height;
- }
-
- if (x < 0) {
- arrow_offset_x = 0;
- x = 0;
- } else if (x + width > maxWidth) {
- arrow_offset_x = x + width - maxWidth;
- x = maxWidth - width;
- }
- this.translateArrows(arrow_offset_x, arrow_offset_y, left_arrow);
-
- this.element.css({
- left: x,
- top: y
- });
- }
-
- /**
- * Toggles the visibility of the tooltip. If no state is provided,
- * the tooltip will be hidden if visible and vice versa. Pretty straight
- * forward and no surprises here.
- * This method implicitely calls paint before a tooltip is shown (in case
- * it was forgotten).
- *
- * @param {bool} visible - Optional visibility parameter to set the
- * tooltip to a certain state
- */
- toggle(visible) {
- if (visible) {
- this.paint();
- }
- this.element.toggle(visible);
- }
-
- /**
- * Reveals the tooltip.
- *
- * @see Tooltip.toggle
- */
- show() {
- this.toggle(true);
- }
-
- /**
- * Hides the tooltip.
- *
- * @see Tooltip.toggle
- */
- hide() {
- this.toggle(false);
- }
-
- /**
- * Removes the tooltip
- */
- remove() {
- this.element.remove();
- }
-}
-
-export default Tooltip;
diff --git a/resources/assets/javascripts/lib/wysiwyg.js b/resources/assets/javascripts/lib/wysiwyg.js
index f9acb81..47c64d0 100644
--- a/resources/assets/javascripts/lib/wysiwyg.js
+++ b/resources/assets/javascripts/lib/wysiwyg.js
@@ -13,17 +13,17 @@ const wysiwyg = {
isHtml: function isHtml(text) {
// NOTE keep this function in sync with
- // Markup::isHtml in Markup.class.php
+ // Markup::isHtml in Markup.php
return this.hasHtmlMarker(text);
},
hasHtmlMarker: function hasHtmlMarker(text) {
// NOTE keep this function in sync with
- // Markup::hasHtmlMarker in Markup.class.php
+ // Markup::hasHtmlMarker in Markup.php
return this.htmlMarkerRegExp.test(text);
},
markAsHtml: function markAsHtml(text) {
// NOTE keep this function in sync with
- // Markup::markAsHtml in Markup.class.php
+ // Markup::markAsHtml in Markup.php
if (this.hasHtmlMarker(text) || text.trim() == '') {
return text; // marker already set, don't set twice
}