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, core, gui, Node, ops, odf */
 26 
 27 /**
 28  * @constructor
 29  * @struct
 30  */
 31 gui.SessionControllerOptions = function () {
 32     "use strict";
 33 
 34     /**
 35      * Sets whether direct paragraph styling should be enabled.
 36      * @type {!boolean}
 37      */
 38     this.directTextStylingEnabled = false;
 39     /**
 40      * Sets whether direct paragraph styling should be enabled.
 41      * @type {!boolean}
 42      */
 43     this.directParagraphStylingEnabled = false;
 44     /**
 45      * Sets whether annotation creation/deletion should be enabled.
 46      * @type {!boolean}
 47      */
 48     this.annotationsEnabled = false;
 49 };
 50 
 51 (function () {
 52     "use strict";
 53 
 54     var /**@const*/FILTER_ACCEPT = core.PositionFilter.FilterResult.FILTER_ACCEPT;
 55 
 56     /**
 57      * @constructor
 58      * @implements {core.Destroyable}
 59      * @param {!ops.Session} session
 60      * @param {!string} inputMemberId
 61      * @param {!ops.OdtCursor} shadowCursor
 62      * @param {!gui.SessionControllerOptions} args
 63      */
 64     gui.SessionController = function SessionController(session, inputMemberId, shadowCursor, args) {
 65         var /**@type{!Window}*/window = /**@type{!Window}*/(runtime.getWindow()),
 66             odtDocument = session.getOdtDocument(),
 67             sessionConstraints = new gui.SessionConstraints(),
 68             sessionContext = new gui.SessionContext(session, inputMemberId),
 69             domUtils = core.DomUtils,
 70             odfUtils = odf.OdfUtils,
 71             mimeDataExporter = new gui.MimeDataExporter(),
 72             clipboard = new gui.Clipboard(mimeDataExporter),
 73             keyDownHandler = new gui.KeyboardHandler(),
 74             keyPressHandler = new gui.KeyboardHandler(),
 75             keyUpHandler = new gui.KeyboardHandler(),
 76             /**@type{boolean}*/
 77             clickStartedWithinCanvas = false,
 78             objectNameGenerator = new odf.ObjectNameGenerator(odtDocument.getOdfCanvas().odfContainer(), inputMemberId),
 79             isMouseMoved = false,
 80             /**@type{core.PositionFilter}*/
 81             mouseDownRootFilter = null,
 82             handleMouseClickTimeoutId,
 83             undoManager = null,
 84             eventManager = new gui.EventManager(odtDocument),
 85             annotationsEnabled = args.annotationsEnabled,
 86             annotationController = new gui.AnnotationController(session, sessionConstraints, inputMemberId),
 87             directFormattingController = new gui.DirectFormattingController(session, sessionConstraints, sessionContext, inputMemberId, objectNameGenerator,
 88                                                                             args.directTextStylingEnabled, args.directParagraphStylingEnabled),
 89             createCursorStyleOp = /**@type {function (!number, !number, !boolean):ops.Operation}*/ (directFormattingController.createCursorStyleOp),
 90             createParagraphStyleOps = /**@type {function (!number):!Array.<!ops.Operation>}*/ (directFormattingController.createParagraphStyleOps),
 91             textController = new gui.TextController(session, sessionConstraints, sessionContext, inputMemberId, createCursorStyleOp, createParagraphStyleOps),
 92             imageController = new gui.ImageController(session, sessionConstraints, sessionContext, inputMemberId, objectNameGenerator),
 93             imageSelector = new gui.ImageSelector(odtDocument.getOdfCanvas()),
 94             shadowCursorIterator = odtDocument.createPositionIterator(odtDocument.getRootNode()),
 95             /**@type{!core.ScheduledTask}*/
 96             drawShadowCursorTask,
 97             /**@type{!core.ScheduledTask}*/
 98             redrawRegionSelectionTask,
 99             pasteController = new gui.PasteController(session, sessionConstraints, sessionContext, inputMemberId),
100             inputMethodEditor = new gui.InputMethodEditor(inputMemberId, eventManager),
101             /**@type{number}*/
102             clickCount = 0,
103             hyperlinkClickHandler = new gui.HyperlinkClickHandler(odtDocument.getOdfCanvas().getElement,
104                                                                     keyDownHandler, keyUpHandler),
105             hyperlinkController = new gui.HyperlinkController(session, sessionConstraints, sessionContext, inputMemberId),
106             selectionController = new gui.SelectionController(session, inputMemberId),
107             metadataController = new gui.MetadataController(session, inputMemberId),
108             modifier = gui.KeyboardHandler.Modifier,
109             keyCode = gui.KeyboardHandler.KeyCode,
110             isMacOS = window.navigator.appVersion.toLowerCase().indexOf("mac") !== -1,
111             isIOS = ["iPad", "iPod", "iPhone"].indexOf(window.navigator.platform) !== -1,
112             /**@type{?gui.IOSSafariSupport}*/
113             iOSSafariSupport;
114 
115         runtime.assert(window !== null,
116             "Expected to be run in an environment which has a global window, like a browser.");
117 
118         /**
119          * @param {!Event} e
120          * @return {Node}
121          */
122         function getTarget(e) {
123             // e.srcElement because IE10 likes to be different...
124             return /**@type{Node}*/(e.target) || e.srcElement || null;
125         }
126 
127         /**
128          * @param {!Event} event
129          * @return {undefined}
130          */
131         function cancelEvent(event) {
132             if (event.preventDefault) {
133                 event.preventDefault();
134             } else {
135                 event.returnValue = false;
136             }
137         }
138 
139         /**
140          * @param {!number} x
141          * @param {!number} y
142          * @return {?{container:!Node, offset:!number}}
143          */
144         function caretPositionFromPoint(x, y) {
145             var doc = odtDocument.getDOMDocument(),
146                 c,
147                 result = null;
148 
149             if (doc.caretRangeFromPoint) {
150                 c = doc.caretRangeFromPoint(x, y);
151                 result = {
152                     container: /**@type{!Node}*/(c.startContainer),
153                     offset: c.startOffset
154                 };
155             } else if (doc.caretPositionFromPoint) {
156                 c = doc.caretPositionFromPoint(x, y);
157                 if (c && c.offsetNode) {
158                     result = {
159                         container: c.offsetNode,
160                         offset: c.offset
161                     };
162                 }
163             }
164             return result;
165         }
166 
167         /**
168          * If the user's current selection is region selection (e.g., an image), any executed operations
169          * could cause the picture to shift relative to the selection rectangle.
170          * @return {undefined}
171          */
172         function redrawRegionSelection() {
173             var cursor = odtDocument.getCursor(inputMemberId),
174                 imageElement;
175 
176             if (cursor && cursor.getSelectionType() === ops.OdtCursor.RegionSelection) {
177                 imageElement = odfUtils.getImageElements(cursor.getSelectedRange())[0];
178                 if (imageElement) {
179                     imageSelector.select(/**@type{!Element}*/(imageElement.parentNode));
180                     return;
181                 }
182             }
183 
184             // May have just processed our own remove cursor operation...
185             // In this case, clear any image selection chrome to prevent user confusion
186             imageSelector.clearSelection();
187         }
188 
189         /**
190          * @param {!Event} event
191          * @return {?string}
192          */
193         function stringFromKeyPress(event) {
194             if (event.which === null || event.which === undefined) {
195                 return String.fromCharCode(event.keyCode); // IE
196             }
197             if (event.which !== 0 && event.charCode !== 0) {
198                 return String.fromCharCode(event.which);   // the rest
199             }
200             return null; // special key
201         }
202 
203         /**
204          * Handle the cut operation request
205          * @param {!Event} e
206          * @return {undefined}
207          */
208         function handleCut(e) {
209             var cursor = odtDocument.getCursor(inputMemberId),
210                 selectedRange = cursor.getSelectedRange();
211 
212             if (selectedRange.collapsed) {
213                 // Modifying the clipboard data will clear any existing data,
214                 // so cut shouldn't touch the clipboard if there is nothing selected
215                 e.preventDefault();
216                 return;
217             }
218 
219             // The document is readonly, so the data will never get placed on
220             // the clipboard in most browsers unless we do it ourselves.
221             if (clipboard.setDataFromRange(e, selectedRange)) {
222                 textController.removeCurrentSelection();
223             } else {
224                 // TODO What should we do if cut isn't supported?
225                 runtime.log("Cut operation failed");
226             }
227         }
228 
229         /**
230          * Tell the browser that it's ok to perform a cut action on our read-only body
231          * @return {!boolean}
232          */
233         function handleBeforeCut() {
234             var cursor = odtDocument.getCursor(inputMemberId),
235                 selectedRange = cursor.getSelectedRange();
236             return selectedRange.collapsed !== false; // return false to enable cut menu... straightforward right?!
237         }
238 
239         /**
240          * Handle the copy operation request
241          * @param {!Event} e
242          * @return {undefined}
243          */
244         function handleCopy(e) {
245             var cursor = odtDocument.getCursor(inputMemberId),
246                 selectedRange = cursor.getSelectedRange();
247 
248             if (selectedRange.collapsed) {
249                 // Modifying the clipboard data will clear any existing data,
250                 // so copy shouldn't touch the clipboard if there is nothing
251                 // selected
252                 e.preventDefault();
253                 return;
254             }
255 
256             // Place the data on the clipboard ourselves to ensure consistency
257             // with cut behaviours
258             if (!clipboard.setDataFromRange(e, selectedRange)) {
259                 // TODO What should we do if copy isn't supported?
260                 runtime.log("Copy operation failed");
261             }
262         }
263 
264         /**
265          * @param {!Event} e
266          * @return {undefined}
267          */
268         function handlePaste(e) {
269             var plainText;
270 
271             if (window.clipboardData && window.clipboardData.getData) { // IE
272                 plainText = window.clipboardData.getData('Text');
273             } else if (e.clipboardData && e.clipboardData.getData) { // the rest
274                 plainText = e.clipboardData.getData('text/plain');
275             }
276 
277             if (plainText) {
278                 textController.removeCurrentSelection();
279                 pasteController.paste(plainText);
280             }
281             cancelEvent(e);
282         }
283 
284         /**
285          * Tell the browser that it's ok to perform a paste action on our read-only body
286          * @return {!boolean}
287          */
288         function handleBeforePaste() {
289             return false;
290         }
291 
292         /**
293          * @param {!ops.Operation} op
294          * @return {undefined}
295          */
296         function updateUndoStack(op) {
297             if (undoManager) {
298                 undoManager.onOperationExecuted(op);
299             }
300         }
301 
302         /**
303          * @param {?Event} e
304          * @return {undefined}
305          */
306         function forwardUndoStackChange(e) {
307             odtDocument.emit(ops.OdtDocument.signalUndoStackChanged, e);
308         }
309 
310         /**
311          * @return {!boolean}
312          */
313         function undo() {
314             var hadFocusBefore;
315 
316             if (undoManager) {
317                 hadFocusBefore = eventManager.hasFocus();
318                 undoManager.moveBackward(1);
319                 if (hadFocusBefore) {
320                     eventManager.focus();
321                 }
322                 return true;
323             }
324 
325             return false;
326         }
327         // TODO it will soon be time to grow an UndoController
328         this.undo = undo;
329 
330         /**
331          * @return {!boolean}
332          */
333         function redo() {
334             var hadFocusBefore;
335             if (undoManager) {
336                 hadFocusBefore = eventManager.hasFocus();
337                 undoManager.moveForward(1);
338                 if (hadFocusBefore) {
339                     eventManager.focus();
340                 }
341                 return true;
342             }
343 
344             return false;
345         }
346         // TODO it will soon be time to grow an UndoController
347         this.redo = redo;
348 
349         /**
350          * This processes our custom drag events and if they are on
351          * a selection handle (with the attribute 'end' denoting the left
352          * or right handle), updates the shadow cursor's selection to
353          * be on those endpoints.
354          * @param {!Event} event
355          * @return {undefined}
356          */
357         function extendSelectionByDrag(event) {
358             var position,
359                 cursor = odtDocument.getCursor(inputMemberId),
360                 selectedRange = cursor.getSelectedRange(),
361                 newSelectionRange,
362                 /**@type{!string}*/
363                 handleEnd = /**@type{!Element}*/(getTarget(event)).getAttribute('end');
364 
365             if (selectedRange && handleEnd) {
366                 position = caretPositionFromPoint(event.clientX, event.clientY);
367                 if (position) {
368                     shadowCursorIterator.setUnfilteredPosition(position.container, position.offset);
369                     if (mouseDownRootFilter.acceptPosition(shadowCursorIterator) === FILTER_ACCEPT) {
370                         newSelectionRange = /**@type{!Range}*/(selectedRange.cloneRange());
371                         if (handleEnd === 'left') {
372                             newSelectionRange.setStart(shadowCursorIterator.container(), shadowCursorIterator.unfilteredDomOffset());
373                         } else {
374                             newSelectionRange.setEnd(shadowCursorIterator.container(), shadowCursorIterator.unfilteredDomOffset());
375                         }
376                         shadowCursor.setSelectedRange(newSelectionRange, handleEnd === 'right');
377                         odtDocument.emit(ops.Document.signalCursorMoved, shadowCursor);
378                     }
379                 }
380             }
381         }
382 
383         function updateCursorSelection() {
384             selectionController.selectRange(shadowCursor.getSelectedRange(), shadowCursor.hasForwardSelection(), 1);
385         }
386 
387         function updateShadowCursor() {
388             var selection = window.getSelection(),
389                 selectionRange = selection.rangeCount > 0 && selectionController.selectionToRange(selection);
390 
391             if (clickStartedWithinCanvas && selectionRange) {
392                 isMouseMoved = true;
393 
394                 imageSelector.clearSelection();
395                 shadowCursorIterator.setUnfilteredPosition(/**@type {!Node}*/(selection.focusNode), selection.focusOffset);
396                 if (mouseDownRootFilter.acceptPosition(shadowCursorIterator) === FILTER_ACCEPT) {
397                     if (clickCount === 2) {
398                         selectionController.expandToWordBoundaries(selectionRange.range);
399                     } else if (clickCount >= 3) {
400                         selectionController.expandToParagraphBoundaries(selectionRange.range);
401                     }
402                     shadowCursor.setSelectedRange(selectionRange.range, selectionRange.hasForwardSelection);
403                     odtDocument.emit(ops.Document.signalCursorMoved, shadowCursor);
404                 }
405             }
406         }
407 
408         /**
409          * In order for drag operations to work, the browser needs to have it's current
410          * selection set. This is called on mouse down to synchronize the user's last selection
411          * to the browser selection
412          * @param {ops.OdtCursor} cursor
413          * @return {undefined}
414          */
415         function synchronizeWindowSelection(cursor) {
416             var selection = window.getSelection(),
417                 range = cursor.getSelectedRange();
418 
419             if (selection.extend) {
420                 if (cursor.hasForwardSelection()) {
421                     selection.collapse(range.startContainer, range.startOffset);
422                     selection.extend(range.endContainer, range.endOffset);
423                 } else {
424                     selection.collapse(range.endContainer, range.endOffset);
425                     selection.extend(range.startContainer, range.startOffset);
426                 }
427             } else {
428                 // Internet explorer does provide any method for
429                 // preserving the range direction
430                 // See http://msdn.microsoft.com/en-us/library/ie/ff974359%28v=vs.85%29.aspx
431                 // Unfortunately, clearing the range will also blur the current focus.
432                 selection.removeAllRanges();
433                 selection.addRange(range.cloneRange());
434             }
435         }
436 
437         /**
438          * Return the number of mouse clicks if the mouse event is for the primary button. Otherwise return 0.
439          * @param {!UIEvent} event
440          * @return {!number}
441          */
442         function computeClickCount(event) {
443             // According to the spec, button === 0 indicates the primary button (the left button by default, or the
444             // right button if the user has switched their mouse buttons around).
445             return event.button === 0 ? event.detail : 0;
446         }
447 
448         /**
449          * Updates a flag indicating whether the mouse down event occurred within the OdfCanvas element.
450          * This is necessary because the mouse-up binding needs to be global in order to handle mouse-up
451          * events that occur when the user releases the mouse button outside the canvas.
452          * This filter limits selection changes to mouse down events that start inside the canvas
453          * @param {!UIEvent} e
454          */
455         function handleMouseDown(e) {
456             var target = getTarget(e),
457                 cursor = odtDocument.getCursor(inputMemberId),
458                 rootNode;
459             clickStartedWithinCanvas = target !== null && domUtils.containsNode(odtDocument.getOdfCanvas().getElement(), target);
460             if (clickStartedWithinCanvas) {
461                 isMouseMoved = false;
462                 rootNode = odtDocument.getRootElement(/**@type{!Node}*/(target)) || odtDocument.getRootNode();
463                 mouseDownRootFilter = odtDocument.createRootFilter(rootNode);
464                 clickCount = computeClickCount(e);
465                 if (cursor && e.shiftKey) {
466                     // Firefox seems to get rather confused about the window selection when shift+extending it.
467                     // Help this poor browser by resetting the window selection back to the anchor node if the user
468                     // is holding shift.
469                     window.getSelection().collapse(cursor.getAnchorNode(), 0);
470                 } else {
471                     synchronizeWindowSelection(cursor);
472                 }
473                 if (clickCount > 1) {
474                     updateShadowCursor();
475                 }
476             }
477         }
478 
479         /**
480          * Return a mutable version of a selection-type object.
481          * @param {?Selection} selection
482          * @return {?{anchorNode: ?Node, anchorOffset: !number, focusNode: ?Node, focusOffset: !number}}
483          */
484         function mutableSelection(selection) {
485             if (selection) {
486                 return {
487                     anchorNode: selection.anchorNode,
488                     anchorOffset: selection.anchorOffset,
489                     focusNode: selection.focusNode,
490                     focusOffset: selection.focusOffset
491                 };
492             }
493             return null;
494         }
495 
496         /**
497          * Gets the next walkable position after the given node.
498          * @param {!Node} node
499          * @return {?{container:!Node, offset:!number}}
500          */
501         function getNextWalkablePosition(node) {
502             var root = odtDocument.getRootElement(node),
503                 rootFilter = odtDocument.createRootFilter(root),
504                 stepIterator = odtDocument.createStepIterator(node, 0, [rootFilter, odtDocument.getPositionFilter()], root);
505             stepIterator.setPosition(node, node.childNodes.length);
506             if (!stepIterator.roundToNextStep()) {
507                 return null;
508             }
509             return {
510                 container: stepIterator.container(),
511                 offset: stepIterator.offset()
512             };
513         }
514 
515         /**
516          * Causes a cursor movement to the position hinted by a mouse click
517          * event.
518          * @param {!UIEvent} event
519          * @return {undefined}
520          */
521         function moveByMouseClickEvent(event) {
522             var selection = mutableSelection(window.getSelection()),
523                 isCollapsed = window.getSelection().isCollapsed,
524                 position,
525                 selectionRange,
526                 rect,
527                 frameNode;
528 
529             if (!selection.anchorNode && !selection.focusNode) {
530                 // chrome & safari will report null for focus and anchor nodes after a right-click in text selection
531                 position = caretPositionFromPoint(event.clientX, event.clientY);
532                 if (position) {
533                     selection.anchorNode = /**@type{!Node}*/(position.container);
534                     selection.anchorOffset = position.offset;
535                     selection.focusNode = selection.anchorNode;
536                     selection.focusOffset = selection.anchorOffset;
537                 }
538             }
539 
540             if (odfUtils.isImage(selection.focusNode) && selection.focusOffset === 0
541                 && odfUtils.isCharacterFrame(selection.focusNode.parentNode)) {
542                 // In FireFox if an image has no text around it, click on either side of the
543                 // image resulting the same selection get returned. focusNode: image, focusOffset: 0
544                 // Move the cursor to the next walkable position when clicking on the right side of an image
545                 frameNode = /**@type{!Element}*/(selection.focusNode.parentNode);
546                 rect = frameNode.getBoundingClientRect();
547                 if (event.clientX > rect.left) {
548                     // On OSX, right-clicking on an image at the end of a range selection will hit
549                     // this particular branch. The image should remain selected if the right-click occurs on top
550                     // of it as technically it's the same behaviour as right clicking on an existing text selection.
551                     position = getNextWalkablePosition(frameNode);
552                     if (position) {
553                         selection.focusNode = position.container;
554                         selection.focusOffset = position.offset;
555                         if (isCollapsed) {
556                             // See above comment for the circumstances when the range might not be collapsed
557                             selection.anchorNode = selection.focusNode;
558                             selection.anchorOffset = selection.focusOffset;
559                         }
560                     }
561                 }
562             } else if (odfUtils.isImage(selection.focusNode.firstChild) && selection.focusOffset === 1
563                 && odfUtils.isCharacterFrame(selection.focusNode)) {
564                 // When click on the right side of an image that has no text elements, non-FireFox browsers
565                 // will return focusNode: frame, focusOffset: 1 as the selection. Since this is not a valid cursor
566                 // position, move the cursor to the next walkable position after the frame node.
567 
568                 // To activate this branch (only applicable on OSX + Linux WebKit-derived browsers AFAIK):
569                 // 1. With a paragraph containing some text followed by an inline image and no trailing text,
570                 //    select from the start of paragraph to the end.
571                 // 2. Now click once to the right hand side of the image. The cursor *should* jump to the right side
572                 position = getNextWalkablePosition(selection.focusNode);
573                 if (position) {
574                     // This should only ever be hit when the selection is intended to become collapsed
575                     selection.anchorNode = selection.focusNode = position.container;
576                     selection.anchorOffset = selection.focusOffset = position.offset;
577                 }
578             }
579 
580             // Need to check the selection again in case the caret position didn't return any result
581             if (selection.anchorNode && selection.focusNode) {
582                 selectionRange = selectionController.selectionToRange(selection);
583                 selectionController.selectRange(selectionRange.range,
584                     selectionRange.hasForwardSelection, computeClickCount(event));
585             }
586             eventManager.focus(); // Mouse clicks often cause focus to shift. Recapture this straight away
587         }
588 
589         /**
590          * @param {!Event} event
591          * @return {undefined}
592          */
593         function selectWordByLongPress(event) {
594             var /**@type{?{anchorNode: ?Node, anchorOffset: !number, focusNode: ?Node, focusOffset: !number}}*/
595                 selection,
596                 position,
597                 selectionRange,
598                 container, offset;
599 
600             position = caretPositionFromPoint(event.clientX, event.clientY);
601             if (position) {
602                 container = /**@type{!Node}*/(position.container);
603                 offset = position.offset;
604 
605                 selection = {
606                     anchorNode: container,
607                     anchorOffset: offset,
608                     focusNode: container,
609                     focusOffset: offset
610                 };
611 
612                 selectionRange = selectionController.selectionToRange(selection);
613                 selectionController.selectRange(selectionRange.range,
614                 selectionRange.hasForwardSelection, 2);
615                 eventManager.focus();
616             }
617         }
618 
619         /**
620          * @param {!UIEvent} event
621          * @return {undefined}
622          */
623         function handleMouseClickEvent(event) {
624             var target = getTarget(event),
625                 clickEvent,
626                 range,
627                 wasCollapsed,
628                 frameNode,
629                 pos;
630 
631             drawShadowCursorTask.processRequests(); // Resynchronise the shadow cursor before processing anything else
632 
633             if (clickStartedWithinCanvas) {
634                 // Each mouse down event should only ever result in a single mouse click being processed.
635                 // This is to cope with there being no hard rules about whether a contextmenu
636                 // should be followed by a mouseup as well according to the HTML5 specs.
637                 // See http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus
638 
639                 // We don't want to just select the image if it is a range selection hence ensure the selection is collapsed.
640                 if (odfUtils.isImage(target) && odfUtils.isCharacterFrame(target.parentNode) && window.getSelection().isCollapsed) {
641                     selectionController.selectImage(/**@type{!Node}*/(target.parentNode));
642                     eventManager.focus(); // Mouse clicks often cause focus to shift. Recapture this straight away
643                 } else if (imageSelector.isSelectorElement(target)) {
644                     eventManager.focus(); // Mouse clicks often cause focus to shift. Recapture this straight away
645                 } else if (isMouseMoved) {
646                     range = shadowCursor.getSelectedRange();
647                     wasCollapsed = range.collapsed;
648                     // Resets the endContainer and endOffset when a forward selection end up on an image;
649                     // Otherwise the image will not be selected because endContainer: image, endOffset 0 is not a valid
650                     // cursor position.
651                     if (odfUtils.isImage(range.endContainer) && range.endOffset === 0
652                             && odfUtils.isCharacterFrame(range.endContainer.parentNode)) {
653                         frameNode = /**@type{!Element}*/(range.endContainer.parentNode);
654                         pos = getNextWalkablePosition(frameNode);
655                         if (pos) {
656                             range.setEnd(pos.container, pos.offset);
657                             if (wasCollapsed) {
658                                 range.collapse(false); // collapses the range to its end
659                             }
660                         }
661                     }
662                     selectionController.selectRange(range, shadowCursor.hasForwardSelection(), computeClickCount(event));
663                     eventManager.focus(); // Mouse clicks often cause focus to shift. Recapture this straight away
664                 } else {
665                     // Clicking in already selected text won't update window.getSelection() until just after
666                     // the click is processed. Set 0 timeout here so the newly clicked position can be updated
667                     // by the browser. Unfortunately this is only working in Firefox. For other browsers, we have to work
668                     // out the caret position from two coordinates.
669                     // In iOS, however, it is not possible to assign focus within a timeout. But in that case
670                     // we do not even need a timeout, because we do not use native selections at all there,
671                     // therefore for that platform, just directly move by the mouse click and give focus.
672                     if (isIOS) {
673                         moveByMouseClickEvent(event);
674                     } else {
675                         // IE10 destructs event objects once the event handler is done, so create a copy of the data.
676                         // "The event object is only available during an event; that is, you can use it in event handlers but not in other code"
677                         // (from http://msdn.microsoft.com/en-us/library/ie/aa703876(v=vs.85).aspx)
678                         // TODO: IE10 on a test machine does not have the "detail" property set on "mouseup" events here,
679                         // even if the docs claim it should exist, cmp. http://msdn.microsoft.com/en-au/library/ie/ff974344(v=vs.85).aspx
680                         // So doubleclicks will not be detected on (some?) IE currently.
681                         clickEvent = /**@type{!UIEvent}*/(domUtils.cloneEvent(event));
682                         handleMouseClickTimeoutId = runtime.setTimeout(function () {
683                             moveByMouseClickEvent(clickEvent);
684                         }, 0);
685                     }
686                 }
687                 // TODO assumes the mouseup/contextmenu is the same button as the mousedown that initialized the clickCount
688                 clickCount = 0;
689                 clickStartedWithinCanvas = false;
690                 isMouseMoved = false;
691             }
692         }
693 
694         /**
695          * @param {!MouseEvent} e
696          * @return {undefined}
697          */
698         function handleDragStart(e) {
699             var cursor = odtDocument.getCursor(inputMemberId),
700                 selectedRange = cursor.getSelectedRange();
701 
702             if (selectedRange.collapsed) {
703                 return;
704             }
705 
706             mimeDataExporter.exportRangeToDataTransfer(/**@type{!DataTransfer}*/(e.dataTransfer), selectedRange);
707         }
708 
709         function handleDragEnd() {
710             // Drag operations consume the corresponding mouse up event.
711             // If this happens, the selection should still be reset.
712             if (clickStartedWithinCanvas) {
713                 eventManager.focus();
714             }
715             clickCount = 0;
716             clickStartedWithinCanvas = false;
717             isMouseMoved = false;
718         }
719 
720         /**
721          * @param {!UIEvent} e
722          */
723         function handleContextMenu(e) {
724             // TODO Various browsers have different default behaviours on right click
725             // We can detect this at runtime without doing any kind of platform sniffing
726             // simply by observing what the browser has tried to do on right-click.
727             // - OSX: Safari/Chrome - Expand to word boundary
728             // - OSX: Firefox - No expansion
729             // - Windows: Safari/Chrome/Firefox - No expansion
730             handleMouseClickEvent(e);
731         }
732 
733         /**
734          * @param {!UIEvent} event
735          */
736         function handleMouseUp(event) {
737             var target = /**@type{!Element}*/(getTarget(event)),
738                 annotationNode = null;
739 
740             if (target.className === "annotationRemoveButton") {
741                 runtime.assert(annotationsEnabled, "Remove buttons are displayed on annotations while annotation editing is disabled in the controller.");
742                 annotationNode = /**@type{!Element}*/(target.parentNode).getElementsByTagNameNS(odf.Namespaces.officens, 'annotation').item(0);
743                 annotationController.removeAnnotation(/**@type{!Element}*/(annotationNode));
744                 eventManager.focus();
745             } else {
746                 if (target.getAttribute('class') !== 'webodf-draggable') {
747                     handleMouseClickEvent(event);
748                 }
749             }
750         }
751 
752         /**
753          * Handle composition end event. If there is data specified, treat this as text
754          * to be inserted into the document.
755          * @param {!CompositionEvent} e
756          */
757         function insertNonEmptyData(e) {
758             // https://dvcs.w3.org/hg/dom3events/raw-file/tip/html/DOM3-Events.html#event-type-compositionend
759             var input = e.data;
760             if (input) {
761                 if (input.indexOf("\n") === -1) {
762                     textController.insertText(input);
763                 } else {
764                     // Multi-line input should be handled as if it was pasted, rather than inserted as one giant
765                     // single string.
766                     pasteController.paste(input);
767                 }
768             }
769         }
770 
771         /**
772          * Executes the provided function and returns true
773          * Used to swallow events regardless of whether an operation was created
774          * @param {!Function} fn
775          * @return {!Function}
776          */
777         function returnTrue(fn) {
778             return function () {
779                 fn();
780                 return true;
781             };
782         }
783 
784         /**
785          * Executes the given function on range selection only
786          * @param {function(T):(boolean|undefined)} fn
787          * @return {function(T):(boolean|undefined)}
788          * @template T
789          */
790         function rangeSelectionOnly(fn) {
791             /**
792              * @param {*} e
793              * @return {!boolean|undefined}
794              */
795             function f(e) {
796                 var selectionType = odtDocument.getCursor(inputMemberId).getSelectionType();
797                 if (selectionType === ops.OdtCursor.RangeSelection) {
798                     return fn(e);
799                 }
800                 return true;
801             }
802             return f;
803         }
804 
805         /**
806          * Inserts the local cursor.
807          * @return {undefined}
808          */
809         function insertLocalCursor() {
810             runtime.assert(session.getOdtDocument().getCursor(inputMemberId) === undefined, "Inserting local cursor a second time.");
811 
812             var op = new ops.OpAddCursor();
813             op.init({memberid: inputMemberId});
814             session.enqueue([op]);
815             // Immediately capture focus when the local cursor is inserted
816             eventManager.focus();
817         }
818         this.insertLocalCursor = insertLocalCursor;
819 
820 
821         /**
822          * Removes the local cursor.
823          * @return {undefined}
824          */
825         function removeLocalCursor() {
826             runtime.assert(session.getOdtDocument().getCursor(inputMemberId) !== undefined, "Removing local cursor without inserting before.");
827 
828             var op = new ops.OpRemoveCursor();
829             op.init({memberid: inputMemberId});
830             session.enqueue([op]);
831         }
832         this.removeLocalCursor = removeLocalCursor;
833 
834         /**
835          * @return {undefined}
836          */
837         this.startEditing = function () {
838             inputMethodEditor.subscribe(gui.InputMethodEditor.signalCompositionStart, textController.removeCurrentSelection);
839             inputMethodEditor.subscribe(gui.InputMethodEditor.signalCompositionEnd, insertNonEmptyData);
840 
841             eventManager.subscribe("beforecut", handleBeforeCut);
842             eventManager.subscribe("cut", handleCut);
843             eventManager.subscribe("beforepaste", handleBeforePaste);
844             eventManager.subscribe("paste", handlePaste);
845 
846             if (undoManager) {
847                 // For most undo managers, the initial state is a clean document *with* a cursor present
848                 undoManager.initialize();
849             }
850 
851             eventManager.setEditing(true);
852             hyperlinkClickHandler.setModifier(isMacOS ? modifier.Meta : modifier.Ctrl);
853             // Most browsers will go back one page when given an unhandled backspace press
854             // To prevent this, the event handler for this key should always return true
855             keyDownHandler.bind(keyCode.Backspace, modifier.None, returnTrue(textController.removeTextByBackspaceKey), true);
856             keyDownHandler.bind(keyCode.Delete, modifier.None, textController.removeTextByDeleteKey);
857 
858             // TODO: deselect the currently selected image when press Esc
859             // TODO: move the image selection box to next image/frame when press tab on selected image
860             keyDownHandler.bind(keyCode.Tab, modifier.None, rangeSelectionOnly(function () {
861                 textController.insertText("\t");
862                 return true;
863             }));
864 
865             if (isMacOS) {
866                 keyDownHandler.bind(keyCode.Clear, modifier.None, textController.removeCurrentSelection);
867                 keyDownHandler.bind(keyCode.B, modifier.Meta, rangeSelectionOnly(directFormattingController.toggleBold));
868                 keyDownHandler.bind(keyCode.I, modifier.Meta, rangeSelectionOnly(directFormattingController.toggleItalic));
869                 keyDownHandler.bind(keyCode.U, modifier.Meta, rangeSelectionOnly(directFormattingController.toggleUnderline));
870                 keyDownHandler.bind(keyCode.L, modifier.MetaShift, rangeSelectionOnly(directFormattingController.alignParagraphLeft));
871                 keyDownHandler.bind(keyCode.E, modifier.MetaShift, rangeSelectionOnly(directFormattingController.alignParagraphCenter));
872                 keyDownHandler.bind(keyCode.R, modifier.MetaShift, rangeSelectionOnly(directFormattingController.alignParagraphRight));
873                 keyDownHandler.bind(keyCode.J, modifier.MetaShift, rangeSelectionOnly(directFormattingController.alignParagraphJustified));
874                 if (annotationsEnabled) {
875                     keyDownHandler.bind(keyCode.C, modifier.MetaShift, annotationController.addAnnotation);
876                 }
877                 keyDownHandler.bind(keyCode.Z, modifier.Meta, undo);
878                 keyDownHandler.bind(keyCode.Z, modifier.MetaShift, redo);
879             } else {
880                 keyDownHandler.bind(keyCode.B, modifier.Ctrl, rangeSelectionOnly(directFormattingController.toggleBold));
881                 keyDownHandler.bind(keyCode.I, modifier.Ctrl, rangeSelectionOnly(directFormattingController.toggleItalic));
882                 keyDownHandler.bind(keyCode.U, modifier.Ctrl, rangeSelectionOnly(directFormattingController.toggleUnderline));
883                 keyDownHandler.bind(keyCode.L, modifier.CtrlShift, rangeSelectionOnly(directFormattingController.alignParagraphLeft));
884                 keyDownHandler.bind(keyCode.E, modifier.CtrlShift, rangeSelectionOnly(directFormattingController.alignParagraphCenter));
885                 keyDownHandler.bind(keyCode.R, modifier.CtrlShift, rangeSelectionOnly(directFormattingController.alignParagraphRight));
886                 keyDownHandler.bind(keyCode.J, modifier.CtrlShift, rangeSelectionOnly(directFormattingController.alignParagraphJustified));
887                 if (annotationsEnabled) {
888                     keyDownHandler.bind(keyCode.C, modifier.CtrlAlt, annotationController.addAnnotation);
889                 }
890                 keyDownHandler.bind(keyCode.Z, modifier.Ctrl, undo);
891                 keyDownHandler.bind(keyCode.Z, modifier.CtrlShift, redo);
892             }
893 
894             // the default action is to insert text into the document
895             /**
896              * @param {!KeyboardEvent} e
897              * @return {boolean|undefined}
898              */
899             function handler(e) {
900                 var text = stringFromKeyPress(e);
901                 if (text && !(e.altKey || e.ctrlKey || e.metaKey)) {
902                     textController.insertText(text);
903                     return true;
904                 }
905                 return false;
906             }
907             keyPressHandler.setDefault(rangeSelectionOnly(handler));
908             keyPressHandler.bind(keyCode.Enter, modifier.None, rangeSelectionOnly(textController.enqueueParagraphSplittingOps));
909         };
910 
911         /**
912          * @return {undefined}
913          */
914         this.endEditing = function () {
915             inputMethodEditor.unsubscribe(gui.InputMethodEditor.signalCompositionStart, textController.removeCurrentSelection);
916             inputMethodEditor.unsubscribe(gui.InputMethodEditor.signalCompositionEnd, insertNonEmptyData);
917 
918             eventManager.unsubscribe("cut", handleCut);
919             eventManager.unsubscribe("beforecut", handleBeforeCut);
920             eventManager.unsubscribe("paste", handlePaste);
921             eventManager.unsubscribe("beforepaste", handleBeforePaste);
922 
923             eventManager.setEditing(false);
924             hyperlinkClickHandler.setModifier(modifier.None);
925             keyDownHandler.bind(keyCode.Backspace, modifier.None, function () { return true; }, true);
926             keyDownHandler.unbind(keyCode.Delete, modifier.None);
927             keyDownHandler.unbind(keyCode.Tab, modifier.None);
928 
929             if (isMacOS) {
930                 keyDownHandler.unbind(keyCode.Clear, modifier.None);
931                 keyDownHandler.unbind(keyCode.B, modifier.Meta);
932                 keyDownHandler.unbind(keyCode.I, modifier.Meta);
933                 keyDownHandler.unbind(keyCode.U, modifier.Meta);
934                 keyDownHandler.unbind(keyCode.L, modifier.MetaShift);
935                 keyDownHandler.unbind(keyCode.E, modifier.MetaShift);
936                 keyDownHandler.unbind(keyCode.R, modifier.MetaShift);
937                 keyDownHandler.unbind(keyCode.J, modifier.MetaShift);
938                 if (annotationsEnabled) {
939                     keyDownHandler.unbind(keyCode.C, modifier.MetaShift);
940                 }
941                 keyDownHandler.unbind(keyCode.Z, modifier.Meta);
942                 keyDownHandler.unbind(keyCode.Z, modifier.MetaShift);
943             } else {
944                 keyDownHandler.unbind(keyCode.B, modifier.Ctrl);
945                 keyDownHandler.unbind(keyCode.I, modifier.Ctrl);
946                 keyDownHandler.unbind(keyCode.U, modifier.Ctrl);
947                 keyDownHandler.unbind(keyCode.L, modifier.CtrlShift);
948                 keyDownHandler.unbind(keyCode.E, modifier.CtrlShift);
949                 keyDownHandler.unbind(keyCode.R, modifier.CtrlShift);
950                 keyDownHandler.unbind(keyCode.J, modifier.CtrlShift);
951                 if (annotationsEnabled) {
952                     keyDownHandler.unbind(keyCode.C, modifier.CtrlAlt);
953                 }
954                 keyDownHandler.unbind(keyCode.Z, modifier.Ctrl);
955                 keyDownHandler.unbind(keyCode.Z, modifier.CtrlShift);
956             }
957 
958             keyPressHandler.setDefault(null);
959             keyPressHandler.unbind(keyCode.Enter, modifier.None);
960         };
961 
962         /**
963          * @return {!string}
964          */
965         this.getInputMemberId = function () {
966             return inputMemberId;
967         };
968 
969         /**
970          * @return {!ops.Session}
971          */
972         this.getSession = function () {
973             return session;
974         };
975 
976         /**
977          * @return {!gui.SessionConstraints}
978          */
979         this.getSessionConstraints = function () {
980             return sessionConstraints;
981         };
982 
983         /**
984          * @param {?gui.UndoManager} manager
985          * @return {undefined}
986          */
987         this.setUndoManager = function (manager) {
988             if (undoManager) {
989                 undoManager.unsubscribe(gui.UndoManager.signalUndoStackChanged, forwardUndoStackChange);
990             }
991 
992             undoManager = manager;
993             if (undoManager) {
994                 undoManager.setDocument(odtDocument);
995                 // As per gui.UndoManager, this should NOT fire any signals or report
996                 // events being executed back to the undo manager.
997                 undoManager.setPlaybackFunction(session.enqueue);
998                 undoManager.subscribe(gui.UndoManager.signalUndoStackChanged, forwardUndoStackChange);
999             }
1000         };
1001 
1002         /**
1003          * @return {?gui.UndoManager}
1004          */
1005         this.getUndoManager = function () {
1006             return undoManager;
1007         };
1008 
1009         /**
1010          * @return {!gui.MetadataController}
1011          */
1012         this.getMetadataController = function () {
1013             return metadataController;
1014         };
1015 
1016         /**
1017          * @return {?gui.AnnotationController}
1018          */
1019         this.getAnnotationController = function () {
1020             return annotationController;
1021         };
1022 
1023         /**
1024          * @return {!gui.DirectFormattingController}
1025          */
1026         this.getDirectFormattingController = function () {
1027             return directFormattingController;
1028         };
1029 
1030         /**
1031          * @return {!gui.HyperlinkClickHandler}
1032          */
1033         this.getHyperlinkClickHandler = function () {
1034             return hyperlinkClickHandler;
1035         };
1036 
1037         /**
1038          * @return {!gui.HyperlinkController}
1039          */
1040         this.getHyperlinkController = function () {
1041             return hyperlinkController;
1042         };
1043 
1044         /**
1045          * @return {!gui.ImageController}
1046          */
1047         this.getImageController = function () {
1048             return imageController;
1049         };
1050 
1051         /**
1052          * @return {!gui.SelectionController}
1053          */
1054         this.getSelectionController = function () {
1055             return selectionController;
1056         };
1057 
1058         /**
1059          * @return {!gui.TextController}
1060          */
1061         this.getTextController = function () {
1062             return textController;
1063         };
1064 
1065         /**
1066          * @return {!gui.EventManager}
1067          */
1068         this.getEventManager = function() {
1069             return eventManager;
1070         };
1071 
1072         /**
1073          * Return the keyboard event handlers
1074          * @return {{keydown: gui.KeyboardHandler, keypress: gui.KeyboardHandler}}
1075          */
1076         this.getKeyboardHandlers = function () {
1077             return {
1078                 keydown: keyDownHandler,
1079                 keypress: keyPressHandler
1080             };
1081         };
1082 
1083         /**
1084          * @param {!function(!Object=)} callback passing an error object in case of error
1085          * @return {undefined}
1086          */
1087         function destroy(callback) {
1088             eventManager.unsubscribe("keydown", keyDownHandler.handleEvent);
1089             eventManager.unsubscribe("keypress", keyPressHandler.handleEvent);
1090             eventManager.unsubscribe("keyup", keyUpHandler.handleEvent);
1091             eventManager.unsubscribe("copy", handleCopy);
1092             eventManager.unsubscribe("mousedown", handleMouseDown);
1093             eventManager.unsubscribe("mousemove", drawShadowCursorTask.trigger);
1094             eventManager.unsubscribe("mouseup", handleMouseUp);
1095             eventManager.unsubscribe("contextmenu", handleContextMenu);
1096             eventManager.unsubscribe("dragstart", handleDragStart);
1097             eventManager.unsubscribe("dragend", handleDragEnd);
1098             eventManager.unsubscribe("click", hyperlinkClickHandler.handleClick);
1099             eventManager.unsubscribe("longpress", selectWordByLongPress);
1100             eventManager.unsubscribe("drag", extendSelectionByDrag);
1101             eventManager.unsubscribe("dragstop", updateCursorSelection);
1102 
1103             odtDocument.unsubscribe(ops.OdtDocument.signalOperationEnd, redrawRegionSelectionTask.trigger);
1104             odtDocument.unsubscribe(ops.Document.signalCursorAdded, inputMethodEditor.registerCursor);
1105             odtDocument.unsubscribe(ops.Document.signalCursorRemoved, inputMethodEditor.removeCursor);
1106             odtDocument.unsubscribe(ops.OdtDocument.signalOperationEnd, updateUndoStack);
1107 
1108             callback();
1109         }
1110 
1111         /**
1112          * @param {!function(!Error=)} callback passing an error object in case of error
1113          * @return {undefined}
1114          */
1115         this.destroy = function (callback) {
1116             var destroyCallbacks = [
1117                 drawShadowCursorTask.destroy,
1118                 redrawRegionSelectionTask.destroy,
1119                 directFormattingController.destroy,
1120                 inputMethodEditor.destroy,
1121                 eventManager.destroy,
1122                 hyperlinkClickHandler.destroy,
1123                 hyperlinkController.destroy,
1124                 metadataController.destroy,
1125                 selectionController.destroy,
1126                 textController.destroy,
1127                 destroy
1128             ];
1129 
1130             if (iOSSafariSupport) {
1131                 destroyCallbacks.unshift(iOSSafariSupport.destroy);
1132             }
1133 
1134             runtime.clearTimeout(handleMouseClickTimeoutId);
1135             core.Async.destroyAll(destroyCallbacks, callback);
1136         };
1137 
1138         function init() {
1139             drawShadowCursorTask = core.Task.createRedrawTask(updateShadowCursor);
1140             redrawRegionSelectionTask = core.Task.createRedrawTask(redrawRegionSelection);
1141 
1142             keyDownHandler.bind(keyCode.Left, modifier.None, rangeSelectionOnly(selectionController.moveCursorToLeft));
1143             keyDownHandler.bind(keyCode.Right, modifier.None, rangeSelectionOnly(selectionController.moveCursorToRight));
1144             keyDownHandler.bind(keyCode.Up, modifier.None, rangeSelectionOnly(selectionController.moveCursorUp));
1145             keyDownHandler.bind(keyCode.Down, modifier.None, rangeSelectionOnly(selectionController.moveCursorDown));
1146             keyDownHandler.bind(keyCode.Left, modifier.Shift, rangeSelectionOnly(selectionController.extendSelectionToLeft));
1147             keyDownHandler.bind(keyCode.Right, modifier.Shift, rangeSelectionOnly(selectionController.extendSelectionToRight));
1148             keyDownHandler.bind(keyCode.Up, modifier.Shift, rangeSelectionOnly(selectionController.extendSelectionUp));
1149             keyDownHandler.bind(keyCode.Down, modifier.Shift, rangeSelectionOnly(selectionController.extendSelectionDown));
1150             keyDownHandler.bind(keyCode.Home, modifier.None, rangeSelectionOnly(selectionController.moveCursorToLineStart));
1151             keyDownHandler.bind(keyCode.End, modifier.None, rangeSelectionOnly(selectionController.moveCursorToLineEnd));
1152             keyDownHandler.bind(keyCode.Home, modifier.Ctrl, rangeSelectionOnly(selectionController.moveCursorToDocumentStart));
1153             keyDownHandler.bind(keyCode.End, modifier.Ctrl, rangeSelectionOnly(selectionController.moveCursorToDocumentEnd));
1154             keyDownHandler.bind(keyCode.Home, modifier.Shift, rangeSelectionOnly(selectionController.extendSelectionToLineStart));
1155             keyDownHandler.bind(keyCode.End, modifier.Shift, rangeSelectionOnly(selectionController.extendSelectionToLineEnd));
1156             keyDownHandler.bind(keyCode.Up, modifier.CtrlShift, rangeSelectionOnly(selectionController.extendSelectionToParagraphStart));
1157             keyDownHandler.bind(keyCode.Down, modifier.CtrlShift, rangeSelectionOnly(selectionController.extendSelectionToParagraphEnd));
1158             keyDownHandler.bind(keyCode.Home, modifier.CtrlShift, rangeSelectionOnly(selectionController.extendSelectionToDocumentStart));
1159             keyDownHandler.bind(keyCode.End, modifier.CtrlShift, rangeSelectionOnly(selectionController.extendSelectionToDocumentEnd));
1160 
1161             if (isMacOS) {
1162                 keyDownHandler.bind(keyCode.Left, modifier.Alt, rangeSelectionOnly(selectionController.moveCursorBeforeWord));
1163                 keyDownHandler.bind(keyCode.Right, modifier.Alt, rangeSelectionOnly(selectionController.moveCursorPastWord));
1164                 keyDownHandler.bind(keyCode.Left, modifier.Meta, rangeSelectionOnly(selectionController.moveCursorToLineStart));
1165                 keyDownHandler.bind(keyCode.Right, modifier.Meta, rangeSelectionOnly(selectionController.moveCursorToLineEnd));
1166                 keyDownHandler.bind(keyCode.Home, modifier.Meta, rangeSelectionOnly(selectionController.moveCursorToDocumentStart));
1167                 keyDownHandler.bind(keyCode.End, modifier.Meta, rangeSelectionOnly(selectionController.moveCursorToDocumentEnd));
1168                 keyDownHandler.bind(keyCode.Left, modifier.AltShift, rangeSelectionOnly(selectionController.extendSelectionBeforeWord));
1169                 keyDownHandler.bind(keyCode.Right, modifier.AltShift, rangeSelectionOnly(selectionController.extendSelectionPastWord));
1170                 keyDownHandler.bind(keyCode.Left, modifier.MetaShift, rangeSelectionOnly(selectionController.extendSelectionToLineStart));
1171                 keyDownHandler.bind(keyCode.Right, modifier.MetaShift, rangeSelectionOnly(selectionController.extendSelectionToLineEnd));
1172                 keyDownHandler.bind(keyCode.Up, modifier.AltShift, rangeSelectionOnly(selectionController.extendSelectionToParagraphStart));
1173                 keyDownHandler.bind(keyCode.Down, modifier.AltShift, rangeSelectionOnly(selectionController.extendSelectionToParagraphEnd));
1174                 keyDownHandler.bind(keyCode.Up, modifier.MetaShift, rangeSelectionOnly(selectionController.extendSelectionToDocumentStart));
1175                 keyDownHandler.bind(keyCode.Down, modifier.MetaShift, rangeSelectionOnly(selectionController.extendSelectionToDocumentEnd));
1176                 keyDownHandler.bind(keyCode.A, modifier.Meta, rangeSelectionOnly(selectionController.extendSelectionToEntireDocument));
1177             } else {
1178                 keyDownHandler.bind(keyCode.Left, modifier.Ctrl, rangeSelectionOnly(selectionController.moveCursorBeforeWord));
1179                 keyDownHandler.bind(keyCode.Right, modifier.Ctrl, rangeSelectionOnly(selectionController.moveCursorPastWord));
1180                 keyDownHandler.bind(keyCode.Left, modifier.CtrlShift, rangeSelectionOnly(selectionController.extendSelectionBeforeWord));
1181                 keyDownHandler.bind(keyCode.Right, modifier.CtrlShift, rangeSelectionOnly(selectionController.extendSelectionPastWord));
1182                 keyDownHandler.bind(keyCode.A, modifier.Ctrl, rangeSelectionOnly(selectionController.extendSelectionToEntireDocument));
1183             }
1184 
1185             if (isIOS) {
1186                 iOSSafariSupport = new gui.IOSSafariSupport(eventManager);
1187             }
1188 
1189             eventManager.subscribe("keydown", keyDownHandler.handleEvent);
1190             eventManager.subscribe("keypress", keyPressHandler.handleEvent);
1191             eventManager.subscribe("keyup", keyUpHandler.handleEvent);
1192             eventManager.subscribe("copy", handleCopy);
1193             eventManager.subscribe("mousedown", handleMouseDown);
1194             eventManager.subscribe("mousemove", drawShadowCursorTask.trigger);
1195             eventManager.subscribe("mouseup", handleMouseUp);
1196             eventManager.subscribe("contextmenu", handleContextMenu);
1197             eventManager.subscribe("dragstart", handleDragStart);
1198             eventManager.subscribe("dragend", handleDragEnd);
1199             eventManager.subscribe("click", hyperlinkClickHandler.handleClick);
1200             eventManager.subscribe("longpress", selectWordByLongPress);
1201             eventManager.subscribe("drag", extendSelectionByDrag);
1202             eventManager.subscribe("dragstop", updateCursorSelection);
1203 
1204             odtDocument.subscribe(ops.OdtDocument.signalOperationEnd, redrawRegionSelectionTask.trigger);
1205             odtDocument.subscribe(ops.Document.signalCursorAdded, inputMethodEditor.registerCursor);
1206             odtDocument.subscribe(ops.Document.signalCursorRemoved, inputMethodEditor.removeCursor);
1207             odtDocument.subscribe(ops.OdtDocument.signalOperationEnd, updateUndoStack);
1208         }
1209 
1210         init();
1211     };
1212 }());
1213 // vim:expandtab
1214