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 = new 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 } 247 248 connectorAngular.style.left = connectorHorizontal.getBoundingClientRect().width / zoomLevel + 'px'; 249 connectorAngular.style.width = 250 lineDistance({ 251 x: connectorAngular.getBoundingClientRect().left / zoomLevel, 252 y: connectorAngular.getBoundingClientRect().top / zoomLevel 253 }, { 254 x: annotationNote.getBoundingClientRect().left / zoomLevel, 255 y: annotationNote.getBoundingClientRect().top / zoomLevel 256 }) + 'px'; 257 258 connectorAngle = Math.asin( 259 (annotationNote.getBoundingClientRect().top - connectorAngular.getBoundingClientRect().top) 260 / (zoomLevel * parseFloat(connectorAngular.style.width)) 261 ); 262 connectorAngular.style.transform = 'rotate(' + connectorAngle + 'rad)'; 263 connectorAngular.style.MozTransform = 'rotate(' + connectorAngle + 'rad)'; 264 connectorAngular.style.WebkitTransform = 'rotate(' + connectorAngle + 'rad)'; 265 connectorAngular.style.msTransform = 'rotate(' + connectorAngle + 'rad)'; 266 } 267 268 /** 269 * Show or hide annotations pane 270 * @param {!boolean} show 271 * @return {undefined} 272 */ 273 function showAnnotationsPane(show) { 274 var sizer = canvas.getSizer(); 275 276 if (show) { 277 annotationsPane.style.display = 'inline-block'; 278 sizer.style.paddingRight = window.getComputedStyle(annotationsPane).width; 279 } else { 280 annotationsPane.style.display = 'none'; 281 sizer.style.paddingRight = 0; 282 } 283 canvas.refreshSize(); 284 } 285 286 /*jslint bitwise:true*/ 287 /** 288 * Sorts the internal annotations array by order of occurence in the document. 289 * Useful for calculating the order of annotations in the sidebar, and positioning them 290 * accordingly 291 * @return {undefined} 292 */ 293 function sortAnnotations() { 294 annotations.sort(function (a, b) { 295 if ((a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0) { 296 return -1; 297 } 298 return 1; 299 }); 300 } 301 /*jslint bitwise:false*/ 302 303 /** 304 * Recalculates the rendering - positions, rotation angles for connectors, 305 * etc - for all tracked annotations. 306 * @return {undefined} 307 */ 308 function rerenderAnnotations() { 309 var i; 310 311 for (i = 0; i < annotations.length; i += 1) { 312 renderAnnotation(annotations[i]); 313 } 314 } 315 this.rerenderAnnotations = rerenderAnnotations; 316 317 /** 318 * Re-highlights the annotations' spans. To be used when a span is broken by, say, 319 * splitting a paragraph. 320 * @return {undefined} 321 */ 322 function rehighlightAnnotations() { 323 annotations.forEach(function (annotation) { 324 highlightAnnotation(annotation); 325 }); 326 } 327 this.rehighlightAnnotations = rehighlightAnnotations; 328 329 /** 330 * Reports the minimum height in pixels needed to display all 331 * annotation notes in the annotation pane. 332 * If there is no pane shown or are no annotations, null is returned. 333 * @return {?string} 334 */ 335 function getMinimumHeightForAnnotationPane() { 336 if (annotationsPane.style.display !== 'none' && annotations.length > 0) { 337 return (/**@type{!Element}*/(annotations[annotations.length-1].parentNode).getBoundingClientRect().bottom - annotationsPane.getBoundingClientRect().top) / canvas.getZoomLevel() + 'px'; 338 } 339 return null; 340 } 341 this.getMinimumHeightForAnnotationPane = getMinimumHeightForAnnotationPane; 342 343 /** 344 * Adds an annotation to track, and wraps and highlights it 345 * @param {!odf.AnnotationElement} annotation 346 * @return {undefined} 347 */ 348 function addAnnotation(annotation) { 349 showAnnotationsPane(true); 350 351 // TODO: make use of the fact that current list is already sorted 352 // instead just iterate over the list until the right index to insert is found 353 annotations.push(annotation); 354 355 sortAnnotations(); 356 357 wrapAnnotation(annotation); 358 if (annotation.annotationEndElement) { 359 highlightAnnotation(annotation); 360 } 361 rerenderAnnotations(); 362 } 363 this.addAnnotation = addAnnotation; 364 365 /** 366 * Unhighlights, unwraps, and ejects an annotation from the tracking 367 * @param {!odf.AnnotationElement} annotation 368 * @return {undefined} 369 */ 370 function forgetAnnotation(annotation) { 371 var index = annotations.indexOf(annotation); 372 unwrapAnnotation(annotation); 373 unhighlightAnnotation(annotation); 374 if (index !== -1) { 375 annotations.splice(index, 1); 376 } 377 if (annotations.length === 0) { 378 showAnnotationsPane(false); 379 } 380 } 381 382 /** 383 * Untracks, unwraps, and unhighlights all annotations 384 * @return {undefined} 385 */ 386 function forgetAnnotations() { 387 while (annotations.length) { 388 forgetAnnotation(annotations[0]); 389 } 390 } 391 this.forgetAnnotations = forgetAnnotations; 392 }; 393