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