aboutsummaryrefslogtreecommitdiff
path: root/resources/assets/javascripts
diff options
context:
space:
mode:
authorJan-Hendrik Willms <tleilax+studip@gmail.com>2024-06-24 13:32:09 +0000
committerJan-Hendrik Willms <tleilax+studip@gmail.com>2024-06-24 13:32:09 +0000
commit50371cbb71065fe95dbcef52d79e13d3db1ccb41 (patch)
tree5843bb3ff74f347c08a05a0e6c73ed464ccbdc84 /resources/assets/javascripts
parent64e35ff574d272d9bf22e2dcd45d37498159db4a (diff)
add aria-label to all tooltips and align usage in vue as well, fixes #4330
Closes #4330 Merge request studip/studip!3132
Diffstat (limited to 'resources/assets/javascripts')
-rw-r--r--resources/assets/javascripts/bootstrap/tooltip.js72
-rw-r--r--resources/assets/javascripts/entry-base.js1
-rw-r--r--resources/assets/javascripts/init.js2
-rw-r--r--resources/assets/javascripts/lib/tooltip.js227
4 files changed, 302 insertions, 0 deletions
diff --git a/resources/assets/javascripts/bootstrap/tooltip.js b/resources/assets/javascripts/bootstrap/tooltip.js
new file mode 100644
index 0000000..4b17cb0
--- /dev/null
+++ b/resources/assets/javascripts/bootstrap/tooltip.js
@@ -0,0 +1,72 @@
+// 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.
+
+let timeout = null;
+
+STUDIP.Tooltip.threshold = 6;
+
+$(document).on('mouseenter mouseleave focusin focusout', '[data-tooltip],.tooltip:has(.tooltip-content)', function(event) {
+ let data = $(this).data();
+
+ const visible = ['mouseenter', 'focusin'].includes(event.type);
+ const offset = $(this).offset();
+ const x = offset.left + $(this).outerWidth(true) / 2;
+ const y = offset.top;
+ const delay = 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.html !== undefined) {
+ content = data.tooltip.html;
+ } else if (data.tooltip.text !== undefined) {
+ content = data.tooltip.text;
+ } else {
+ throw "Invalid content for tooltip via data";
+ }
+ if (!content) {
+ content = $(this).find('.tooltip-content').remove().html();
+ }
+ $(this).attr('title', null);
+ $(this).attr('data-tooltip', content);
+
+ tooltip = new STUDIP.Tooltip(x, y, content);
+
+ data.tooltipObject = tooltip;
+
+ if (!this.hasAttribute('aria-label')) {
+ const div = document.createElement('div');
+ div.innerHTML = content;
+ this.setAttribute('aria-label', div.innerText.trim());
+ }
+
+ $(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 focusin', '.studip-tooltip', () => {
+ clearTimeout(timeout);
+}).on('mouseleave focusout', '.studip-tooltip', function() {
+ $(this).hide();
+});
diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js
index 5f88c30..f551c6d 100644
--- a/resources/assets/javascripts/entry-base.js
+++ b/resources/assets/javascripts/entry-base.js
@@ -57,6 +57,7 @@ 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"
diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js
index 1d7d5ac..3a8402e 100644
--- a/resources/assets/javascripts/init.js
+++ b/resources/assets/javascripts/init.js
@@ -75,6 +75,7 @@ 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 Tooltip from './lib/tooltip.js';
import Tour from './lib/tour.js';
import * as Gettext from './lib/gettext';
import UserFilter from './lib/user_filter.js';
@@ -160,6 +161,7 @@ window.STUDIP = _.assign(window.STUDIP || {}, {
study_area_selection,
Table,
TableOfContents,
+ Tooltip,
Tour,
URLHelper,
UserFilter,
diff --git a/resources/assets/javascripts/lib/tooltip.js b/resources/assets/javascripts/lib/tooltip.js
new file mode 100644
index 0000000..2cdac27
--- /dev/null
+++ b/resources/assets/javascripts/lib/tooltip.js
@@ -0,0 +1,227 @@
+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;