1 /** 2 * Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com> 3 * 4 * @licstart 5 * This file is part of WebODF. 6 * 7 * WebODF is free software: you can redistribute it and/or modify it 8 * under the terms of the GNU Affero General Public License (GNU AGPL) 9 * as published by the Free Software Foundation, either version 3 of 10 * the License, or (at your option) any later version. 11 * 12 * WebODF is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU Affero General Public License for more details. 16 * 17 * You should have received a copy of the GNU Affero General Public License 18 * along with WebODF. If not, see <http://www.gnu.org/licenses/>. 19 * @licend 20 * 21 * @source: http://www.webodf.org/ 22 * @source: https://github.com/kogmbh/WebODF/ 23 */ 24 25 /*global runtime, gui, core, odf, Node*/ 26 /*jslint sub: true*/ 27 28 /** 29 * TODO: There is currently brokenness in how annotations which overlap are handled. 30 * This needs to be fixed soon. 31 */ 32 33 /*jslint emptyblock:true*/ 34 /** 35 * Abstraction of document canvas that can have annotations. 36 * @class 37 * @interface 38 */ 39 gui.AnnotatableCanvas = function AnnotatableCanvas() {"use strict"; }; 40 gui.AnnotatableCanvas.prototype.refreshSize = function () {"use strict"; }; 41 /** 42 * @return {!number} 43 */ 44 gui.AnnotatableCanvas.prototype.getZoomLevel = function () {"use strict"; }; 45 /** 46 * @return {Element} 47 */ 48 gui.AnnotatableCanvas.prototype.getSizer = function () {"use strict"; }; 49 /*jslint emptyblock:false*/ 50 51 /** 52 * A GUI class for wrapping Annotation nodes inside html wrappers, positioning 53 * them on the sidebar, drawing connectors, and highlighting comments. 54 * @constructor 55 * @param {!gui.AnnotatableCanvas} canvas 56 * @param {!Element} odfFragment 57 * @param {!Element} annotationsPane 58 * @param {!boolean} showAnnotationRemoveButton 59 */ 60 gui.AnnotationViewManager = function AnnotationViewManager(canvas, odfFragment, annotationsPane, showAnnotationRemoveButton) { 61 "use strict"; 62 var /**@type{!Array.<!odf.AnnotationElement>}*/ 63 annotations = [], 64 doc = odfFragment.ownerDocument, 65 odfUtils = odf.OdfUtils, 66 /**@const*/ 67 CONNECTOR_MARGIN = 30, 68 /**@const*/ 69 NOTE_MARGIN = 20, 70 window = runtime.getWindow(), 71 htmlns = "http://www.w3.org/1999/xhtml"; 72 73 runtime.assert(Boolean(window), 74 "Expected to be run in an environment which has a global window, like a browser."); 75 /** 76 * Wraps an annotation with various HTML elements for styling, including connectors 77 * @param {!odf.AnnotationElement} annotation 78 * @return {undefined} 79 */ 80 function wrapAnnotation(annotation) { 81 var annotationWrapper = doc.createElement('div'), 82 annotationNote = doc.createElement('div'), 83 connectorHorizontal = doc.createElement('div'), 84 connectorAngular = doc.createElement('div'), 85 removeButton; 86 87 annotationWrapper.className = 'annotationWrapper'; 88 annotationWrapper.setAttribute("creator", odfUtils.getAnnotationCreator(annotation)); 89 annotation.parentNode.insertBefore(annotationWrapper, annotation); 90 91 annotationNote.className = 'annotationNote'; 92 annotationNote.appendChild(annotation); 93 if (showAnnotationRemoveButton) { 94 removeButton = doc.createElement('div'); 95 removeButton.className = 'annotationRemoveButton'; 96 annotationNote.appendChild(removeButton); 97 } 98 99 connectorHorizontal.className = 'annotationConnector horizontal'; 100 connectorAngular.className = 'annotationConnector angular'; 101 102 annotationWrapper.appendChild(annotationNote); 103 annotationWrapper.appendChild(connectorHorizontal); 104 annotationWrapper.appendChild(connectorAngular); 105 } 106 107 /** 108 * Unwraps an annotation 109 * @param {!odf.AnnotationElement} annotation 110 * @return {undefined} 111 */ 112 function unwrapAnnotation(annotation) { 113 var annotationWrapper = annotation.parentNode.parentNode; 114 115 if (annotationWrapper.localName === 'div') { 116 annotationWrapper.parentNode.insertBefore(annotation, annotationWrapper); 117 annotationWrapper.parentNode.removeChild(annotationWrapper); 118 } 119 } 120 121 /** 122 * Returns true if the given node is within the highlighted range of 123 * the given annotation, else returns false. 124 * @param {!Node} node 125 * @param {!string} annotationName 126 * @return {!boolean} 127 */ 128 function isNodeWithinAnnotationHighlight(node, annotationName) { 129 var iteratingNode = node.parentNode; 130 131 while (!(iteratingNode.namespaceURI === odf.Namespaces.officens 132 && iteratingNode.localName === "body")) { 133 if (iteratingNode.namespaceURI === htmlns 134 && /**@type{!HTMLElement}*/(iteratingNode).className === "webodf-annotationHighlight" 135 && /**@type{!HTMLElement}*/(iteratingNode).getAttribute("annotation") === annotationName) { 136 return true; 137 } 138 iteratingNode = iteratingNode.parentNode; 139 } 140 return false; 141 } 142 143 /** 144 * Highlights the text between the annotation node and it's end 145 * Only highlights text that has not already been highlighted 146 * @param {!odf.AnnotationElement} annotation 147 * @return {undefined} 148 */ 149 function highlightAnnotation(annotation) { 150 var annotationEnd = annotation.annotationEndElement, 151 range = doc.createRange(), 152 annotationName = annotation.getAttributeNS(odf.Namespaces.officens, 'name'), 153 textNodes; 154 155 if (annotationEnd) { 156 range.setStart(annotation, annotation.childNodes.length); 157 range.setEnd(annotationEnd, 0); 158 159 textNodes = odfUtils.getTextNodes(range, false); 160 161 textNodes.forEach(function (n) { 162 if (!isNodeWithinAnnotationHighlight(n, annotationName)) { 163 var container = doc.createElement('span'); 164 container.className = 'webodf-annotationHighlight'; 165 container.setAttribute('annotation', annotationName); 166 167 n.parentNode.replaceChild(container, n); 168 container.appendChild(n); 169 } 170 }); 171 } 172 173 range.detach(); 174 } 175 176 /** 177 * Unhighlights the text between the annotation node and it's end 178 * @param {!odf.AnnotationElement} annotation 179 * @return {undefined} 180 */ 181 function unhighlightAnnotation(annotation) { 182 var annotationName = annotation.getAttributeNS(odf.Namespaces.officens, 'name'), 183 highlightSpans = doc.querySelectorAll('span.webodf-annotationHighlight[annotation="' + annotationName + '"]'), 184 i, 185 container; 186 187 for (i = 0; i < highlightSpans.length; i += 1) { 188 container = highlightSpans.item(i); 189 while (container.firstChild) { 190 container.parentNode.insertBefore(container.firstChild, container); 191 } 192 container.parentNode.removeChild(container); 193 } 194 } 195 196 /** 197 * @param {!{x:number,y:number}} point1 198 * @param {!{x:number,y:number}} point2 199 * @return {number} 200 */ 201 function lineDistance(point1, point2) { 202 var xs = 0, 203 ys = 0; 204 205 xs = point2.x - point1.x; 206 xs = xs * xs; 207 208 ys = point2.y - point1.y; 209 ys = ys * ys; 210 211 return Math.sqrt(xs + ys); 212 } 213 214 /** 215 * Recalculates the positions, widths, and rotation angles of things like the annotation note and it's 216 * connectors. Can and should be called frequently to update the UI 217 * @param {!odf.AnnotationElement} annotation 218 * @return {undefined} 219 */ 220 function renderAnnotation(annotation) { 221 var annotationNote = /**@type{!Element}*/(annotation.parentNode), 222 connectorHorizontal = annotationNote.nextElementSibling, 223 connectorAngular = connectorHorizontal.nextElementSibling, 224 annotationWrapper = /**@type{!Element}*/(annotationNote.parentNode), 225 connectorAngle = 0, 226 previousAnnotation = annotations[annotations.indexOf(annotation) - 1], 227 previousRect, 228 zoomLevel = canvas.getZoomLevel(); 229 230 annotationNote.style.left = 231 (annotationsPane.getBoundingClientRect().left 232 - annotationWrapper.getBoundingClientRect().left) / zoomLevel + 'px'; 233 annotationNote.style.width = annotationsPane.getBoundingClientRect().width / zoomLevel + 'px'; 234 235 236 connectorHorizontal.style.width = parseFloat(annotationNote.style.left) 237 - CONNECTOR_MARGIN + 'px'; 238 239 if (previousAnnotation) { 240 previousRect = /**@type{!Element}*/(previousAnnotation.parentNode).getBoundingClientRect(); 241 if ((annotationWrapper.getBoundingClientRect().top - previousRect.bottom) / zoomLevel <= NOTE_MARGIN) { 242 annotationNote.style.top = Math.abs(annotationWrapper.getBoundingClientRect().top - previousRect.bottom) / zoomLevel + NOTE_MARGIN + 'px'; 243 } else { 244 annotationNote.style.top = '0px'; 245 } 246 } else { 247 annotationNote.style.top = '0px'; 248 } 249 250 connectorAngular.style.left = connectorHorizontal.getBoundingClientRect().width / zoomLevel + 'px'; 251 connectorAngular.style.width = 252 lineDistance({ 253 x: connectorAngular.getBoundingClientRect().left / zoomLevel, 254 y: connectorAngular.getBoundingClientRect().top / zoomLevel 255 }, { 256 x: annotationNote.getBoundingClientRect().left / zoomLevel, 257 y: annotationNote.getBoundingClientRect().top / zoomLevel 258 }) + 'px'; 259 260 connectorAngle = Math.asin( 261 (annotationNote.getBoundingClientRect().top - connectorAngular.getBoundingClientRect().top) 262 / (zoomLevel * parseFloat(connectorAngular.style.width)) 263 ); 264 connectorAngular.style.transform = 'rotate(' + connectorAngle + 'rad)'; 265 connectorAngular.style.MozTransform = 'rotate(' + connectorAngle + 'rad)'; 266 connectorAngular.style.WebkitTransform = 'rotate(' + connectorAngle + 'rad)'; 267 connectorAngular.style.msTransform = 'rotate(' + connectorAngle + 'rad)'; 268 } 269 270 /** 271 * Show or hide annotations pane 272 * @param {!boolean} show 273 * @return {undefined} 274 */ 275 function showAnnotationsPane(show) { 276 var sizer = canvas.getSizer(); 277 278 if (show) { 279 annotationsPane.style.display = 'inline-block'; 280 sizer.style.paddingRight = window.getComputedStyle(annotationsPane).width; 281 } else { 282 annotationsPane.style.display = 'none'; 283 sizer.style.paddingRight = 0; 284 } 285 canvas.refreshSize(); 286 } 287 288 /*jslint bitwise:true*/ 289 /** 290 * Sorts the internal annotations array by order of occurence in the document. 291 * Useful for calculating the order of annotations in the sidebar, and positioning them 292 * accordingly 293 * @return {undefined} 294 */ 295 function sortAnnotations() { 296 annotations.sort(function (a, b) { 297 if ((a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0) { 298 return -1; 299 } 300 return 1; 301 }); 302 } 303 /*jslint bitwise:false*/ 304 305 /** 306 * Recalculates the rendering - positions, rotation angles for connectors, 307 * etc - for all tracked annotations. 308 * @return {undefined} 309 */ 310 function rerenderAnnotations() { 311 var i; 312 313 for (i = 0; i < annotations.length; i += 1) { 314 renderAnnotation(annotations[i]); 315 } 316 } 317 this.rerenderAnnotations = rerenderAnnotations; 318 319 /** 320 * Re-highlights the annotations' spans. To be used when a span is broken by, say, 321 * splitting a paragraph. 322 * @return {undefined} 323 */ 324 function rehighlightAnnotations() { 325 annotations.forEach(function (annotation) { 326 highlightAnnotation(annotation); 327 }); 328 } 329 this.rehighlightAnnotations = rehighlightAnnotations; 330 331 /** 332 * Reports the minimum height in pixels needed to display all 333 * annotation notes in the annotation pane. 334 * If there is no pane shown or are no annotations, null is returned. 335 * @return {?string} 336 */ 337 function getMinimumHeightForAnnotationPane() { 338 if (annotationsPane.style.display !== 'none' && annotations.length > 0) { 339 return (/**@type{!Element}*/(annotations[annotations.length-1].parentNode).getBoundingClientRect().bottom - annotationsPane.getBoundingClientRect().top) / canvas.getZoomLevel() + 'px'; 340 } 341 return null; 342 } 343 this.getMinimumHeightForAnnotationPane = getMinimumHeightForAnnotationPane; 344 345 /** 346 * Adds annotations to track, and wraps and highlights them 347 * @param {!Array.<!odf.AnnotationElement>} annotationElements 348 * @return {undefined} 349 */ 350 function addAnnotations(annotationElements) { 351 if (annotationElements.length === 0) { 352 return; 353 } 354 355 showAnnotationsPane(true); 356 357 annotationElements.forEach(function (annotation) { 358 // TODO: make use of the fact that current list is already sorted 359 // instead just iterate over the list until the right index to insert is found 360 annotations.push(annotation); 361 362 wrapAnnotation(annotation); 363 if (annotation.annotationEndElement) { 364 highlightAnnotation(annotation); 365 } 366 }); 367 368 sortAnnotations(); 369 370 rerenderAnnotations(); 371 } 372 this.addAnnotations = addAnnotations; 373 374 /** 375 * Unhighlights, unwraps, and ejects an annotation from the tracking 376 * @param {!odf.AnnotationElement} annotation 377 * @return {undefined} 378 */ 379 function forgetAnnotation(annotation) { 380 var index = annotations.indexOf(annotation); 381 unwrapAnnotation(annotation); 382 unhighlightAnnotation(annotation); 383 if (index !== -1) { 384 annotations.splice(index, 1); 385 } 386 if (annotations.length === 0) { 387 showAnnotationsPane(false); 388 } 389 } 390 this.forgetAnnotation = forgetAnnotation; 391 392 /** 393 * Untracks, unwraps, and unhighlights all annotations 394 * @return {undefined} 395 */ 396 function forgetAnnotations() { 397 while (annotations.length) { 398 forgetAnnotation(annotations[0]); 399 } 400 } 401 this.forgetAnnotations = forgetAnnotations; 402 }; 403