1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require('system/ready'); 9 sc_require('system/platform'); 10 sc_require('system/touch'); 11 12 /** Set to NO to leave the backspace key under the control of the browser.*/ 13 SC.CAPTURE_BACKSPACE_KEY = NO ; 14 15 /** @class 16 17 The RootResponder captures events coming from a web browser and routes them 18 to the correct view in the view hierarchy. Usually you do not work with a 19 RootResponder directly. Instead you will work with Pane objects, which 20 register themselves with the RootResponder as needed to receive events. 21 22 RootResponder and Platforms 23 --- 24 25 RootResponder contains core functionality common among the different web 26 platforms. You will likely be working with a subclass of RootResponder that 27 implements functionality unique to that platform. 28 29 The correct instance of RootResponder is detected at runtime and loaded 30 transparently. 31 32 Event Types 33 --- 34 35 RootResponders can route four types of events: 36 37 - Direct events, such as mouse and touch events. These are routed to the 38 nearest view managing the target DOM elment. RootResponder also handles 39 multitouch events so that they are delegated to the correct views. 40 - Keyboard events. These are sent to the keyPane, which will then send the 41 event to the current firstResponder and up the responder chain. 42 - Resize events. When the viewport resizes, these events will be sent to all 43 panes. 44 - Keyboard shortcuts. Shortcuts are sent to the keyPane first, which 45 will go down its view hierarchy. Then they go to the mainPane, which will 46 go down its view hierarchy. 47 - Actions. Actions are generic messages that your application can send in 48 response to user action or other events. You can either specify an 49 explicit target, or allow the action to traverse the hierarchy until a 50 view is found that handles it. 51 */ 52 SC.RootResponder = SC.Object.extend( 53 /** @scope SC.RootResponder.prototype */{ 54 55 /** 56 Contains a list of all panes currently visible on screen. Every time a 57 pane attaches or detaches, it will update itself in this array. 58 */ 59 panes: null, 60 61 init: function() { 62 sc_super(); 63 this.panes = SC.Set.create(); 64 }, 65 66 // ....................................................... 67 // MAIN PANE 68 // 69 70 /** 71 The main pane. This pane receives shortcuts and actions if the 72 focusedPane does not respond to them. There can be only one main pane. 73 You can swap main panes by calling makeMainPane() here. 74 75 Usually you will not need to edit the main pane directly. Instead, you 76 should use a MainPane subclass, which will automatically make itself main 77 when you append it to the document. 78 79 @type SC.MainPane 80 */ 81 mainPane: null, 82 83 /** 84 Swaps the main pane. If the current main pane is also the key pane, then 85 the new main pane will also be made key view automatically. In addition 86 to simply updating the mainPane property, this method will also notify the 87 panes themselves that they will lose/gain their mainView status. 88 89 Note that this method does not actually change the Pane's place in the 90 document body. That will be handled by the Pane itself. 91 92 @param {SC.Pane} pane 93 @returns {SC.RootResponder} 94 */ 95 makeMainPane: function(pane) { 96 var currentMain = this.get('mainPane') ; 97 if (currentMain === pane) return this ; // nothing to do 98 99 this.beginPropertyChanges() ; 100 101 // change key focus if needed. 102 if (this.get('keyPane') === currentMain) this.makeKeyPane(pane) ; 103 104 // change setting 105 this.set('mainPane', pane) ; 106 107 // notify panes. This will allow them to remove themselves. 108 if (currentMain) currentMain.blurMainTo(pane) ; 109 if (pane) pane.focusMainFrom(currentMain) ; 110 111 this.endPropertyChanges() ; 112 return this ; 113 }, 114 115 // .......................................................... 116 // MENU PANE 117 // 118 119 /** 120 The current menu pane. This pane receives keyboard events before all other 121 panes, but tends to be transient, as it is only set when a pane is open. 122 123 @type SC.MenuPane 124 */ 125 menuPane: null, 126 127 /** 128 Sets a pane as the menu pane. All key events will be directed to this 129 pane, but the current key pane will not lose focus. 130 131 Usually you would not call this method directly, but allow instances of 132 SC.MenuPane to manage the menu pane for you. If your pane does need to 133 become menu pane, you should relinquish control by calling this method 134 with a null parameter. Otherwise, key events will always be delivered to 135 that pane. 136 137 @param {SC.MenuPane} pane 138 @returns {SC.RootResponder} receiver 139 */ 140 makeMenuPane: function(pane) { 141 // Does the specified pane accept being the menu pane? If not, there's 142 // nothing to do. 143 if (pane && !pane.get('acceptsMenuPane')) { 144 return this; 145 } else { 146 var currentMenu = this.get('menuPane'); 147 if (currentMenu === pane) return this; // nothing to do 148 149 this.set('menuPane', pane); 150 } 151 152 return this; 153 }, 154 155 // ....................................................... 156 // KEY PANE 157 // 158 159 /** 160 The current key pane. This pane receives keyboard events, shortcuts, and 161 actions first, unless a menu is open. This pane is usually the highest 162 ordered pane or the mainPane. 163 164 @type SC.Pane 165 */ 166 keyPane: null, 167 168 /** @private 169 A stack of previous key panes. Used to allow panes to resign key pane 170 status without having to know who had it before them. 171 172 NOTE: This property is not observable. 173 */ 174 previousKeyPanes: [], 175 176 /** 177 Makes the passed pane the new key pane. If you pass null or if the pane 178 does not accept key focus, then key focus will transfer to the previous 179 key pane (if it is still attached), and so on down the stack. This will 180 notify both the old pane and the new root View that key focus has changed. 181 182 @param {SC.Pane} pane 183 @returns {SC.RootResponder} receiver 184 */ 185 makeKeyPane: function(pane) { 186 // Quick note about previousKeyPanes: if a pane is destroyed while in the 187 // previous panes stack, it will retain a reference to it here, causing a 188 // brief leak. The reference will be removed as soon as the panes above it 189 // in the stack resign, so it's rarely an issue, and fixing it would require 190 // a dedicated method and some extra coordination that's probably not worth 191 // it. 192 193 // Was a pane specified? 194 var newKeyPane, previousKeyPane, previousKeyPanes ; 195 196 if (pane) { 197 // Does the specified pane accept being the key pane? If not, there's 198 // nothing to do. 199 if (!pane.get('acceptsKeyPane')) { 200 return this ; 201 } 202 else { 203 // It does accept key pane status? Then push the current keyPane to 204 // the top of the stack and make the specified pane the new keyPane. 205 // First, though, do a sanity-check to make sure it's not already the 206 // key pane, in which case we have nothing to do. 207 previousKeyPane = this.get('keyPane') ; 208 if (previousKeyPane === pane) { 209 return this ; 210 } 211 else { 212 if (previousKeyPane) { 213 previousKeyPanes = this.get('previousKeyPanes') ; 214 previousKeyPanes.push(previousKeyPane) ; 215 } 216 217 newKeyPane = pane ; 218 } 219 } 220 } else { 221 // No pane was specified? Then pop the previous key pane off the top of 222 // the stack and make it the new key pane, assuming that it's still 223 // attached and accepts key pane (its value for acceptsKeyPane might 224 // have changed in the meantime). Otherwise, we'll keep going up the 225 // stack. 226 previousKeyPane = this.get('keyPane') ; 227 previousKeyPanes = this.get('previousKeyPanes') ; 228 229 newKeyPane = null ; 230 var candidate; 231 while (previousKeyPanes.length > 0) { 232 candidate = previousKeyPanes.pop(); 233 if (candidate.get('isVisibleInWindow') && candidate.get('acceptsKeyPane')) { 234 newKeyPane = candidate ; 235 break ; 236 } 237 } 238 } 239 240 241 // If we found an appropriate candidate, make it the new key pane. 242 // Otherwise, make the main pane the key pane (if it accepts it). 243 if (!newKeyPane) { 244 var mainPane = this.get('mainPane') ; 245 if (mainPane && mainPane.get('acceptsKeyPane')) newKeyPane = mainPane ; 246 } 247 248 // now notify old and new key views of change after edit 249 if (previousKeyPane) previousKeyPane.willLoseKeyPaneTo(newKeyPane) ; 250 if (newKeyPane) newKeyPane.willBecomeKeyPaneFrom(previousKeyPane) ; 251 252 this.set('keyPane', newKeyPane) ; 253 254 if (newKeyPane) newKeyPane.didBecomeKeyPaneFrom(previousKeyPane) ; 255 if (previousKeyPane) previousKeyPane.didLoseKeyPaneTo(newKeyPane) ; 256 257 return this ; 258 }, 259 260 // .......................................................... 261 // VIEWPORT STATE 262 // 263 264 /** 265 The last known window size. 266 @type Rect 267 @isReadOnly 268 */ 269 currentWindowSize: null, 270 271 /** 272 Computes the window size from the DOM. 273 274 @returns Rect 275 */ 276 computeWindowSize: function() { 277 var size, bod, docElement; 278 if(!this._bod || !this._docElement){ 279 bod = document.body; 280 docElement = document.documentElement; 281 this._bod=bod; 282 this._docElement=docElement; 283 }else{ 284 bod = this._bod; 285 docElement = this._docElement; 286 } 287 288 if (window.innerHeight) { 289 size = { 290 width: window.innerWidth, 291 height: window.innerHeight 292 } ; 293 } else if (docElement && docElement.clientHeight) { 294 size = { 295 width: docElement.clientWidth, 296 height: docElement.clientHeight 297 }; 298 } else if (bod) { 299 size = { 300 width: bod.clientWidth, 301 height: bod.clientHeight 302 } ; 303 } 304 return size; 305 }, 306 307 /** 308 On window resize, notifies panes of the change. 309 310 @returns {Boolean} 311 */ 312 resize: function() { 313 this._resize(); 314 this._assignDesignMode(); 315 316 return YES; //always allow normal processing to continue. 317 }, 318 319 /** @private */ 320 _resize: function() { 321 // calculate new window size... 322 var newSize = this.computeWindowSize(), oldSize = this.get('currentWindowSize'); 323 this.set('currentWindowSize', newSize); // update size 324 325 if (!SC.rectsEqual(newSize, oldSize)) { 326 SC.run(function() { 327 //Notify orientation change. This is faster than waiting for the orientation 328 //change event. 329 SC.device.windowSizeDidChange(newSize); 330 331 // notify panes 332 if (this.panes) { 333 if (oldSize !== newSize) { 334 this.panes.invoke('windowSizeDidChange', oldSize, newSize); 335 } 336 } 337 }, this); 338 } 339 }, 340 341 /** @private */ 342 _assignDesignMode: function () { 343 var newDesignMode = this.computeDesignMode(), 344 oldDesignMode = this.get('currentDesignMode'); 345 346 if (oldDesignMode !== newDesignMode) { 347 this.set('currentDesignMode', newDesignMode); 348 349 if (this.panes) { 350 SC.run(function() { 351 this.panes.invoke('updateDesignMode', oldDesignMode, newDesignMode); 352 }, this); 353 } 354 } 355 }, 356 357 /** 358 Indicates whether or not the window currently has focus. If you need 359 to do something based on whether or not the window is in focus, you can 360 setup a binding or observer to this property. Note that the SproutCore 361 automatically adds an sc-focus or sc-blur CSS class to the body tag as 362 appropriate. If you only care about changing the appearance of your 363 controls, you should use those classes in your CSS rules instead. 364 */ 365 hasFocus: NO, 366 367 /** 368 Handle window focus. Change hasFocus and add sc-focus CSS class 369 (removing sc-blur). Also notify panes. 370 */ 371 focus: function(evt) { 372 if (!this.get('hasFocus')) { 373 SC.$('body').addClass('sc-focus').removeClass('sc-blur'); 374 375 SC.run(function () { 376 // If the app is getting focus again set the first responder to the first 377 // valid firstResponder view in the view's tree 378 if(!SC.TABBING_ONLY_INSIDE_DOCUMENT && !SC.browser.isIE8OrLower){ 379 var keyPane = SC.RootResponder.responder.get('keyPane'); 380 if (keyPane) { 381 var nextValidKeyView = keyPane.get('nextValidKeyView'); 382 if (nextValidKeyView) keyPane.makeFirstResponder(nextValidKeyView); 383 } 384 } 385 386 this.set('hasFocus', YES); 387 }, this); 388 } 389 390 return YES ; // allow default 391 }, 392 393 /** 394 Handle window focus event for IE. Listening to the focus event is not 395 reliable as per every focus event you receive you immediately get a blur 396 event (Only on IE of course ;) 397 */ 398 focusin: function(evt) { 399 if(this._focusTimeout) clearTimeout(this._focusTimeout); 400 this.focus(evt); 401 }, 402 403 /** 404 Handle window blur event for IE. Listening to the focus event is not 405 reliable as per every focus event you receive you immediately get a blur 406 event (Only on IE of course ;) 407 */ 408 focusout: function(evt) { 409 var that = this; 410 this._focusTimeout = setTimeout(function(){that.blur(evt);}, 300); 411 }, 412 413 414 /** 415 Handle window focus. Change hasFocus and add sc-focus CSS class (removing 416 sc-blur). Also notify panes. 417 */ 418 blur: function(evt) { 419 if (this.get('hasFocus')) { 420 SC.$('body').addClass('sc-blur').removeClass('sc-focus'); 421 422 SC.run(function() { 423 this.set('hasFocus', NO); 424 }, this); 425 } 426 return YES ; // allow default 427 }, 428 429 dragDidStart: function(drag) { 430 this._mouseDownView = drag ; 431 this._drag = drag ; 432 }, 433 434 // ------------------------------------------------------------------------ 435 // Design Modes 436 // 437 438 /** @private */ 439 currentDesignMode: null, 440 441 /** @private Managed by SC.Application. */ 442 designModes: function (key, value) { 443 if (SC.none(value)) { 444 // Clear previous values. 445 if (this._designModeNames) { 446 delete this._designModeNames; 447 delete this._designModeThresholds; 448 } 449 450 value = null; 451 } else { 452 this._prepOrderedArrays(value); 453 } 454 455 this._assignDesignMode(); 456 457 return value; 458 }.property().cacheable(), 459 460 /** @private Determine the design mode based on area and pixel density. */ 461 computeDesignMode: function () { 462 var designMode = null, 463 designModeNames = this._designModeNames, 464 designModeThresholds = this._designModeThresholds, 465 currentWindowSize, 466 area; 467 468 // Fast path! 469 if (!designModeNames) { return null; } 470 471 currentWindowSize = this.get('currentWindowSize'); 472 area = (currentWindowSize.width * currentWindowSize.height); 473 var i, len; 474 for (i = 0, len = designModeThresholds.get('length'); i < len; i++) { 475 var layoutWidthThreshold = designModeThresholds.objectAt(i); 476 if (area < layoutWidthThreshold) { 477 designMode = designModeNames.objectAt(i); 478 break; 479 } 480 } 481 482 // If no smaller designMode was found, use the biggest designMode. 483 if (SC.none(designMode) && designModeNames && designModeNames.get('length') > 0) { 484 designMode = designModeNames.objectAt(i); 485 } 486 487 return SC.device.orientation === SC.PORTRAIT_ORIENTATION ? designMode + '_p' : designMode + '_l'; 488 }, 489 490 /** @private (semi-private) 491 Returns the fallback design mode for the given design mode. This is 492 primarily used by SC.View for the case where an adjustment isn't found 493 for the current design mode and we want to apply the next best design 494 mode as a fallback. 495 */ 496 fallbackDesignMode: function (designMode) { 497 var designModeNames = this._designModeNames, 498 index, 499 ret = null; 500 501 index = designModeNames.indexOf(designMode); 502 if (index >= 0) { 503 ret = designModeNames[index - 1]; 504 } 505 506 return ret; 507 }, 508 509 /** @private Prepares ordered design modes & widths arrays when designModes changes. */ 510 _prepOrderedArrays: function (designModes) { 511 var designModeNames, 512 designModeThresholds; 513 514 // Order the design modes for easier access later. 515 if (designModes) { 516 designModeNames = this._designModeNames = []; 517 designModeThresholds = this._designModeThresholds = []; 518 519 var key; 520 521 outer: 522 for (key in designModes) { 523 var i, value; 524 525 // Assume that the keys will be ordered smallest to largest so run backwards. 526 value = designModes[key]; 527 inner: 528 for (i = designModeThresholds.length - 1; i >= 0; i--) { 529 if (designModeThresholds[i] < value) { 530 // Exit early! 531 break inner; 532 } 533 } 534 535 i += 1; 536 designModeNames.splice(i, 0, key); 537 designModeThresholds.splice(i, 0, value); 538 } 539 } 540 }, 541 542 // ....................................................... 543 // ACTIONS 544 // 545 546 /** 547 Set this to a delegate object that can respond to actions as they are sent 548 down the responder chain. 549 550 @type SC.Object 551 */ 552 defaultResponder: null, 553 554 /** 555 Route an action message to the appropriate responder. This method will 556 walk the responder chain, attempting to find a responder that implements 557 the action name you pass to this method. Set 'target' to null to search 558 the responder chain. 559 560 **IMPORTANT**: This method's API and implementation will likely change 561 significantly after SproutCore 1.0 to match the version found in 562 SC.ResponderContext. 563 564 You generally should not call or override this method in your own 565 applications. 566 567 @param {String} action The action to perform - this is a method name. 568 @param {SC.Responder} target object to set method to (can be null) 569 @param {Object} sender The sender of the action 570 @param {SC.Pane} pane optional pane to start search with 571 @param {Object} context optional. only passed to ResponderContexts 572 @returns {Boolean} YES if action was performed, NO otherwise 573 @test in targetForAction 574 */ 575 sendAction: function( action, target, sender, pane, context, firstResponder) { 576 target = this.targetForAction(action, target, sender, pane, firstResponder) ; 577 578 if (target) { 579 // HACK: If the target is a ResponderContext, forward the action. 580 if (target.isResponderContext) { 581 return !!target.sendAction(action, sender, context, firstResponder); 582 } else { 583 return target.tryToPerform(action, sender, context); 584 } 585 } 586 }, 587 588 _responderFor: function(target, methodName, firstResponder) { 589 var defaultResponder = target ? target.get('defaultResponder') : null; 590 591 if (target) { 592 target = firstResponder || target.get('firstResponder') || target; 593 do { 594 if (target.respondsTo(methodName)) return target ; 595 } while ((target = target.get('nextResponder'))) ; 596 } 597 598 // HACK: Eventually we need to normalize the sendAction() method between 599 // this and the ResponderContext, but for the moment just look for a 600 // ResponderContext as the defaultResponder and return it if present. 601 if (typeof defaultResponder === SC.T_STRING) { 602 defaultResponder = SC.objectForPropertyPath(defaultResponder); 603 } 604 605 if (!defaultResponder) return null; 606 else if (defaultResponder.isResponderContext) return defaultResponder; 607 else if (defaultResponder.respondsTo(methodName)) return defaultResponder; 608 else return null; 609 }, 610 611 /** 612 Attempts to determine the initial target for a given action/target/sender 613 tuple. This is the method used by sendAction() to try to determine the 614 correct target starting point for an action before trickling up the 615 responder chain. 616 617 You send actions for user interface events and for menu actions. 618 619 This method returns an object if a starting target was found or null if no 620 object could be found that responds to the target action. 621 622 Passing an explicit target or pane constrains the target lookup to just 623 them; the defaultResponder and other panes are *not* searched. 624 625 @param {Object|String} target or null if no target is specified 626 @param {String} method name for target 627 @param {Object} sender optional sender 628 @param {SC.Pane} optional pane 629 @param {firstResponder} a first responder to use 630 @returns {Object} target object or null if none found 631 */ 632 targetForAction: function(methodName, target, sender, pane, firstResponder) { 633 634 // 1. no action, no target... 635 if (!methodName || (SC.typeOf(methodName) !== SC.T_STRING)) { 636 return null ; 637 } 638 639 // 2. an explicit target was passed... 640 if (target) { 641 // Normalize String targets to Objects 642 if (SC.typeOf(target) === SC.T_STRING) { 643 target = SC.objectForPropertyPath(target) || 644 SC.objectForPropertyPath(target, sender); 645 } 646 647 // Ensure that the target responds to the method. 648 if (target && !target.isResponderContext) { 649 if (target.respondsTo && !target.respondsTo(methodName)) { 650 target = null ; 651 } else if (SC.typeOf(target[methodName]) !== SC.T_FUNCTION) { 652 target = null ; 653 } 654 } 655 656 return target ; 657 } 658 659 // 3. an explicit pane was passed... 660 if (pane) { 661 target = this._responderFor(pane, methodName, firstResponder); 662 if (target) return target; 663 } 664 665 // 4. no target or pane passed... try to find target in the active panes 666 // and the defaultResponder 667 var keyPane = this.get('keyPane'), mainPane = this.get('mainPane') ; 668 669 // ...check key and main panes first 670 if (keyPane && (keyPane !== pane)) { 671 target = this._responderFor(keyPane, methodName) ; 672 } 673 if (!target && mainPane && (mainPane !== keyPane)) { 674 target = this._responderFor(mainPane, methodName) ; 675 } 676 677 // ...still no target? check the defaultResponder... 678 if (!target && (target = this.get('defaultResponder'))) { 679 if (SC.typeOf(target) === SC.T_STRING) { 680 target = SC.objectForPropertyPath(target) ; 681 if (target) this.set('defaultResponder', target) ; // cache if found 682 } 683 if (target && !target.isResponderContext) { 684 if (target.respondsTo && !target.respondsTo(methodName)) { 685 target = null ; 686 } else if (SC.typeOf(target[methodName]) !== SC.T_FUNCTION) { 687 target = null ; 688 } 689 } 690 } 691 692 return target ; 693 }, 694 695 /** 696 Finds the view that appears to be targeted by the passed event. This only 697 works on events with a valid target property. 698 699 @param {SC.Event} evt 700 @returns {SC.View} view instance or null 701 */ 702 targetViewForEvent: function (evt) { 703 var ret = null; 704 if (evt.target) { ret = SC.viewFor(evt.target); } 705 706 return ret; 707 }, 708 709 /** 710 Attempts to send an event down the responder chain. This method will 711 invoke the sendEvent() method on either the keyPane or on the pane owning 712 the target view you pass in. It will also automatically begin and end 713 a new run loop. 714 715 If you want to trap additional events, you should use this method to 716 send the event down the responder chain. 717 718 @param {String} action 719 @param {SC.Event} evt 720 @param {Object} target 721 @returns {Object} object that handled the event or null if not handled 722 */ 723 sendEvent: function(action, evt, target) { 724 var pane, ret ; 725 726 SC.run(function send_event() { 727 // get the target pane 728 if (target) pane = target.get('pane') ; 729 else pane = this.get('menuPane') || this.get('keyPane') || this.get('mainPane') ; 730 731 // if we found a valid pane, send the event to it 732 ret = (pane) ? pane.sendEvent(action, evt, target) : null ; 733 }, this); 734 735 return ret ; 736 }, 737 738 // ....................................................... 739 // EVENT LISTENER SETUP 740 // 741 742 /** 743 Default method to add an event listener for the named event. If you simply 744 need to add listeners for a type of event, you can use this method as 745 shorthand. Pass an array of event types to listen for and the element to 746 listen in. A listener will only be added if a handler is actually installed 747 on the RootResponder (or receiver) of the same name. 748 749 @param {Array} keyNames 750 @param {Element} target 751 @param {Object} receiver - optional if you don't want 'this' 752 @param {Boolean} useCapture 753 @returns {SC.RootResponder} receiver 754 */ 755 listenFor: function(keyNames, target, receiver, useCapture) { 756 receiver = receiver ? receiver : this; 757 keyNames.forEach( function(keyName) { 758 var method = receiver[keyName] ; 759 if (method) SC.Event.add(target, keyName, receiver, method, null, useCapture) ; 760 },this) ; 761 762 target = null ; 763 764 return receiver ; 765 }, 766 767 /** 768 Called when the document is ready to begin handling events. Setup event 769 listeners in this method that you are interested in observing for your 770 particular platform. Be sure to call sc_super(). 771 772 @returns {void} 773 */ 774 setup: function() { 775 // handle basic events 776 this.listenFor(['touchstart', 'touchmove', 'touchend', 'touchcancel', 'keydown', 'keyup', 'beforedeactivate', 'mousedown', 'mouseup', 'dragenter', 'dragover', 'dragleave', 'drop', 'click', 'dblclick', 'mousemove', 'contextmenu'], document) 777 .listenFor(['resize'], window); 778 779 if(SC.browser.isIE8OrLower) this.listenFor(['focusin', 'focusout'], document); 780 else this.listenFor(['focus', 'blur'], window); 781 782 // handle special case for keypress- you can't use normal listener to block 783 // the backspace key on Mozilla 784 if (this.keypress) { 785 if (SC.CAPTURE_BACKSPACE_KEY && SC.browser.isMozilla) { 786 var responder = this ; 787 document.onkeypress = function(e) { 788 e = SC.Event.normalizeEvent(e); 789 return responder.keypress.call(responder, e); 790 }; 791 792 // Otherwise, just add a normal event handler. 793 } else { 794 SC.Event.add(document, 'keypress', this, this.keypress); 795 } 796 } 797 798 // Add an array of transition listeners for immediate use (these will be cleaned up when actual testing completes). 799 // Because the transition test happens asynchronously and because we don't want to 800 // delay the launch of the application in order to a transition test (the app won't 801 // load if the browser tab is not visible), we start off by listening to everything 802 // and when the test is completed, we remove the extras to avoid double callbacks. 803 if (SC.platform.supportsCSSTransitions) { 804 var domPrefix = SC.browser.domPrefix, 805 lowerDomPrefix = domPrefix.toLowerCase(), 806 variation1 = lowerDomPrefix + 'transitionend', 807 variation2 = lowerDomPrefix + 'TransitionEnd', 808 variation3 = domPrefix + 'TransitionEnd'; 809 810 // Ensure that the callback name used maps to our implemented function name. 811 this[variation1] = this[variation2] = this[variation3] = this.transitionend; 812 813 // ex. transitionend, webkittransitionend, webkitTransitionEnd, WebkitTransitionEnd 814 this.listenFor(['transitionend', variation1, variation2, variation3], document); 815 816 if (SC.platform.supportsCSSAnimations) { 817 variation1 = lowerDomPrefix + 'animationstart'; 818 variation2 = lowerDomPrefix + 'AnimationStart'; 819 variation3 = domPrefix + 'AnimationStart'; 820 821 // Ensure that the callback name used maps to our implemented function name. 822 this[variation1] = this[variation2] = this[variation3] = this.animationstart; 823 824 // ex. animationstart, webkitanimationstart, webkitAnimationStart, WebkitAnimationStart 825 this.listenFor(['animationstart', variation1, variation2, variation3], document); 826 827 variation1 = lowerDomPrefix + 'animationiteration'; 828 variation2 = lowerDomPrefix + 'AnimationIteration'; 829 variation3 = domPrefix + 'AnimationIteration'; 830 831 // Ensure that the callback name used maps to our implemented function name. 832 this[variation1] = this[variation2] = this[variation3] = this.animationiteration; 833 834 // ex. animationiteration, webkitanimationiteration, webkitAnimationIteration, WebkitAnimationIteration 835 this.listenFor(['animationiteration', variation1, variation2, variation3], document); 836 837 variation1 = lowerDomPrefix + 'animationend'; 838 variation2 = lowerDomPrefix + 'AnimationEnd'; 839 variation3 = domPrefix + 'AnimationEnd'; 840 841 // Ensure that the callback name used maps to our implemented function name. 842 this[variation1] = this[variation2] = this[variation3] = this.animationend; 843 844 // ex. animationend, webkitanimationend, webkitAnimationEnd, WebkitAnimationEnd 845 this.listenFor(['animationend', variation1, variation2, variation3], document); 846 } 847 } 848 849 // handle these two events specially in IE 850 ['drag', 'selectstart'].forEach(function(keyName) { 851 var method = this[keyName] ; 852 if (method) { 853 if (SC.browser.isIE) { 854 var responder = this ; 855 856 document.body['on' + keyName] = function(e) { 857 return method.call(responder, SC.Event.normalizeEvent(event || window.event)); // this is IE :( 858 }; 859 860 // be sure to cleanup memory leaks 861 SC.Event.add(window, 'unload', this, function() { 862 document.body['on' + keyName] = null; 863 }); 864 865 } else { 866 SC.Event.add(document, keyName, this, method); 867 } 868 } 869 }, this); 870 871 var mousewheel = 'mousewheel'; 872 873 // Firefox emits different mousewheel events than other browsers 874 if (SC.browser.isMozilla) { 875 // For Firefox < 3.5, subscribe to DOMMouseScroll events 876 if (SC.browser.compare(SC.browser.engineVersion, '1.9.1') < 0) { 877 mousewheel = 'DOMMouseScroll'; 878 879 // For Firefox 3.5 and greater, we can listen for MozMousePixelScroll, 880 // which supports pixel-precision scrolling devices, like MacBook 881 // trackpads. 882 } else { 883 mousewheel = 'MozMousePixelScroll'; 884 } 885 } 886 SC.Event.add(document, mousewheel, this, this.mousewheel); 887 888 // Do some initial set up. 889 this.set('currentWindowSize', this.computeWindowSize()) ; 890 891 // TODO: Is this workaround still valid? 892 if (SC.browser.os === SC.OS.ios && SC.browser.name === SC.BROWSER.safari) { 893 894 // If the browser is identifying itself as a touch-enabled browser, but 895 // touch events are not present, assume this is a desktop browser doing 896 // user agent spoofing and simulate touch events automatically. 897 if (SC.platform && !SC.platform.touch) { 898 SC.platform.simulateTouchEvents(); 899 } 900 901 // Monkey patch RunLoop if we're in MobileSafari 902 var f = SC.RunLoop.prototype.endRunLoop, patch; 903 904 patch = function() { 905 // Call original endRunLoop implementation. 906 if (f) f.apply(this, arguments); 907 908 // This is a workaround for a bug in MobileSafari. 909 // Specifically, if the target of a touchstart event is removed from the DOM, 910 // you will not receive future touchmove or touchend events. What we do is, at the 911 // end of every runloop, check to see if the target of any touches has been removed 912 // from the DOM. If so, we re-append it to the DOM and hide it. We then mark the target 913 // as having been moved, and it is de-allocated in the corresponding touchend event. 914 var touches = SC.RootResponder.responder._touches, touch, elem, target, found = NO; 915 if (touches) { 916 // Iterate through the touches we're currently tracking 917 for (touch in touches) { 918 if (touches[touch]._rescuedElement) continue; // only do once 919 920 target = elem = touches[touch].target; 921 922 // Travel up the hierarchy looking for the document body 923 while (elem && (elem = elem.parentNode) && !found) { 924 found = (elem === document.body); 925 } 926 927 // If we aren't part of the body, move the element back 928 // but make sure we hide it from display. 929 if (!found && target) { 930 931 // Actually clone this node and replace it in the original 932 // layer if needed 933 if (target.parentNode && target.cloneNode) { 934 var clone = target.cloneNode(true); 935 target.parentNode.replaceChild(clone, target); 936 target.swapNode = clone; // save for restore later 937 } 938 939 // Create a holding pen if needed for these views... 940 var pen = SC.touchHoldingPen; 941 if (!pen) { 942 pen = SC.touchHoldingPen = document.createElement('div'); 943 pen.style.display = 'none'; 944 document.body.appendChild(pen); 945 } 946 947 // move element back into document... 948 pen.appendChild(target); 949 950 // ...and save the element to be garbage collected on touchEnd. 951 touches[touch]._rescuedElement = target; 952 } 953 } 954 } 955 }; 956 SC.RunLoop.prototype.endRunLoop = patch; 957 } 958 }, 959 960 /** 961 Cleans up the additional transition event listeners. 962 963 NOTE: requires that SC.RootResponser.responder.transitionendEventName 964 has been determined. 965 966 @returns {void} 967 */ 968 cleanUpTransitionListeners: function () { 969 var actualEventName = SC.platform.transitionendEventName, 970 domPrefix = SC.browser.domPrefix, 971 lowerDomPrefix = domPrefix.toLowerCase(), 972 variation1 = lowerDomPrefix + 'transitionend', 973 variation2 = lowerDomPrefix + 'TransitionEnd', 974 variation3 = domPrefix + 'TransitionEnd'; 975 976 // Once the actual event name is determined, simply remove all the extras. 977 // This should prevent any problems with browsers that fire multiple events. 978 ['transitionend', variation1, variation2, variation3].forEach(function (keyName) { 979 if (keyName !== actualEventName) { 980 SC.Event.remove(document, keyName, this, this[keyName]); 981 this[keyName] = null; 982 } 983 }); 984 }, 985 986 /** 987 Cleans up the additional animation event listeners. 988 989 NOTE: requires that SC.RootResponser.responder.animationstartEventName, 990 SC.RootResponser.responder.animationendEventName and 991 SC.RootResponser.responder.animationiterationEventName have been 992 determined. 993 994 @returns {void} 995 */ 996 cleanUpAnimationListeners: function () { 997 var domPrefix = SC.browser.domPrefix, 998 lowerDomPrefix = domPrefix.toLowerCase(), 999 actualEventName = SC.platform.animationendEventName, 1000 variation1 = lowerDomPrefix + 'animationend', 1001 variation2 = lowerDomPrefix + 'AnimationEnd', 1002 variation3 = domPrefix + 'AnimationEnd'; 1003 1004 // Once the actual event name is determined, simply remove all the extras. 1005 // This should prevent any problems with browsers that fire multiple events. 1006 ['animationend', variation1, variation2, variation3].forEach(function (keyName) { 1007 if (keyName !== actualEventName) { 1008 SC.Event.remove(document, keyName, this, this[keyName]); 1009 this[keyName] = null; 1010 } 1011 }); 1012 1013 actualEventName = SC.platform.animationiterationEventName; 1014 variation1 = lowerDomPrefix + 'animationiteration'; 1015 variation2 = lowerDomPrefix + 'AnimationIteration'; 1016 variation3 = domPrefix + 'AnimationIteration'; 1017 ['animationiteration', variation1, variation2, variation3].forEach(function (keyName) { 1018 if (keyName !== actualEventName) { 1019 SC.Event.remove(document, keyName, this, this[keyName]); 1020 this[keyName] = null; 1021 } 1022 }); 1023 1024 actualEventName = SC.platform.animationstartEventName; 1025 variation1 = lowerDomPrefix + 'animationstart'; 1026 variation2 = lowerDomPrefix + 'AnimationStart'; 1027 variation3 = domPrefix + 'AnimationStart'; 1028 ['animationstart', variation1, variation2, variation3].forEach(function (keyName) { 1029 if (keyName !== actualEventName) { 1030 SC.Event.remove(document, keyName, this, this[keyName]); 1031 this[keyName] = null; 1032 } 1033 }); 1034 }, 1035 1036 // ........................................................................... 1037 // TOUCH SUPPORT 1038 // 1039 1040 /** 1041 @private 1042 A map from views to internal touch entries. 1043 1044 Note: the touch entries themselves also reference the views. 1045 */ 1046 _touchedViews: {}, 1047 1048 /** 1049 @private 1050 A map from internal touch ids to the touch entries themselves. 1051 1052 The touch entry ids currently come from the touch event's identifier. 1053 */ 1054 _touches: {}, 1055 1056 /** 1057 Returns the touches that are registered to the specified view or responder; undefined if none. 1058 1059 When views receive a touch event, they have the option to subscribe to it. 1060 They are then mapped to touch events and vice-versa. This returns touches mapped to the view. 1061 1062 This method is also available on SC.Touch objects, and you will usually call it from there. 1063 */ 1064 touchesForView: function(view) { 1065 if (this._touchedViews[SC.guidFor(view)]) { 1066 return this._touchedViews[SC.guidFor(view)].touches; 1067 } 1068 }, 1069 1070 /** 1071 Computes a hash with x, y, and d (distance) properties, containing the average position 1072 of all touches, and the average distance of all touches from that average. This is useful 1073 for implementing scaling. 1074 1075 This method is also available on SC.Touch objects, and you will usually call it from there. 1076 1077 @param {SC.View} view The view whose touches should be averaged. 1078 @param {SC.Touch} additionalTouch This method uses touchesForView; if you call it from 1079 touchStart, that touch will not yet be included in touchesForView. To accommodate this, 1080 you should pass the view to this method (or pass YES to SC.Touch#averagedTouchesForView's 1081 `addSelf` argument). 1082 */ 1083 averagedTouchesForView: function(view, additionalTouch) { 1084 var t = this.touchesForView(view), 1085 len, averaged, additionalTouchIsDuplicate; 1086 1087 // Each view gets its own cached average touches object for performance. 1088 averaged = view._scrr_averagedTouches || (view._scrr_averagedTouches = {}); 1089 1090 // FAST PATH: no touches to track. 1091 if ((!t || t.length === 0) && !additionalTouch) { 1092 averaged.x = 0; 1093 averaged.y = 0; 1094 averaged.d = 0; 1095 averaged.velocityX = 0; 1096 averaged.velocityY = 0; 1097 1098 // Otherwise, average the touches. 1099 } else { 1100 // Cache the array object used by this method. (Cleared at the end to prevent memory leaks.) 1101 var touches = this._averagedTouches_touches || (this._averagedTouches_touches = []); 1102 1103 // copy touches into array 1104 if (t) { 1105 var i; 1106 len = t.length; 1107 for(i = 0; i < len; i++) { 1108 touches.push(t[i]); 1109 if (additionalTouch && t[i] === additionalTouch) additionalTouchIsDuplicate = YES; 1110 } 1111 } 1112 1113 // Add additionalTouch if present and not duplicated. 1114 if (additionalTouch && !additionalTouchIsDuplicate) touches.push(additionalTouch); 1115 1116 // Calculate the average. 1117 SC.Touch.averagedTouch(touches, averaged); 1118 1119 // Clear the touches array to prevent touch object leaks. 1120 touches.length = 0; 1121 } 1122 1123 return averaged; 1124 }, 1125 1126 assignTouch: function(touch, view) { 1127 // sanity-check 1128 if (touch.hasEnded) throw new Error("Attempt to assign a touch that is already finished."); 1129 1130 // Fast path, the touch is already assigned to the view. 1131 if (touch.view === view) return; 1132 1133 // unassign from old view if necessary 1134 if (touch.view) { 1135 this.unassignTouch(touch); 1136 } 1137 1138 // create view entry if needed 1139 if (!this._touchedViews[SC.guidFor(view)]) { 1140 this._touchedViews[SC.guidFor(view)] = { 1141 view: view, 1142 touches: SC.CoreSet.create([]), 1143 touchCount: 0 1144 }; 1145 view.set("hasTouch", YES); 1146 } 1147 1148 // add touch 1149 touch.view = view; 1150 this._touchedViews[SC.guidFor(view)].touches.add(touch); 1151 this._touchedViews[SC.guidFor(view)].touchCount++; 1152 }, 1153 1154 unassignTouch: function(touch) { 1155 // find view entry 1156 var view, viewEntry; 1157 1158 // Fast path, the touch is not assigned to a view. 1159 if (!touch.view) return; // touch.view should===touch.touchResponder eventually :) 1160 1161 // get view 1162 view = touch.view; 1163 1164 // get view entry 1165 viewEntry = this._touchedViews[SC.guidFor(view)]; 1166 viewEntry.touches.remove(touch); 1167 viewEntry.touchCount--; 1168 1169 // remove view entry if needed 1170 if (viewEntry.touchCount < 1) { 1171 view.set("hasTouch", NO); 1172 viewEntry.view = null; 1173 delete this._touchedViews[SC.guidFor(view)]; 1174 } 1175 1176 // clear view 1177 touch.view = undefined; 1178 }, 1179 1180 _flushQueuedTouchResponder: function(){ 1181 if (this._queuedTouchResponder) { 1182 var queued = this._queuedTouchResponder; 1183 this._queuedTouchResponder = null; 1184 this.makeTouchResponder.apply(this, queued); 1185 } 1186 }, 1187 1188 /** 1189 This method attempts to change the responder for a particular touch. The touch's responder is the 1190 view which will receive touch events for that touch. 1191 1192 You will usually not call this method directly, instead calling one of the convenience methods on 1193 the touch itself. See documentation for SC.Touch for more. 1194 1195 Possible gotchas: 1196 1197 - Because this method must search for a view which implements touchStart (without returning NO), 1198 touchStart is called on the new responder before touchCancelled is called on the old one. 1199 - While a touch exposes its current responder at `touchResponder` and any previous stacked one at 1200 `nextTouchResponder`, their relationship is ad hoc and arbitrary, and so are not chained by 1201 `nextResponder` like in a standard responder chain. To query the touch's current responder stack 1202 (or, though it's not recommended, change it), check touch.touchResponders. 1203 1204 @param {SC.Touch} touch 1205 @param {SC.Responder} responder The view to assign to the touch. (It, or if bubbling then an ancestor, 1206 must implement touchStart.) 1207 @param {Boolean} shouldStack Whether the new responder should replace the old one, or stack with it. 1208 Stacked responders are easy to revert via `SC.Touch#restoreLastTouchResponder`. 1209 @param {Boolean|SC.Responder} bubblesTo If YES, will attempt to find a `touchStart` responder up the 1210 responder chain. If NO or undefined, will only check the passed responder. If you pass a responder 1211 for this argument, the attempt will bubble until it reaches the passed responder, allowing you to 1212 restrict the bubbling to a portion of the responder chain. ((Note that this responder will not be 1213 given an opportunity to respond to the event.) 1214 @returns {Boolean} Whether a valid touch responder was found and assigned. 1215 */ 1216 makeTouchResponder: function(touch, responder, shouldStack, bubblesTo) { 1217 // In certain cases (SC.Gesture being one), we have to call makeTouchResponder 1218 // from inside makeTouchResponder so we queue it up here. 1219 if (this._isMakingTouchResponder) { 1220 this._queuedTouchResponder = [touch, responder, shouldStack, bubblesTo]; 1221 return YES; // um? 1222 } 1223 this._isMakingTouchResponder = YES; 1224 1225 var stack = touch.touchResponders, touchesForView; 1226 1227 // find the actual responder (if any, I suppose) 1228 // note that the pane's sendEvent function is slightly clever: 1229 // if the target is already touch responder, it will just return it without calling touchStart 1230 // we must do the same. 1231 if (touch.touchResponder === responder) { 1232 this._isMakingTouchResponder = NO; 1233 this._flushQueuedTouchResponder(); 1234 return YES; // more um 1235 } 1236 1237 // send touchStart 1238 // get the target pane 1239 var pane; 1240 if (responder) pane = responder.get('pane') ; 1241 else pane = this.get('keyPane') || this.get('mainPane') ; 1242 1243 // if the responder is not already in the stack... 1244 if (stack.indexOf(responder) < 0) { 1245 1246 // if we need to go up the view chain, do so via SC.Pane#sendEvent. 1247 if (bubblesTo) { 1248 // if we found a valid pane, send the event to it 1249 try { 1250 responder = pane ? pane.sendEvent("touchStart", touch, responder, bubblesTo) : null ; 1251 } catch (e) { 1252 SC.Logger.error("Error in touchStart: " + e); 1253 responder = null; 1254 } 1255 } else { 1256 // If the responder doesn't currently have a touch, or it does but it accepts multitouch, test it. Otherwise it's cool. 1257 if (responder && ((responder.get ? responder.get("acceptsMultitouch") : responder.acceptsMultitouch) || !responder.hasTouch)) { 1258 // If it doesn't respond to touchStart, it's no good. 1259 if (!responder.respondsTo("touchStart")) { 1260 responder = null; 1261 } 1262 // If it returns NO from touchStart, it's no good. Otherwise it's cool. 1263 else if (responder.touchStart(touch) === NO) { 1264 responder = null; 1265 } 1266 } 1267 } 1268 } 1269 1270 // if the item is in the stack, we will go to it (whether shouldStack is true or not) 1271 // as it is already stacked 1272 if (!shouldStack || (stack.indexOf(responder) > -1 && stack[stack.length - 1] !== responder)) { 1273 // first, we should unassign the touch. Note that we only do this IF WE ARE removing 1274 // the current touch responder. Otherwise we cause all sorts of headaches; why? Because, 1275 // if we are not (suppose, for instance, that it is stacked), then the touch does not 1276 // get passed back to the touch responder-- even while it continues to get events because 1277 // the touchResponder is still set! 1278 this.unassignTouch(touch); 1279 1280 // pop all other items 1281 var idx = stack.length - 1, last = stack[idx]; 1282 while (last && last !== responder) { 1283 // unassign the touch 1284 touchesForView = this.touchesForView(last); // won't even exist if there are no touches 1285 1286 // send touchCancelled (or, don't, if the view doesn't accept multitouch and it is not the last touch) 1287 if ((last.get ? last.get("acceptsMultitouch") : last.acceptsMultitouch) || !touchesForView) { 1288 if (last.touchCancelled) last.touchCancelled(touch); 1289 } 1290 1291 // go to next (if < 0, it will be undefined, so lovely) 1292 idx--; 1293 last = stack[idx]; 1294 1295 // update responders (for consistency) 1296 stack.pop(); 1297 1298 touch.touchResponder = stack[idx]; 1299 touch.nextTouchResponder = stack[idx - 1]; 1300 } 1301 1302 } 1303 1304 // now that we've popped off, we can push on 1305 if (responder) { 1306 this.assignTouch(touch, responder); 1307 1308 // keep in mind, it could be one we popped off _to_ above... 1309 if (responder !== touch.touchResponder) { 1310 stack.push(responder); 1311 1312 // update responder helpers 1313 touch.touchResponder = responder; 1314 touch.nextTouchResponder = stack[stack.length - 2]; 1315 } 1316 } 1317 1318 // Unflag that this method is running, and flush the queue if any. 1319 this._isMakingTouchResponder = NO; 1320 this._flushQueuedTouchResponder(); // this may need to be &&'ed with the responder to give the correct return value... 1321 1322 return !!responder; 1323 }, 1324 1325 /** 1326 Before the touchStart event is sent up the usual responder chain, the views along that same responder chain 1327 are given the opportunity to capture the touch event, preventing child views (including the target) from 1328 hearing about it. This of course proceeds in the opposite direction from a usual event bubbling, starting at 1329 the target's first ancestor and proceeding towards the target. This method implements the capture phase. 1330 1331 If no view captures the touch, this method will return NO, and makeTouchResponder is then called for the 1332 target, proceeding with standard target-to-pane event bubbling for `touchStart`. 1333 1334 For an example of captureTouch in action, see SC.ScrollView's touch handling, which by default captures the 1335 touch and holds it for 150ms to allow it to determine whether the user is tapping or scrolling. 1336 1337 You will usually not call this method yourself, and if you do, you should call the corresponding convenience 1338 method on the touch itself. 1339 1340 @param {SC.Touch} touch The touch to offer up for capture. 1341 @param {?SC.Responder} startingPoint The view whose children should be given an opportunity to capture 1342 the event. (The starting point itself is not asked.) 1343 @param {Boolean} shouldStack Whether any capturing responder should stack with existing responders. 1344 Stacked responders are easy to revert via `SC.Touch#restoreLastTouchResponder`. 1345 1346 @returns {Boolean} Whether or not the touch was captured. If it was not, you should pass it to 1347 `makeTouchResponder` for standard event bubbling. 1348 */ 1349 captureTouch: function(touch, startingPoint, shouldStack) { 1350 if (!startingPoint) startingPoint = this; 1351 1352 var target = touch.targetView, view = target, 1353 chain = [], idx, len; 1354 1355 //@if (debug) 1356 if (SC.LOG_TOUCH_EVENTS) { 1357 SC.Logger.info(' -- Received one touch on %@'.fmt(target.toString())); 1358 } 1359 //@endif 1360 // Generate the captureTouch responder chain by working backwards from the target 1361 // to the starting point. (Don't include the starting point.) 1362 while (view && (view !== startingPoint)) { 1363 chain.unshift(view); 1364 view = view.get('nextResponder'); 1365 } 1366 1367 // work down the chain 1368 for (len = chain.length, idx = 0; idx < len; idx++) { 1369 view = chain[idx]; 1370 //@if (debug) 1371 if (SC.LOG_TOUCH_EVENTS) SC.Logger.info(' -- Checking %@ for captureTouch response…'.fmt(view.toString())); 1372 //@endif 1373 1374 // see if it captured the touch 1375 if (view.tryToPerform('captureTouch', touch)) { 1376 //@if (debug) 1377 if (SC.LOG_TOUCH_EVENTS) SC.Logger.info(' -- Making %@ touch responder because it returns YES to captureTouch'.fmt(view.toString())); 1378 //@endif 1379 1380 // if so, make it the touch's responder 1381 this.makeTouchResponder(touch, view, shouldStack, startingPoint); // (touch, target, should stack, bubbles back to startingPoint, or all the way up.) 1382 return YES; // and that's all we need 1383 } 1384 } 1385 1386 //@if (debug) 1387 if (SC.LOG_TOUCH_EVENTS) SC.Logger.info(" -- Didn't find a view that returned YES to captureTouch."); 1388 //@endif 1389 1390 return NO; 1391 }, 1392 1393 //@if(debug) 1394 /** @private 1395 Artificially calls endTouch for any touch which is no longer present. This is necessary because 1396 _sometimes_, WebKit ends up not sending endtouch. 1397 */ 1398 endMissingTouches: function(presentTouches) { 1399 var idx, len = presentTouches.length, map = {}, end = []; 1400 1401 // make a map of what touches _are_ present 1402 for (idx = 0; idx < len; idx++) { 1403 map[presentTouches[idx].identifier] = YES; 1404 } 1405 1406 // check if any of the touches we have recorded are NOT present 1407 for (idx in this._touches) { 1408 var id = this._touches[idx].identifier; 1409 if (!map[id]) end.push(this._touches[idx]); 1410 } 1411 1412 // end said touches 1413 if (end.length) { 1414 console.warn('Ending missing touches: ' + end.toString()); 1415 } 1416 for (idx = 0, len = end.length; idx < len; idx++) { 1417 this.endTouch(end[idx]); 1418 this.finishTouch(end[idx]); 1419 } 1420 }, 1421 //@endif 1422 1423 _touchCount: 0, 1424 1425 /** @private 1426 Ends a specific touch (for a bit, at least). This does not "finish" a touch; it merely calls 1427 touchEnd, touchCancelled, etc. A re-dispatch (through recapture or makeTouchResponder) will terminate 1428 the process; it would have to be restarted separately, through touch.end(). 1429 */ 1430 endTouch: function(touchEntry, action, evt) { 1431 if (!action) { action = "touchEnd"; } 1432 1433 var responderIdx, responders, responder, originalResponder; 1434 1435 // unassign 1436 this.unassignTouch(touchEntry); 1437 1438 // call end for all items in chain 1439 if (touchEntry.touchResponder) { 1440 originalResponder = touchEntry.touchResponder; 1441 1442 responders = touchEntry.touchResponders; 1443 responderIdx = responders.length - 1; 1444 responder = responders[responderIdx]; 1445 while (responder) { 1446 if (responder[action]) { responder[action](touchEntry, evt); } 1447 1448 // check to see if the responder changed, and stop immediately if so. 1449 if (touchEntry.touchResponder !== originalResponder) { break; } 1450 1451 // next 1452 responderIdx--; 1453 responder = responders[responderIdx]; 1454 action = "touchCancelled"; // any further ones receive cancelled 1455 } 1456 } 1457 }, 1458 1459 /** 1460 @private 1461 "Finishes" a touch. That is, it eradicates it from our touch entries and removes all responder, etc. properties. 1462 */ 1463 finishTouch: function(touch) { 1464 // ensure the touch is indeed unassigned. 1465 this.unassignTouch(touch); 1466 1467 // If we rescued this touch's initial element, we should remove it 1468 // from the DOM and garbage collect now. See setup() for an 1469 // explanation of this bug/workaround. 1470 var elem = touch._rescuedElement; 1471 if (elem) { 1472 if (elem.swapNode && elem.swapNode.parentNode) { 1473 elem.swapNode.parentNode.replaceChild(elem, elem.swapNode); 1474 } else if (elem.parentNode === SC.touchHoldingPen) { 1475 SC.touchHoldingPen.removeChild(elem); 1476 } 1477 delete touch._rescuedElement; 1478 elem.swapNode = null; 1479 elem = null; 1480 } 1481 1482 // clear responders (just to be thorough) 1483 touch.touchResponders = null; 1484 touch.touchResponder = null; 1485 touch.nextTouchResponder = null; 1486 touch.hasEnded = YES; 1487 1488 // and remove from our set 1489 if (this._touches[touch.identifier]) delete this._touches[touch.identifier]; 1490 }, 1491 1492 /** @private 1493 Called when the user touches their finger to the screen. This method 1494 dispatches the touchstart event to the appropriate view. 1495 1496 We may receive a touchstart event for each touch, or we may receive a 1497 single touchstart event with multiple touches, so we may have to dispatch 1498 events to multiple views. 1499 1500 @param {Event} evt the event 1501 @returns {Boolean} 1502 */ 1503 touchstart: function(evt) { 1504 // Starting iOS5 touch events are handled by textfields. 1505 // As a workaround just let the browser to use the default behavior. 1506 if(this.ignoreTouchHandle(evt)) return YES; 1507 1508 var hidingTouchIntercept = NO; 1509 1510 SC.run(function() { 1511 //@if(debug) 1512 // When using breakpoints on touch start, we will lose the end touch event. 1513 this.endMissingTouches(evt.touches); 1514 //@endif 1515 1516 // loop through changed touches, calling touchStart, etc. 1517 var changedTouches = evt.changedTouches, 1518 len = changedTouches.length, 1519 idx, 1520 touch, touchEntry; 1521 1522 // prepare event for touch mapping. 1523 evt.touchContext = this; 1524 1525 // Loop through each touch we received in this event 1526 for (idx = 0; idx < len; idx++) { 1527 touch = changedTouches[idx]; 1528 1529 // Create an SC.Touch instance for every touch. 1530 touchEntry = SC.Touch.create(touch, this); 1531 1532 // skip the touch if there was no target 1533 if (!touchEntry.targetView) continue; 1534 1535 // account for hidden touch intercept (passing through touches, etc.) 1536 if (touchEntry.hidesTouchIntercept) hidingTouchIntercept = YES; 1537 1538 // set timestamp 1539 touchEntry.timeStamp = evt.timeStamp; 1540 1541 // Store the SC.Touch object. We use the identifier property (provided 1542 // by the browser) to disambiguate between touches. These will be used 1543 // later to determine if the touches have changed. 1544 this._touches[touch.identifier] = touchEntry; 1545 1546 // set the event (so default action, etc. can be stopped) 1547 touchEntry.event = evt; // will be unset momentarily 1548 1549 // First we allow any view in the responder chain to capture the touch, before triggering the standard touchStart 1550 // handler chain. 1551 var captured = this.captureTouch(touchEntry, this); 1552 if (!captured) this.makeTouchResponder(touchEntry, touchEntry.targetView, NO, YES); // (touch, target, shouldn't stack, bubbles all the way) 1553 1554 // Unset the reference to the original event so we can garbage collect. 1555 touchEntry.event = null; 1556 } 1557 1558 evt.touchContext = null; 1559 }, this); 1560 1561 // hack for text fields 1562 if (hidingTouchIntercept) { 1563 return YES; 1564 } 1565 1566 return evt.hasCustomEventHandling; 1567 }, 1568 1569 /** 1570 @private 1571 used to keep track of when a specific type of touch event was last handled, to see if it needs to be re-handled 1572 */ 1573 touchmove: function(evt) { 1574 // Starting iOS5 touch events are handled by textfields. 1575 // As a workaround just let the browser to use the default behavior. 1576 if(this.ignoreTouchHandle(evt)) return YES; 1577 1578 SC.run(function() { 1579 // pretty much all we gotta do is update touches, and figure out which views need updating. 1580 var touches = evt.changedTouches, touch, touchEntry, 1581 idx, len = touches.length, view, changedTouches, viewTouches, firstTouch, 1582 changedViews = {}, guid, hidingTouchIntercept = NO; 1583 1584 if (this._drag) { 1585 touch = SC.Touch.create(evt.changedTouches[0], this); 1586 this._drag.tryToPerform('mouseDragged', touch); 1587 } 1588 1589 // figure out what views had touches changed, and update our internal touch objects 1590 for (idx = 0; idx < len; idx++) { 1591 touch = touches[idx]; 1592 1593 // get our touch 1594 touchEntry = this._touches[touch.identifier]; 1595 1596 // we may have no touch entry; this can happen if somehow the touch came to a non-SC area. 1597 if (!touchEntry) { 1598 continue; 1599 } 1600 1601 if (touchEntry.hidesTouchIntercept) hidingTouchIntercept = YES; 1602 1603 // update touch velocity (moving average) 1604 var duration = evt.timeStamp - touchEntry.timeStamp, 1605 velocityLambda, latestXVelocity, latestYVelocity; 1606 // Given uneven timing between events, we should give less weight to shorter (less accurate) 1607 // events, with no consideration at all given zero-time events. 1608 if (duration !== 0) { 1609 // Lambda (how heavily we're weighting the latest number) 1610 velocityLambda = Math.min(1, duration / 80); 1611 // X 1612 latestXVelocity = (touch.pageX - touchEntry.pageX) / duration; 1613 touchEntry.velocityX = (1.0 - velocityLambda) * touchEntry.velocityX + velocityLambda * (latestXVelocity); 1614 // Y 1615 latestYVelocity = (touch.pageY - touchEntry.pageY) / duration; 1616 touchEntry.velocityY = (1.0 - velocityLambda) * touchEntry.velocityY + velocityLambda * (latestYVelocity); 1617 } 1618 1619 // update touch position et al. 1620 touchEntry.pageX = touch.pageX; 1621 touchEntry.pageY = touch.pageY; 1622 touchEntry.clientX = touch.clientX; 1623 touchEntry.clientY = touch.clientY; 1624 touchEntry.screenX = touch.screenX; 1625 touchEntry.screenY = touch.screenY; 1626 touchEntry.timeStamp = evt.timeStamp; 1627 touchEntry.type = evt.type; 1628 touchEntry.event = evt; 1629 1630 // if the touch entry has a view 1631 if (touchEntry.touchResponder) { 1632 view = touchEntry.touchResponder; 1633 1634 guid = SC.guidFor(view); 1635 // create a view entry 1636 if (!changedViews[guid]) changedViews[guid] = { "view": view, "touches": [] }; 1637 1638 // add touch 1639 changedViews[guid].touches.push(touchEntry); 1640 } 1641 } 1642 1643 // HACK: DISABLE OTHER TOUCH DRAGS WHILE MESSING WITH TEXT FIELDS 1644 if (hidingTouchIntercept) { 1645 evt.allowDefault(); 1646 return YES; 1647 } 1648 1649 // loop through changed views and send events 1650 for (idx in changedViews) { 1651 // get info 1652 view = changedViews[idx].view; 1653 changedTouches = changedViews[idx].touches; 1654 1655 // prepare event; note that views often won't use this method anyway (they'll call touchesForView instead) 1656 evt.viewChangedTouches = changedTouches; 1657 1658 // the first VIEW touch should be the touch info sent 1659 viewTouches = this.touchesForView(view); 1660 firstTouch = viewTouches.firstObject(); 1661 1662 // Load the event up with data from the first touch. THIS IS FOR CONVENIENCE ONLY in cases where the developer 1663 // only cares about one touch. 1664 evt.pageX = firstTouch.pageX; 1665 evt.pageY = firstTouch.pageY; 1666 evt.clientX = firstTouch.clientX; 1667 evt.clientY = firstTouch.clientY; 1668 evt.screenX = firstTouch.screenX; 1669 evt.screenY = firstTouch.screenY; 1670 evt.startX = firstTouch.startX; 1671 evt.startY = firstTouch.startY; 1672 evt.velocityX = firstTouch.velocityX; 1673 evt.velocityY = firstTouch.velocityY; 1674 evt.touchContext = this; // Injects the root responder so it can call e.g. `touchesForView`. 1675 1676 // Give the view a chance to handle touchesDragged. (Don't bubble; viewTouches is view-specific.) 1677 view.tryToPerform("touchesDragged", evt, viewTouches); 1678 } 1679 1680 // clear references to event 1681 touches = evt.changedTouches; 1682 len = touches.length; 1683 for (idx = 0; idx < len; idx++) { 1684 touch = touches[idx]; 1685 touchEntry = this._touches[touch.identifier]; 1686 if (touchEntry) touchEntry.event = null; 1687 } 1688 evt.touchContext = null; 1689 evt.viewChangedTouches = null; 1690 }, this); 1691 1692 return evt.hasCustomEventHandling; 1693 }, 1694 1695 touchend: function(evt) { 1696 var hidesTouchIntercept = NO; 1697 1698 // Starting iOS5 touch events are handled by textfields. 1699 // As a workaround just let the browser to use the default behavior. 1700 if(this.ignoreTouchHandle(evt)) return YES; 1701 1702 SC.run(function() { 1703 var touches = evt.changedTouches, touch, touchEntry, 1704 idx, len = touches.length, 1705 action = evt.isCancel ? "touchCancelled" : "touchEnd"; 1706 1707 for (idx = 0; idx < len; idx++) { 1708 //get touch+entry 1709 touch = touches[idx]; 1710 touch.type = 'touchend'; 1711 touchEntry = this._touches[touch.identifier]; 1712 1713 // check if there is an entry 1714 if (!touchEntry) continue; 1715 1716 // update touch velocity (moving average) 1717 var duration = evt.timeStamp - touchEntry.timeStamp, 1718 velocityLambda, latestXVelocity, latestYVelocity; 1719 // Given uneven timing between events, we should give less weight to shorter (less accurate) 1720 // events, with no consideration at all given zero-time events. 1721 if (duration !== 0) { 1722 // Lambda (how heavily we're weighting the latest number) 1723 velocityLambda = Math.min(1, duration / 80); 1724 // X 1725 latestXVelocity = (touch.pageX - touchEntry.pageX) / duration; 1726 touchEntry.velocityX = (1.0 - velocityLambda) * touchEntry.velocityX + velocityLambda * (latestXVelocity); 1727 // Y 1728 latestYVelocity = (touch.pageY - touchEntry.pageY) / duration; 1729 touchEntry.velocityY = (1.0 - velocityLambda) * touchEntry.velocityY + velocityLambda * (latestYVelocity); 1730 } 1731 1732 // update touch position et al. 1733 touchEntry.timeStamp = evt.timeStamp; 1734 touchEntry.pageX = touch.pageX; 1735 touchEntry.pageY = touch.pageY; 1736 touchEntry.clientX = touch.clientX; 1737 touchEntry.clientY = touch.clientY; 1738 touchEntry.screenX = touch.screenX; 1739 touchEntry.screenY = touch.screenY; 1740 touchEntry.type = 'touchend'; 1741 touchEntry.event = evt; 1742 1743 //@if (debug) 1744 if (SC.LOG_TOUCH_EVENTS) SC.Logger.info('-- Received touch end'); 1745 //@endif 1746 if (touchEntry.hidesTouchIntercept) { 1747 touchEntry.unhideTouchIntercept(); 1748 hidesTouchIntercept = YES; 1749 } 1750 1751 if (this._drag) { 1752 this._drag.tryToPerform('mouseUp', touch) ; 1753 this._drag = null ; 1754 } 1755 1756 // unassign 1757 this.endTouch(touchEntry, action, evt); 1758 this.finishTouch(touchEntry); 1759 } 1760 }, this); 1761 1762 1763 // for text fields 1764 if (hidesTouchIntercept) { 1765 return YES; 1766 } 1767 1768 return evt.hasCustomEventHandling; 1769 }, 1770 1771 /** @private 1772 Handle touch cancel event. Works just like cancelling a touch for any other reason. 1773 touchend handles it as a special case (sending cancel instead of end if needed). 1774 */ 1775 touchcancel: function(evt) { 1776 evt.isCancel = YES; 1777 this.touchend(evt); 1778 }, 1779 1780 /** @private 1781 Ignore Touch events on textfields and links. starting iOS 5 textfields 1782 get touch events. Textfields just need to get the default focus action. 1783 */ 1784 ignoreTouchHandle: function(evt) { 1785 if(SC.browser.isMobileSafari){ 1786 var tag = evt.target.tagName; 1787 if(tag==="INPUT" || tag==="TEXTAREA" || tag==="A" || tag==="SELECT"){ 1788 evt.allowDefault(); 1789 return YES; 1790 } 1791 } 1792 return NO; 1793 }, 1794 1795 // .......................................................... 1796 // KEYBOARD HANDLING 1797 // 1798 1799 1800 /** 1801 Invoked on a keyDown event that is not handled by any actual value. This 1802 will get the key equivalent string and then walk down the keyPane, then 1803 the focusedPane, then the mainPane, looking for someone to handle it. 1804 Note that this will walk DOWN the view hierarchy, not up it like most. 1805 1806 @returns {Object} Object that handled evet or null 1807 */ 1808 attemptKeyEquivalent: function(evt) { 1809 var ret = null ; 1810 1811 // keystring is a method name representing the keys pressed (i.e 1812 // 'alt_shift_escape') 1813 var keystring = evt.commandCodes()[0]; 1814 1815 // couldn't build a keystring for this key event, nothing to do 1816 if (!keystring) return NO; 1817 1818 var menuPane = this.get('menuPane'), 1819 keyPane = this.get('keyPane'), 1820 mainPane = this.get('mainPane'); 1821 1822 if (menuPane) { 1823 ret = menuPane.performKeyEquivalent(keystring, evt) ; 1824 if (ret) return ret; 1825 } 1826 1827 // Try the keyPane. If it's modal, then try the equivalent there but on 1828 // nobody else. 1829 if (keyPane) { 1830 ret = keyPane.performKeyEquivalent(keystring, evt) ; 1831 if (ret || keyPane.get('isModal')) return ret ; 1832 } 1833 1834 // if not, then try the main pane 1835 if (!ret && mainPane && (mainPane!==keyPane)) { 1836 ret = mainPane.performKeyEquivalent(keystring, evt); 1837 if (ret || mainPane.get('isModal')) return ret ; 1838 } 1839 1840 return ret ; 1841 }, 1842 1843 _lastModifiers: null, 1844 1845 /** @private 1846 Modifier key changes are notified with a keydown event in most browsers. 1847 We turn this into a flagsChanged keyboard event. Normally this does not 1848 stop the normal browser behavior. 1849 */ 1850 _handleModifierChanges: function(evt) { 1851 // if the modifier keys have changed, then notify the first responder. 1852 var m; 1853 m = this._lastModifiers = (this._lastModifiers || { alt: false, ctrl: false, shift: false }); 1854 1855 var changed = false; 1856 if (evt.altKey !== m.alt) { 1857 m.alt = evt.altKey; 1858 changed = true; 1859 } 1860 1861 if (evt.ctrlKey !== m.ctrl) { 1862 m.ctrl = evt.ctrlKey; 1863 changed = true; 1864 } 1865 1866 if (evt.shiftKey !== m.shift) { 1867 m.shift = evt.shiftKey; 1868 changed = true; 1869 } 1870 evt.modifiers = m; // save on event 1871 1872 return (changed) ? (this.sendEvent('flagsChanged', evt) ? evt.hasCustomEventHandling : YES) : YES ; 1873 }, 1874 1875 /** @private 1876 Determines if the keyDown event is a nonprintable or function key. These 1877 kinds of events are processed as keyboard shortcuts. If no shortcut 1878 handles the event, then it will be sent as a regular keyDown event. 1879 This function is only valid when called with a keydown event. 1880 */ 1881 _isFunctionOrNonPrintableKey: function(evt) { 1882 return !!(evt.altKey || evt.ctrlKey || evt.metaKey || SC.FUNCTION_KEYS[evt.which]); 1883 }, 1884 1885 /** @private 1886 Determines if the event simply reflects a modifier key change. These 1887 events may generate a flagsChanged event, but are otherwise ignored. 1888 */ 1889 _isModifierKey: function(evt) { 1890 return !!SC.MODIFIER_KEYS[evt.charCode]; 1891 }, 1892 1893 /** 1894 @private 1895 Determines if the key is printable (and therefore should be dispatched from keypress). 1896 Some browsers send backspace, tab, enter, and escape on keypress, so we want to 1897 explicitly ignore those here. 1898 1899 @param {KeyboardEvent} evt keypress event 1900 @returns {Boolean} 1901 */ 1902 _isPrintableKey: function (evt) { 1903 return ((evt.originalEvent.which === undefined || evt.originalEvent.which > 0) && 1904 !(evt.which === 8 || evt.which === 9 || evt.which === 13 || evt.which === 27)); 1905 }, 1906 1907 /** @private 1908 The keydown event occurs whenever the physically depressed key changes. 1909 This event is used to deliver the flagsChanged event and to with function 1910 keys and keyboard shortcuts. 1911 1912 All actions that might cause an actual insertion of text are handled in 1913 the keypress event. 1914 1915 References: 1916 http://www.quirksmode.org/js/keys.html 1917 https://developer.mozilla.org/en/DOM/KeyboardEvent 1918 http://msdn.microsoft.com/library/ff974342.aspx 1919 */ 1920 keydown: function(evt) { 1921 if (SC.none(evt)) return YES; 1922 var keyCode = evt.keyCode; 1923 if (SC.browser.isMozilla && evt.keyCode===9) { 1924 this.keydownCounter = 1; 1925 } 1926 // Fix for IME input (japanese, mandarin). 1927 // If the KeyCode is 229 wait for the keyup and 1928 // trigger a keyDown if it is is enter onKeyup. 1929 if (keyCode===229){ 1930 this._IMEInputON = YES; 1931 return this.sendEvent('keyDown', evt); 1932 } 1933 1934 // If user presses the escape key while we are in the middle of a 1935 // drag operation, cancel the drag operation and handle the event. 1936 if (keyCode === 27 && this._drag) { 1937 this._drag.cancelDrag(evt); 1938 this._drag = null; 1939 this._mouseDownView = null; 1940 return YES; 1941 } 1942 1943 // Firefox does NOT handle delete here... 1944 if (SC.browser.isMozilla && (evt.which === 8)) return true ; 1945 1946 // modifier keys are handled separately by the 'flagsChanged' event 1947 // send event for modifier key changes, but only stop processing if this 1948 // is only a modifier change 1949 var ret = this._handleModifierChanges(evt), 1950 target = evt.target || evt.srcElement, 1951 forceBlock = (evt.which === 8) && !SC.allowsBackspaceToPreviousPage && (target === document.body); 1952 1953 if (this._isModifierKey(evt)) return (forceBlock ? NO : ret); 1954 1955 // if this is a function or non-printable key, try to use this as a key 1956 // equivalent. Otherwise, send as a keyDown event so that the focused 1957 // responder can do something useful with the event. 1958 ret = YES ; 1959 if (this._isFunctionOrNonPrintableKey(evt)) { 1960 // otherwise, send as keyDown event. If no one was interested in this 1961 // keyDown event (probably the case), just let the browser do its own 1962 // processing. 1963 1964 // Arrow keys are handled in keypress for firefox 1965 if (keyCode>=37 && keyCode<=40 && SC.browser.isMozilla) return YES; 1966 1967 ret = this.sendEvent('keyDown', evt) ; 1968 1969 // attempt key equivalent if key not handled 1970 if (!ret) { 1971 SC.run(function () { 1972 ret = !this.attemptKeyEquivalent(evt) ; 1973 }, this); 1974 } else { 1975 ret = evt.hasCustomEventHandling ; 1976 if (ret) forceBlock = NO ; // code asked explicitly to let delete go 1977 } 1978 } 1979 1980 return forceBlock ? NO : ret ; 1981 }, 1982 1983 /** @private 1984 The keypress event occurs after the user has typed something useful that 1985 the browser would like to insert. Unlike keydown, the input codes here 1986 have been processed to reflect that actual text you might want to insert. 1987 1988 Normally ignore any function or non-printable key events. Otherwise, just 1989 trigger a keyDown. 1990 */ 1991 keypress: function(evt) { 1992 var ret, 1993 keyCode = evt.keyCode, 1994 isFirefox = SC.browser.isMozilla; 1995 1996 if (isFirefox && evt.keyCode===9) { 1997 this.keydownCounter++; 1998 if (this.keydownCounter === 2) return YES; 1999 } 2000 2001 // delete is handled in keydown() for most browsers 2002 if (isFirefox && (evt.which === 8)) { 2003 //get the keycode and set it for which. 2004 evt.which = keyCode; 2005 ret = this.sendEvent('keyDown', evt); 2006 return ret ? (SC.allowsBackspaceToPreviousPage || evt.hasCustomEventHandling) : YES ; 2007 2008 // normal processing. send keyDown for printable keys... 2009 //there is a special case for arrow key repeating of events in FF. 2010 } else { 2011 var isFirefoxArrowKeys = (keyCode >= 37 && keyCode <= 40 && isFirefox), 2012 charCode = evt.charCode; 2013 2014 if ((charCode !== undefined && charCode === 0 && evt.keyCode!==9) && !isFirefoxArrowKeys) return YES; 2015 if (isFirefoxArrowKeys) evt.which = keyCode; 2016 // we only want to rethrow if this is a printable key so that we don't 2017 // duplicate the event sent in keydown when a modifier key is pressed. 2018 if (isFirefoxArrowKeys || this._isPrintableKey(evt)) { 2019 return this.sendEvent('keyDown', evt) ? evt.hasCustomEventHandling : YES; 2020 } 2021 } 2022 }, 2023 2024 keyup: function(evt) { 2025 // to end the simulation of keypress in firefox set the _ffevt to null 2026 if(this._ffevt) this._ffevt=null; 2027 2028 // modifier keys are handled separately by the 'flagsChanged' event 2029 // send event for modifier key changes, but only stop processing if this is only a modifier change 2030 var ret = this._handleModifierChanges(evt); 2031 if (this._isModifierKey(evt)) return ret; 2032 // Fix for IME input (japanese, mandarin). 2033 // If the KeyCode is 229 wait for the keyup and 2034 // trigger a keyDown if it is is enter onKeyup. 2035 if (this._IMEInputON && evt.keyCode===13){ 2036 evt.isIMEInput = YES; 2037 this.sendEvent('keyDown', evt); 2038 this._IMEInputON = NO; 2039 } 2040 return this.sendEvent('keyUp', evt) ? evt.hasCustomEventHandling:YES; 2041 }, 2042 2043 /** 2044 IE's default behavior to blur textfields and other controls can only be 2045 blocked by returning NO to this event. However we don't want to block 2046 its default behavior otherwise textfields won't lose focus by clicking on 2047 an empty area as it's expected. If you want to block IE from blurring another 2048 control set blockIEDeactivate to true on the specific view in which you 2049 want to avoid this. Think of an autocomplete menu, you want to click on 2050 the menu but don't loose focus. 2051 */ 2052 beforedeactivate: function(evt) { 2053 var toElement = evt.toElement; 2054 if (toElement && toElement.tagName && toElement.tagName!=="IFRAME") { 2055 var view = SC.viewFor(toElement); 2056 //The following line is necessary to allow/block text selection for IE, 2057 // in combination with the selectstart event. 2058 if (view && view.get('blocksIEDeactivate')) return NO; 2059 } 2060 return YES; 2061 }, 2062 2063 // .......................................................... 2064 // MOUSE HANDLING 2065 // 2066 2067 mousedown: function(evt) { 2068 // First, save the click count. The click count resets if the mouse down 2069 // event occurs more than 250 ms later than the mouse up event or more 2070 // than 8 pixels away from the mouse down event or if the button used is different. 2071 this._clickCount += 1; 2072 2073 var view = this.targetViewForEvent(evt); 2074 2075 view = this._mouseDownView = this.sendEvent('mouseDown', evt, view) ; 2076 if (view && view.respondsTo('mouseDragged')) this._mouseCanDrag = YES ; 2077 2078 // Determine if any views took responsibility for the event. If so, save that information so we 2079 // can prevent the next click event we receive from propagating to the browser. 2080 var ret = view ? evt.hasCustomEventHandling : YES; 2081 this._lastMouseDownCustomHandling = ret; 2082 2083 // If it has been too long since the last click, the handler has changed or the mouse has moved 2084 // too far, reset the click count. 2085 if (!this._lastMouseUpAt || this._lastMouseDownView !== this._mouseDownView || ((Date.now() - this._lastMouseUpAt) > 250)) { 2086 this._clickCount = 1; 2087 } else { 2088 var deltaX = this._lastMouseDownX - evt.clientX, 2089 deltaY = this._lastMouseDownY - evt.clientY, 2090 distance = Math.sqrt(deltaX*deltaX + deltaY*deltaY) ; 2091 2092 if (distance > 8.0) this._clickCount = 1; 2093 } 2094 evt.clickCount = this._clickCount; 2095 2096 // Cache the handler and point of the last mouse down in order to determine whether a successive mouse down should 2097 // still increment the click count. 2098 this._lastMouseDownView = this._mouseDownView; 2099 this._lastMouseDownX = evt.clientX; 2100 this._lastMouseDownY = evt.clientY; 2101 2102 return ret; 2103 }, 2104 2105 /** 2106 mouseUp only gets delivered to the view that handled the mouseDown evt. 2107 we also handle click and double click notifications through here to 2108 ensure consistant delivery. Note that if mouseDownView is not 2109 implemented, then no mouseUp event will be sent, but a click will be 2110 sent. 2111 */ 2112 mouseup: function(evt) { 2113 var clickOrDoubleClickDidTrigger = NO, 2114 dragView = this._drag, 2115 handler = null; 2116 2117 if (dragView) { 2118 SC.run(function () { 2119 dragView.tryToPerform('mouseUp', evt); 2120 }); 2121 } else { 2122 var view = this._mouseDownView, 2123 targetView = this.targetViewForEvent(evt); 2124 2125 // record click count. 2126 evt.clickCount = this._clickCount ; 2127 2128 // attempt the mouseup call only if there's a target. 2129 // don't want a mouseup going to anyone unless they handled the mousedown... 2130 if (view) { 2131 handler = this.sendEvent('mouseUp', evt, view) ; 2132 2133 // try doubleClick 2134 if (!handler && this._clickCount === 2) { 2135 handler = this.sendEvent('doubleClick', evt, view) ; 2136 clickOrDoubleClickDidTrigger = YES; 2137 } 2138 2139 // try single click 2140 if (!handler) { 2141 handler = this.sendEvent('click', evt, view) ; 2142 clickOrDoubleClickDidTrigger = YES; 2143 } 2144 } 2145 2146 // try whoever's under the mouse if we haven't handle the mouse up yet 2147 if (!handler && !clickOrDoubleClickDidTrigger) { 2148 2149 // try doubleClick 2150 if (this._clickCount === 2) { 2151 handler = this.sendEvent('doubleClick', evt, targetView); 2152 } 2153 2154 // try singleClick 2155 if (!handler) { 2156 handler = this.sendEvent('click', evt, targetView) ; 2157 } 2158 } 2159 } 2160 2161 // cleanup 2162 this._mouseCanDrag = NO; 2163 this._mouseDownView = this._drag = null; 2164 2165 // Save timestamp of mouseup at last possible moment. 2166 // (This is used to calculate double click events) 2167 this._lastMouseUpAt = Date.now() ; 2168 2169 // Determine if any views took responsibility for the 2170 // event. If so, save that information so we can prevent 2171 // the next click event we receive from propagating to the browser. 2172 var ret = handler ? evt.hasCustomEventHandling : YES; 2173 this._lastMouseUpCustomHandling = ret; 2174 2175 return ret; 2176 }, 2177 2178 /** 2179 Certain browsers ignore us overriding mouseup and mousedown events and 2180 still allow default behavior (such as navigating away when the user clicks 2181 on a link). To block default behavior, we store whether or not the last 2182 mouseup or mousedown events resulted in us calling preventDefault() or 2183 stopPropagation(), in which case we make the same calls on the click event. 2184 2185 @param {Event} evt the click event 2186 @returns {Boolean} whether the event should be propagated to the browser 2187 */ 2188 click: function(evt) { 2189 if (!this._lastMouseUpCustomHandling || !this._lastMouseDownCustomHandling) { 2190 evt.preventDefault(); 2191 evt.stopPropagation(); 2192 return NO; 2193 } 2194 2195 return YES; 2196 }, 2197 2198 dblclick: function(evt){ 2199 if (SC.browser.isIE8OrLower) { 2200 this._clickCount = 2; 2201 // this._onmouseup(evt); 2202 this.mouseup(evt); 2203 } 2204 }, 2205 2206 mousewheel: function(evt) { 2207 var view = this.targetViewForEvent(evt) , 2208 handler = this.sendEvent('mouseWheel', evt, view) ; 2209 2210 return (handler) ? evt.hasCustomEventHandling : YES ; 2211 }, 2212 2213 _lastHovered: null, 2214 2215 /** 2216 This will send mouseEntered, mouseExited, mousedDragged and mouseMoved 2217 to the views you hover over. To receive these events, you must implement 2218 the method. If any subviews implement them and return true, then you won't 2219 receive any notices. 2220 2221 If there is a target mouseDown view, then mouse moved events will also 2222 trigger calls to mouseDragged. 2223 */ 2224 mousemove: function(evt) { 2225 2226 if (SC.browser.isIE) { 2227 if (this._lastMoveX === evt.clientX && this._lastMoveY === evt.clientY) return; 2228 } 2229 2230 // We'll record the last positions in all browsers, in case a special pane 2231 // or some such UI absolutely needs this information. 2232 this._lastMoveX = evt.clientX; 2233 this._lastMoveY = evt.clientY; 2234 2235 SC.run(function() { 2236 var dragView = this._drag; 2237 2238 // make sure the view gets focus no matter what. FF is inconsistent 2239 // about this. 2240 // this.focus(); 2241 // only do mouse[Moved|Entered|Exited|Dragged] if not in a drag session 2242 // drags send their own events, e.g. drag[Moved|Entered|Exited] 2243 if (dragView) { 2244 //IE triggers mousemove at the same time as mousedown 2245 if(SC.browser.isIE){ 2246 if (this._lastMouseDownX !== evt.clientX || this._lastMouseDownY !== evt.clientY) { 2247 dragView.tryToPerform('mouseDragged', evt); 2248 } 2249 } else { 2250 dragView.tryToPerform('mouseDragged', evt); 2251 } 2252 } else { 2253 var lh = this._lastHovered || [], nh = [], loc, len, 2254 view = this.targetViewForEvent(evt) ; 2255 2256 // first collect all the responding view starting with the 2257 // target view from the given mouse move event 2258 while (view && (view !== this)) { 2259 nh.push(view); 2260 view = view.get('nextResponder'); 2261 } 2262 // next exit views that are no longer part of the 2263 // responding chain 2264 for (loc=0, len=lh.length; loc < len; loc++) { 2265 view = lh[loc] ; 2266 if (nh.indexOf(view) === -1 && !view.isDestroyed) { // Usually we don't want to have to manually check isDestroyed, but in this case we're explicitly checking an out-of-date cache. 2267 view.tryToPerform('mouseExited', evt); 2268 } 2269 } 2270 // finally, either perform mouse moved or mouse entered depending on 2271 // whether a responding view was or was not part of the last 2272 // hovered views 2273 for (loc=0, len=nh.length; loc < len; loc++) { 2274 view = nh[loc]; 2275 if (lh.indexOf(view) !== -1) { 2276 view.tryToPerform('mouseMoved', evt); 2277 } else { 2278 view.tryToPerform('mouseEntered', evt); 2279 } 2280 } 2281 // Keep track of the view that were last hovered 2282 this._lastHovered = nh; 2283 // also, if a mouseDownView exists, call the mouseDragged action, if 2284 // it exists. 2285 if (this._mouseDownView) { 2286 if(SC.browser.isIE){ 2287 if (this._lastMouseDownX !== evt.clientX && this._lastMouseDownY !== evt.clientY) { 2288 this._mouseDownView.tryToPerform('mouseDragged', evt); 2289 } 2290 } 2291 else { 2292 this._mouseDownView.tryToPerform('mouseDragged', evt); 2293 } 2294 } 2295 } 2296 }, this); 2297 }, 2298 2299 // These event handlers prevent default file handling, and enable the dataDrag API. 2300 /** @private The dragenter event comes from the browser when a data-ful drag enters any element. */ 2301 dragenter: function(evt) { 2302 SC.run(function() { this._dragenter(evt); }, this); 2303 }, 2304 2305 /** @private */ 2306 _dragenter: function(evt) { 2307 if (!this._dragCounter) { 2308 this._dragCounter = 1; 2309 } 2310 else this._dragCounter++; 2311 return this._dragover(evt); 2312 }, 2313 2314 /** @private The dragleave event comes from the browser when a data-ful drag leaves any element. */ 2315 dragleave: function(evt) { 2316 SC.run(function() { this._dragleave(evt); }, this); 2317 }, 2318 2319 /** @private */ 2320 _dragleave: function(evt) { 2321 this._dragCounter--; 2322 var ret = this._dragover(evt); 2323 return ret; 2324 }, 2325 /** @private 2326 Dragleave doesn't fire reliably in all browsers, so this method forces it (scheduled below). Note 2327 that, being scheduled via SC.Timer, this method is already in a run loop. 2328 */ 2329 _forceDragLeave: function() { 2330 // Give it another runloop to ensure that we're not in the middle of a drag. 2331 this.invokeLast(function() { 2332 if (this._dragCounter === 0) return; 2333 this._dragCounter = 0; 2334 var evt = this._lastDraggedEvt; 2335 this._dragover(evt); 2336 }); 2337 }, 2338 2339 /** @private This event fires continuously while the dataful drag is over the document. */ 2340 dragover: function(evt) { 2341 SC.run(function() { this._dragover(evt); }, this); 2342 }, 2343 2344 /** @private */ 2345 _dragover: function(evt) { 2346 // If it's a file being dragged, prevent the default (leaving the app and opening the file). 2347 if (evt.dataTransfer.types && (evt.dataTransfer.types.contains('Files') || evt.dataTransfer.types.contains('text/uri-list'))) { 2348 evt.preventDefault(); 2349 evt.stopPropagation(); 2350 // Set the default drag effect to 'none'. Views may reverse this if they wish. 2351 evt.dataTransfer.dropEffect = 'none'; 2352 } 2353 2354 // Walk the responder chain, alerting anyone that would like to know. 2355 var ld = this._lastDraggedOver || [], nd = [], loc, len, 2356 view = this.targetViewForEvent(evt); 2357 2358 // Build the responder chain, starting with the view's target and (presumably) moving 2359 // up through parentViews to the pane. 2360 while (view && (view !== this)) { 2361 nd.push(view); 2362 view = view.get('nextResponder'); 2363 } 2364 2365 // Invalidate the force-drag-leave timer, if we have one set up. 2366 if (this._dragLeaveTimer) this._dragLeaveTimer.invalidate(); 2367 2368 // If this is our final drag event then we've left the document and everybody gets a 2369 // dataDragExited. 2370 if (this._dragCounter === 0) { 2371 for (loc = 0, len = nd.length; loc < len; loc++) { 2372 view = nd[loc]; 2373 view.tryToPerform('dataDragExited', evt); 2374 } 2375 this._lastDraggedOver = this._lastDraggedEvt = this._dragLeaveTimer = null; 2376 } 2377 // Otherwise, we process the responder chain normally, ignoring dragleaves. 2378 // (We skip dragleave events because they are sent after the adjacent dragenter event; checking 2379 // through both stacks would result in views being exited, re-entered and re-exited each time. 2380 // As a consequence, views are left ignorant of a very small number of dragleave events; those 2381 // shouldn't end up being the crucial just-before-drop events, though, so they should be of no 2382 // consequence.) 2383 else if (evt.type !== 'dragleave') { 2384 // First, exit views that are no longer part of the responder chain, child to parent. 2385 for (loc = 0, len = ld.length; loc < len; loc++) { 2386 view = ld[loc]; 2387 if (nd.indexOf(view) === -1) { 2388 view.tryToPerform('dataDragExited', evt); 2389 } 2390 } 2391 // Next, enter views that have just joined the responder chain, parent to child. 2392 for (loc = nd.length - 1; loc >= 0; loc--) { 2393 view = nd[loc]; 2394 if (ld.indexOf(view) === -1) { 2395 view.tryToPerform('dataDragEntered', evt); 2396 } 2397 } 2398 // Finally, send hover events to everybody. 2399 for (loc = 0, len = nd.length; loc < len; loc++) { 2400 view = nd[loc]; 2401 view.tryToPerform('dataDragHovered', evt); 2402 } 2403 this._lastDraggedOver = nd; 2404 this._lastDraggedEvt = evt; 2405 // For browsers that don't reliably call a dragleave for every dragenter, we have a timer fallback. 2406 this._dragLeaveTimer = SC.Timer.schedule({ target: this, action: '_forceDragLeave', interval: 300 }); 2407 } 2408 }, 2409 2410 /** @private This event is called if the most recent dragover event returned with a non-"none" dropEffect. */ 2411 drop: function(evt) { 2412 SC.run(function() { this._drop(evt); }, this); 2413 }, 2414 2415 /** @private */ 2416 _drop: function(evt) { 2417 // If it's a file being dragged, prevent the default (leaving the app and opening the file). 2418 if (evt.dataTransfer.types && (evt.dataTransfer.types.contains('Files') || evt.dataTransfer.types.contains('text/uri-list'))) { 2419 evt.preventDefault(); 2420 evt.stopPropagation(); 2421 // Set the default drag effect to 'none'. Views may reverse this if they wish. 2422 evt.dataTransfer.dropEffect = 'none'; 2423 } 2424 2425 // Bubble up the responder chain until we have a successful responder. 2426 var ld = this._lastDraggedOver || [], nd = [], loc, len, 2427 view = this.targetViewForEvent(evt); 2428 2429 // First collect all the responding view starting with the target view from the given drag event. 2430 while (view && (view !== this)) { 2431 nd.push(view); 2432 view = view.get('nextResponder'); 2433 } 2434 // Next, exit views that are no longer part of the responding chain. (This avoids the pixel-wide 2435 // edge case where a drop event fires on a new view without a final dragover event.) 2436 for (loc = 0, len = ld.length; loc < len; loc++) { 2437 view = ld[loc]; 2438 if (nd.indexOf(view) === -1) { 2439 view.tryToPerform('dataDragExited', evt); 2440 } 2441 } 2442 // Next, bubble the drop event itself until we find someone that successfully responds. 2443 for (loc = 0, len = nd.length; loc < len; loc++) { 2444 view = nd[loc]; 2445 if (view.tryToPerform('dataDragDropped', evt)) break; 2446 } 2447 // Finally, notify all interested views that the drag is dead and gone. 2448 for (loc = 0, len = nd.length; loc < len; loc++) { 2449 view = nd[loc]; 2450 view.tryToPerform('dataDragExited', evt); 2451 } 2452 2453 // Reset caches and counters. 2454 this._lastDraggedOver = null; 2455 this._lastDraggedAt = null; 2456 this._dragCounter = 0; 2457 if (this._dragLeaveTimer) this._dragLeaveTimer.invalidate(); 2458 this._dragLeaveTimer = null; 2459 }, 2460 2461 2462 // these methods are used to prevent unnecessary text-selection in IE, 2463 // there could be some more work to improve this behavior and make it 2464 // a bit more useful; right now it's just to prevent bugs when dragging 2465 // and dropping. 2466 2467 _mouseCanDrag: YES, 2468 2469 selectstart: function(evt) { 2470 var targetView = this.targetViewForEvent(evt), 2471 result = this.sendEvent('selectStart', evt, targetView); 2472 2473 // If the target view implements mouseDragged, then we want to ignore the 2474 // 'selectstart' event. 2475 if (targetView && targetView.respondsTo('mouseDragged')) { 2476 return (result !==null ? YES: NO) && !this._mouseCanDrag; 2477 } else { 2478 return (result !==null ? YES: NO); 2479 } 2480 }, 2481 2482 drag: function() { return false; }, 2483 2484 contextmenu: function(evt) { 2485 var view = this.targetViewForEvent(evt), 2486 ret; 2487 2488 // Determine if any views took responsibility for the event. 2489 view = this.sendEvent('contextMenu', evt, view); 2490 ret = view ? evt.hasCustomEventHandling : YES; 2491 2492 return ret; 2493 }, 2494 2495 // .......................................................... 2496 // ANIMATION HANDLING 2497 // 2498 2499 /* @private Handler for animationstart events. */ 2500 animationstart: function (evt) { 2501 var view = this.targetViewForEvent(evt); 2502 this.sendEvent('animationDidStart', evt, view); 2503 2504 return view ? evt.hasCustomEventHandling : YES; 2505 }, 2506 2507 /* @private Handler for animationiteration events. */ 2508 animationiteration: function (evt) { 2509 var view = this.targetViewForEvent(evt); 2510 this.sendEvent('animationDidIterate', evt, view); 2511 2512 return view ? evt.hasCustomEventHandling : YES; 2513 }, 2514 2515 /* @private Handler for animationend events. */ 2516 animationend: function (evt) { 2517 var view = this.targetViewForEvent(evt); 2518 this.sendEvent('animationDidEnd', evt, view); 2519 2520 return view ? evt.hasCustomEventHandling : YES; 2521 }, 2522 2523 /* @private Handler for transitionend events. */ 2524 transitionend: function (evt) { 2525 var view = this.targetViewForEvent(evt); 2526 this.sendEvent('transitionDidEnd', evt, view); 2527 2528 return view ? evt.hasCustomEventHandling : YES; 2529 } 2530 2531 }); 2532 2533 /* 2534 Invoked when the document is ready, but before main is called. Creates 2535 an instance and sets up event listeners as needed. 2536 */ 2537 SC.ready(SC.RootResponder, SC.RootResponder.ready = function () { 2538 var r; 2539 2540 r = SC.RootResponder.responder = SC.RootResponder.create(); 2541 r.setup(); 2542 }); 2543