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