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 {!Event} 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 {!Event} 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 {!Event} 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 {!Event} 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 = 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 {!Event} 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 {!Event} 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 {function(*):(boolean|undefined)
794              */
795             return function (e) {
796                 var selectionType = odtDocument.getCursor(inputMemberId).getSelectionType();
797                 if (selectionType === ops.OdtCursor.RangeSelection) {
798                     return fn(e);
799                 }
800                 return true;
801             };
802         }
803 
804         /**
805          * Inserts the local cursor.
806          * @return {undefined}
807          */
808         function insertLocalCursor() {
809             runtime.assert(session.getOdtDocument().getCursor(inputMemberId) === undefined, "Inserting local cursor a second time.");
810 
811             var op = new ops.OpAddCursor();
812             op.init({memberid: inputMemberId});
813             session.enqueue([op]);
814             // Immediately capture focus when the local cursor is inserted
815             eventManager.focus();
816         }
817         this.insertLocalCursor = insertLocalCursor;
818 
819 
820         /**
821          * Removes the local cursor.
822          * @return {undefined}
823          */
824         function removeLocalCursor() {
825             runtime.assert(session.getOdtDocument().getCursor(inputMemberId) !== undefined, "Removing local cursor without inserting before.");
826 
827             var op = new ops.OpRemoveCursor();
828             op.init({memberid: inputMemberId});
829             session.enqueue([op]);
830         }
831         this.removeLocalCursor = removeLocalCursor;
832 
833         /**
834          * @return {undefined}
835          */
836         this.startEditing = function () {
837             inputMethodEditor.subscribe(gui.InputMethodEditor.signalCompositionStart, textController.removeCurrentSelection);
838             inputMethodEditor.subscribe(gui.InputMethodEditor.signalCompositionEnd, insertNonEmptyData);
839 
840             eventManager.subscribe("beforecut", handleBeforeCut);
841             eventManager.subscribe("cut", handleCut);
842             eventManager.subscribe("beforepaste", handleBeforePaste);
843             eventManager.subscribe("paste", handlePaste);
844 
845             if (undoManager) {
846                 // For most undo managers, the initial state is a clean document *with* a cursor present
847                 undoManager.initialize();
848             }
849 
850             eventManager.setEditing(true);
851             hyperlinkClickHandler.setModifier(isMacOS ? modifier.Meta : modifier.Ctrl);
852             // Most browsers will go back one page when given an unhandled backspace press
853             // To prevent this, the event handler for this key should always return true
854             keyDownHandler.bind(keyCode.Backspace, modifier.None, returnTrue(textController.removeTextByBackspaceKey), true);
855             keyDownHandler.bind(keyCode.Delete, modifier.None, textController.removeTextByDeleteKey);
856 
857             // TODO: deselect the currently selected image when press Esc
858             // TODO: move the image selection box to next image/frame when press tab on selected image
859             keyDownHandler.bind(keyCode.Tab, modifier.None, rangeSelectionOnly(function () {
860                 textController.insertText("\t");
861                 return true;
862             }));
863 
864             if (isMacOS) {
865                 keyDownHandler.bind(keyCode.Clear, modifier.None, textController.removeCurrentSelection);
866                 keyDownHandler.bind(keyCode.B, modifier.Meta, rangeSelectionOnly(directFormattingController.toggleBold));
867                 keyDownHandler.bind(keyCode.I, modifier.Meta, rangeSelectionOnly(directFormattingController.toggleItalic));
868                 keyDownHandler.bind(keyCode.U, modifier.Meta, rangeSelectionOnly(directFormattingController.toggleUnderline));
869                 keyDownHandler.bind(keyCode.L, modifier.MetaShift, rangeSelectionOnly(directFormattingController.alignParagraphLeft));
870                 keyDownHandler.bind(keyCode.E, modifier.MetaShift, rangeSelectionOnly(directFormattingController.alignParagraphCenter));
871                 keyDownHandler.bind(keyCode.R, modifier.MetaShift, rangeSelectionOnly(directFormattingController.alignParagraphRight));
872                 keyDownHandler.bind(keyCode.J, modifier.MetaShift, rangeSelectionOnly(directFormattingController.alignParagraphJustified));
873                 if (annotationsEnabled) {
874                     keyDownHandler.bind(keyCode.C, modifier.MetaShift, annotationController.addAnnotation);
875                 }
876                 keyDownHandler.bind(keyCode.Z, modifier.Meta, undo);
877                 keyDownHandler.bind(keyCode.Z, modifier.MetaShift, redo);
878             } else {
879                 keyDownHandler.bind(keyCode.B, modifier.Ctrl, rangeSelectionOnly(directFormattingController.toggleBold));
880                 keyDownHandler.bind(keyCode.I, modifier.Ctrl, rangeSelectionOnly(directFormattingController.toggleItalic));
881                 keyDownHandler.bind(keyCode.U, modifier.Ctrl, rangeSelectionOnly(directFormattingController.toggleUnderline));
882                 keyDownHandler.bind(keyCode.L, modifier.CtrlShift, rangeSelectionOnly(directFormattingController.alignParagraphLeft));
883                 keyDownHandler.bind(keyCode.E, modifier.CtrlShift, rangeSelectionOnly(directFormattingController.alignParagraphCenter));
884                 keyDownHandler.bind(keyCode.R, modifier.CtrlShift, rangeSelectionOnly(directFormattingController.alignParagraphRight));
885                 keyDownHandler.bind(keyCode.J, modifier.CtrlShift, rangeSelectionOnly(directFormattingController.alignParagraphJustified));
886                 if (annotationsEnabled) {
887                     keyDownHandler.bind(keyCode.C, modifier.CtrlAlt, annotationController.addAnnotation);
888                 }
889                 keyDownHandler.bind(keyCode.Z, modifier.Ctrl, undo);
890                 keyDownHandler.bind(keyCode.Z, modifier.CtrlShift, redo);
891             }
892 
893             // the default action is to insert text into the document
894             /**
895              * @param {!KeyboardEvent} e
896              * @return {boolean|undefined}
897              */
898             function handler(e) {
899                 var text = stringFromKeyPress(e);
900                 if (text && !(e.altKey || e.ctrlKey || e.metaKey)) {
901                     textController.insertText(text);
902                     return true;
903                 }
904                 return false;
905             }
906             keyPressHandler.setDefault(rangeSelectionOnly(handler));
907             keyPressHandler.bind(keyCode.Enter, modifier.None, rangeSelectionOnly(textController.enqueueParagraphSplittingOps));
908         };
909 
910         /**
911          * @return {undefined}
912          */
913         this.endEditing = function () {
914             inputMethodEditor.unsubscribe(gui.InputMethodEditor.signalCompositionStart, textController.removeCurrentSelection);
915             inputMethodEditor.unsubscribe(gui.InputMethodEditor.signalCompositionEnd, insertNonEmptyData);
916 
917             eventManager.unsubscribe("cut", handleCut);
918             eventManager.unsubscribe("beforecut", handleBeforeCut);
919             eventManager.unsubscribe("paste", handlePaste);
920             eventManager.unsubscribe("beforepaste", handleBeforePaste);
921 
922             eventManager.setEditing(false);
923             hyperlinkClickHandler.setModifier(modifier.None);
924             keyDownHandler.bind(keyCode.Backspace, modifier.None, function () { return true; }, true);
925             keyDownHandler.unbind(keyCode.Delete, modifier.None);
926             keyDownHandler.unbind(keyCode.Tab, modifier.None);
927 
928             if (isMacOS) {
929                 keyDownHandler.unbind(keyCode.Clear, modifier.None);
930                 keyDownHandler.unbind(keyCode.B, modifier.Meta);
931                 keyDownHandler.unbind(keyCode.I, modifier.Meta);
932                 keyDownHandler.unbind(keyCode.U, modifier.Meta);
933                 keyDownHandler.unbind(keyCode.L, modifier.MetaShift);
934                 keyDownHandler.unbind(keyCode.E, modifier.MetaShift);
935                 keyDownHandler.unbind(keyCode.R, modifier.MetaShift);
936                 keyDownHandler.unbind(keyCode.J, modifier.MetaShift);
937                 if (annotationsEnabled) {
938                     keyDownHandler.unbind(keyCode.C, modifier.MetaShift);
939                 }
940                 keyDownHandler.unbind(keyCode.Z, modifier.Meta);
941                 keyDownHandler.unbind(keyCode.Z, modifier.MetaShift);
942             } else {
943                 keyDownHandler.unbind(keyCode.B, modifier.Ctrl);
944                 keyDownHandler.unbind(keyCode.I, modifier.Ctrl);
945                 keyDownHandler.unbind(keyCode.U, modifier.Ctrl);
946                 keyDownHandler.unbind(keyCode.L, modifier.CtrlShift);
947                 keyDownHandler.unbind(keyCode.E, modifier.CtrlShift);
948                 keyDownHandler.unbind(keyCode.R, modifier.CtrlShift);
949                 keyDownHandler.unbind(keyCode.J, modifier.CtrlShift);
950                 if (annotationsEnabled) {
951                     keyDownHandler.unbind(keyCode.C, modifier.CtrlAlt);
952                 }
953                 keyDownHandler.unbind(keyCode.Z, modifier.Ctrl);
954                 keyDownHandler.unbind(keyCode.Z, modifier.CtrlShift);
955             }
956 
957             keyPressHandler.setDefault(null);
958             keyPressHandler.unbind(keyCode.Enter, modifier.None);
959         };
960 
961         /**
962          * @return {!string}
963          */
964         this.getInputMemberId = function () {
965             return inputMemberId;
966         };
967 
968         /**
969          * @return {!ops.Session}
970          */
971         this.getSession = function () {
972             return session;
973         };
974 
975         /**
976          * @return {!gui.SessionConstraints}
977          */
978         this.getSessionConstraints = function () {
979             return sessionConstraints;
980         };
981 
982         /**
983          * @param {?gui.UndoManager} manager
984          * @return {undefined}
985          */
986         this.setUndoManager = function (manager) {
987             if (undoManager) {
988                 undoManager.unsubscribe(gui.UndoManager.signalUndoStackChanged, forwardUndoStackChange);
989             }
990 
991             undoManager = manager;
992             if (undoManager) {
993                 undoManager.setDocument(odtDocument);
994                 // As per gui.UndoManager, this should NOT fire any signals or report
995                 // events being executed back to the undo manager.
996                 undoManager.setPlaybackFunction(session.enqueue);
997                 undoManager.subscribe(gui.UndoManager.signalUndoStackChanged, forwardUndoStackChange);
998             }
999         };
1000 
1001         /**
1002          * @return {?gui.UndoManager}
1003          */
1004         this.getUndoManager = function () {
1005             return undoManager;
1006         };
1007 
1008         /**
1009          * @return {!gui.MetadataController}
1010          */
1011         this.getMetadataController = function () {
1012             return metadataController;
1013         };
1014 
1015         /**
1016          * @return {?gui.AnnotationController}
1017          */
1018         this.getAnnotationController = function () {
1019             return annotationController;
1020         };
1021 
1022         /**
1023          * @return {!gui.DirectFormattingController}
1024          */
1025         this.getDirectFormattingController = function () {
1026             return directFormattingController;
1027         };
1028 
1029         /**
1030          * @return {!gui.HyperlinkClickHandler}
1031          */
1032         this.getHyperlinkClickHandler = function () {
1033             return hyperlinkClickHandler;
1034         };
1035 
1036         /**
1037          * @return {!gui.HyperlinkController}
1038          */
1039         this.getHyperlinkController = function () {
1040             return hyperlinkController;
1041         };
1042 
1043         /**
1044          * @return {!gui.ImageController}
1045          */
1046         this.getImageController = function () {
1047             return imageController;
1048         };
1049 
1050         /**
1051          * @return {!gui.SelectionController}
1052          */
1053         this.getSelectionController = function () {
1054             return selectionController;
1055         };
1056 
1057         /**
1058          * @return {!gui.TextController}
1059          */
1060         this.getTextController = function () {
1061             return textController;
1062         };
1063 
1064         /**
1065          * @return {!gui.EventManager}
1066          */
1067         this.getEventManager = function() {
1068             return eventManager;
1069         };
1070 
1071         /**
1072          * Return the keyboard event handlers
1073          * @return {{keydown: gui.KeyboardHandler, keypress: gui.KeyboardHandler}}
1074          */
1075         this.getKeyboardHandlers = function () {
1076             return {
1077                 keydown: keyDownHandler,
1078                 keypress: keyPressHandler
1079             };
1080         };
1081 
1082         /**
1083          * @param {!function(!Object=)} callback passing an error object in case of error
1084          * @return {undefined}
1085          */
1086         function destroy(callback) {
1087             eventManager.unsubscribe("keydown", keyDownHandler.handleEvent);
1088             eventManager.unsubscribe("keypress", keyPressHandler.handleEvent);
1089             eventManager.unsubscribe("keyup", keyUpHandler.handleEvent);
1090             eventManager.unsubscribe("copy", handleCopy);
1091             eventManager.unsubscribe("mousedown", handleMouseDown);
1092             eventManager.unsubscribe("mousemove", drawShadowCursorTask.trigger);
1093             eventManager.unsubscribe("mouseup", handleMouseUp);
1094             eventManager.unsubscribe("contextmenu", handleContextMenu);
1095             eventManager.unsubscribe("dragstart", handleDragStart);
1096             eventManager.unsubscribe("dragend", handleDragEnd);
1097             eventManager.unsubscribe("click", hyperlinkClickHandler.handleClick);
1098             eventManager.unsubscribe("longpress", selectWordByLongPress);
1099             eventManager.unsubscribe("drag", extendSelectionByDrag);
1100             eventManager.unsubscribe("dragstop", updateCursorSelection);
1101 
1102             odtDocument.unsubscribe(ops.OdtDocument.signalOperationEnd, redrawRegionSelectionTask.trigger);
1103             odtDocument.unsubscribe(ops.Document.signalCursorAdded, inputMethodEditor.registerCursor);
1104             odtDocument.unsubscribe(ops.Document.signalCursorRemoved, inputMethodEditor.removeCursor);
1105             odtDocument.unsubscribe(ops.OdtDocument.signalOperationEnd, updateUndoStack);
1106 
1107             callback();
1108         }
1109 
1110         /**
1111          * @param {!function(!Error=)} callback passing an error object in case of error
1112          * @return {undefined}
1113          */
1114         this.destroy = function (callback) {
1115             var destroyCallbacks = [
1116                 drawShadowCursorTask.destroy,
1117                 redrawRegionSelectionTask.destroy,
1118                 directFormattingController.destroy,
1119                 inputMethodEditor.destroy,
1120                 eventManager.destroy,
1121                 hyperlinkClickHandler.destroy,
1122                 hyperlinkController.destroy,
1123                 metadataController.destroy,
1124                 selectionController.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