1 /**
  2  * Copyright (C) 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 Node, NodeFilter, gui, odf, ops, runtime, core*/
 26 
 27 /**
 28  *  A GUI class that attaches to a cursor and renders it's selection
 29  *  as an SVG polygon.
 30  * @constructor
 31  * @implements {core.Destroyable}
 32  * @implements {gui.SelectionView}
 33  * @param {!ops.OdtCursor} cursor
 34  */
 35 gui.SvgSelectionView = function SvgSelectionView(cursor) {
 36     "use strict";
 37 
 38     var /**@type{!ops.Document}*/
 39         document = cursor.getDocument(),
 40         documentRoot, // initialized by addOverlay
 41         /**@type{!HTMLElement}*/
 42         sizer,
 43         doc = document.getDOMDocument(),
 44         svgns = "http://www.w3.org/2000/svg",
 45         overlay = doc.createElementNS(svgns, 'svg'),
 46         polygon = doc.createElementNS(svgns, 'polygon'),
 47         handle1 = doc.createElementNS(svgns, 'circle'),
 48         handle2 = doc.createElementNS(svgns, 'circle'),
 49         odfUtils = odf.OdfUtils,
 50         domUtils = core.DomUtils,
 51         /**@type{!gui.ZoomHelper}*/
 52         zoomHelper = document.getCanvas().getZoomHelper(),
 53         /**@type{boolean}*/
 54         isVisible = true,
 55         positionIterator = cursor.getDocument().createPositionIterator(document.getRootNode()),
 56         /**@const*/
 57         FILTER_ACCEPT = NodeFilter.FILTER_ACCEPT,
 58         /**@const*/
 59         FILTER_REJECT = NodeFilter.FILTER_REJECT,
 60         /**@const*/
 61         HANDLE_RADIUS = 8,
 62         /**@type{!core.ScheduledTask}*/
 63         renderTask;
 64 
 65     /**
 66      * This evil little check is necessary because someone, not mentioning any names *cough*
 67      * added an extremely hacky undo manager that replaces the root node in order to go back
 68      * to a prior document state.
 69      * This makes things very sad, and kills baby kittens.
 70      * Unfortunately, no-one has had time yet to write a *real* undo stack... so we just need
 71      * to cope with it for now.
 72      */
 73     function addOverlay() {
 74         var newDocumentRoot = document.getRootNode();
 75         if (documentRoot !== newDocumentRoot) {
 76             documentRoot = newDocumentRoot;
 77             sizer = document.getCanvas().getSizer();
 78             sizer.appendChild(overlay);
 79             overlay.setAttribute('class', 'webodf-selectionOverlay');
 80             handle1.setAttribute('class', 'webodf-draggable');
 81             handle2.setAttribute('class', 'webodf-draggable');
 82             handle1.setAttribute('end', 'left');
 83             handle2.setAttribute('end', 'right');
 84             handle1.setAttribute('r', HANDLE_RADIUS);
 85             handle2.setAttribute('r', HANDLE_RADIUS);
 86             overlay.appendChild(polygon);
 87             overlay.appendChild(handle1);
 88             overlay.appendChild(handle2);
 89         }
 90     }
 91 
 92     /**
 93      * Returns true if the supplied range has 1 or more visible client rectangles.
 94      * A range might not be visible if it:
 95      * - contains only hidden nodes
 96      * - contains only collapsed whitespace (e.g., multiple whitespace characters will only display as 1 character)
 97      *
 98      * @param {!Range} range
 99      * @return {!boolean}
100      */
101     function isRangeVisible(range) {
102         var bcr = range.getBoundingClientRect();
103         return Boolean(bcr && bcr.height !== 0);
104     }
105 
106     /**
107      * Set the range to the last visible selection in the text nodes array
108      * @param {!Range} range
109      * @param {!Array.<!Element|!Text>} nodes
110      * @return {!boolean}
111      */
112     function lastVisibleRect(range, nodes) {
113         var nextNodeIndex = nodes.length - 1,
114             node = nodes[nextNodeIndex],
115             startOffset,
116             endOffset;
117         if (range.endContainer === node) {
118             startOffset = range.endOffset;
119         } else if (node.nodeType === Node.TEXT_NODE) {
120             startOffset = node.length;
121         } else {
122             startOffset = node.childNodes.length;
123         }
124         endOffset = startOffset;
125         range.setStart(node, startOffset);
126         range.setEnd(node, endOffset);
127         while (!isRangeVisible(range)) {
128             if (node.nodeType === Node.ELEMENT_NODE && startOffset > 0) {
129                 // Extending start to cover character node. End offset remains unchanged
130                 startOffset = 0;
131             } else if (node.nodeType === Node.TEXT_NODE && startOffset > 0) {
132                 // Extending start to include one more text char. End offset remains unchanged
133                 startOffset -= 1;
134             } else if (nodes[nextNodeIndex]) {
135                 // Moving range to a new node. Start collapsed at last available point
136                 node = nodes[nextNodeIndex];
137                 nextNodeIndex -= 1;
138                 startOffset = endOffset = node.length || node.childNodes.length;
139             } else {
140                 // Iteration complete. No more nodes left to explore
141                 return false;
142             }
143             range.setStart(node, startOffset);
144             range.setEnd(node, endOffset);
145         }
146         return true;
147     }
148 
149     /**
150      * Set the range to the first visible selection in the text nodes array
151      * @param {!Range} range
152      * @param {!Array.<!Element|!Text>} nodes
153      * @return {!boolean}
154      */
155     function firstVisibleRect(range, nodes) {
156         var nextNodeIndex = 0,
157             node = nodes[nextNodeIndex],
158             startOffset = range.startContainer === node ? range.startOffset : 0,
159             endOffset = startOffset;
160         range.setStart(node, startOffset);
161         range.setEnd(node, endOffset);
162         while (!isRangeVisible(range)) {
163             if (node.nodeType === Node.ELEMENT_NODE && endOffset < node.childNodes.length) {
164                 // Extending end to cover character node. Start offset remains unchanged
165                 endOffset = node.childNodes.length;
166             } else if (node.nodeType === Node.TEXT_NODE && endOffset < node.length) {
167                 // Extending end to include one more text char. Start offset remains unchanged
168                 endOffset += 1;
169             } else if (nodes[nextNodeIndex]) {
170                 // Moving range to a new node. Start collapsed at first available point
171                 node = nodes[nextNodeIndex];
172                 nextNodeIndex += 1;
173                 startOffset = endOffset = 0;
174             } else {
175                 // Iteration complete. No more nodes left to explore
176                 return false;
177             }
178             range.setStart(node, startOffset);
179             range.setEnd(node, endOffset);
180         }
181         return true;
182     }
183 
184     /**
185      * Returns the 'extreme' ranges for a range.
186      * This returns 3 ranges, where the firstRange is attached to the first
187      * position in the first text node in the original range,
188      * the lastRange is attached to the last text node's last position,
189      * and the fillerRange starts at the start of firstRange and ends at the end of
190      * lastRange.
191      * @param {!Range} range
192      * @return {?{firstRange: !Range, lastRange: !Range, fillerRange: !Range}}
193      */
194     function getExtremeRanges(range) {
195         var nodes = odfUtils.getTextElements(range, true, false),
196             firstRange = /**@type {!Range}*/(range.cloneRange()),
197             lastRange = /**@type {!Range}*/(range.cloneRange()),
198             fillerRange = range.cloneRange();
199 
200         if (!nodes.length) {
201             return null;
202         }
203 
204         if (!firstVisibleRect(firstRange, nodes)) {
205             return null;
206         }
207 
208         if (!lastVisibleRect(lastRange, nodes)) {
209             return null;
210         }
211 
212         fillerRange.setStart(firstRange.startContainer, firstRange.startOffset);
213         fillerRange.setEnd(lastRange.endContainer, lastRange.endOffset);
214 
215         return {
216             firstRange: firstRange,
217             lastRange: lastRange,
218             fillerRange: fillerRange
219         };
220     }
221 
222     /**
223      * Returns the bounding rectangle of two given rectangles
224      * @param {!ClientRect|!{top: !number, left: !number, bottom: !number, right: !number, width: !number, height: !number}} rect1
225      * @param {!ClientRect|!{top: !number, left: !number, bottom: !number, right: !number, width: !number, height: !number}} rect2
226      * @return {!{top: !number, left: !number, bottom: !number, right: !number, width: !number, height: !number}}
227      */
228     function getBoundingRect(rect1, rect2) {
229         var resultRect = {};
230         resultRect.top = Math.min(rect1.top, rect2.top);
231         resultRect.left = Math.min(rect1.left, rect2.left);
232         resultRect.right = Math.max(rect1.right, rect2.right);
233         resultRect.bottom = Math.max(rect1.bottom, rect2.bottom);
234         resultRect.width = resultRect.right - resultRect.left;
235         resultRect.height = resultRect.bottom - resultRect.top;
236         return resultRect;
237     }
238 
239     /**
240      * Checks if the newRect is a collapsed rect, and if it is not,
241      * returns the bounding rect of the originalRect and the newRect.
242      * If it is collapsed, returns the originalRect.
243      * Bad ad-hoc function, but I want to keep the size of the code smaller
244      * @param {ClientRect|{top: !number, left: !number, bottom: !number, right: !number, width: !number, height: !number}} originalRect 
245      * @param {ClientRect|{top: !number, left: !number, bottom: !number, right: !number, width: !number, height: !number}} newRect
246      * @return {?ClientRect|{top: !number, left: !number, bottom: !number, right: !number, width: !number, height: !number}}
247      */
248     function checkAndGrowOrCreateRect(originalRect, newRect) {
249         if (newRect && newRect.width > 0 && newRect.height > 0) {
250             if (!originalRect) {
251                 originalRect = newRect;
252             } else {
253                 originalRect = getBoundingRect(originalRect, newRect);
254             }
255         }
256         return originalRect;
257     }
258 
259     /**
260      * Chrome's implementation of getBoundingClientRect is buggy in that it sometimes
261      * includes the ClientRect of a partially covered parent in the bounding rect.
262      * Therefore, instead of simply using getBoundingClientRect on the fillerRange,
263      * we have to carefully compute our own filler rect.
264      * This is done by climbing up the ancestries of both the startContainer and endContainer,
265      * to just one level below the commonAncestorContainer. Then, we iterate between the
266      * 'firstNode' and 'lastNode' and compute the bounding rect of all the siblings in between.
267      * The resulting rect will have the correct width, but the height will be equal or greater than
268      * what a correct getBoundingClientRect would give us. This is not a problem though, because
269      * we only require the width of this filler rect; the top and bottom of the firstRect and lastRect
270      * are enough for the rest.
271      * This function also improves upon getBoundingClientRect in another way:
272      * it computes the bounding rects of the paragraph nodes between the two ends, instead of the
273      * bounding rect of the *range*. This means that unlike gBCR, the bounding rect will not cover absolutely
274      * positioned children such as annotation nodes.
275      * @param {!Range} fillerRange
276      * @return {ClientRect|{top: number, left: number, bottom: number, right: number, width: number, height: number}}
277      */
278     function getFillerRect(fillerRange) {
279         var containerNode = fillerRange.commonAncestorContainer,
280             /**@type{!Node}*/
281             firstNode = /**@type{!Node}*/(fillerRange.startContainer),
282             /**@type{!Node}*/
283             lastNode = /**@type{!Node}*/(fillerRange.endContainer),
284             firstOffset = fillerRange.startOffset,
285             lastOffset = fillerRange.endOffset,
286             currentNode,
287             lastMeasuredNode,
288             firstSibling,
289             lastSibling,
290             grownRect = null,
291             currentRect,
292             range = doc.createRange(),
293             /**@type{!core.PositionFilter}*/
294             rootFilter,
295             odfNodeFilter = new odf.OdfNodeFilter(),
296             treeWalker;
297 
298         /**
299          * This checks if the node is allowed by the odf filter and the root filter.
300          * @param {!Node} node
301          * @return {!number}
302          */
303         function acceptNode(node) {
304             positionIterator.setUnfilteredPosition(node, 0);
305             if (odfNodeFilter.acceptNode(node) === FILTER_ACCEPT
306                     && rootFilter.acceptPosition(positionIterator) === FILTER_ACCEPT) {
307                 return FILTER_ACCEPT;
308             }
309             return FILTER_REJECT;
310         }
311 
312         /**
313          * If the node is acceptable, check if the node is a grouping element.
314          * If yes, then get it's complete bounding rect (we should use the
315          *getBoundingClientRect call on nodes whenever possible, since it is
316          * extremely buggy on ranges. This has the added good side-effect of
317          * not taking annotations' rects into the bounding rect.
318          * @param {!Node} node
319          * @return {?ClientRect}
320          */
321         function getRectFromNodeAfterFiltering(node) {
322             var rect = null;
323             // If the sibling is acceptable by the odfNodeFilter and the rootFilter,
324             // only then take into account it's dimensions
325             if (acceptNode(node) === FILTER_ACCEPT) {
326                 rect = domUtils.getBoundingClientRect(node);
327             }
328             return rect;
329         }
330 
331 
332         // If the entire range is for just one node
333         // then we can get the bounding rect for the range and be done with it
334         if (firstNode === containerNode || lastNode === containerNode) {
335             range = fillerRange.cloneRange();
336             grownRect = range.getBoundingClientRect();
337             range.detach();
338             return grownRect;
339         }
340 
341         // Compute the firstSibling and lastSibling,
342         // which are top-level siblings just below the common ancestor node
343         firstSibling = firstNode;
344         while (firstSibling.parentNode !== containerNode) {
345             firstSibling = firstSibling.parentNode;
346         }
347         lastSibling = lastNode;
348         while (lastSibling.parentNode !== containerNode) {
349             lastSibling = lastSibling.parentNode;
350         }
351 
352         // We use a root filter to avoid taking any rects of nodes in other roots
353         // into the bounding rect, should it happen that the selection contains
354         // nodes from more than one root. Example: Paragraphs containing annotations
355         rootFilter = document.createRootFilter(firstNode);
356 
357         // Now since this function is called a lot of times,
358         // we need to iterate between and not including the
359         // first and last top-level siblings (below the common
360         // ancestor), and grow our rect from their bounding rects.
361         // This is cheap technique, compared to actually iterating
362         // over each node in the range.
363         currentNode = firstSibling.nextSibling;
364         while (currentNode && currentNode !== lastSibling) {
365             currentRect = getRectFromNodeAfterFiltering(currentNode);
366             grownRect = checkAndGrowOrCreateRect(grownRect, currentRect);
367             currentNode = currentNode.nextSibling;
368         }
369 
370         // If the first top-level sibling is a paragraph, then use it's
371         // bounding rect for growing. This is actually not very necessary, but
372         // makes our selections look more intuitive and more native-ish.
373         // Case in point: If you draw a selection starting on the last (half-full) line of
374         // text in a paragraph and ending somewhere in the middle of the first line of
375         // the next paragraph, the selection will be only as wide as the distance between
376         // the start and end of the selection.
377         // This is where we'd prefer full-width selections, therefore using the paragraph
378         // width is nicer.
379         // We don't need to look deeper into the node, so this is very cheap.
380         if (odfUtils.isParagraph(firstSibling)) {
381             grownRect = checkAndGrowOrCreateRect(grownRect, domUtils.getBoundingClientRect(firstSibling));
382         } else if (firstSibling.nodeType === Node.TEXT_NODE) {
383             currentNode = firstSibling;
384             range.setStart(currentNode, firstOffset);
385             range.setEnd(currentNode, currentNode === lastSibling ? lastOffset : /**@type{!Text}*/(currentNode).length);
386             currentRect = range.getBoundingClientRect();
387             grownRect = checkAndGrowOrCreateRect(grownRect, currentRect);
388         } else {
389             // The first top-level sibling was not a paragraph, so we now need to
390             // Grow the rect in a detailed manner using the selected area *inside* the first sibling.
391             // For that, we start walking over textNodes within the firstSibling,
392             // and grow using the the rects of all textnodes that lie including and after the
393             // firstNode (the startContainer of the original fillerRange), and stop
394             // when either the firstSibling ends or we encounter the lastNode.
395             treeWalker = doc.createTreeWalker(firstSibling, NodeFilter.SHOW_TEXT, acceptNode, false);
396             currentNode = treeWalker.currentNode = firstNode;
397             while (currentNode && currentNode !== lastNode) {
398                 range.setStart(currentNode, firstOffset);
399                 range.setEnd(currentNode, /**@type{!Text}*/(currentNode).length);
400 
401                 currentRect = range.getBoundingClientRect();
402                 grownRect = checkAndGrowOrCreateRect(grownRect, currentRect);
403 
404                 // We keep track of the lastMeasuredNode, so that the next block where
405                 // we iterate backwards can know when to stop.
406                 lastMeasuredNode = currentNode;
407                 firstOffset = 0;
408                 currentNode = treeWalker.nextNode();
409             }
410         }
411 
412         // If there was no lastMeasuredNode, it means that even the firstNode
413         // was not iterated over.
414         if (!lastMeasuredNode) {
415             lastMeasuredNode = firstNode;
416         }
417 
418         // Just like before, a cheap way to avoid looking deeper into the listSibling
419         // if it is a paragraph.
420         if (odfUtils.isParagraph(lastSibling)) {
421             grownRect = checkAndGrowOrCreateRect(grownRect, domUtils.getBoundingClientRect(lastSibling));
422         } else if (lastSibling.nodeType === Node.TEXT_NODE) {
423             currentNode = lastSibling;
424             range.setStart(currentNode, currentNode === firstSibling ? firstOffset : 0);
425             range.setEnd(currentNode, lastOffset);
426             currentRect = range.getBoundingClientRect();
427             grownRect = checkAndGrowOrCreateRect(grownRect, currentRect);
428         } else {
429             // Grow the rect using the selected area inside
430             // the last sibling, iterating backwards from the lastNode
431             // till we reach either the beginning of the lastSibling
432             // or encounter the lastMeasuredNode
433             treeWalker = doc.createTreeWalker(lastSibling, NodeFilter.SHOW_TEXT, acceptNode, false);
434             currentNode = treeWalker.currentNode = lastNode;
435             while (currentNode && currentNode !== lastMeasuredNode) {
436                 range.setStart(currentNode, 0);
437                 range.setEnd(currentNode, lastOffset);
438 
439                 currentRect = range.getBoundingClientRect();
440                 grownRect = checkAndGrowOrCreateRect(grownRect, currentRect);
441 
442                 currentNode = treeWalker.previousNode();
443                 if (currentNode) {
444                     lastOffset = /**@type{!Text}*/(currentNode).length;
445                 }
446             }
447         }
448 
449         return grownRect;
450     }
451 
452     /**
453      * Gets the clientRect of a range within a textNode, and
454      * collapses the rect to the left or right edge, and returns it
455      * @param {!Range} range
456      * @param {boolean} useRightEdge
457      * @return {{width:number,top:number,bottom:number,height:number,left:number,right:number}}
458      */
459     function getCollapsedRectOfTextRange(range, useRightEdge) {
460         var clientRect = range.getBoundingClientRect(),
461             collapsedRect = {};
462 
463         collapsedRect.width = 0;
464         collapsedRect.top = clientRect.top;
465         collapsedRect.bottom = clientRect.bottom;
466         collapsedRect.height = clientRect.height;
467         collapsedRect.left = collapsedRect.right = useRightEdge ? clientRect.right : clientRect.left;
468         return collapsedRect;
469     }
470 
471     /**
472      * Resets and grows the polygon from the supplied
473      * points.
474      * @param {!Array.<{x: !number, y: !number}>} points
475      * @return {undefined}
476      */
477     function setPoints(points) {
478         var pointsString = "",
479             i;
480 
481         for (i = 0; i < points.length; i += 1) {
482             pointsString += points[i].x + "," + points[i].y + " ";
483         }
484         polygon.setAttribute('points', pointsString);
485     }
486 
487     /**
488      * Repositions overlay over the given selected range of the cursor. If the
489      * selected range has no visible rectangles (as may happen if the selection only
490      * encompasses collapsed whitespace, or does not span any ODT text elements), this
491      * function will return false to indicate the overlay element can be hidden.
492      *
493      * @param {!Range} selectedRange
494      * @return {!boolean} Returns true if the selected range is visible (i.e., height +
495      *    width are non-zero), otherwise returns false
496      */
497     function repositionOverlays(selectedRange) {
498         var rootRect = /**@type{!ClientRect}*/(domUtils.getBoundingClientRect(sizer)),
499             zoomLevel = zoomHelper.getZoomLevel(),
500             extremes = getExtremeRanges(selectedRange),
501             firstRange,
502             lastRange,
503             fillerRange,
504             firstRect,
505             fillerRect,
506             lastRect,
507             left,
508             right,
509             top,
510             bottom;
511 
512         // If the range is collapsed (no selection) or no extremes were found, do not show
513         // any virtual selections.
514         if (extremes) {
515             firstRange = extremes.firstRange;
516             lastRange = extremes.lastRange;
517             fillerRange = extremes.fillerRange;
518 
519             firstRect = domUtils.translateRect(getCollapsedRectOfTextRange(firstRange, false), rootRect, zoomLevel);
520             lastRect = domUtils.translateRect(getCollapsedRectOfTextRange(lastRange, true), rootRect, zoomLevel);
521             fillerRect = getFillerRect(fillerRange);
522 
523             if (!fillerRect) {
524                 fillerRect = getBoundingRect(firstRect, lastRect);
525             } else {
526                 fillerRect = domUtils.translateRect(fillerRect, rootRect, zoomLevel);
527             }
528 
529             // These are the absolute bounding left, right, top, and bottom coordinates of the
530             // entire selection.
531             left = fillerRect.left;
532             right = firstRect.left + Math.max(0, fillerRect.width - (firstRect.left - fillerRect.left));
533             // We will use the topmost 'top' value, because if lastRect.top lies above
534             // firstRect.top, then both are most likely on the same line, and font sizes
535             // are different, so the selection should be taller.
536             top = Math.min(firstRect.top, lastRect.top);
537             bottom = lastRect.top + lastRect.height;
538 
539             // Now we grow the polygon by adding the corners one by one,
540             // and finally we make sure that the last point is the same
541             // as the first.
542 
543             setPoints([
544                 { x: firstRect.left,    y: top + firstRect.height   },
545                 { x: firstRect.left,    y: top                      },
546                 { x: right,             y: top                      },
547                 { x: right,             y: bottom - lastRect.height },
548                 { x: lastRect.right,    y: bottom - lastRect.height },
549                 { x: lastRect.right,    y: bottom                   },
550                 { x: left,              y: bottom                   },
551                 { x: left,              y: top + firstRect.height   },
552                 { x: firstRect.left,    y: top + firstRect.height   }
553             ]);
554 
555             handle1.setAttribute('cx', firstRect.left);
556             handle1.setAttribute('cy', top + firstRect.height / 2);
557             handle2.setAttribute('cx', lastRect.right);
558             handle2.setAttribute('cy', bottom - lastRect.height / 2);
559 
560             firstRange.detach();
561             lastRange.detach();
562             fillerRange.detach();
563         }
564         return Boolean(extremes);
565     }
566 
567     /**
568      * Update the visible selection, or hide if it should no
569      * longer be visible
570      * @return {undefined}
571      */
572     function rerender() {
573         var range = cursor.getSelectedRange(),
574             shouldShow;
575         shouldShow = isVisible
576                         && cursor.getSelectionType() === ops.OdtCursor.RangeSelection
577                         && !range.collapsed;
578         if (shouldShow) {
579             addOverlay();
580             shouldShow = repositionOverlays(range);
581         }
582         if (shouldShow) {
583             overlay.style.display = "block";
584         } else {
585             overlay.style.display = "none";
586         }
587     }
588 
589     /**
590      * @inheritDoc
591      */
592     this.rerender = function () {
593         if (isVisible) {
594             renderTask.trigger();
595         }
596     };
597 
598     /**
599      * @inheritDoc
600      */
601     this.show = function () {
602         isVisible = true;
603         renderTask.trigger();
604     };
605 
606     /**
607      * @inheritDoc
608      */
609     this.hide = function () {
610         isVisible = false;
611         renderTask.trigger();
612     };
613 
614     /**
615      * @param {!gui.ShadowCursor|ops.OdtCursor} movedCursor
616      * @return {undefined}
617      */
618     function handleCursorMove(movedCursor) {
619         if (isVisible && movedCursor === cursor) {
620             renderTask.trigger();
621         }
622     }
623 
624     /**
625      * Scale handles to 1/zoomLevel,so they are
626      * finger-friendly at every zoom level.
627      * @param {!number} zoomLevel
628      * @return {undefined}
629      */
630     function scaleHandles(zoomLevel) {
631         var radius = HANDLE_RADIUS / zoomLevel;
632 
633         handle1.setAttribute('r', radius);
634         handle2.setAttribute('r', radius);
635     }
636 
637     /**
638      * @param {function(!Object=)} callback
639      */
640     function destroy(callback) {
641         sizer.removeChild(overlay);
642         sizer.classList.remove('webodf-virtualSelections');
643         cursor.getDocument().unsubscribe(ops.Document.signalCursorMoved, handleCursorMove);
644         zoomHelper.unsubscribe(gui.ZoomHelper.signalZoomChanged, scaleHandles);
645         callback();
646     }
647 
648     /**
649      * @inheritDoc
650      * @param {function(!Error=)} callback
651      */
652     this.destroy = function (callback) {
653         core.Async.destroyAll([renderTask.destroy, destroy], callback);
654     };
655 
656     function init() {
657         var editinfons = 'urn:webodf:names:editinfo',
658             memberid = cursor.getMemberId();
659 
660         renderTask = core.Task.createRedrawTask(rerender);
661         addOverlay();
662         overlay.setAttributeNS(editinfons, 'editinfo:memberid', memberid);
663         sizer.classList.add('webodf-virtualSelections');
664         cursor.getDocument().subscribe(ops.Document.signalCursorMoved, handleCursorMove);
665         zoomHelper.subscribe(gui.ZoomHelper.signalZoomChanged, scaleHandles);
666         scaleHandles(zoomHelper.getZoomLevel());
667     }
668 
669     init();
670 };
671