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/core_query') ; 9 10 /** 11 The event class provides a simple cross-platform library for capturing and 12 delivering events on DOM elements and other objects. While this library 13 is based on code from both jQuery and Prototype.js, it includes a number of 14 additional features including support for handler objects and event 15 delegation. 16 17 Since native events are implemented very unevenly across browsers, 18 SproutCore will convert all native events into a standardized instance of 19 this special event class. 20 21 SproutCore events implement the standard W3C event API as well as some 22 additional helper methods. 23 24 @constructor 25 @param {Event} originalEvent 26 @returns {SC.Event} event instance 27 28 @since SproutCore 1.0 29 */ 30 SC.Event = function(originalEvent) { 31 this._sc_updateNormalizedEvent(originalEvent); 32 33 return this; 34 }; 35 36 SC.mixin(SC.Event, /** @scope SC.Event */ { 37 38 /** 39 We need this because some browsers deliver different values 40 for mouse wheel deltas. Once the first mouse wheel event has 41 been run, this value will get set. 42 43 @field 44 @type Number 45 @default 1 46 */ 47 MOUSE_WHEEL_MULTIPLIER: function() { 48 var deltaMultiplier = 1, 49 version = SC.browser.engineVersion; 50 51 if (SC.browser.name === SC.BROWSER.safari) { 52 deltaMultiplier = 0.4; 53 // Safari 5.0.1 and up 54 if (SC.browser.compare(version, '533.17') > 0 && SC.browser.compare(version, '534') < 0) { 55 deltaMultiplier = 0.004; 56 } else if (SC.browser.compare(version, '533') < 0) { 57 // Scrolling in Safari 5.0 58 deltaMultiplier = 40; 59 } 60 }else if(SC.browser.name === SC.BROWSER.ie){ 61 deltaMultiplier = 0.3; 62 }else if(SC.browser.name === SC.BROWSER.chrome){ 63 deltaMultiplier = 0.4; 64 } 65 return deltaMultiplier; 66 }(), 67 68 /** 69 This represents the limit in the delta before a different multiplier 70 will be applied. Because we can't generated an accurate mouse 71 wheel event ahead of time, and browsers deliver differing values 72 for mouse wheel deltas, this is necessary to ensure that 73 browsers that scale their values largely are dealt with correctly 74 in the future. 75 76 @type Number 77 @default 1000 78 */ 79 MOUSE_WHEEL_DELTA_LIMIT: 1000, 80 81 /** @private 82 We only want to invalidate once 83 */ 84 _MOUSE_WHEEL_LIMIT_INVALIDATED: NO, 85 86 /** 87 Standard method to create a new event. Pass the native browser event you 88 wish to wrap if needed. 89 */ 90 create: function(e) { return new SC.Event(e); }, 91 92 // the code below was borrowed from jQuery, Dean Edwards, and Prototype.js 93 94 /** 95 Bind an event to an element. 96 97 This method will cause the passed handler to be executed whenever a 98 relevant event occurs on the named element. This method supports a 99 variety of handler types, depending on the kind of support you need. 100 101 ## Simple Function Handlers 102 103 SC.Event.add(anElement, "click", myClickHandler) ; 104 105 The most basic type of handler you can pass is a function. This function 106 will be executed every time an event of the type you specify occurs on the 107 named element. You can optionally pass an additional context object which 108 will be included on the event in the event.data property. 109 110 When your handler function is called, the function's "this" property 111 will point to the element the event occurred on. 112 113 The click handler for this method must have a method signature like: 114 115 function(event) { return YES|NO; } 116 117 ## Method Invocations 118 119 SC.Event.add(anElement, "click", myObject, myObject.aMethod) ; 120 121 Optionally you can specify a target object and a method on the object to 122 be invoked when the event occurs. This will invoke the method function 123 with the target object you pass as "this". The method should have a 124 signature like: 125 126 function(event, targetElement) { return YES|NO; } 127 128 Like function handlers, you can pass an additional context data paramater 129 that will be included on the event in the event.data property. 130 131 ## Handler Return Values 132 133 Both handler functions should return YES if you want the event to 134 continue to propagate and NO if you want it to stop. Returning NO will 135 both stop bubbling of the event and will prevent any default action 136 taken by the browser. You can also control these two behaviors separately 137 by calling the stopPropagation() or preventDefault() methods on the event 138 itself, returning YES from your method. 139 140 ## Limitations 141 142 Although SproutCore's event implementation is based on jQuery, it is 143 much simpler in design. Notably, it does not support namespaced events 144 and you can only pass a single type at a time. 145 146 If you need more advanced event handling, consider the SC.ClassicResponder 147 functionality provided by SproutCore or use your favorite DOM library. 148 149 @param {Element} elem a DOM element, window, or document object 150 @param {String} eventType the event type you want to respond to 151 @param {Object} target The target object for a method call or a function. 152 @param {Object} method optional method or method name if target passed 153 @param {Object} context optional context to pass to the handler as event.data 154 @returns {Object} receiver 155 */ 156 add: function(elem, eventType, target, method, context, useCapture) { 157 158 // if a CQ object is passed in, either call add on each item in the 159 // matched set, or simply get the first element and use that. 160 if (elem && elem.isCoreQuery) { 161 if (elem.length > 0) { 162 elem.forEach(function(e) { 163 this.add(e, eventType, target, method, context); 164 }, this); 165 return this; 166 } else elem = elem[0]; 167 } 168 if (!elem) return this; // nothing to do 169 170 if (!useCapture) { 171 useCapture = NO; 172 } 173 174 // cannot register events on text nodes, etc. 175 if ( elem.nodeType === 3 || elem.nodeType === 8 ) return SC.Event; 176 177 // For whatever reason, IE has trouble passing the window object 178 // around, causing it to be cloned in the process 179 if (SC.browser.name === SC.BROWSER.ie && elem.setInterval) elem = window; 180 181 // if target is a function, treat it as the method, with optional context 182 if (SC.typeOf(target) === SC.T_FUNCTION) { 183 context = method; 184 method = target; 185 target = null; 186 187 // handle case where passed method is a key on the target. 188 } else if (target && SC.typeOf(method) === SC.T_STRING) { 189 method = target[method]; 190 } 191 192 // Get the handlers queue for this element/eventType. If the queue does 193 // not exist yet, create it and also setup the shared listener for this 194 // eventType. 195 var events = SC.data(elem, "sc_events") || SC.data(elem, "sc_events", {}), 196 handlers = events[eventType]; 197 198 if (!handlers) { 199 handlers = events[eventType] = {}; 200 this._addEventListener(elem, eventType, useCapture); 201 } 202 203 // Build the handler array and add to queue 204 handlers[SC.hashFor(target, method)] = [target, method, context]; 205 SC.Event._global[eventType] = YES ; // optimization for global triggers 206 207 // Nullify elem to prevent memory leaks in IE 208 elem = events = handlers = null ; 209 return this ; 210 }, 211 212 /** 213 Removes a specific handler or all handlers for an event or event+type. 214 215 To remove a specific handler, you must pass in the same function or the 216 same target and method as you passed into SC.Event.add(). See that method 217 for full documentation on the parameters you can pass in. 218 219 If you omit a specific handler but provide both an element and eventType, 220 then all handlers for that element will be removed. If you provide only 221 and element, then all handlers for all events on that element will be 222 removed. 223 224 ## Limitations 225 226 Although SproutCore's event implementation is based on jQuery, it is 227 much simpler in design. Notably, it does not support namespaced events 228 and you can only pass a single type at a time. 229 230 If you need more advanced event handling, consider the SC.ClassicResponder 231 functionality provided by SproutCore or use your favorite DOM library. 232 233 @param {Element} elem a DOM element, window, or document object 234 @param {String} eventType the event type to remove 235 @param {Object} target The target object for a method call. Or a function. 236 @param {Object} method optional name of method 237 @returns {Object} receiver 238 */ 239 remove: function(elem, eventType, target, method) { 240 241 // if a CQ object is passed in, either call add on each item in the 242 // matched set, or simply get the first element and use that. 243 if (elem && elem.isCoreQuery) { 244 if (elem.length > 0) { 245 elem.forEach(function(e) { 246 this.remove(e, eventType, target, method); 247 }, this); 248 return this; 249 } else elem = elem[0]; 250 } 251 if (!elem) return this; // nothing to do 252 253 // don't do events on text and comment nodes 254 if ( elem.nodeType === 3 || elem.nodeType === 8 ) return SC.Event; 255 256 var handlers, key, events = SC.data(elem, "sc_events") ; 257 if (!events) return this ; // nothing to do if no events are registered 258 259 // if no type is provided, remove all types for this element. 260 if (eventType === undefined) { 261 for (var anEventType in events) this.remove(elem, anEventType) ; 262 263 // otherwise, remove the handler for this specific eventType if found 264 } else if ((handlers = events[eventType])) { 265 266 var cleanupHandlers = NO ; 267 268 // if a target/method is provided, remove only that one 269 if (target || method) { 270 271 // normalize the target/method 272 if (SC.typeOf(target) === SC.T_FUNCTION) { 273 method = target; target = null ; 274 } else if (SC.typeOf(method) === SC.T_STRING) { 275 method = target[method] ; 276 } 277 278 delete handlers[SC.hashFor(target, method)]; 279 280 // check to see if there are handlers left on this event/eventType. 281 // if not, then cleanup the handlers. 282 key = null ; 283 for(key in handlers) break ; 284 if (key===null) cleanupHandlers = YES ; 285 286 // otherwise, just cleanup all handlers 287 } else cleanupHandlers = YES ; 288 289 // If there are no more handlers left on this event type, remove 290 // eventType hash from queue. 291 if (cleanupHandlers) { 292 delete events[eventType] ; 293 this._removeEventListener(elem, eventType) ; 294 } 295 296 // verify that there are still events registered on this element. If 297 // there aren't, cleanup the element completely to avoid memory leaks. 298 key = null ; 299 for (key in events) break; 300 if (!key) { 301 SC.removeData(elem, "sc_events") ; 302 delete this._elements[SC.guidFor(elem)]; // important to avoid leaks 303 304 // Clean up the cached listener to prevent a memory leak. 305 SC.removeData(elem, 'listener'); 306 } 307 308 } 309 310 elem = events = handlers = null ; // avoid memory leaks 311 return this ; 312 }, 313 314 NO_BUBBLE: ['blur', 'focus', 'change'], 315 316 /** 317 Generates a simulated event object. This is mostly useful for unit 318 testing. You can pass the return value of this property into the 319 trigger() method to actually send the event. 320 321 @param {Element} elem the element the event targets 322 @param {String} eventType event type. mousedown, mouseup, etc 323 @param {Hash} attrs optional additional attributes to apply to event. 324 @returns {Hash} simulated event object 325 */ 326 simulateEvent: function(elem, eventType, attrs) { 327 var ret = SC.Event.create({ 328 type: eventType, 329 target: elem, 330 preventDefault: function(){ this.cancelled = YES; }, 331 stopPropagation: function(){ this.bubbles = NO; }, 332 allowDefault: function() { this.hasCustomEventHandling = YES; }, 333 timeStamp: Date.now(), 334 bubbles: (this.NO_BUBBLE.indexOf(eventType)<0), 335 cancelled: NO, 336 normalized: YES, 337 simulated: true 338 }); 339 if (attrs) SC.mixin(ret, attrs) ; 340 return ret ; 341 }, 342 343 /** 344 Trigger an event execution immediately. You can use this method to 345 simulate arbitrary events on arbitrary elements. 346 347 ## Limitations 348 349 Note that although this is based on the jQuery implementation, it is 350 much simpler. Notably namespaced events are not supported and you cannot 351 trigger events globally. 352 353 If you need more advanced event handling, consider the SC.Responder 354 functionality provided by SproutCore or use your favorite DOM library. 355 356 ## Example 357 358 SC.Event.trigger(view.get('layer'), 'mousedown'); 359 360 @param elem {Element} the target element 361 @param eventType {String} the event type 362 @param event {SC.Event} [event] pre-normalized event to pass to handler 363 @param donative ?? 364 @returns {Boolean} Return value of trigger or undefined if not fired 365 */ 366 trigger: function(elem, eventType, event, donative) { 367 368 // if a CQ object is passed in, either call add on each item in the 369 // matched set, or simply get the first element and use that. 370 if (elem && elem.isCoreQuery) { 371 if (elem.length > 0) { 372 elem.forEach(function(e) { 373 this.trigger(e, eventType, event, donative); 374 }, this); 375 return this; 376 } else elem = elem[0]; 377 } 378 if (!elem) return this; // nothing to do 379 380 // don't do events on text and comment nodes 381 if ( elem.nodeType === 3 || elem.nodeType === 8 ) return undefined; 382 383 // Backwards-compatibility. Normalize from an Array. 384 if (SC.typeOf(event) === SC.T_ARRAY) { event = event[0]; } 385 386 var ret, fn = SC.typeOf(elem[eventType] || null) === SC.T_FUNCTION , 387 current, onfoo, isClick; 388 389 // Get the event to pass, creating a fake one if necessary 390 if (!event || !event.preventDefault) { 391 event = this.simulateEvent(elem, eventType); 392 } 393 394 event.type = eventType; 395 396 // Trigger the event - bubble if enabled 397 current = elem; 398 do { 399 ret = SC.Event.handle.call(current, event); 400 current = (current===document) ? null : (current.parentNode || document); 401 } while(!ret && event.bubbles && current); 402 current = null ; 403 404 // Handle triggering native .onfoo handlers 405 onfoo = elem["on" + eventType] ; 406 isClick = SC.$.nodeName(elem, 'a') && eventType === 'click'; 407 if ((!fn || isClick) && onfoo && onfoo.call(elem, event) === NO) ret = NO; 408 409 // Trigger the native events (except for clicks on links) 410 if (fn && donative !== NO && ret !== NO && !isClick) { 411 this.triggered = YES; 412 try { 413 elem[ eventType ](); 414 // prevent IE from throwing an error for some hidden elements 415 } catch (e) {} 416 } 417 418 this.triggered = NO; 419 420 return ret; 421 }, 422 423 /** 424 This method will handle the passed event, finding any registered listeners 425 and executing them. If you have an event you want handled, you can 426 manually invoke this method. This function expects it's "this" value to 427 be the element the event occurred on, so you should always call this 428 method like: 429 430 SC.Event.handle.call(element, event) ; 431 432 Note that like other parts of this library, the handle function does not 433 support namespaces. 434 435 @param event {DOMEvent} the event to handle 436 @returns {Boolean} 437 */ 438 handle: function (event) { 439 440 // ignore events triggered after window is unloaded or if double-called 441 // from within a trigger. 442 if ((typeof SC === "undefined") || SC.Event.triggered) return true; 443 444 // returned undefined or false 445 var val, ret, handlers, method, target; 446 447 // get the handlers for this event type 448 handlers = (SC.data(this, "sc_events") || {})[event.type]; 449 450 // no handlers for the event 451 if (!handlers) { 452 val = false; // nothing to do 453 454 } else { 455 // normalize event across browsers. The new event will actually wrap the real event with a normalized API. 456 event = SC.Event.normalizeEvent(event || window.event); 457 458 // invoke all handlers 459 for (var key in handlers) { 460 var handler = handlers[key]; 461 462 method = handler[1]; 463 464 // Pass in a reference to the handler function itself so that we can remove it later. 465 event.handler = method; 466 event.data = event.context = handler[2]; 467 468 target = handler[0] || this; 469 ret = method.call(target, event); 470 471 if (val !== false) val = ret; 472 473 // if method returned NO, do not continue. Stop propagation and 474 // return default. Note that we test explicitly for NO since 475 // if the handler returns no specific value, we do not want to stop. 476 if ( ret === false ) { 477 event.preventDefault(); 478 event.stopPropagation(); 479 } 480 } 481 482 // Clean up the cached normalized SC.Event so that it's not holding onto extra memory. 483 if (event.originalEvent && !event.originalEvent.simulated) { event._sc_clearNormalizedEvent(); } 484 } 485 486 return val; 487 }, 488 489 /** 490 This method is called just before the window unloads to unhook all 491 registered events. 492 */ 493 unload: function() { 494 var key, elements = this._elements ; 495 for(key in elements) this.remove(elements[key]) ; 496 497 // just in case some book-keeping was screwed up. avoid memory leaks 498 for(key in elements) delete elements[key] ; 499 delete this._elements ; 500 }, 501 502 /** 503 This hash contains handlers for special or custom events. You can add 504 your own handlers for custom events here by simply naming the event and 505 including a hash with the following properties: 506 507 - setup: this function should setup the handler or return NO 508 - teardown: this function should remove the event listener 509 510 */ 511 special: { 512 513 ready: { 514 setup: function() { 515 // Make sure the ready event is setup 516 SC._bindReady() ; 517 return; 518 }, 519 520 teardown: function() { return; } 521 522 }, 523 524 /** @private 525 Implement support for mouseenter on browsers other than IE */ 526 mouseenter: { 527 setup: function() { 528 if ( SC.browser.name === SC.BROWSER.ie ) return NO; 529 SC.Event.add(this, 'mouseover', SC.Event.special.mouseenter.handler); 530 return YES; 531 }, 532 533 teardown: function() { 534 if ( SC.browser.name === SC.BROWSER.ie ) return NO; 535 SC.Event.remove(this, 'mouseover', SC.Event.special.mouseenter.handler); 536 return YES; 537 }, 538 539 handler: function (event) { 540 // If we actually just moused on to a sub-element, ignore it 541 if ( SC.Event._withinElement(event, this) ) return YES; 542 // Execute the right handlers by setting the event type to mouseenter 543 event.type = "mouseenter"; 544 545 return SC.Event.handle.call(this, event); 546 } 547 }, 548 549 /** @private 550 Implement support for mouseleave on browsers other than IE */ 551 mouseleave: { 552 setup: function() { 553 if ( SC.browser.name === SC.BROWSER.ie ) return NO; 554 SC.Event.add(this, "mouseout", SC.Event.special.mouseleave.handler); 555 return YES; 556 }, 557 558 teardown: function() { 559 if ( SC.browser.name === SC.BROWSER.ie ) return NO; 560 SC.Event.remove(this, "mouseout", SC.Event.special.mouseleave.handler); 561 return YES; 562 }, 563 564 handler: function (event) { 565 // If we actually just moused on to a sub-element, ignore it 566 if ( SC.Event._withinElement(event, this) ) return YES; 567 // Execute the right handlers by setting the event type to mouseleave 568 event.type = "mouseleave"; 569 return SC.Event.handle.call(this, event); 570 } 571 } 572 }, 573 574 KEY_BACKSPACE: 8, 575 KEY_TAB: 9, 576 KEY_RETURN: 13, 577 KEY_ESC: 27, 578 KEY_SPACE: 32, 579 KEY_LEFT: 37, 580 KEY_UP: 38, 581 KEY_RIGHT: 39, 582 KEY_DOWN: 40, 583 KEY_DELETE: 46, 584 KEY_HOME: 36, 585 KEY_END: 35, 586 KEY_PAGEUP: 33, 587 KEY_PAGEDOWN: 34, 588 KEY_INSERT: 45, 589 590 _withinElement: function(event, elem) { 591 // Check if mouse(over|out) are still within the same parent element 592 var parent = event.relatedTarget; 593 594 // Traverse up the tree 595 while ( parent && parent !== elem ) { 596 try { parent = parent.parentNode; } catch(error) { parent = elem; } 597 } 598 599 // Return YES if we actually just moused on to a sub-element 600 return parent === elem; 601 }, 602 603 /** @private 604 Adds the primary event listener for the named type on the element. 605 606 If the event type has a special handler defined in SC.Event.special, 607 then that handler will be used. Otherwise the normal browser method will 608 be used. 609 610 @param elem {Element} the target element 611 @param eventType {String} the event type 612 */ 613 _addEventListener: function(elem, eventType, useCapture) { 614 var listener, 615 special = this.special[eventType] ; 616 617 if (!useCapture) { 618 useCapture = false; 619 } 620 621 // Check for a special event handler 622 // Only use addEventListener/attachEvent if the special 623 // events handler returns false 624 if ( !special || special.setup.call(elem) === false) { 625 626 // Save element in cache. This must be removed later to avoid 627 // memory leaks. 628 var guid = SC.guidFor(elem) ; 629 this._elements[guid] = elem; 630 631 // Either retrieve the previously cached listener or cache a new one. 632 listener = SC.data(elem, "listener") || SC.data(elem, "listener", 633 function handle_event (event) { 634 return SC.Event.handle.call(SC.Event._elements[guid], event); 635 }); 636 637 // Bind the global event handler to the element 638 if (elem.addEventListener) { 639 elem.addEventListener(eventType, listener, useCapture); 640 } else if (elem.attachEvent) { 641 // attachEvent is not working for IE8 and xhr objects 642 // there is currently a hack in request , but it needs to fixed here. 643 elem.attachEvent("on" + eventType, listener); 644 } 645 } 646 647 elem = special = listener = null; // avoid memory leak 648 }, 649 650 /** @private 651 Removes the primary event listener for the named type on the element. 652 653 If the event type has a special handler defined in SC.Event.special, 654 then that handler will be used. Otherwise the normal browser method will 655 be used. 656 657 Note that this will not clear the _elements hash from the element. You 658 must call SC.Event.unload() on unload to make sure that is cleared. 659 660 @param elem {Element} the target element 661 @param eventType {String} the event type 662 */ 663 _removeEventListener: function(elem, eventType) { 664 var listener, special = SC.Event.special[eventType] ; 665 if (!special || (special.teardown.call(elem)===NO)) { 666 // Retrieve the cached listener. 667 listener = SC.data(elem, "listener") ; 668 if (listener) { 669 if (elem.removeEventListener) { 670 elem.removeEventListener(eventType, listener, NO); 671 } else if (elem.detachEvent) { 672 elem.detachEvent("on" + eventType, listener); 673 } 674 } 675 } 676 677 elem = special = listener = null ; 678 }, 679 680 _elements: {}, 681 682 _sc_normalizedEvents: null, 683 684 // implement preventDefault() in a cross platform way 685 686 /** @private Take an incoming event and convert it to a normalized event. */ 687 normalizeEvent: function (event) { 688 var ret; 689 690 // Create the cache the first time. 691 if (!this._sc_normalizedEvents) { this._sc_normalizedEvents = {}; } 692 ret = this._sc_normalizedEvents[event.type]; 693 694 // Create a new normalized SC.Event. 695 if (!ret) { 696 if (event === window.event) { 697 // IE can't do event.normalized on an Event object 698 ret = SC.Event.create(event) ; 699 } else { 700 ret = event.normalized ? event : SC.Event.create(event) ; 701 } 702 703 // When passed an SC.Event, don't re-normalize it. 704 // TODO: This is hacky nonsense left over from a whole pile of bad decisions in SC.Event— 705 } else if (event.normalized) { 706 ret = event; 707 708 // Update the cached normalized SC.Event with the new DOM event. 709 } else { 710 ret._sc_updateNormalizedEvent(event); 711 } 712 713 // Cache the normalized event object for this type of event. This allows us to avoid recreating 714 // SC.Event objects constantly for noisy events such as 'mousemove' or 'mousewheel'. 715 this._sc_normalizedEvents[event.type] = ret; 716 717 return ret; 718 }, 719 720 _global: {}, 721 722 /** @private properties to copy from native event onto the event */ 723 // TODO: Remove this needless copy. 724 _props: ['altKey', 'attrChange', 'attrName', 'bubbles', 'button', 'cancelable', 'charCode', 'clientX', 'clientY', 'ctrlKey', 'currentTarget', 'data', 'detail', 'fromElement', 'handler', 'keyCode', 'metaKey', 'newValue', 'originalTarget', 'pageX', 'pageY', 'prevValue', 'relatedNode', 'relatedTarget', 'screenX', 'screenY', 'shiftKey', 'srcElement', 'target', 'timeStamp', 'toElement', 'type', 'view', 'which', 'touches', 'targetTouches', 'changedTouches', 'animationName', 'elapsedTime', 'dataTransfer'] 725 726 }); 727 728 SC.Event.prototype = { 729 730 /** 731 Set to YES if you have called either preventDefault() or stopPropagation(). 732 This allows a generic event handler to notice if you want to provide 733 detailed control over how the browser handles the real event. 734 735 @type Boolean 736 */ 737 hasCustomEventHandling: false, 738 739 /** @private Clear out the originalEvent from the SC.Event instance. */ 740 _sc_clearNormalizedEvent: function () { 741 // Remove the original event. 742 this.originalEvent = null; 743 744 // Reset the custom event handling and normalized flag. 745 this.hasCustomEventHandling = false; 746 this.normalized = false; 747 748 // Remove non-primitive properties copied over from the original event. While these will 749 // be overwritten, it's best to quickly null them out to avoid any issues. 750 var props = SC.Event._props, 751 idx; 752 753 idx = props.length; 754 while(--idx >= 0) { 755 var key = props[idx]; 756 this[key] = null; 757 } 758 759 // Remove the custom properties associated with the previous original event. While these will 760 // be overwritten, it's best to quickly null them out to avoid any issues. 761 this.timeStamp = null; 762 this.target = null; 763 this.relatedTarget = null; 764 this.pageX = null; 765 this.pageY = null; 766 this.which = null; 767 this.metaKey = null; 768 this.wheelDelta = null; 769 this.wheelDeltaY = null; 770 this.wheelDeltaX = null; 771 }, 772 773 /** @private Update the SC.Event instance with the new originalEvent. */ 774 _sc_updateNormalizedEvent: function (originalEvent) { 775 var idx, len; 776 777 // Flag. 778 this.normalized = true; 779 780 // copy properties from original event, if passed in. 781 if (originalEvent) { 782 this.originalEvent = originalEvent ; 783 var props = SC.Event._props, 784 key; 785 786 len = props.length; 787 idx = len; 788 while(--idx >= 0) { 789 key = props[idx] ; 790 this[key] = originalEvent[key] ; 791 } 792 } 793 794 // Fix timeStamp 795 this.timeStamp = this.timeStamp || Date.now(); 796 797 // Fix target property, if necessary 798 // Fixes #1925 where srcElement might not be defined either 799 if (!this.target) this.target = this.srcElement || document; 800 801 // check if target is a textnode (safari) 802 if (this.target.nodeType === 3 ) this.target = this.target.parentNode; 803 804 // Add relatedTarget, if necessary 805 if (!this.relatedTarget && this.fromElement) { 806 this.relatedTarget = (this.fromElement === this.target) ? this.toElement : this.fromElement; 807 } 808 809 // Calculate pageX/Y if missing and clientX/Y available 810 if (SC.none(this.pageX) && !SC.none(this.clientX)) { 811 var doc = document.documentElement, body = document.body; 812 this.pageX = this.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0); 813 this.pageY = this.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0); 814 } 815 816 // Add which for key events 817 if (!this.which && ((this.charCode || originalEvent.charCode === 0) ? this.charCode : this.keyCode)) { 818 this.which = this.charCode || this.keyCode; 819 } 820 821 // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) 822 if (!this.metaKey && this.ctrlKey) this.metaKey = this.ctrlKey; 823 824 // Add which for click: 1 == left; 2 == middle; 3 == right 825 // Note: button is not normalized, so don't use it 826 if (!this.which && this.button) { 827 this.which = ((this.button & 1) ? 1 : ((this.button & 2) ? 3 : ( (this.button & 4) ? 2 : 0 ) )); 828 } 829 830 // Normalize wheel delta values for mousewheel events. 831 /* 832 Taken from https://developer.mozilla.org/en-US/docs/Web/Events/mousewheel 833 IE and Opera (Presto) only support wheelDelta attribute and do not support horizontal scroll. 834 835 The wheelDeltaX attribute value indicates the wheelDelta attribute value along the horizontal axis. When a user operates the device for scrolling to right, the value is negative. Otherwise, i.e., if it's to left, the value is positive. 836 837 The wheelDeltaY attribute value indicates the wheelDelta attribute value along the vertical axis. The sign of the value is the same as the wheelDelta attribute value. 838 839 IE 840 841 The value is the same as the delta value of WM_MOUSEWHEEL or WM_MOUSEHWHEEL. It means that if the mouse wheel doesn't support high resolution scroll, the value is 120 per notch. The value isn't changed even if the scroll amount of system settings is page scroll. 842 843 ## Chrome 844 845 On Windows, the value is the same as the delta value of WM_MOUSEWHEEL or WM_MOUSEHWHEEL. And also, the value isn't changed even if the scroll amount of system settings is page scroll, i.e., the value is the same as IE on Windows. 846 847 On Linux, the value is 120 or -120 per native wheel event. This makes the same behavior as IE and Chrome for Windows. 848 849 On Mac, the value is complicated. The value is changed if the device that causes the native wheel event supports continuous scroll. 850 851 If the device supports continuous scroll (e.g., trackpad of MacBook or mouse wheel which can be turned smoothly), the value is computed from accelerated scroll amount. In this case, the value is the same as Safari. 852 853 If the device does not support continuous scroll (typically, old mouse wheel which cannot be turned smoothly), the value is computed from non-accelerated scroll amount (120 per notch). In this case, the value is different from Safari. 854 855 This difference makes a serious issue for web application developers. That is, web developers cannot know if mousewheel event is caused by which device. 856 857 See WebInputEventFactory::mouseWheelEvent of the Chromium's source code for the detail. 858 859 ## Safari 860 861 The value is always computed from accelerated scroll amount. This is really different from other browsers except Chrome with continuous scroll supported device. 862 863 Note: tested with the Windows package, the earliest available version was Safari 3.0 from 2007. It could be that earlier versions (on Mac) support the properties too. 864 865 ## Opera (Presto) 866 867 The value is always the detail attribute value ✕ 40. 868 869 On Windows, since the detail attribute value is computed from actual scroll amount, the value is different from other browsers except the scroll amount per notch is 3 lines in system settings or a page. 870 871 On Linux, the value is 80 or -80 per native wheel event. This is different from other browsers. 872 873 On Mac, the detail attribute value is computed from accelerated scroll amout of native event. The value is usually much bigger than Safari's or Chrome's value. 874 */ 875 if (this.type === 'mousewheel' || this.type === 'DOMMouseScroll' || this.type === 'MozMousePixelScroll') { 876 var deltaMultiplier = SC.Event.MOUSE_WHEEL_MULTIPLIER; 877 878 // normalize wheelDelta, wheelDeltaX, & wheelDeltaY for Safari 879 if (SC.browser.isWebkit && originalEvent.wheelDelta !== undefined) { 880 this.wheelDelta = 0 - (originalEvent.wheelDeltaY || originalEvent.wheelDeltaX); 881 this.wheelDeltaY = 0 - (originalEvent.wheelDeltaY || 0); 882 this.wheelDeltaX = 0 - (originalEvent.wheelDeltaX || 0); 883 884 // normalize wheelDelta for Firefox (all Mozilla browsers) 885 // note that we multiple the delta on FF to make it's acceleration more natural. 886 } else if (!SC.none(originalEvent.detail) && SC.browser.isMozilla) { 887 if (originalEvent.axis && (originalEvent.axis === originalEvent.HORIZONTAL_AXIS)) { 888 this.wheelDeltaX = originalEvent.detail; 889 this.wheelDelta = this.wheelDeltaY = 0; 890 } else { 891 this.wheelDelta = this.wheelDeltaY = originalEvent.detail; 892 this.wheelDeltaX = 0; 893 } 894 895 // handle all other legacy browser 896 } else { 897 this.wheelDelta = this.wheelDeltaY = SC.browser.isIE || SC.browser.isOpera ? 0 - originalEvent.wheelDelta : originalEvent.wheelDelta; 898 this.wheelDeltaX = 0; 899 } 900 901 this.wheelDelta *= deltaMultiplier; 902 this.wheelDeltaX *= deltaMultiplier; 903 this.wheelDeltaY *= deltaMultiplier; 904 } 905 }, 906 907 /** 908 Returns the touches owned by the supplied view. 909 910 @param {SC.View} 911 @returns {Array} touches an array of SC.Touch objects 912 */ 913 touchesForView: function(view) { 914 if (this.touchContext) return this.touchContext.touchesForView(view); 915 }, 916 917 /** 918 Same as touchesForView, but sounds better for responders. 919 920 @param {SC.RootResponder} 921 @returns {Array} touches an array of SC.Touch objects 922 */ 923 touchesForResponder: function(responder) { 924 if (this.touchContext) return this.touchContext.touchesForView(responder); 925 }, 926 927 /** 928 Returns average data--x, y, and d (distance)--for the touches owned by the 929 supplied view. 930 931 @param {SC.View} 932 @returns {Array} touches an array of SC.Touch objects 933 */ 934 averagedTouchesForView: function(view) { 935 if (this.touchContext) return this.touchContext.averagedTouchesForView(view); 936 return null; 937 }, 938 939 /** 940 Indicates that you want to allow the normal default behavior. Sets 941 the hasCustomEventHandling property to YES but does not cancel the event. 942 943 @returns {SC.Event} receiver 944 */ 945 allowDefault: function() { 946 this.hasCustomEventHandling = YES ; 947 return this ; 948 }, 949 950 /** 951 Implements W3C standard. Will prevent the browser from performing its 952 default action on this event. 953 954 @returns {SC.Event} receiver 955 */ 956 preventDefault: function() { 957 var evt = this.originalEvent ; 958 if (evt) { 959 if (evt.preventDefault) evt.preventDefault() ; 960 else evt.returnValue = NO ; // IE8 961 } 962 this.hasCustomEventHandling = YES ; 963 return this ; 964 }, 965 966 /** 967 Implements W3C standard. Prevents further bubbling of the event. 968 969 @returns {SC.Event} receiver 970 */ 971 stopPropagation: function() { 972 var evt = this.originalEvent ; 973 if (evt) { 974 if (evt.stopPropagation) evt.stopPropagation() ; 975 evt.cancelBubble = YES ; // IE 976 } 977 this.hasCustomEventHandling = YES ; 978 return this ; 979 }, 980 981 /** 982 Stops both the default action and further propagation. This is more 983 convenient than calling both. 984 985 @returns {SC.Event} receiver 986 */ 987 stop: function() { 988 return this.preventDefault().stopPropagation(); 989 }, 990 991 /** 992 Always YES to indicate the event was normalized. 993 994 @type Boolean 995 */ 996 normalized: YES, 997 998 /** 999 Returns the pressed character as a String. 1000 1001 @returns {String} 1002 */ 1003 // Warning. 1004 // Older versions of IE don't support charCode, but on keypress return the 1005 // ASCII value in keyCode instead of the key code. Therefore, if this code is 1006 // used on keyDown in IE versions prior to 9.0, it will fail. 1007 // Since SproutCore passes the keydown and keypress events as a keyDown 1008 // method, it's most likely that this code will cause unexpected problems 1009 // in IE 7 & IE 8. 1010 // 1011 // Reference: http://unixpapa.com/js/key.html 1012 getCharString: function() { 1013 if(SC.browser.name === SC.BROWSER.ie && 1014 SC.browser.compare(SC.browser.version, '9.0') < 0) { 1015 // Return an empty String for backspace, tab, left, right, up or down. 1016 if(this.keyCode === 8 || this.keyCode === 9 || 1017 (this.keyCode >= 37 && this.keyCode <= 40)) { 1018 return String.fromCharCode(0); 1019 } else { 1020 // This will only be accurate if the event is a keypress event. 1021 return (this.keyCode>0) ? String.fromCharCode(this.keyCode) : null; 1022 } 1023 } else { 1024 return (this.charCode>0) ? String.fromCharCode(this.charCode) : null; 1025 } 1026 }, 1027 1028 /** 1029 Returns characters with command codes for the event. 1030 1031 The first value is a normalized command code identifying the modifier keys that are pressed in 1032 combination with a character key. Command codes can be used to map key combinations to an action 1033 in the application. A basic example of a normalized command code would be `ctrl_x`, which 1034 corresponds to the combination of the `command` key with the `x` key being pressed on OS X or 1035 the `ctrl` key with the `x` key in Windows. 1036 1037 The second value is the character string by itself. So for `ctrl_x`, this would be simply `x`. However, 1038 for `alt_x` it would be `≈` and for `alt_shift_x`, it would be `˛`. 1039 1040 ## Considerations for Different OS's 1041 1042 At this time, the meta (command) key in OS X is mapped to the `ctrl_` command code prefix. This 1043 means that on OS X, command + s, and on Windows, ctrl + s, both become the same command code, `ctrl_s`. 1044 1045 Note that the order of command code prefixes is important and goes in the order: `ctrl_`, `alt_`, 1046 `shift_`. The following are examples of command codes: 1047 1048 * ctrl_x 1049 * alt_x 1050 * ctrl_shift_x 1051 * ctrl_alt_shift_x 1052 * alt_shift_x 1053 1054 @returns {Array} 1055 */ 1056 commandCodes: function () { 1057 var charCode = this.charCode, 1058 keyCode = this.keyCode, 1059 charString = null, 1060 commandCode = null, 1061 baseKeyName; 1062 1063 // WebKit browsers have equal values for `keyCode` and `charCode` on the keypress event. For example, 1064 // the letter `r` and the function `f3` both have a `keyCode` of 114. But the `r` also has a `charCode` 1065 // of 114. 1066 // If there is a keyCode and no matching charCode it is a function key. 1067 if (keyCode && keyCode !== charCode) { 1068 commandCode = SC.FUNCTION_KEYS[keyCode]; 1069 } 1070 1071 // Use the function name as the key name in the command code (ex. `down` could become `shift_down`). 1072 if (commandCode) { 1073 baseKeyName = commandCode; 1074 1075 // Find the base character for the key (i.e. `alt` + `a` becomes `å`, but we really want the key name, `a`). 1076 } else { 1077 baseKeyName = SC.PRINTABLE_KEYS[keyCode]; 1078 } 1079 1080 // If there is a base key name, append any modifiers to generate the command code. 1081 if (baseKeyName) { 1082 var modifiers = ''; 1083 1084 // Append the pressed modifier keys into a name used to identify command codes. 1085 // For example, holding the keys: shift, command & x, will map to the name "ctrl_shift_x". 1086 if (this.ctrlKey || this.metaKey) modifiers += 'ctrl_'; 1087 // UNUSED. In a future version we should scrap ctrl vs. meta for the proper intent depending on the current OS. 1088 // if (this.ctrlKey) modifiers += 'ctrl_'; 1089 // In OS X at least, when the ctrl key is pressed, both ctrlKey & metaKey are true. This makes it impossible to identify 1090 // ctrl + meta vs. just ctrl. We can identify just meta though. 1091 // else if (this.metaKey) modifiers += 'meta_'; 1092 1093 if (this.altKey) modifiers += 'alt_'; 1094 if (this.shiftKey) modifiers += 'shift_'; 1095 1096 commandCode = modifiers + baseKeyName; 1097 } 1098 1099 charString = this.getCharString(); // A character string or null. 1100 1101 return [commandCode, charString]; 1102 } 1103 1104 }; 1105 1106 // Also provide a Prototype-like API so that people can use either one. 1107 1108 /** Alias for add() method. This provides a Prototype-like API. */ 1109 SC.Event.observe = SC.Event.add ; 1110 1111 /** Alias for remove() method. This provides a Prototype-like API */ 1112 SC.Event.stopObserving = SC.Event.remove ; 1113 1114 /** Alias for trigger() method. This provides a Prototype-like API */ 1115 SC.Event.fire = SC.Event.trigger; 1116 1117 // Register unload handler to eliminate any registered handlers 1118 // This avoids leaks in IE and issues with mouseout or other handlers on 1119 // other browsers. 1120 1121 if(SC.browser.name === SC.BROWSER.ie) SC.Event.add(window, 'unload', SC.Event.prototype, SC.Event.unload) ; 1122 1123 SC.MODIFIER_KEYS = { 1124 16:'shift', 17:'ctrl', 18: 'alt' 1125 }; 1126 1127 SC.FUNCTION_KEYS = { 1128 8: 'backspace', 9: 'tab', 13: 'return', 19: 'pause', 27: 'escape', 1129 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 1130 37: 'left', 38: 'up', 39: 'right', 40: 'down', 44: 'printscreen', 1131 45: 'insert', 46: 'delete', 112: 'f1', 113: 'f2', 114: 'f3', 115: 'f4', 1132 116: 'f5', 117: 'f7', 119: 'f8', 120: 'f9', 121: 'f10', 122: 'f11', 1133 123: 'f12', 144: 'numlock', 145: 'scrolllock' 1134 } ; 1135 1136 SC.PRINTABLE_KEYS = { 1137 32: ' ', 48:"0", 49:"1", 50:"2", 51:"3", 52:"4", 53:"5", 54:"6", 55:"7", 1138 56:"8", 57:"9", 59:";", 61:"=", 65:"a", 66:"b", 67:"c", 68:"d", 69:"e", 1139 70:"f", 71:"g", 72:"h", 73:"i", 74:"j", 75:"k", 76:"l", 77:"m", 78:"n", 1140 79:"o", 80:"p", 81:"q", 82:"r", 83:"s", 84:"t", 85:"u", 86:"v", 87:"w", 1141 88:"x", 89:"y", 90:"z", 107:"+", 109:"-", 110:".", 188:",", 190:".", 1142 191:"/", 192:"`", 219:"[", 220:"\\", 221:"]", 222:"\"" 1143 } ; 1144