Änderungen von Dokument Script

Zuletzt geändert von Daniel Herrmann am 2026/02/07 23:23

Von Version Icon 2.1 Icon
bearbeitet von Daniel Herrmann
am 2025/07/19 16:47
Änderungskommentar: Install extension [org.xwiki.platform:xwiki-platform-annotation-ui/17.5.0]
Auf Version Icon 5.1
bearbeitet von Daniel Herrmann
am 2026/02/07 23:23
Änderungskommentar: Install extension [org.xwiki.platform:xwiki-platform-annotation-ui/18.0.1]

Zusammenfassung

Details

Icon XWiki.JavaScriptExtension[0]
Code
... ... @@ -37,11 +37,13 @@
37 37   return;
38 38   }
39 39   this.range = window.getSelection().getRangeAt(0);
40 - // ignore if the selection is in the passed container
40 + // ignore if the selection is not in the passed container
41 41   if (!this.isDescendantOrSelf(this.container, this.range.commonAncestorContainer)) {
42 42   return;
43 43   }
44 44   this.selectionText = this.range.toString();
45 + this.selectionText = this.selectionText.replaceAll(
46 + "$escapetool.xml($services.localization.render('annotations.annotated.highlight.toggle.hint'))",'')
45 45   if (this.selectionText.strip() == '') {
46 46   this.selectionText = false;
47 47   }
... ... @@ -61,13 +61,15 @@
61 61  
62 62   // these functions are here because they depend on selection
63 63  
64 - highlightSelection : function(color) {
66 + highlightSelection : function() {
65 65   if (!this.range) {
66 66   // there should be some selection at this point
67 67   return;
68 68   }
69 69   // create an annotation highlight span around this content
70 - var highlightWrapperTemplate = new Element('span', {'style': 'background-color: ' + color, 'class' : 'selection-highlight'});
72 + var highlightWrapperTemplate = new Element('mark', {
73 + 'class': 'selection-highlight'
74 + });
71 71   // get all the text nodes of this range
72 72   var rangeTextNodes = this.getRangeTextNodes();
73 73   // and remove all the ranges in this selection, otherwise so messed up things will happen
Inhalt parsen
... ... @@ -1,1 +1,1 @@
1 -Nein
1 +Ja
Icon XWiki.JavaScriptExtension[1]
Code
... ... @@ -67,7 +67,7 @@
67 67   require(['jquery'], ($) => {
68 68   $(document).on('click', 'blockquote.annotatedText', (event) => {
69 69   if(this.fetchedAnnotations) {
70 - // In this case the annotations are just made visible (if need be). This is synchrnous and
70 + // In this case the annotations are just made visible (if need be). This is synchronous and
71 71   // we can just let the click event bubble up.
72 72   this.setAnnotationVisibility(true);
73 73   this.toggleAnnotations(this.displayingAnnotations);
... ... @@ -247,8 +247,6 @@
247 247   if (this.displayHighlight) {
248 248   this.annotatedElement.select('.annotation').invoke('toggleClassName', 'annotation-highlight', !!visible);
249 249   }
250 - // Toggle all annotation markers.
251 - this.annotatedElement.select('.annotation-marker').invoke('toggleClassName', 'hidden', !visible);
252 252   this.setAnnotationVisibility(visible);
253 253   if (!visible) {
254 254   // Close all open bubbles.
... ... @@ -345,7 +345,7 @@
345 345   }
346 346   // hide message at page bottom
347 347   this._x_notification.hide();
348 - // Load the received annotations, along with annotations markers.
346 + // Load the received annotations, along with annotations highlights.
349 349   this.loadAnnotations(response.responseJSON.annotatedContent, andShow, false, force);
350 350   // store the state of the annotations
351 351   this.fetchedAnnotations = true;
... ... @@ -406,39 +406,87 @@
406 406   if (plainTextStartOffset.value === null || plainTextEndOffset.value === null) {
407 407   return false;
408 408   }
409 -
410 410   var annDOMRange = this.getDOMRange(this.annotatedElement, plainTextStartOffset.value, plainTextEndOffset.value);
411 411   // Since the annotation could start at a specific offset, the node is splitted for not wrapping the whole text and
412 412   // a new range is created to recalculate the new ends.
413 413   var strictRange = this.fixRangeEndPoints(annDOMRange);
414 414  
415 - // Wrap each text node from this range inside an annotation markup SPAN.
412 + // Wrap each text node from this range inside an annotation markup node.
416 416   this.getTextNodesInRange(strictRange).forEach(textNode => this.markAnnotation(textNode, ann));
417 -
418 - // Add the marker span after the last span of this annotation.
419 419   var allSpans = this.annotatedElement.select('[class~=ID' + ann.annotationId + ']');
420 420   if (!allSpans.length) {
421 421   return;
422 422   }
423 - var lastSpan = allSpans[allSpans.length - 1];
424 - // Create the annotation markers hidden by default, since annotations are added on the document hidden by default.
425 - var markerSpan = new Element('span', {'id': 'ID' + ann.annotationId, 'class' : 'hidden annotation-marker ' + ann.state});
426 - lastSpan.insert({after: markerSpan});
427 - // Annotations are displayed on mouseover.
428 - markerSpan.observe('click', this.onMarkerClick.bindAsEventListener(this, ann.annotationId));
418 + // We generate the keyboard interactive button.
419 + let toggleButton = document.createElement('button');
420 + toggleButton.id = 'ID' + ann.annotationId;
421 + toggleButton.classList.add('annotation-toggle');
422 + toggleButton.classList.add('sr-only');
423 + toggleButton.classList.add('btn');
424 + toggleButton.classList.add('btn-xs');
425 + toggleButton.classList.add('btn-warning');
426 + toggleButton.innerHTML = "$!escapetool.javascript($services.icon.renderHTML('note'))";
427 + // Add the button at the end of the last mark for the annotation.
428 + allSpans[allSpans.length-1].appendChild(toggleButton);
429 + // Add the onclick listener on every span of this annotation.
430 + allSpans.forEach(markNode => markNode.addEventListener('click', (event) =>
431 + this.onMarkerClick(event, ann.annotationId)));
432 + // Clicking specifically on the toggle blocks any other event, such as a page reload if the parent is an anchor
433 + // The other event can still be triggered by clicking on the other text in it.
434 + toggleButton.addEventListener('click', (event) => {
435 + this.onMarkerClick(event, ann.annotationId);
436 + event.stopPropagation();
437 + event.preventDefault();
438 + });
439 + let onMarkerHover = function(event, allSpans) {
440 + allSpans.forEach((span) => span.classList.add('hovered'));
441 + };
442 + let onMarkerHoverLeft = function(event, allSpans) {
443 + allSpans.forEach((span) => span.classList.remove('hovered'));
444 + };
445 + // Add the hover listener on every span to highlight every part of its annotation
446 + allSpans.forEach(function(markNode){
447 + markNode.addEventListener('mouseover', (event) => onMarkerHover(event, allSpans));
448 + markNode.addEventListener('mouseout', (event) => onMarkerHoverLeft(event, allSpans));
449 + });
450 + // Only the button can be focused with the keyboard.
451 + toggleButton.addEventListener('focus', (event) => {
452 + event.target.classList.remove('sr-only');
453 + onMarkerHover(event, allSpans);
454 + });
455 + toggleButton.addEventListener('focusout', (event) => {
456 + event.target.classList.add('sr-only');
457 + onMarkerHoverLeft(event, allSpans);
458 + });
429 429   },
430 430  
461 + addAnnotationsToggleHint : function(annotations) {
462 + annotations.forEach((ann) => {
463 + var plainTextStartOffset = ann.fields.find(field => field.name === 'plainTextStartOffset');
464 + var plainTextEndOffset = ann.fields.find(field => field.name === 'plainTextEndOffset');
465 + if(plainTextStartOffset.value === null || plainTextEndOffset.value === null) {
466 + return false;
467 + }
468 + let toggleButtonDescription = document.createElement('span');
469 + toggleButtonDescription.addClassName('sr-only');
470 + toggleButtonDescription.id = 'annotation-toggle-description-' + ann.annotationId;
471 + toggleButtonDescription.textContent =
472 + "$escapetool.xml($services.localization.render('annotations.annotated.highlight.toggle.hint'))";
473 + let toggleButton = document.getElementById('ID' + ann.annotationId);
474 + toggleButton.appendChild(toggleButtonDescription);
475 + });
476 + },
477 +
431 431   /**
432 - * Surround this node with a span corresponding to it's annotation.
479 + * Surround this node with a span corresponding to its annotation.
433 433   *
434 434   * @param markedNode the node that corresponds to the current annotation
435 435   * @param ann object holding information about the annotation
436 436   */
437 437   markAnnotation: function(markedNode, ann) {
438 - var wrapper = document.createElement('span');
485 + var wrapper = document.createElement('mark');
439 439   wrapper.addClassName('annotation');
440 440   wrapper.addClassName('ID' + ann.annotationId);
441 -
442 442   var parentNode = markedNode.parentElement;
443 443   parentNode.replaceChild(wrapper, markedNode);
444 444   wrapper.appendChild(markedNode);
... ... @@ -594,13 +594,13 @@
594 594   },
595 595  
596 596   /**
597 - * Remove the wrapper and marker of annotations. The selection highlight is also removed in case the new annotation
643 + * Remove the wrappers for annotations. The selection highlight is also removed in case the new annotation
598 598   * was deleted.
599 599   */
600 600   removeAnnotationsAndSelectionMarkups: function() {
601 - document.querySelectorAll("span.annotation, span.selection-highlight")
647 + document.querySelectorAll("mark.annotation, mark.selection-highlight")
602 602   .forEach(annotationNode => annotationNode.replaceWith(...annotationNode.childNodes));
603 - document.querySelectorAll("span.annotation-marker").forEach(marker => marker.remove());
649 + document.querySelectorAll("button.annotation-toggle, span[id^='annotation-toggle-description-']").forEach(description => description.remove());
604 604   this.fetchedAnnotations = false;
605 605   },
606 606  
... ... @@ -752,8 +752,7 @@
752 752   }
753 753   }.bindAsEventListener(this));
754 754   },
755 -
756 - // maybe this should be moved in a function to display a bubble from an address, to call for all dialogs for different parameters
801 +
757 757   onMarkerClick : function(event, id) {
758 758   var bubbleId = 'annotation-bubble-' + id;
759 759   var bubble = $(bubbleId);
... ... @@ -765,9 +765,23 @@
765 765   this.hideBubble(bubble);
766 766   } else {
767 767   // Show the bubble and fetch the annotation display in it.
768 - var bubble = this.displayLoadingBubble(event.element().cumulativeOffset().top,
769 - event.element().cumulativeOffset().left);
813 + // If the event is triggered via keyboard, we use the annotation highlight position instead of the event position
814 + let bubbleCoordX,bubbleCoordY ;
815 + if (event.pageX === 0 && event.pageY === 0) {
816 + let highlight = event.target;
817 + let rect = highlight.getBoundingClientRect();
818 + bubbleCoordX = rect.left + window.scrollX + highlight.offsetWidth / 2;
819 + bubbleCoordY = rect.top + window.scrollY + highlight.offsetHeight / 2;
820 + }
821 + else {
822 + bubbleCoordX = event.pageX;
823 + bubbleCoordY = event.pageY;
824 + }
825 + var bubble = this.displayLoadingBubble(bubbleCoordX, bubbleCoordY, true);
770 770   bubble.writeAttribute('id', bubbleId);
827 + // Link semantically the clicked mark to the generated bubble
828 + // Adding this info earlier is useless since the element is not even loaded before the click.
829 + event.target.writeAttribute('aria-details', bubbleId);
771 771   this.fetchAndShowAnnotationDetails(id, bubble);
772 772   }
773 773   },
... ... @@ -799,29 +799,37 @@
799 799   }.bind(this));
800 800   },
801 801  
802 - displayLoadingBubble : function(top, left) {
803 - // create an element with the form
861 + displayLoadingBubble : function(horizontalCoordinate, verticalCoordinate) {
862 + // create a bubble element wrapping the form
804 804   var bubble = new Element('div', {'class' : 'annotation-bubble'});
805 - // and a nice loading panel inside
864 + // Add a nice loading panel inside
806 806   bubble.insert({top : new Element('div', {'class' : 'loading'})});
807 - // and put it in the content
866 + // Position it off-screen at first, before we measure it and compute its final position
867 + bubble.style.top = verticalCoordinate + 'px';
868 + bubble.style.left = '-100%';
869 + // and insert the bubble in the content
808 808   document.body.insert({bottom : bubble});
809 - // make it hidden for the moment
810 - bubble.toggleClassName('hidden');
811 - // position it
812 - bubble.style.left = left + 'px';
813 - bubble.style.top = top + 'px';
814 - // make it visible
815 - bubble.toggleClassName('hidden');
816 816   // put this bubble in the bubbles stack
817 817   this.bubbles.push(bubble);
873 + // position the bubble
874 + // By default, we position it to the right and under the coordinates
875 + // Position it horizontally.
876 + let bubbleStyles = getComputedStyle(bubble);
877 + let bubbleWidth = bubble.offsetWidth
878 + + parseInt(bubbleStyles.marginLeft)
879 + + parseInt(bubbleStyles.marginRight);
880 + if (horizontalCoordinate < bubbleWidth
881 + || horizontalCoordinate + bubbleWidth < window.innerWidth) {
882 + bubble.style.left = horizontalCoordinate + 'px';
883 + } else {
884 + // There isn't enough space on the right of the window to fit the modal, and there is enough on the left.
885 + bubble.style.left = (horizontalCoordinate - bubbleWidth) + 'px';
886 + bubble.toggleClassName('annotation-bubble-position-left');
887 + }
818 818  
819 819   return bubble;
820 820   },
821 821  
822 - displayAnnotationViewBubble : function(marker) {
823 - },
824 -
825 825   /**
826 826   * Updates the container with the passed content only if the container is still displayed, and returns true if this is the case.
827 827   */
... ... @@ -1014,6 +1014,11 @@
1014 1014   // Cancel the edit otherwise the user will be asked for confirmation when leaving the page.
1015 1015   document.fire('xwiki:actions:cancel');
1016 1016  
1084 + // Remove all the places where the bubble id is used as an attribute on other elements.
1085 + document.querySelectorAll('[aria-details="' + bubble.id + '"]').forEach((mark) => {
1086 + mark.removeAttribute('aria-details');
1087 + });
1088 +
1017 1017   bubble.remove();
1018 1018   var bubbleIndex = this.bubbles.indexOf(bubble);
1019 1019   if (bubbleIndex >= 0) {
... ... @@ -1134,11 +1134,10 @@
1134 1134   },
1135 1135  
1136 1136   displayAnnotationCreationForm : function() {
1137 - // TODO: get this color from the color theme
1138 - this.selectionService.highlightSelection('#FFEE99');
1209 + this.selectionService.highlightSelection();
1139 1139   // get the position and build the loading bubble
1140 1140   var position = this.selectionService.getPositionNextToSelection();
1141 - this.createPanel = this.displayLoadingBubble(position.top, position.left);
1212 + this.createPanel = this.displayLoadingBubble(position.left, position.top, false);
1142 1142   // remove the ctrl + M listeners, so that only one dialog is displayed at one moment
1143 1143   this.unregisterAddAnnotationShortcut();
1144 1144   },
... ... @@ -1231,16 +1231,6 @@
1231 1231   }
1232 1232  })
1233 1233  
1234 -define('node-module', ['jquery'], function($) {
1235 - return {
1236 - load: function(name, req, onLoad, config) {
1237 - $.get(req.toUrl(name + '.js'), function(text) {
1238 - onLoad.fromText(`define(function(require, exports, module) {${text}});`);
1239 - }, 'text');
1240 - }
1241 - }
1242 -});
1243 -
1244 1244  define('xwiki-text-offset-updater', ['jquery', 'node-module!fast-diff'], function($, diff) {
1245 1245   /**
1246 1246   * Compute the changes between different versions of a text.
... ... @@ -1331,6 +1331,8 @@
1331 1331   this.removeAnnotationsAndSelectionMarkups();
1332 1332   this.updateAnnotationsOffsets(annotatedContent);
1333 1333   this.addAnnotationsMarkup(annotatedContent.annotations);
1395 + // We add the hints afterwards because they shift the ranges in the text.
1396 + this.addAnnotationsToggleHint(annotatedContent.annotations);
1334 1334   // Notify the content change.
1335 1335   $(document).trigger('xwiki:dom:updated', {'elements': [this.annotatedElement]});
1336 1336   // Also handle the tab 'downstairs' when the annotations list changes.