Änderungen von Dokument Script
Zuletzt geändert von Daniel Herrmann am 2026/02/07 23:23
Von Version
2.1
bearbeitet von Daniel Herrmann
am 2025/07/19 16:47
am 2025/07/19 16:47
Änderungskommentar:
Install extension [org.xwiki.platform:xwiki-platform-annotation-ui/17.5.0]
Auf Version
5.1
bearbeitet von Daniel Herrmann
am 2026/02/07 23:23
am 2026/02/07 23:23
Änderungskommentar:
Install extension [org.xwiki.platform:xwiki-platform-annotation-ui/18.0.1]
Zusammenfassung
Details
- 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 - Nein1 +Ja
- 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 markerofannotations. The selection highlight is also removed in case the new annotation643 + * 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(to p,left) {803 - // create a nelement withthe form861 + 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 inside864 + // 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.t op, 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.