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