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