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             /**@type{!core.DomUtils}*/
 70             domUtils = new core.DomUtils(),
 71             odfUtils = new odf.OdfUtils(),
 72             mimeDataExporter = new gui.MimeDataExporter(),
 73             clipboard = new gui.Clipboard(mimeDataExporter),
 74             keyDownHandler = new gui.KeyboardHandler(),
 75             keyPressHandler = new gui.KeyboardHandler(),
 76             keyUpHandler = new gui.KeyboardHandler(),
 77             /**@type{boolean}*/
 78             clickStartedWithinCanvas = false,
 79             objectNameGenerator = new odf.ObjectNameGenerator(odtDocument.getOdfCanvas().odfContainer(), inputMemberId),
 80             isMouseMoved = false,
 81             /**@type{core.PositionFilter}*/
 82             mouseDownRootFilter = null,
 83             handleMouseClickTimeoutId,
 84             undoManager = null,
 85             eventManager = new gui.EventManager(odtDocument),
 86             annotationsEnabled = args.annotationsEnabled,
 87             annotationController = new gui.AnnotationController(session, sessionConstraints, inputMemberId),
 88             directFormattingController = new gui.DirectFormattingController(session, sessionConstraints, sessionContext, inputMemberId, objectNameGenerator,
 89                                                                             args.directTextStylingEnabled, args.directParagraphStylingEnabled),
 90             createCursorStyleOp = /**@type {function (!number, !number, !boolean):ops.Operation}*/ (directFormattingController.createCursorStyleOp),
 91             createParagraphStyleOps = /**@type {function (!number):!Array.<!ops.Operation>}*/ (directFormattingController.createParagraphStyleOps),
 92             textController = new gui.TextController(session, sessionConstraints, sessionContext, inputMemberId, createCursorStyleOp, createParagraphStyleOps),
 93             imageController = new gui.ImageController(session, sessionConstraints, sessionContext, inputMemberId, objectNameGenerator),
 94             imageSelector = new gui.ImageSelector(odtDocument.getOdfCanvas()),
 95             shadowCursorIterator = gui.SelectionMover.createPositionIterator(odtDocument.getRootNode()),
 96             /**@type{!core.ScheduledTask}*/
 97             drawShadowCursorTask,
 98             /**@type{!core.ScheduledTask}*/
 99             redrawRegionSelectionTask,
100             pasteController = new gui.PasteController(session, sessionConstraints, sessionContext, inputMemberId),
101             inputMethodEditor = new gui.InputMethodEditor(inputMemberId, eventManager),
102             /**@type{number}*/
103             clickCount = 0,
104             hyperlinkClickHandler = new gui.HyperlinkClickHandler(odtDocument.getOdfCanvas().getElement,
105                                                                     keyDownHandler, keyUpHandler),
106             hyperlinkController = new gui.HyperlinkController(session, sessionConstraints, sessionContext, inputMemberId),
107             selectionController = new gui.SelectionController(session, inputMemberId),
108             metadataController = new gui.MetadataController(session, inputMemberId),
109             modifier = gui.KeyboardHandler.Modifier,
110             keyCode = gui.KeyboardHandler.KeyCode,
111             isMacOS = window.navigator.appVersion.toLowerCase().indexOf("mac") !== -1,
112             isIOS = ["iPad", "iPod", "iPhone"].indexOf(window.navigator.platform) !== -1,
113             /**@type{?gui.IOSSafariSupport}*/
114             iOSSafariSupport;
115 
116         runtime.assert(window !== null,
117             "Expected to be run in an environment which has a global window, like a browser.");
118 
119         /**
120          * @param {!Event} e
121          * @return {Node}
122          */
123         function getTarget(e) {
124             // e.srcElement because IE10 likes to be different...
125             return /**@type{Node}*/(e.target) || e.srcElement || null;
126         }
127 
128         /**
129          * @param {!Event} event
130          * @return {undefined}
131          */
132         function cancelEvent(event) {
133             if (event.preventDefault) {
134                 event.preventDefault();
135             } else {
136                 event.returnValue = false;
137             }
138         }
139 
140         /**
141          * @param {!number} x
142          * @param {!number} y
143          * @return {?{container:!Node, offset:!number}}
144          */
145         function caretPositionFromPoint(x, y) {
146             var doc = odtDocument.getDOMDocument(),
147                 c,
148                 result = null;
149 
150             if (doc.caretRangeFromPoint) {
151                 c = doc.caretRangeFromPoint(x, y);
152                 result = {
153                     container: /**@type{!Node}*/(c.startContainer),
154                     offset: c.startOffset
155                 };
156             } else if (doc.caretPositionFromPoint) {
157                 c = doc.caretPositionFromPoint(x, y);
158                 if (c && c.offsetNode) {
159                     result = {
160                         container: c.offsetNode,
161                         offset: c.offset
162                     };
163                 }
164             }
165             return result;
166         }
167 
168         /**
169          * If the user's current selection is region selection (e.g., an image), any executed operations
170          * could cause the picture to shift relative to the selection rectangle.
171          * @return {undefined}
172          */
173         function redrawRegionSelection() {
174             var cursor = odtDocument.getCursor(inputMemberId),
175                 imageElement;
176 
177             if (cursor && cursor.getSelectionType() === ops.OdtCursor.RegionSelection) {
178                 imageElement = odfUtils.getImageElements(cursor.getSelectedRange())[0];
179                 if (imageElement) {
180                     imageSelector.select(/**@type{!Element}*/(imageElement.parentNode));
181                     return;
182                 }
183             }
184 
185             // May have just processed our own remove cursor operation...
186             // In this case, clear any image selection chrome to prevent user confusion
187             imageSelector.clearSelection();
188         }
189 
190         /**
191          * @param {!Event} event
192          * @return {?string}
193          */
194         function stringFromKeyPress(event) {
195             if (event.which === null || event.which === undefined) {
196                 return String.fromCharCode(event.keyCode); // IE
197             }
198             if (event.which !== 0 && event.charCode !== 0) {
199                 return String.fromCharCode(event.which);   // the rest
200             }
201             return null; // special key
202         }
203 
204         /**
205          * Handle the cut operation request
206          * @param {!Event} e
207          * @return {undefined}
208          */
209         function handleCut(e) {
210             var cursor = odtDocument.getCursor(inputMemberId),
211                 selectedRange = cursor.getSelectedRange();
212 
213             if (selectedRange.collapsed) {
214                 // Modifying the clipboard data will clear any existing data,
215                 // so cut shouldn't touch the clipboard if there is nothing selected
216                 e.preventDefault();
217                 return;
218             }
219 
220             // The document is readonly, so the data will never get placed on
221             // the clipboard in most browsers unless we do it ourselves.
222             if (clipboard.setDataFromRange(e, selectedRange)) {
223                 textController.removeCurrentSelection();
224             } else {
225                 // TODO What should we do if cut isn't supported?
226                 runtime.log("Cut operation failed");
227             }
228         }
229 
230         /**
231          * Tell the browser that it's ok to perform a cut action on our read-only body
232          * @return {!boolean}
233          */
234         function handleBeforeCut() {
235             var cursor = odtDocument.getCursor(inputMemberId),
236                 selectedRange = cursor.getSelectedRange();
237             return selectedRange.collapsed !== false; // return false to enable cut menu... straightforward right?!
238         }
239 
240         /**
241          * Handle the copy operation request
242          * @param {!Event} e
243          * @return {undefined}
244          */
245         function handleCopy(e) {
246             var cursor = odtDocument.getCursor(inputMemberId),
247                 selectedRange = cursor.getSelectedRange();
248 
249             if (selectedRange.collapsed) {
250                 // Modifying the clipboard data will clear any existing data,
251                 // so copy shouldn't touch the clipboard if there is nothing
252                 // selected
253                 e.preventDefault();
254                 return;
255             }
256 
257             // Place the data on the clipboard ourselves to ensure consistency
258             // with cut behaviours
259             if (!clipboard.setDataFromRange(e, selectedRange)) {
260                 // TODO What should we do if copy isn't supported?
261                 runtime.log("Copy operation failed");
262             }
263         }
264 
265         /**
266          * @param {!Event} e
267          * @return {undefined}
268          */
269         function handlePaste(e) {
270             var plainText;
271 
272             if (window.clipboardData && window.clipboardData.getData) { // IE
273                 plainText = window.clipboardData.getData('Text');
274             } else if (e.clipboardData && e.clipboardData.getData) { // the rest
275                 plainText = e.clipboardData.getData('text/plain');
276             }
277 
278             if (plainText) {
279                 textController.removeCurrentSelection();
280                 pasteController.paste(plainText);
281             }
282             cancelEvent(e);
283         }
284 
285         /**
286          * Tell the browser that it's ok to perform a paste action on our read-only body
287          * @return {!boolean}
288          */
289         function handleBeforePaste() {
290             return false;
291         }
292 
293         /**
294          * @param {!ops.Operation} op
295          * @return {undefined}
296          */
297         function updateUndoStack(op) {
298             if (undoManager) {
299                 undoManager.onOperationExecuted(op);
300             }
301         }
302 
303         /**
304          * @param {?Event} e
305          * @return {undefined}
306          */
307         function forwardUndoStackChange(e) {
308             odtDocument.emit(ops.OdtDocument.signalUndoStackChanged, e);
309         }
310 
311         /**
312          * @return {!boolean}
313          */
314         function undo() {
315             var hadFocusBefore;
316 
317             if (undoManager) {
318                 hadFocusBefore = eventManager.hasFocus();
319                 undoManager.moveBackward(1);
320                 if (hadFocusBefore) {
321                     eventManager.focus();
322                 }
323                 return true;
324             }
325 
326             return false;
327         }
328         // TODO it will soon be time to grow an UndoController
329         this.undo = undo;
330 
331         /**
332          * @return {!boolean}
333          */
334         function redo() {
335             var hadFocusBefore;
336             if (undoManager) {
337                 hadFocusBefore = eventManager.hasFocus();
338                 undoManager.moveForward(1);
339                 if (hadFocusBefore) {
340                     eventManager.focus();
341                 }
342                 return true;
343             }
344 
345             return false;
346         }
347         // TODO it will soon be time to grow an UndoController
348         this.redo = redo;
349 
350         /**
351          * This processes our custom drag events and if they are on
352          * a selection handle (with the attribute 'end' denoting the left
353          * or right handle), updates the shadow cursor's selection to
354          * be on those endpoints.
355          * @param {!Event} event
356          * @return {undefined}
357          */
358         function extendSelectionByDrag(event) {
359             var position,
360                 cursor = odtDocument.getCursor(inputMemberId),
361                 selectedRange = cursor.getSelectedRange(),
362                 newSelectionRange,
363                 /**@type{!string}*/
364                 handleEnd = /**@type{!Element}*/(getTarget(event)).getAttribute('end');
365 
366             if (selectedRange && handleEnd) {
367                 position = caretPositionFromPoint(event.clientX, event.clientY);
368                 if (position) {
369                     shadowCursorIterator.setUnfilteredPosition(position.container, position.offset);
370                     if (mouseDownRootFilter.acceptPosition(shadowCursorIterator) === FILTER_ACCEPT) {
371                         newSelectionRange = /**@type{!Range}*/(selectedRange.cloneRange());
372                         if (handleEnd === 'left') {
373                             newSelectionRange.setStart(shadowCursorIterator.container(), shadowCursorIterator.unfilteredDomOffset());
374                         } else {
375                             newSelectionRange.setEnd(shadowCursorIterator.container(), shadowCursorIterator.unfilteredDomOffset());
376                         }
377                         shadowCursor.setSelectedRange(newSelectionRange, handleEnd === 'right');
378                         odtDocument.emit(ops.Document.signalCursorMoved, shadowCursor);
379                     }
380                 }
381             }
382         }
383 
384         function updateCursorSelection() {
385             selectionController.selectRange(shadowCursor.getSelectedRange(), shadowCursor.hasForwardSelection(), 1);
386         }
387 
388         function updateShadowCursor() {
389             var selection = window.getSelection(),
390                 selectionRange = selection.rangeCount > 0 && selectionController.selectionToRange(selection);
391 
392             if (clickStartedWithinCanvas && selectionRange) {
393                 isMouseMoved = true;
394 
395                 imageSelector.clearSelection();
396                 shadowCursorIterator.setUnfilteredPosition(/**@type {!Node}*/(selection.focusNode), selection.focusOffset);
397                 if (mouseDownRootFilter.acceptPosition(shadowCursorIterator) === FILTER_ACCEPT) {
398                     if (clickCount === 2) {
399                         selectionController.expandToWordBoundaries(selectionRange.range);
400                     } else if (clickCount >= 3) {
401                         selectionController.expandToParagraphBoundaries(selectionRange.range);
402                     }
403                     shadowCursor.setSelectedRange(selectionRange.range, selectionRange.hasForwardSelection);
404                     odtDocument.emit(ops.Document.signalCursorMoved, shadowCursor);
405                 }
406             }
407         }
408 
409         /**
410          * In order for drag operations to work, the browser needs to have it's current
411          * selection set. This is called on mouse down to synchronize the user's last selection
412          * to the browser selection
413          * @param {ops.OdtCursor} cursor
414          * @return {undefined}
415          */
416         function synchronizeWindowSelection(cursor) {
417             var selection = window.getSelection(),
418                 range = cursor.getSelectedRange();
419 
420             if (selection.extend) {
421                 if (cursor.hasForwardSelection()) {
422                     selection.collapse(range.startContainer, range.startOffset);
423                     selection.extend(range.endContainer, range.endOffset);
424                 } else {
425                     selection.collapse(range.endContainer, range.endOffset);
426                     selection.extend(range.startContainer, range.startOffset);
427                 }
428             } else {
429                 // Internet explorer does provide any method for
430                 // preserving the range direction
431                 // See http://msdn.microsoft.com/en-us/library/ie/ff974359%28v=vs.85%29.aspx
432                 // Unfortunately, clearing the range will also blur the current focus.
433                 selection.removeAllRanges();
434                 selection.addRange(range.cloneRange());
435             }
436         }
437 
438         /**
439          * Return the number of mouse clicks if the mouse event is for the primary button. Otherwise return 0.
440          * @param {!Event} event
441          * @return {!number}
442          */
443         function computeClickCount(event) {
444             // According to the spec, button === 0 indicates the primary button (the left button by default, or the
445             // right button if the user has switched their mouse buttons around).
446             return event.button === 0 ? event.detail : 0;
447         }
448 
449         /**
450          * Updates a flag indicating whether the mouse down event occurred within the OdfCanvas element.
451          * This is necessary because the mouse-up binding needs to be global in order to handle mouse-up
452          * events that occur when the user releases the mouse button outside the canvas.
453          * This filter limits selection changes to mouse down events that start inside the canvas
454          * @param {!Event} e
455          */
456         function handleMouseDown(e) {
457             var target = getTarget(e),
458                 cursor = odtDocument.getCursor(inputMemberId),
459                 rootNode;
460             clickStartedWithinCanvas = target !== null && domUtils.containsNode(odtDocument.getOdfCanvas().getElement(), target);
461             if (clickStartedWithinCanvas) {
462                 isMouseMoved = false;
463                 rootNode = odtDocument.getRootElement(/**@type{!Node}*/(target)) || odtDocument.getRootNode();
464                 mouseDownRootFilter = odtDocument.createRootFilter(rootNode);
465                 clickCount = computeClickCount(e);
466                 if (cursor && e.shiftKey) {
467                     // Firefox seems to get rather confused about the window selection when shift+extending it.
468                     // Help this poor browser by resetting the window selection back to the anchor node if the user
469                     // is holding shift.
470                     window.getSelection().collapse(cursor.getAnchorNode(), 0);
471                 } else {
472                     synchronizeWindowSelection(cursor);
473                 }
474                 if (clickCount > 1) {
475                     updateShadowCursor();
476                 }
477             }
478         }
479 
480         /**
481          * Return a mutable version of a selection-type object.
482          * @param {?Selection} selection
483          * @return {?{anchorNode: ?Node, anchorOffset: !number, focusNode: ?Node, focusOffset: !number}}
484          */
485         function mutableSelection(selection) {
486             if (selection) {
487                 return {
488                     anchorNode: selection.anchorNode,
489                     anchorOffset: selection.anchorOffset,
490                     focusNode: selection.focusNode,
491                     focusOffset: selection.focusOffset
492                 };
493             }
494             return null;
495         }
496 
497         /**
498          * Gets the next walkable position after the given node.
499          * @param {!Node} node
500          * @return {?{container:!Node, offset:!number}}
501          */
502         function getNextWalkablePosition(node) {
503             var root = odtDocument.getRootElement(node),
504                 rootFilter = odtDocument.createRootFilter(root),
505                 stepIterator = odtDocument.createStepIterator(node, 0, [rootFilter, odtDocument.getPositionFilter()], root);
506             stepIterator.setPosition(node, node.childNodes.length);
507             if (!stepIterator.roundToNextStep()) {
508                 return null;
509             }
510             return {
511                 container: stepIterator.container(),
512                 offset: stepIterator.offset()
513             };
514         }
515 
516         /**
517          * Causes a cursor movement to the position hinted by a mouse click
518          * event.
519          * @param {!Event} event
520          * @return {undefined}
521          */
522         function moveByMouseClickEvent(event) {
523             var selection = mutableSelection(window.getSelection()),
524                 isCollapsed = window.getSelection().isCollapsed,
525                 position,
526                 selectionRange,
527                 rect,
528                 frameNode;
529 
530             if (!selection.anchorNode && !selection.focusNode) {
531                 // chrome & safari will report null for focus and anchor nodes after a right-click in text selection
532                 position = caretPositionFromPoint(event.clientX, event.clientY);
533                 if (position) {
534                     selection.anchorNode = /**@type{!Node}*/(position.container);
535                     selection.anchorOffset = position.offset;
536                     selection.focusNode = selection.anchorNode;
537                     selection.focusOffset = selection.anchorOffset;
538                 }
539             }
540 
541             if (odfUtils.isImage(selection.focusNode) && selection.focusOffset === 0
542                 && odfUtils.isCharacterFrame(selection.focusNode.parentNode)) {
543                 // In FireFox if an image has no text around it, click on either side of the
544                 // image resulting the same selection get returned. focusNode: image, focusOffset: 0
545                 // Move the cursor to the next walkable position when clicking on the right side of an image
546                 frameNode = /**@type{!Element}*/(selection.focusNode.parentNode);
547                 rect = frameNode.getBoundingClientRect();
548                 if (event.clientX > rect.left) {
549                     // On OSX, right-clicking on an image at the end of a range selection will hit
550                     // this particular branch. The image should remain selected if the right-click occurs on top
551                     // of it as technically it's the same behaviour as right clicking on an existing text selection.
552                     position = getNextWalkablePosition(frameNode);
553                     if (position) {
554                         selection.focusNode = position.container;
555                         selection.focusOffset = position.offset;
556                         if (isCollapsed) {
557                             // See above comment for the circumstances when the range might not be collapsed
558                             selection.anchorNode = selection.focusNode;
559                             selection.anchorOffset = selection.focusOffset;
560                         }
561                     }
562                 }
563             } else if (odfUtils.isImage(selection.focusNode.firstChild) && selection.focusOffset === 1
564                 && odfUtils.isCharacterFrame(selection.focusNode)) {
565                 // When click on the right side of an image that has no text elements, non-FireFox browsers
566                 // will return focusNode: frame, focusOffset: 1 as the selection. Since this is not a valid cursor
567                 // position, move the cursor to the next walkable position after the frame node.
568 
569                 // To activate this branch (only applicable on OSX + Linux WebKit-derived browsers AFAIK):
570                 // 1. With a paragraph containing some text followed by an inline image and no trailing text,
571                 //    select from the start of paragraph to the end.
572                 // 2. Now click once to the right hand side of the image. The cursor *should* jump to the right side
573                 position = getNextWalkablePosition(selection.focusNode);
574                 if (position) {
575                     // This should only ever be hit when the selection is intended to become collapsed
576                     selection.anchorNode = selection.focusNode = position.container;
577                     selection.anchorOffset = selection.focusOffset = position.offset;
578                 }
579             }
580 
581             // Need to check the selection again in case the caret position didn't return any result
582             if (selection.anchorNode && selection.focusNode) {
583                 selectionRange = selectionController.selectionToRange(selection);
584                 selectionController.selectRange(selectionRange.range,
585                     selectionRange.hasForwardSelection, computeClickCount(event));
586             }
587             eventManager.focus(); // Mouse clicks often cause focus to shift. Recapture this straight away
588         }
589 
590         /**
591          * @param {!Event} event
592          * @return {undefined}
593          */
594         function selectWordByLongPress(event) {
595             var /**@type{?{anchorNode: ?Node, anchorOffset: !number, focusNode: ?Node, focusOffset: !number}}*/
596                 selection,
597                 position,
598                 selectionRange,
599                 container, offset;
600 
601             position = caretPositionFromPoint(event.clientX, event.clientY);
602             if (position) {
603                 container = /**@type{!Node}*/(position.container);
604                 offset = position.offset;
605 
606                 selection = {
607                     anchorNode: container,
608                     anchorOffset: offset,
609                     focusNode: container,
610                     focusOffset: offset
611                 };
612 
613                 selectionRange = selectionController.selectionToRange(selection);
614                 selectionController.selectRange(selectionRange.range,
615                 selectionRange.hasForwardSelection, 2);
616                 eventManager.focus();
617             }
618         }
619 
620         /**
621          * @param {!Event} event
622          * @return {undefined}
623          */
624         function handleMouseClickEvent(event) {
625             var target = getTarget(event),
626                 clickEvent,
627                 range,
628                 wasCollapsed,
629                 frameNode,
630                 pos;
631 
632             drawShadowCursorTask.processRequests(); // Resynchronise the shadow cursor before processing anything else
633 
634             if (clickStartedWithinCanvas) {
635                 // Each mouse down event should only ever result in a single mouse click being processed.
636                 // This is to cope with there being no hard rules about whether a contextmenu
637                 // should be followed by a mouseup as well according to the HTML5 specs.
638                 // See http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus
639 
640                 // We don't want to just select the image if it is a range selection hence ensure the selection is collapsed.
641                 if (odfUtils.isImage(target) && odfUtils.isCharacterFrame(target.parentNode) && window.getSelection().isCollapsed) {
642                     selectionController.selectImage(/**@type{!Node}*/(target.parentNode));
643                     eventManager.focus(); // Mouse clicks often cause focus to shift. Recapture this straight away
644                 } else if (imageSelector.isSelectorElement(target)) {
645                     eventManager.focus(); // Mouse clicks often cause focus to shift. Recapture this straight away
646                 } else if (isMouseMoved) {
647                     range = shadowCursor.getSelectedRange();
648                     wasCollapsed = range.collapsed;
649                     // Resets the endContainer and endOffset when a forward selection end up on an image;
650                     // Otherwise the image will not be selected because endContainer: image, endOffset 0 is not a valid
651                     // cursor position.
652                     if (odfUtils.isImage(range.endContainer) && range.endOffset === 0
653                             && odfUtils.isCharacterFrame(range.endContainer.parentNode)) {
654                         frameNode = /**@type{!Element}*/(range.endContainer.parentNode);
655                         pos = getNextWalkablePosition(frameNode);
656                         if (pos) {
657                             range.setEnd(pos.container, pos.offset);
658                             if (wasCollapsed) {
659                                 range.collapse(false); // collapses the range to its end
660                             }
661                         }
662                     }
663                     selectionController.selectRange(range, shadowCursor.hasForwardSelection(), computeClickCount(event));
664                     eventManager.focus(); // Mouse clicks often cause focus to shift. Recapture this straight away
665                 } else {
666                     // Clicking in already selected text won't update window.getSelection() until just after
667                     // the click is processed. Set 0 timeout here so the newly clicked position can be updated
668                     // by the browser. Unfortunately this is only working in Firefox. For other browsers, we have to work
669                     // out the caret position from two coordinates.
670                     // In iOS, however, it is not possible to assign focus within a timeout. But in that case
671                     // we do not even need a timeout, because we do not use native selections at all there,
672                     // therefore for that platform, just directly move by the mouse click and give focus.
673                     if (isIOS) {
674                         moveByMouseClickEvent(event);
675                     } else {
676                         // IE10 destructs event objects once the event handler is done, so create a copy of the data.
677                         // "The event object is only available during an event; that is, you can use it in event handlers but not in other code"
678                         // (from http://msdn.microsoft.com/en-us/library/ie/aa703876(v=vs.85).aspx)
679                         // TODO: IE10 on a test machine does not have the "detail" property set on "mouseup" events here,
680                         // even if the docs claim it should exist, cmp. http://msdn.microsoft.com/en-au/library/ie/ff974344(v=vs.85).aspx
681                         // So doubleclicks will not be detected on (some?) IE currently.
682                         clickEvent = domUtils.cloneEvent(event);
683                         handleMouseClickTimeoutId = runtime.setTimeout(function () {
684                             moveByMouseClickEvent(clickEvent);
685                         }, 0);
686                     }
687                 }
688                 // TODO assumes the mouseup/contextmenu is the same button as the mousedown that initialized the clickCount
689                 clickCount = 0;
690                 clickStartedWithinCanvas = false;
691                 isMouseMoved = false;
692             }
693         }
694 
695         /**
696          * @param {!MouseEvent} e
697          * @return {undefined}
698          */
699         function handleDragStart(e) {
700             var cursor = odtDocument.getCursor(inputMemberId),
701                 selectedRange = cursor.getSelectedRange();
702 
703             if (selectedRange.collapsed) {
704                 return;
705             }
706 
707             mimeDataExporter.exportRangeToDataTransfer(/**@type{!DataTransfer}*/(e.dataTransfer), selectedRange);
708         }
709 
710         function handleDragEnd() {
711             // Drag operations consume the corresponding mouse up event.
712             // If this happens, the selection should still be reset.
713             if (clickStartedWithinCanvas) {
714                 eventManager.focus();
715             }
716             clickCount = 0;
717             clickStartedWithinCanvas = false;
718             isMouseMoved = false;
719         }
720 
721         /**
722          * @param {!Event} e
723          */
724         function handleContextMenu(e) {
725             // TODO Various browsers have different default behaviours on right click
726             // We can detect this at runtime without doing any kind of platform sniffing
727             // simply by observing what the browser has tried to do on right-click.
728             // - OSX: Safari/Chrome - Expand to word boundary
729             // - OSX: Firefox - No expansion
730             // - Windows: Safari/Chrome/Firefox - No expansion
731             handleMouseClickEvent(e);
732         }
733 
734         /**
735          * @param {!Event} event
736          */
737         function handleMouseUp(event) {
738             var target = /**@type{!Element}*/(getTarget(event)),
739                 annotationNode = null;
740 
741             if (target.className === "annotationRemoveButton") {
742                 runtime.assert(annotationsEnabled, "Remove buttons are displayed on annotations while annotation editing is disabled in the controller.");
743                 annotationNode = domUtils.getElementsByTagNameNS(/**@type{!Element}*/(target.parentNode), odf.Namespaces.officens, 'annotation')[0];
744                 annotationController.removeAnnotation(annotationNode);
745                 eventManager.focus();
746             } else {
747                 if (target.getAttribute('class') !== 'webodf-draggable') {
748                     handleMouseClickEvent(event);
749                 }
750             }
751         }
752 
753         /**
754          * Handle composition end event. If there is data specified, treat this as text
755          * to be inserted into the document.
756          * @param {!CompositionEvent} e
757          */
758         function insertNonEmptyData(e) {
759             // https://dvcs.w3.org/hg/dom3events/raw-file/tip/html/DOM3-Events.html#event-type-compositionend
760             var input = e.data;
761             if (input) {
762                 if (input.indexOf("\n") === -1) {
763                     textController.insertText(input);
764                 } else {
765                     // Multi-line input should be handled as if it was pasted, rather than inserted as one giant
766                     // single string.
767                     pasteController.paste(input);
768                 }
769             }
770         }
771 
772         /**
773          * Executes the provided function and returns true
774          * Used to swallow events regardless of whether an operation was created
775          * @param {!Function} fn
776          * @return {!Function}
777          */
778         function returnTrue(fn) {
779             return function () {
780                 fn();
781                 return true;
782             };
783         }
784 
785         /**
786          * Executes the given function on range selection only
787          * @param {function(T):(boolean|undefined)} fn
788          * @return {function(T):(boolean|undefined)}
789          * @template T
790          */
791         function rangeSelectionOnly(fn) {
792             /**
793              * @param {*} e
794              * return {function(*):(boolean|undefined)
795              */
796             return function (e) {
797                 var selectionType = odtDocument.getCursor(inputMemberId).getSelectionType();
798                 if (selectionType === ops.OdtCursor.RangeSelection) {
799                     return fn(e);
800                 }
801                 return true;
802             };
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                 textController.destroy,
1126                 destroy
1127             ];
1128 
1129             if (iOSSafariSupport) {
1130                 destroyCallbacks.unshift(iOSSafariSupport.destroy);
1131             }
1132 
1133             runtime.clearTimeout(handleMouseClickTimeoutId);
1134             core.Async.destroyAll(destroyCallbacks, callback);
1135         };
1136 
1137         function init() {
1138             drawShadowCursorTask = core.Task.createRedrawTask(updateShadowCursor);
1139             redrawRegionSelectionTask = core.Task.createRedrawTask(redrawRegionSelection);
1140 
1141             keyDownHandler.bind(keyCode.Left, modifier.None, rangeSelectionOnly(selectionController.moveCursorToLeft));
1142             keyDownHandler.bind(keyCode.Right, modifier.None, rangeSelectionOnly(selectionController.moveCursorToRight));
1143             keyDownHandler.bind(keyCode.Up, modifier.None, rangeSelectionOnly(selectionController.moveCursorUp));
1144             keyDownHandler.bind(keyCode.Down, modifier.None, rangeSelectionOnly(selectionController.moveCursorDown));
1145             keyDownHandler.bind(keyCode.Left, modifier.Shift, rangeSelectionOnly(selectionController.extendSelectionToLeft));
1146             keyDownHandler.bind(keyCode.Right, modifier.Shift, rangeSelectionOnly(selectionController.extendSelectionToRight));
1147             keyDownHandler.bind(keyCode.Up, modifier.Shift, rangeSelectionOnly(selectionController.extendSelectionUp));
1148             keyDownHandler.bind(keyCode.Down, modifier.Shift, rangeSelectionOnly(selectionController.extendSelectionDown));
1149             keyDownHandler.bind(keyCode.Home, modifier.None, rangeSelectionOnly(selectionController.moveCursorToLineStart));
1150             keyDownHandler.bind(keyCode.End, modifier.None, rangeSelectionOnly(selectionController.moveCursorToLineEnd));
1151             keyDownHandler.bind(keyCode.Home, modifier.Ctrl, rangeSelectionOnly(selectionController.moveCursorToDocumentStart));
1152             keyDownHandler.bind(keyCode.End, modifier.Ctrl, rangeSelectionOnly(selectionController.moveCursorToDocumentEnd));
1153             keyDownHandler.bind(keyCode.Home, modifier.Shift, rangeSelectionOnly(selectionController.extendSelectionToLineStart));
1154             keyDownHandler.bind(keyCode.End, modifier.Shift, rangeSelectionOnly(selectionController.extendSelectionToLineEnd));
1155             keyDownHandler.bind(keyCode.Up, modifier.CtrlShift, rangeSelectionOnly(selectionController.extendSelectionToParagraphStart));
1156             keyDownHandler.bind(keyCode.Down, modifier.CtrlShift, rangeSelectionOnly(selectionController.extendSelectionToParagraphEnd));
1157             keyDownHandler.bind(keyCode.Home, modifier.CtrlShift, rangeSelectionOnly(selectionController.extendSelectionToDocumentStart));
1158             keyDownHandler.bind(keyCode.End, modifier.CtrlShift, rangeSelectionOnly(selectionController.extendSelectionToDocumentEnd));
1159 
1160             if (isMacOS) {
1161                 keyDownHandler.bind(keyCode.Left, modifier.Alt, rangeSelectionOnly(selectionController.moveCursorBeforeWord));
1162                 keyDownHandler.bind(keyCode.Right, modifier.Alt, rangeSelectionOnly(selectionController.moveCursorPastWord));
1163                 keyDownHandler.bind(keyCode.Left, modifier.Meta, rangeSelectionOnly(selectionController.moveCursorToLineStart));
1164                 keyDownHandler.bind(keyCode.Right, modifier.Meta, rangeSelectionOnly(selectionController.moveCursorToLineEnd));
1165                 keyDownHandler.bind(keyCode.Home, modifier.Meta, rangeSelectionOnly(selectionController.moveCursorToDocumentStart));
1166                 keyDownHandler.bind(keyCode.End, modifier.Meta, rangeSelectionOnly(selectionController.moveCursorToDocumentEnd));
1167                 keyDownHandler.bind(keyCode.Left, modifier.AltShift, rangeSelectionOnly(selectionController.extendSelectionBeforeWord));
1168                 keyDownHandler.bind(keyCode.Right, modifier.AltShift, rangeSelectionOnly(selectionController.extendSelectionPastWord));
1169                 keyDownHandler.bind(keyCode.Left, modifier.MetaShift, rangeSelectionOnly(selectionController.extendSelectionToLineStart));
1170                 keyDownHandler.bind(keyCode.Right, modifier.MetaShift, rangeSelectionOnly(selectionController.extendSelectionToLineEnd));
1171                 keyDownHandler.bind(keyCode.Up, modifier.AltShift, rangeSelectionOnly(selectionController.extendSelectionToParagraphStart));
1172                 keyDownHandler.bind(keyCode.Down, modifier.AltShift, rangeSelectionOnly(selectionController.extendSelectionToParagraphEnd));
1173                 keyDownHandler.bind(keyCode.Up, modifier.MetaShift, rangeSelectionOnly(selectionController.extendSelectionToDocumentStart));
1174                 keyDownHandler.bind(keyCode.Down, modifier.MetaShift, rangeSelectionOnly(selectionController.extendSelectionToDocumentEnd));
1175                 keyDownHandler.bind(keyCode.A, modifier.Meta, rangeSelectionOnly(selectionController.extendSelectionToEntireDocument));
1176             } else {
1177                 keyDownHandler.bind(keyCode.Left, modifier.Ctrl, rangeSelectionOnly(selectionController.moveCursorBeforeWord));
1178                 keyDownHandler.bind(keyCode.Right, modifier.Ctrl, rangeSelectionOnly(selectionController.moveCursorPastWord));
1179                 keyDownHandler.bind(keyCode.Left, modifier.CtrlShift, rangeSelectionOnly(selectionController.extendSelectionBeforeWord));
1180                 keyDownHandler.bind(keyCode.Right, modifier.CtrlShift, rangeSelectionOnly(selectionController.extendSelectionPastWord));
1181                 keyDownHandler.bind(keyCode.A, modifier.Ctrl, rangeSelectionOnly(selectionController.extendSelectionToEntireDocument));
1182             }
1183 
1184             if (isIOS) {
1185                 iOSSafariSupport = new gui.IOSSafariSupport(eventManager);
1186             }
1187 
1188             eventManager.subscribe("keydown", keyDownHandler.handleEvent);
1189             eventManager.subscribe("keypress", keyPressHandler.handleEvent);
1190             eventManager.subscribe("keyup", keyUpHandler.handleEvent);
1191             eventManager.subscribe("copy", handleCopy);
1192             eventManager.subscribe("mousedown", handleMouseDown);
1193             eventManager.subscribe("mousemove", drawShadowCursorTask.trigger);
1194             eventManager.subscribe("mouseup", handleMouseUp);
1195             eventManager.subscribe("contextmenu", handleContextMenu);
1196             eventManager.subscribe("dragstart", handleDragStart);
1197             eventManager.subscribe("dragend", handleDragEnd);
1198             eventManager.subscribe("click", hyperlinkClickHandler.handleClick);
1199             eventManager.subscribe("longpress", selectWordByLongPress);
1200             eventManager.subscribe("drag", extendSelectionByDrag);
1201             eventManager.subscribe("dragstop", updateCursorSelection);
1202 
1203             odtDocument.subscribe(ops.OdtDocument.signalOperationEnd, redrawRegionSelectionTask.trigger);
1204             odtDocument.subscribe(ops.Document.signalCursorAdded, inputMethodEditor.registerCursor);
1205             odtDocument.subscribe(ops.Document.signalCursorRemoved, inputMethodEditor.removeCursor);
1206             odtDocument.subscribe(ops.OdtDocument.signalOperationEnd, updateUndoStack);
1207         }
1208 
1209         init();
1210     };
1211 }());
1212 // vim:expandtab
1213