/* * CPPlatformWindow+DOM.j * AppKit * * Created by Francisco Tolmasky. * Copyright 2008, 280 North, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /* * THIS DOCUMENTATION STOLEN DIRECTLY FROM GOOGLE CLOSURE (licensed under Apache 2) * * Different web browsers have very different keyboard event handling. Most * importantly is that only certain browsers repeat keydown events: * IE, Opera, FF/Win32, and Safari 3 repeat keydown events. * FF/Mac and Safari 2 do not. * * For the purposes of this code, "Safari 3" means WebKit 525+, when WebKit * decided that they should try to match IE's key handling behavior. * Safari 3.0.4, which shipped with Leopard (WebKit 523), has the * Safari 2 behavior. * * Firefox, Safari, Opera prevent on keypress * * IE prevents on keydown * * Firefox does not fire keypress for shift, ctrl, alt * Firefox does fire keydown for shift, ctrl, alt, meta * Firefox does not repeat keydown for shift, ctrl, alt, meta * * Firefox does not fire keypress for up and down in an input * * Opera fires keypress for shift, ctrl, alt, meta * Opera does not repeat keypress for shift, ctrl, alt, meta * * Safari 2 and 3 do not fire keypress for shift, ctrl, alt * Safari 2 does not fire keydown for shift, ctrl, alt * Safari 3 *does* fire keydown for shift, ctrl, alt * * IE provides the keycode for keyup/down events and the charcode (in the * keycode field) for keypress. * * Mozilla provides the keycode for keyup/down and the charcode for keypress * unless it's a non text modifying key in which case the keycode is provided. * * Safari 3 provides the keycode and charcode for all events. * * Opera provides the keycode for keyup/down event and either the charcode or * the keycode (in the keycode field) for keypress events. * * Firefox x11 doesn't fire keydown events if a another key is already held down * until the first key is released. This can cause a key event to be fired with * a keyCode for the first key and a charCode for the second key. * * Safari 2 in keypress (not supported) * * charCode keyCode which * ENTER: 13 13 13 * F1: 63236 63236 63236 * F8: 63243 63243 63243 * ... * p: 112 112 112 * P: 80 80 80 * * Firefox, keypress: * * charCode keyCode which * ENTER: 0 13 13 * F1: 0 112 0 * F8: 0 119 0 * ... * p: 112 0 112 * P: 80 0 80 * * Opera, Mac+Win32, keypress: * * charCode keyCode which * ENTER: undefined 13 13 * F1: undefined 112 0 * F8: undefined 119 0 * ... * p: undefined 112 112 * P: undefined 80 80 * * IE7, keydown * * charCode keyCode which * ENTER: undefined 13 undefined * F1: undefined 112 undefined * F8: undefined 119 undefined * ... * p: undefined 80 undefined * P: undefined 80 undefined */ @import @import @import @import @import @import "CPApplication_Constants.j" @import "CPCompatibility.j" @import "CPCursor.j" @import "CPDOMWindowLayer.j" @import "CPDragServer_Constants.j" @import "CPEvent.j" @import "CPPasteboard.j" @import "CPPlatform.j" @import "CPPlatformPasteboard.j" @import "CPPlatformWindow+DOMKeys.j" @import "CPPlatformWindow.j" @import "CPText.j" @import "CPWindow_Constants.j" @class CPApplication @class CPDragServer @class _CPToolTip @global _CPRunModalLoop // List of all open native windows var PlatformWindows = [CPSet set]; // Define up here so compressor knows about them. var CPDOMEventGetClickCount, StopDOMEventPropagation, StopContextMenuDOMEventPropagation; var KeyCodesToPrevent = {}, CharacterKeysToPrevent = {}, KeyCodesToAllow = {}, MozKeyCodeToKeyCodeMap = { 61: 187, // =, equals 59: 186 // ;, semicolon }, KeyCodesToUnicodeMap = {}; KeyCodesToPrevent[CPKeyCodes.A] = YES; KeyCodesToAllow[CPKeyCodes.F1] = YES; KeyCodesToAllow[CPKeyCodes.F2] = YES; KeyCodesToAllow[CPKeyCodes.F3] = YES; KeyCodesToAllow[CPKeyCodes.F4] = YES; KeyCodesToAllow[CPKeyCodes.F5] = YES; KeyCodesToAllow[CPKeyCodes.F6] = YES; KeyCodesToAllow[CPKeyCodes.F7] = YES; KeyCodesToAllow[CPKeyCodes.F8] = YES; KeyCodesToAllow[CPKeyCodes.F9] = YES; KeyCodesToAllow[CPKeyCodes.F10] = YES; KeyCodesToAllow[CPKeyCodes.F11] = YES; KeyCodesToAllow[CPKeyCodes.F12] = YES; KeyCodesToUnicodeMap[CPKeyCodes.BACKSPACE] = CPDeleteCharacter; KeyCodesToUnicodeMap[CPKeyCodes.DELETE] = CPDeleteFunctionKey; KeyCodesToUnicodeMap[CPKeyCodes.TAB] = CPTabCharacter; KeyCodesToUnicodeMap[CPKeyCodes.ENTER] = CPCarriageReturnCharacter; KeyCodesToUnicodeMap[CPKeyCodes.ESC] = CPEscapeFunctionKey; KeyCodesToUnicodeMap[CPKeyCodes.PAGE_UP] = CPPageUpFunctionKey; KeyCodesToUnicodeMap[CPKeyCodes.PAGE_DOWN] = CPPageDownFunctionKey; KeyCodesToUnicodeMap[CPKeyCodes.LEFT] = CPLeftArrowFunctionKey; KeyCodesToUnicodeMap[CPKeyCodes.UP] = CPUpArrowFunctionKey; KeyCodesToUnicodeMap[CPKeyCodes.RIGHT] = CPRightArrowFunctionKey; KeyCodesToUnicodeMap[CPKeyCodes.DOWN] = CPDownArrowFunctionKey; KeyCodesToUnicodeMap[CPKeyCodes.HOME] = CPHomeFunctionKey; KeyCodesToUnicodeMap[CPKeyCodes.END] = CPEndFunctionKey; KeyCodesToUnicodeMap[CPKeyCodes.SEMICOLON] = ";"; KeyCodesToUnicodeMap[CPKeyCodes.DASH] = "-"; KeyCodesToUnicodeMap[CPKeyCodes.EQUALS] = "="; KeyCodesToUnicodeMap[CPKeyCodes.COMMA] = ","; KeyCodesToUnicodeMap[CPKeyCodes.PERIOD] = "."; KeyCodesToUnicodeMap[CPKeyCodes.SLASH] = "/"; KeyCodesToUnicodeMap[CPKeyCodes.APOSTROPHE] = "`"; KeyCodesToUnicodeMap[CPKeyCodes.SINGLE_QUOTE] = "'"; KeyCodesToUnicodeMap[CPKeyCodes.OPEN_SQUARE_BRACKET] = "["; KeyCodesToUnicodeMap[CPKeyCodes.BACKSLASH] = "\\"; KeyCodesToUnicodeMap[CPKeyCodes.CLOSE_SQUARE_BRACKET] = "]"; var ModifierKeyCodes = [ CPKeyCodes.META, CPKeyCodes.WEBKIT_RIGHT_META, CPKeyCodes.MAC_FF_META, CPKeyCodes.CTRL, CPKeyCodes.ALT, CPKeyCodes.SHIFT ], supportsNativeDragAndDrop = [CPPlatform supportsDragAndDrop]; var resizeTimer = nil; var PreventScroll = true; var blurTimer = nil; _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotification"; // When scrolling with an old-style scroll wheel with discete steps ('clicks'), the scroll amount can indicate how many "lines" to // scroll. #define SCROLLWHEEL_LINE_PIXELS 6.0 #if PLATFORM(DOM) @implementation CPPlatformWindow (DOM) - (id)_init { self = [super init]; if (self) { _DOMWindow = window; _contentRect = CGRectMakeZero(); _windowLevels = []; _windowLayers = @{}; _platformPasteboard = [CPPlatformPasteboard new]; [self registerDOMWindow]; [self updateFromNativeContentRect]; _charCodes = {}; } return self; } - (CGRect)nativeContentRect { if (!_DOMWindow) return [self contentRect]; if (_DOMWindow.cpFrame) return _DOMWindow.cpFrame(); var contentRect = CGRectMakeZero(); if (window.screenTop) contentRect.origin = CGPointMake(_DOMWindow.screenLeft, _DOMWindow.screenTop); else if (window.screenX) contentRect.origin = CGPointMake(_DOMWindow.screenX, _DOMWindow.screenY); // Safari, Mozilla, Firefox, and Opera if (_DOMWindow.innerWidth) contentRect.size = CGSizeMake(_DOMWindow.innerWidth, _DOMWindow.innerHeight); // Internet Explorer 6 in Strict Mode else if (document.documentElement && document.documentElement.clientWidth) contentRect.size = CGSizeMake(_DOMWindow.document.documentElement.clientWidth, _DOMWindow.document.documentElement.clientHeight); // Internet Explorer X else contentRect.size = CGSizeMake(_DOMWindow.document.body.clientWidth, _DOMWindow.document.body.clientHeight); return contentRect; } - (void)updateNativeContentRect { if (!_DOMWindow) return; if (typeof _DOMWindow["cpSetFrame"] === "function") return _DOMWindow.cpSetFrame([self contentRect]); var origin = [self contentRect].origin, nativeOrigin = [self nativeContentRect].origin; if (origin.x !== nativeOrigin.x || origin.y !== nativeOrigin.y) { _DOMWindow.moveBy(origin.x - nativeOrigin.x, origin.y - nativeOrigin.y); } var size = [self contentRect].size, nativeSize = [self nativeContentRect].size; if (size.width !== nativeSize.width || size.height !== nativeSize.height) { _DOMWindow.resizeBy(size.width - nativeSize.width, size.height - nativeSize.height); } } - (void)orderBack:(CPWindow)aWindow { if (_DOMWindow) _DOMWindow.blur(); } - (void)createDOMElements { var theDocument = _DOMWindow.document; // This guy fixes an issue in Firefox where if you focus the URL field, we stop getting key events _DOMFocusElement = theDocument.createElement("input"); _DOMFocusElement.style.position = "absolute"; _DOMFocusElement.style.zIndex = "-1000"; _DOMFocusElement.style.opacity = "0"; _DOMFocusElement.style.filter = "alpha(opacity=0)"; _DOMFocusElement.className = "cpdontremove"; _DOMBodyElement.appendChild(_DOMFocusElement); // Create a full screen div to protect against iframes and other elements // from consuming events during tracking // FIXME: multiple windows _DOMEventGuard = theDocument.createElement("div"); _DOMEventGuard.style.position = "absolute"; _DOMEventGuard.style.top = "0px"; _DOMEventGuard.style.left = "0px"; _DOMEventGuard.style.width = "100%"; _DOMEventGuard.style.height = "100%"; _DOMEventGuard.style.zIndex = "999"; _DOMEventGuard.style.display = "none"; _DOMEventGuard.className = "cpdontremove"; _DOMBodyElement.appendChild(_DOMEventGuard); // We get scrolling deltas from this element _DOMScrollingElement = theDocument.createElement("div"); _DOMScrollingElement.style.position = "absolute"; _DOMScrollingElement.style.visibility = "hidden"; _DOMScrollingElement.style.zIndex = @"999"; _DOMScrollingElement.style.height = "500px"; _DOMScrollingElement.style.width = "500px"; _DOMScrollingElement.style.overflow = "scroll"; //_DOMScrollingElement.style.backgroundColor = "rgba(0,0,0,1.0)"; // debug help. _DOMScrollingElement.style.opacity = "0"; _DOMScrollingElement.style.filter = "alpha(opacity=0)"; _DOMScrollingElement.className = "cpdontremove"; _DOMBodyElement.appendChild(_DOMScrollingElement); var _DOMInnerScrollingElement = theDocument.createElement("div"); _DOMInnerScrollingElement.style.width = "5000px"; _DOMInnerScrollingElement.style.height = "5000px"; _DOMScrollingElement.appendChild(_DOMInnerScrollingElement); // Set an initial scroll offset _DOMScrollingElement.scrollTop = 150; _DOMScrollingElement.scrollLeft = 150; } - (void)registerDOMWindow { var theDocument = _DOMWindow.document; _DOMBodyElement = theDocument.getElementById("cappuccino-body") || theDocument.body; // FIXME: Always do this? if (supportsNativeDragAndDrop) _DOMBodyElement.style["-khtml-user-select"] = "none"; _DOMBodyElement.webkitTouchCallout = "none"; _DOMBodyElement.style[CPBrowserStyleProperty(@"user-select")] = @"none"; [self createDOMElements]; [self _addLayers]; [_platformPasteboard setDOMWindow:_DOMWindow]; var theClass = [self class], dragEventImplementation = class_getMethodImplementation(theClass, @selector(dragEvent:)), dragEventCallback = function (anEvent) { dragEventImplementation(self, nil, anEvent); }, resizeEventSelector = @selector(resizeEvent:), resizeEventImplementation = class_getMethodImplementation(theClass, resizeEventSelector), resizeEventCallback = function (anEvent) { resizeEventImplementation(self, nil, anEvent); }, keyEventSelector = @selector(keyEvent:), keyEventImplementation = class_getMethodImplementation(theClass, keyEventSelector), keyEventCallback = function (anEvent) { return keyEventImplementation(self, nil, anEvent); }, mouseEventSelector = @selector(mouseEvent:), mouseEventImplementation = class_getMethodImplementation(theClass, mouseEventSelector), mouseEventCallback = function (anEvent) { mouseEventImplementation(self, nil, anEvent); }, contextMenuEventSelector = @selector(contextMenuEvent:), contextMenuEventImplementation = class_getMethodImplementation(theClass, contextMenuEventSelector), contextMenuEventCallback = function (anEvent) { return contextMenuEventImplementation(self, nil, anEvent); }, scrollEventSelector = @selector(scrollEvent:), scrollEventImplementation = class_getMethodImplementation(theClass, scrollEventSelector), scrollEventCallback = function (anEvent) { scrollEventImplementation(self, nil, anEvent); }, touchEventSelector = @selector(touchEvent:), touchEventImplementation = class_getMethodImplementation(theClass, touchEventSelector), touchEventCallback = function (anEvent) { touchEventImplementation(self, nil, anEvent); }, onFocusEventSelector = @selector(focusEvent:), onFocusEventImplementation = class_getMethodImplementation(theClass, onFocusEventSelector), onFocusEventCallback = function (anEvent) { onFocusEventImplementation(self, nil, anEvent); }, onBlurEventSelector = @selector(blurEvent:), onBlurEventImplementation = class_getMethodImplementation(theClass, onBlurEventSelector), onBlurEventCallback = function (anEvent) { onBlurEventImplementation(self, nil, anEvent); }; if (theDocument.addEventListener) { if ([CPPlatform supportsDragAndDrop]) { theDocument.addEventListener("dragstart", dragEventCallback, NO); theDocument.addEventListener("drag", dragEventCallback, NO); theDocument.addEventListener("dragend", dragEventCallback, NO); theDocument.addEventListener("dragover", dragEventCallback, NO); theDocument.addEventListener("dragleave", dragEventCallback, NO); theDocument.addEventListener("drop", dragEventCallback, NO); } theDocument.addEventListener("mouseup", mouseEventCallback, NO); theDocument.addEventListener("mousedown", mouseEventCallback, NO); theDocument.addEventListener("mousemove", mouseEventCallback, NO); theDocument.addEventListener("contextmenu", contextMenuEventCallback, NO); theDocument.addEventListener("keyup", keyEventCallback, NO); theDocument.addEventListener("keydown", keyEventCallback, NO); theDocument.addEventListener("keypress", keyEventCallback, NO); theDocument.addEventListener("touchstart", touchEventCallback, NO); theDocument.addEventListener("touchend", touchEventCallback, NO); theDocument.addEventListener("touchmove", touchEventCallback, NO); theDocument.addEventListener("touchcancel", touchEventCallback, NO); _DOMWindow.addEventListener("DOMMouseScroll", scrollEventCallback, NO); _DOMWindow.addEventListener("wheel", scrollEventCallback, NO); _DOMWindow.addEventListener("mousewheel", scrollEventCallback, NO); _DOMWindow.addEventListener("resize", resizeEventCallback, NO); _DOMWindow.addEventListener("blur", onBlurEventCallback, NO); _DOMWindow.addEventListener("focus", onFocusEventCallback, NO); _DOMWindow.addEventListener("unload", function() { _DOMWindow.removeEventListener("unload", arguments.callee, NO); [self blurEvent:nil]; [self _notifyPlatformWindowWillClose]; [self updateFromNativeContentRect]; [self _removeLayers]; theDocument.removeEventListener("mouseup", mouseEventCallback, NO); theDocument.removeEventListener("mousedown", mouseEventCallback, NO); theDocument.removeEventListener("mousemove", mouseEventCallback, NO); theDocument.removeEventListener("contextmenu", contextMenuEventCallback, NO); theDocument.removeEventListener("keyup", keyEventCallback, NO); theDocument.removeEventListener("keydown", keyEventCallback, NO); theDocument.removeEventListener("keypress", keyEventCallback, NO); theDocument.removeEventListener("touchstart", touchEventCallback, NO); theDocument.removeEventListener("touchend", touchEventCallback, NO); theDocument.removeEventListener("touchmove", touchEventCallback, NO); _DOMWindow.removeEventListener("resize", resizeEventCallback, NO); _DOMWindow.removeEventListener("blur", onBlurEventCallback, NO); _DOMWindow.removeEventListener("focus", onFocusEventCallback, NO); //FIXME: does firefox really need a different value? _DOMWindow.removeEventListener("DOMMouseScroll", scrollEventCallback, NO); _DOMWindow.removeEventListener("wheel", scrollEventCallback, NO); _DOMWindow.removeEventListener("mousewheel", scrollEventCallback, NO); [PlatformWindows removeObject:self]; [_platformPasteboard setDOMWindow:nil]; self._DOMWindow = nil; }, NO); } else { theDocument.attachEvent("onmouseup", mouseEventCallback); theDocument.attachEvent("onmousedown", mouseEventCallback); theDocument.attachEvent("onmousemove", mouseEventCallback); theDocument.attachEvent("ondblclick", mouseEventCallback); theDocument.attachEvent("oncontextmenu", contextMenuEventCallback); theDocument.attachEvent("onkeyup", keyEventCallback); theDocument.attachEvent("onkeydown", keyEventCallback); theDocument.attachEvent("onkeypress", keyEventCallback); _DOMWindow.attachEvent("onresize", resizeEventCallback); _DOMWindow.attachEvent("onfocus", onFocusEventCallback); _DOMWindow.attachEvent("onblur", onBlurEventCallback); _DOMWindow.onmousewheel = scrollEventCallback; theDocument.onmousewheel = scrollEventCallback; _DOMBodyElement.ondrag = function () { return NO; }; _DOMBodyElement.onselectstart = function () { return _DOMWindow.event.srcElement === _platformPasteboard._DOMPasteboardElement; }; _DOMWindow.attachEvent("onunload", function() { _DOMWindow.detachEvent("unload", arguments.callee); [self blurEvent:nil]; [self _notifyPlatformWindowWillClose]; [self updateFromNativeContentRect]; [self _removeLayers]; theDocument.detachEvent("onmouseup", mouseEventCallback); theDocument.detachEvent("onmousedown", mouseEventCallback); theDocument.detachEvent("onmousemove", mouseEventCallback); theDocument.detachEvent("ondblclick", mouseEventCallback); theDocument.detachEvent("oncontextmenu", contextMenuEventCallback); theDocument.detachEvent("onkeyup", keyEventCallback); theDocument.detachEvent("onkeydown", keyEventCallback); theDocument.detachEvent("onkeypress", keyEventCallback); _DOMWindow.detachEvent("onresize", resizeEventCallback); _DOMWindow.detachEvent("onfocus", onBlurEventCallback); _DOMWindow.detachEvent("onblur", onFocusEventCallback); _DOMWindow.onmousewheel = NULL; theDocument.onmousewheel = NULL; _DOMBodyElement.ondrag = NULL; _DOMBodyElement.onselectstart = NULL; [PlatformWindows removeObject:self]; [_platformPasteboard setDOMWindow:nil]; self._DOMWindow = nil; }, NO); } } + (CPSet)visiblePlatformWindows { if ([[CPPlatformWindow primaryPlatformWindow] isVisible]) { var set = [CPSet setWithSet:PlatformWindows]; [set addObject:[CPPlatformWindow primaryPlatformWindow]]; return set; } else return PlatformWindows; } - (void)orderFront:(CPWindow)aWindow { if ([aWindow parentWindow]) return; if (_DOMWindow) return _DOMWindow.focus(); _DOMWindow = window.open("about:blank", "_blank", "menubar=no,location=no,resizable=yes,scrollbars=no,status=no,left=" + CGRectGetMinX(_contentRect) + ",top=" + CGRectGetMinY(_contentRect) + ",width=" + CGRectGetWidth(_contentRect) + ",height=" + CGRectGetHeight(_contentRect)); if (!_DOMWindow) return; [PlatformWindows addObject:self]; // FIXME: cpSetFrame? _DOMWindow.document.write(''); _DOMWindow.document.close(); if (self != [CPPlatformWindow primaryPlatformWindow]) _DOMWindow.document.title = _title; if (![CPPlatform isBrowser]) { _DOMWindow.cpWindowNumber = [self._only windowNumber]; _DOMWindow.cpSetFrame(_contentRect); _DOMWindow.cpSetLevel(_level); _DOMWindow.cpSetHasShadow(_hasShadow); _DOMWindow.cpSetShadowStyle(_shadowStyle); } [self registerDOMWindow]; _DOMBodyElement.style.cursor = [[CPCursor currentCursor] _cssString]; } - (void)orderOut:(CPWindow)aWindow { if (!_DOMWindow) return; _DOMWindow.close(); } - (void)dragEvent:(DOMEvent)aDOMEvent { var type = aDOMEvent.type, dragServer = [CPDragServer sharedDragServer], location = CGPointMake(aDOMEvent.clientX, aDOMEvent.clientY), pasteboard = [_CPDOMDataTransferPasteboard DOMDataTransferPasteboard]; [pasteboard _setDataTransfer:aDOMEvent.dataTransfer]; if (aDOMEvent.type === "dragstart") { [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; [pasteboard _setPasteboard:[dragServer draggingPasteboard]]; var draggedWindow = [dragServer draggedWindow], draggedWindowFrame = [draggedWindow frame], DOMDragElement = draggedWindow._DOMElement; DOMDragElement.style.left = -CGRectGetWidth(draggedWindowFrame) + "px"; DOMDragElement.style.top = -CGRectGetHeight(draggedWindowFrame) + "px"; var parentNode = DOMDragElement.parentNode; if (parentNode) parentNode.removeChild(DOMDragElement); _DOMBodyElement.appendChild(DOMDragElement); var draggingOffset = [dragServer draggingOffset]; aDOMEvent.dataTransfer.setDragImage(DOMDragElement, draggingOffset.width, draggingOffset.height); aDOMEvent.dataTransfer.effectAllowed = "all"; [dragServer draggingStartedInPlatformWindow:self globalLocation:[CPPlatform isBrowser] ? location : CGPointMake(aDOMEvent.screenX, aDOMEvent.screenY)]; } else if (type === "drag") { var y = aDOMEvent.screenY; if (CPFeatureIsCompatible(CPHTML5DragAndDropSourceYOffBy1)) y -= 1; [dragServer draggingSourceUpdatedWithGlobalLocation:[CPPlatform isBrowser] ? location : CGPointMake(aDOMEvent.screenX, y)]; } else if (type === "dragover" || type === "dragleave") { if (aDOMEvent.preventDefault) aDOMEvent.preventDefault(); var dropEffect = "none", dragOperation = [dragServer draggingUpdatedInPlatformWindow:self location:location]; if (dragOperation === CPDragOperationMove || dragOperation === CPDragOperationGeneric || dragOperation === CPDragOperationPrivate) dropEffect = "move"; else if (dragOperation === CPDragOperationCopy) dropEffect = "copy"; else if (dragOperation === CPDragOperationLink) dropEffect = "link"; aDOMEvent.dataTransfer.dropEffect = dropEffect; } else if (type === "dragend") { var dropEffect = aDOMEvent.dataTransfer.dropEffect; if (dropEffect === "move") dragOperation = CPDragOperationMove; else if (dropEffect === "copy") dragOperation = CPDragOperationCopy; else if (dropEffect === "link") dragOperation = CPDragOperationLink; else dragOperation = CPDragOperationNone; [dragServer draggingEndedInPlatformWindow:self globalLocation:[CPPlatform isBrowser] ? location : CGPointMake(aDOMEvent.screenX, aDOMEvent.screenY) operation:dragOperation]; } else //if (type === "drop") { [dragServer performDragOperationInPlatformWindow:self]; // W3C Model if (aDOMEvent.preventDefault) aDOMEvent.preventDefault(); if (aDOMEvent.stopPropagation) aDOMEvent.stopPropagation(); } [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; } - (void)keyEvent:(DOMEvent)aDOMEvent { var event, location = _lastMouseEventLocation || CGPointMakeZero(), timestamp = [CPEvent currentTimestamp], sourceElement = aDOMEvent.target || aDOMEvent.srcElement, windowNumber = [[CPApp keyWindow] windowNumber], modifierFlags = (aDOMEvent.shiftKey ? CPShiftKeyMask : 0) | (aDOMEvent.ctrlKey ? CPControlKeyMask : 0) | (aDOMEvent.altKey ? CPAlternateKeyMask : 0) | (aDOMEvent.metaKey ? CPCommandKeyMask : 0); // With a few exceptions, all key events are blocked from propagating to // the browser. Here the following exceptions are being allowed: // // - All keys pressed along with a ctrl or cmd key _unless_ they are in // one of the two blacklists. // - Any key listed in the whitelist. // // The ctrl/cmd keys are used for browser hotkeys as are the keys listed in // the whitelist (F1-F12 at the time of writing). // // If a key is listed in both the blacklist and whitelist, the blacklist is // checked first. The key will be blocked from propagating in that case. StopDOMEventPropagation = YES; // Make sure it is not in the blacklists. if (!(CharacterKeysToPrevent[String.fromCharCode(aDOMEvent.keyCode || aDOMEvent.charCode).toLowerCase()] || KeyCodesToPrevent[aDOMEvent.keyCode])) { // It is not in the blacklist, let it through if the ctrl/cmd key is // also down or it's in the whitelist. if ((modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) || KeyCodesToAllow[aDOMEvent.keyCode]) StopDOMEventPropagation = NO; } var overrideCharacters = nil, charactersIgnoringModifiers = @""; switch (aDOMEvent.type) { case "keydown": // Grab and store the keycode now since it is correct and consistent at this point. if (aDOMEvent.keyCode in MozKeyCodeToKeyCodeMap) _keyCode = MozKeyCodeToKeyCodeMap[aDOMEvent.keyCode]; else _keyCode = aDOMEvent.keyCode; var characters; // Handle key codes for which String.fromCharCode won't work. // Refs #1036: In Internet Explorer, both 'which' and 'charCode' are undefined for special keys. if (aDOMEvent.which === 0 || aDOMEvent.charCode === 0 || (aDOMEvent.which === undefined && aDOMEvent.charCode === undefined)) characters = KeyCodesToUnicodeMap[_keyCode]; if (!characters) characters = String.fromCharCode(_keyCode).toLowerCase(); overrideCharacters = (modifierFlags & CPShiftKeyMask || _capsLockActive) ? characters.toUpperCase() : characters; // check for caps lock state if (_keyCode === CPKeyCodes.CAPS_LOCK) _capsLockActive = YES; if ([ModifierKeyCodes containsObject:_keyCode]) { // A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break. event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:_keyCode]; break; } else if (modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) { //we are simply going to skip all keypress events that use cmd/ctrl key //this lets us be consistent in all browsers and send on the keydown //which means we can cancel the event early enough, but only if sendEvent needs to } else if (CPKeyCodes.firesKeyPressEvent(_keyCode, _lastKey, aDOMEvent.shiftKey, aDOMEvent.ctrlKey, aDOMEvent.altKey)) { // this branch is taken by events which fire keydown, keypress, and keyup. // this is the only time we'll ALLOW character keys to propagate (needed for text fields) StopDOMEventPropagation = NO; break; } else { //this branch is taken by "remedial" key events // In this state we continue to keypress and send the CPEvent } case "keypress": // we unconditionally break on keypress events with modifiers, // because we forced the event to be sent on the keydown if (aDOMEvent.type === "keypress" && (modifierFlags & (CPControlKeyMask | CPCommandKeyMask))) break; var keyCode = _keyCode, charCode = aDOMEvent.keyCode || aDOMEvent.charCode, isARepeat = (_charCodes[keyCode] != nil); _lastKey = keyCode; _charCodes[keyCode] = charCode; var characters = overrideCharacters; // Is this a special key? if (!characters && (aDOMEvent.which === 0 || aDOMEvent.charCode === 0)) characters = KeyCodesToUnicodeMap[charCode]; if (!characters) characters = String.fromCharCode(charCode); charactersIgnoringModifiers = characters.toLowerCase(); // FIXME: This isn't correct. It SHOULD include Shift. // Safari won't send proper capitalization during cmd-key events if (!overrideCharacters && (modifierFlags & CPCommandKeyMask) && ((modifierFlags & CPShiftKeyMask) || _capsLockActive)) characters = characters.toUpperCase(); event = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:charCode]; break; case "keyup": var keyCode = aDOMEvent.keyCode, charCode = _charCodes[keyCode]; _keyCode = -1; _lastKey = -1; _charCodes[keyCode] = nil; // check for caps lock state if (keyCode === CPKeyCodes.CAPS_LOCK) _capsLockActive = NO; if ([ModifierKeyCodes containsObject:keyCode]) { // A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break. event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:_keyCode]; break; } var characters = KeyCodesToUnicodeMap[charCode] || String.fromCharCode(charCode); charactersIgnoringModifiers = characters.toLowerCase(); if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive) characters = charactersIgnoringModifiers; event = [CPEvent keyEventWithType:CPKeyUp location:location modifierFlags:modifierFlags timestamp: timestamp windowNumber:windowNumber context:nil characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode]; break; } if (event) event._DOMEvent = aDOMEvent; [_platformPasteboard windowMaySendKeyEvent:event]; if (event && ![_platformPasteboard windowShouldSuppressKeyEvent]) { [CPApp sendEvent:event]; [_platformPasteboard windowDidSendKeyEvent:event]; } var didStop = NO; // Platform pasteboard can overrule the decision to stop propagation either way, or it might have no opinion. if ([_platformPasteboard windowShouldStopPropagation] || (StopDOMEventPropagation && ![_platformPasteboard windowShouldNotStopPropagation])) { didStop = YES; _CPDOMEventStop(aDOMEvent, self); } [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; return !didStop; } - (void)scrollEvent:(DOMEvent)aDOMEvent { if (PreventScroll) { PreventScroll = false; aDOMEvent.preventDefault(); } if (_hideDOMScrollingElementTimeout) { clearTimeout(_hideDOMScrollingElementTimeout); _hideDOMScrollingElementTimeout = nil; } if (!aDOMEvent) aDOMEvent = window.event; var location = nil; if (CPFeatureIsCompatible(CPJavaScriptMouseWheelValues_8_15)) { var x = aDOMEvent._offsetX || 0.0, y = aDOMEvent._offsetY || 0.0, element = aDOMEvent.target; while (element.nodeType !== 1) element = element.parentNode; if (element.offsetParent) { do { x += element.offsetLeft; y += element.offsetTop; } while (element = element.offsetParent); } location = CGPointMake((x + ((aDOMEvent.clientX - 8) / 15)), (y + ((aDOMEvent.clientY - 8) / 15))); } else if (aDOMEvent._overrideLocation) location = aDOMEvent._overrideLocation; else location = CGPointMake(aDOMEvent.clientX, aDOMEvent.clientY); var deltaX = 0.0, deltaY = 0.0, windowNumber = 0, timestamp = [CPEvent currentTimestamp], modifierFlags = (aDOMEvent.shiftKey ? CPShiftKeyMask : 0) | (aDOMEvent.ctrlKey ? CPControlKeyMask : 0) | (aDOMEvent.altKey ? CPAlternateKeyMask : 0) | (aDOMEvent.metaKey ? CPCommandKeyMask : 0); // Show the dom element _DOMScrollingElement.style.visibility = "visible"; _DOMScrollingElement.style.top = (location.y - 15) + @"px"; _DOMScrollingElement.style.left = (location.x - 15) + @"px"; // We let the browser handle the scrolling StopDOMEventPropagation = NO; var theWindow = [self hitTest:location]; if (!theWindow) return; var windowNumber = [theWindow windowNumber]; location = [theWindow convertBridgeToBase:location]; var event = [CPEvent mouseEventWithType:CPScrollWheel location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:-1 clickCount:1 pressure:0]; event._DOMEvent = aDOMEvent; // We lag 1 event behind without this approach window.requestAnimationFrame(function() { if (aDOMEvent.deltaMode !== undefined && aDOMEvent.deltaMode !== 0) { event._hasPreciseScrollingDeltas = NO; event._scrollingDeltaX = aDOMEvent.deltaX; event._scrollingDeltaY = aDOMEvent.deltaY; event._deltaX = aDOMEvent.deltaX * SCROLLWHEEL_LINE_PIXELS; event._deltaY = aDOMEvent.deltaY * SCROLLWHEEL_LINE_PIXELS; } else { event._hasPreciseScrollingDeltas = YES; event._scrollingDeltaX = (_DOMScrollingElement.scrollLeft - 150) || aDOMEvent.deltaX || 0; event._scrollingDeltaY = (_DOMScrollingElement.scrollTop - 150) || aDOMEvent.deltaY || 0; event._deltaX = event._scrollingDeltaX; event._deltaY = event._scrollingDeltaY; } // If we scroll super with momentum, // there are so many events going off that // a tiny percent don't actually have any deltas. // // This does *not* make scrolling appear sluggish, // it just seems like that is something that happens. // // We get free performance boost if we skip sending these events, // as sending a scroll event with no deltas doesn't do anything. if (event._deltaX || event._deltaY) [CPApp sendEvent:event]; // We set StopDOMEventPropagation = NO on line 1008 //if (StopDOMEventPropagation) // _CPDOMEventStop(aDOMEvent, self); // Reset the DOM elements scroll offset _DOMScrollingElement.scrollLeft = 150; _DOMScrollingElement.scrollTop = 150; // this is needed to prevent flickering during scrolling [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; }); // We hide the dom element after a little bit // so that other DOM elements such as inputs // can receive events. _hideDOMScrollingElementTimeout = setTimeout(function() { PreventScroll = true; _DOMScrollingElement.style.visibility = "hidden"; }, 300); } - (void)resizeEvent:(DOMEvent)aDOMEvent { // This is a hack for the browser resize bug in safari. // See bug ID: 1325 // https://github.com/cappuccino/cappuccino/issues/1325 // Addendum by Antoine Mercadal : I also noticed that reszing is causing // problem under latest Firefox 13.0. Let's just use this hack // for all browser now. [resizeTimer invalidate]; resizeTimer = [CPTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(_actualResizeEvent) userInfo:nil repeats:NO]; } - (void)_actualResizeEvent { _shouldUpdateContentRect = NO; resizeTimer = nil; // FIXME: This is not the right way to do this. // We should pay attention to mouse down and mouse up in conjunction with this. //window.liveResize = YES; if ([CPPlatform isBrowser]) [CPApp._activeMenu cancelTracking]; var oldSize = [self contentRect].size; [self updateFromNativeContentRect]; var levels = _windowLevels, layers = _windowLayers, levelCount = levels.length; while (levelCount--) { var windows = [layers objectForKey:levels[levelCount]]._windows, windowCount = windows.length; while (windowCount--) [windows[windowCount] resizeWithOldPlatformWindowSize:oldSize]; } [[CPNotificationCenter defaultCenter] postNotificationName:CPApplicationDidChangeScreenParametersNotification object:CPApp userInfo:nil]; //window.liveResize = NO; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; _shouldUpdateContentRect = YES; } /*! @ignore */ - (void)blurEvent:(DOMEvent)aDOMEvent { if ([CPApp keyWindow] == _currentKeyWindow) [_currentKeyWindow resignKeyWindow]; if ([CPApp mainWindow] == _currentMainWindow) [_currentMainWindow resignMainWindow]; _previousKeyWindow = aDOMEvent ? _currentKeyWindow : nil; _previousMainWindow = aDOMEvent ? _currentMainWindow : nil; [blurTimer invalidate]; blurTimer = [CPTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(_blurEventTimer:) userInfo:nil repeats:NO]; } /*! @ignore */ - (void)_blurEventTimer:(CPTimer)aTimer { if (![CPApp mainWindow]) [[CPApplication sharedApplication] deactivate]; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; } /*! @ignore */ - (void)focusEvent:(DOMEvent)aDOMEvent { [blurTimer invalidate]; [CPApp activateIgnoringOtherApps:YES]; var keyWindow = _previousKeyWindow; if (!keyWindow) keyWindow = [[[_windowLayers objectForKey:[_windowLevels firstObject]] orderedWindows] firstObject]; if (!keyWindow) return; [self _makeKeyWindow:keyWindow]; if ([keyWindow isKeyWindow] && ([keyWindow firstResponder] === keyWindow || ![keyWindow firstResponder])) [keyWindow makeFirstResponder:[keyWindow initialFirstResponder]]; [self _makeMainWindow:keyWindow]; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; _previousKeyWindow = nil; _previousMainWindow = nil; } /*! @ignore */ - (void)_makeKeyWindow:(CPWindow)aWindow { if ([CPApp keyWindow] === aWindow || ![aWindow canBecomeKeyWindow]) return; [[CPApp keyWindow] resignKeyWindow]; [aWindow becomeKeyWindow]; } /*! @ignore */ - (void)_makeMainWindow:(CPWindow)aWindow { // Sheets cannot be main. Their parent window becomes main. if (aWindow._isSheet) { [self _makeMainWindow:aWindow._parentView]; return; } if ([CPApp mainWindow] === aWindow || ![aWindow canBecomeMainWindow]) return; [[CPApp mainWindow] resignMainWindow]; [aWindow becomeMainWindow]; } - (void)touchEvent:(DOMEvent)aDOMEvent { if (aDOMEvent.touches && (aDOMEvent.touches.length == 1 || (aDOMEvent.touches.length == 0 && aDOMEvent.changedTouches.length == 1))) { var newEvent = {}; switch (aDOMEvent.type) { case CPDOMEventTouchStart: newEvent.type = CPDOMEventMouseDown; break; case CPDOMEventTouchEnd: newEvent.type = CPDOMEventMouseUp; break; case CPDOMEventTouchMove: newEvent.type = CPDOMEventMouseMoved; break; case CPDOMEventTouchCancel: newEvent.type = CPDOMEventMouseUp; break; } var touch = aDOMEvent.touches.length ? aDOMEvent.touches[0] : aDOMEvent.changedTouches[0]; newEvent.clientX = touch.clientX; /* Normally the document can't scroll in Cappuccino: our body element has top:0 and bottom:0 with absolute positioning. So it should always be exactly the height of the viewport. The below handles a special case. iOS scrolls the document when the virtual keyboard is present and it needs to move a text input upwards visually to avoid covering the input with the keyboard. For most purposes we can ignore this, except here. In theory I think we could always apply this (scrollTop should always be 0 on every other device and situation) but let's be defensive and only apply it for touch events to minimise the risk of surprises. */ newEvent.clientY = _DOMWindow.document.body.scrollTop + touch.clientY; newEvent.timestamp = [CPEvent currentTimestamp]; newEvent.target = aDOMEvent.target; newEvent.shiftKey = newEvent.ctrlKey = newEvent.altKey = newEvent.metaKey = false; newEvent.preventDefault = function() { if (aDOMEvent.preventDefault) aDOMEvent.preventDefault() }; newEvent.stopPropagation = function() { if (aDOMEvent.stopPropagation) aDOMEvent.stopPropagation() }; [self mouseEvent:newEvent]; return; } else { if (aDOMEvent.preventDefault) aDOMEvent.preventDefault(); if (aDOMEvent.stopPropagation) aDOMEvent.stopPropagation(); } // handle touch cases specifically } - (void)mouseEvent:(DOMEvent)aDOMEvent { var type = _overriddenEventType || aDOMEvent.type; // IE's event order is down, up, up, dblclick, so we have create these events artificially. if (type === @"dblclick") { _overriddenEventType = CPDOMEventMouseDown; [self mouseEvent:aDOMEvent]; _overriddenEventType = CPDOMEventMouseUp; [self mouseEvent:aDOMEvent]; _overriddenEventType = nil; return; } var event, location = CGPointMake(aDOMEvent.clientX, aDOMEvent.clientY), timestamp = [CPEvent currentTimestamp], sourceElement = (aDOMEvent.target || aDOMEvent.srcElement), windowNumber = 0, modifierFlags = (aDOMEvent.shiftKey ? CPShiftKeyMask : 0) | (aDOMEvent.ctrlKey ? CPControlKeyMask : 0) | (aDOMEvent.altKey ? CPAlternateKeyMask : 0) | (aDOMEvent.metaKey ? CPCommandKeyMask : 0); StopDOMEventPropagation = YES; if (_mouseDownWindow) windowNumber = [_mouseDownWindow windowNumber]; else { var theWindow = [self _mouseHitTest:location]; if ((aDOMEvent.type === CPDOMEventMouseDown) && theWindow) _mouseDownWindow = theWindow; windowNumber = [theWindow windowNumber]; } if (windowNumber) location = [CPApp._windows[windowNumber] convertPlatformWindowToBase:location]; if (type === "mouseup") { if (_mouseIsDown) { if (aDOMEvent.button !== _firstMouseDownButton) return; event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp, location, modifierFlags, timestamp, windowNumber, nil, -1, CPDOMEventGetClickCount(_lastMouseUp, timestamp, location), 0, nil); _mouseIsDown = NO; _lastMouseUp = event; _mouseDownWindow = nil; _mouseDownIsRightClick = NO; _firstMouseDownButton = -1; } if (_DOMEventMode) { _DOMEventMode = NO; return; } } else if (type === "mousedown") { // If we receive a click event, then we invalidate any scheduled // or visible tooltips [_CPToolTip invalidateCurrentToolTipIfNeeded]; var button = aDOMEvent.button; _mouseDownIsRightClick = button == 2 || (CPBrowserIsOperatingSystem(CPMacOperatingSystem) && button == 0 && modifierFlags & CPControlKeyMask); // If mouse is already down, that means that a second mouse button is pushed. This could interfere in mouse events treatment. Just ignore it. // BUT we have to track which button will be first released. if (_mouseIsDown) { _mouseDownIsRightClick = !_mouseDownIsRightClick; return; } _firstMouseDownButton = button; if ((sourceElement.tagName === "INPUT" || sourceElement.tagName === "TEXTAREA") && sourceElement != _DOMFocusElement) { if ([CPPlatform supportsDragAndDrop]) { _DOMBodyElement.setAttribute("draggable", "false"); _DOMBodyElement.style["-khtml-user-drag"] = "none"; } _DOMEventMode = YES; _mouseIsDown = YES; // Fake a down and up event so that event tracking mode will work correctly [CPApp sendEvent:[CPEvent mouseEventWithType:_mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:-1 clickCount:CPDOMEventGetClickCount(_lastMouseDown, timestamp, location) pressure:0]]; [CPApp sendEvent:[CPEvent mouseEventWithType:_mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:-1 clickCount:CPDOMEventGetClickCount(_lastMouseDown, timestamp, location) pressure:0]]; return; } else if ([CPPlatform supportsDragAndDrop]) { _DOMBodyElement.setAttribute("draggable", "true"); _DOMBodyElement.style["-khtml-user-drag"] = "element"; } StopContextMenuDOMEventPropagation = YES; event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown, location, modifierFlags, timestamp, windowNumber, nil, -1, CPDOMEventGetClickCount(_lastMouseDown, timestamp, location), 0, nil); _mouseIsDown = YES; _lastMouseDown = event; } else // if (type === "mousemove" || type === "drag") { if (_DOMEventMode) return; // _lastMouseEventLocation might be nil on the very first mousemove event. Just send in the current location // in this case - this will result in a delta x and delta y of 0 which seems natural for the first event. event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseIsDown ? (_mouseDownIsRightClick ? CPRightMouseDragged : CPLeftMouseDragged) : CPMouseMoved, location, modifierFlags, timestamp, windowNumber, nil, -1, 1, 0, _lastMouseEventLocation || location); } var isDragging = [[CPDragServer sharedDragServer] isDragging]; if (event && (!isDragging || !supportsNativeDragAndDrop)) { event._DOMEvent = aDOMEvent; [CPApp sendEvent:event]; } var didStop = NO; if (StopDOMEventPropagation && (!supportsNativeDragAndDrop || type !== "mousedown" && !isDragging)) { didStop = YES; _CPDOMEventStop(aDOMEvent, self); } // If there are any tracking event listeners (listening for CPLeftMouseDraggedMask) // then show the event guard so we don't lose events to iframes var hasTrackingEventListener = NO; for (var i = 0; i < CPApp._eventListeners.length; i++) { var listener = CPApp._eventListeners[i]; if (listener._callback !== _CPRunModalLoop && (listener._mask & CPLeftMouseDraggedMask)) { hasTrackingEventListener = YES; break; } } _lastMouseEventLocation = location; _DOMEventGuard.style.display = hasTrackingEventListener ? "" : "none"; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; return !didStop; } - (void)contextMenuEvent:(DOMEvent)aDOMEvent { if (StopContextMenuDOMEventPropagation) _CPDOMEventStop(aDOMEvent, self); return !StopContextMenuDOMEventPropagation; } - (CPArray)orderedWindowsAtLevel:(int)aLevel { var layer = [self layerAtLevel:aLevel create:NO]; if (!layer) return []; return [layer orderedWindows]; } - (CPDOMWindowLayer)layerAtLevel:(int)aLevel create:(BOOL)aFlag { var layer = [_windowLayers objectForKey:aLevel]; // If the layer doesn't currently exist, and the create flag is true, // create the layer. if (!layer && aFlag) { layer = [[CPDOMWindowLayer alloc] initWithLevel:aLevel]; [_windowLayers setObject:layer forKey:aLevel]; // Find the nearest layer. This is similar to a binary search, // only we know we won't find the value. var low = 0, high = _windowLevels.length - 1, middle; while (low <= high) { middle = FLOOR((low + high) / 2); if (_windowLevels[middle] > aLevel) high = middle - 1; else low = middle + 1; } var insertionIndex = 0; if (middle !== undefined) insertionIndex = _windowLevels[middle] > aLevel ? middle : middle + 1 [_windowLevels insertObject:aLevel atIndex:insertionIndex]; layer._DOMElement.style.zIndex = aLevel + 1; // adding one avoids negative zIndices. These have been causing issues in Chrome _DOMBodyElement.appendChild(layer._DOMElement); } return layer; } - (void)order:(CPWindowOrderingMode)orderingMode window:(CPWindow)aWindow relativeTo:(CPWindow)otherWindow { if (!_DOMWindow) return; [CPPlatform initializeScreenIfNecessary]; // Grab the appropriate level for the layer, and create it if // necessary (if we are not simply removing the window). var layer = [self layerAtLevel:[aWindow level] create:orderingMode !== CPWindowOut]; // When ordering out, ignore otherWindow, simply remove aWindow from its level. // If layer is nil, this will be a no-op. if (orderingMode === CPWindowOut) { [aWindow _windowWillBeRemovedFromTheDOM]; return [layer removeWindow:aWindow]; } /* If aWindow is a child of otherWindow and is not yet visible, aWindow must actually be ordered relative to: - otherWindow's last child which is not aWindow, or - the furthest parent of aWindow whichever is frontmost (orderingMode === CPWindowAbove) or rearmost (orderingMode === CPWindowBelow). */ if (![aWindow isVisible] && otherWindow && [aWindow parentWindow] === otherWindow) { var children = [otherWindow childWindows], lastChild = [children lastObject]; if (lastChild === aWindow) { if ([children count] > 1) otherWindow = [children objectAtIndex:[children count] - 2]; } else if (lastChild) otherWindow = lastChild; var furthestParent = [self _furthestParentOf:otherWindow]; if ((orderingMode === CPWindowAbove && furthestParent._index > otherWindow._index) || (orderingMode === CPWindowBelow && furthestParent._index < otherWindow._index)) otherWindow = furthestParent; } /* If a child window is ordered front, the furthest parent is actually the one that is ordered front, and all of the descendent children are ordered after it. */ else if (orderingMode === CPWindowAbove && !otherWindow) aWindow = [self _furthestParentOf:aWindow]; var insertionIndex = CPNotFound; if (otherWindow) insertionIndex = orderingMode === CPWindowAbove ? otherWindow._index + 1 : otherWindow._index; [aWindow _windowWillBeAddedToTheDOM]; // Place the window at the appropriate index. [layer insertWindow:aWindow atIndex:insertionIndex]; // If aWindow is a parent, recursively order all of its children after it if ([[aWindow childWindows] count]) [self _orderChildWindowsOf:aWindow furthestParent:[self _furthestParentOf:aWindow] layer:layer]; [aWindow _setHasBeenOrderedIn:YES]; } - (CPWindow)_furthestParentOf:(CPWindow)aWindow { var parent; while ((parent = [aWindow parentWindow])) aWindow = parent; return aWindow; } - (void)_orderChildWindowsOf:(CPWindow)aWindow furthestParent:(CPWindow)furthestParent layer:(CPDOMWindowLayer)aLayer { // When a parent window is ordered, Cocoa orders its child windows // relative to it or the furthest parent. var children = [aWindow childWindows], count = [children count], parent = aWindow, parentLevel = [parent level]; for (var i = 0; i < count; ++i) { var child = children[i], childWasVisible = [child isVisible]; // If a child is not visible and has not yet been ordered in, skip it if (!childWasVisible && ![child _hasBeenOrderedIn]) continue; // If a user moved level of the child window, we should respect that if ([child level] !== parentLevel) continue; var ordering = [child _childOrdering]; if ((ordering === CPWindowAbove && furthestParent._index > parent._index) || (ordering === CPWindowBelow && furthestParent._index < parent._index)) parent = furthestParent; var index = ordering === CPWindowAbove ? parent._index + 1 : parent._index; if (!childWasVisible) [child _windowWillBeAddedToTheDOM]; [aLayer insertWindow:child atIndex:index]; if (!childWasVisible) [child _parentDidOrderInChild]; if ([[child childWindows] count]) [self _orderChildWindowsOf:child furthestParent:furthestParent layer:aLayer]; parent = child; } } - (void)_removeLayers { var levels = _windowLevels, layers = _windowLayers, levelCount = levels.length; while (levelCount--) { var layer = [layers objectForKey:levels[levelCount]]; _DOMBodyElement.removeChild(layer._DOMElement); } } - (void)_addLayers { var levels = _windowLevels, layers = _windowLayers, levelCount = levels.length; while (levelCount--) { var layer = [layers objectForKey:levels[levelCount]]; _DOMBodyElement.appendChild(layer._DOMElement); } } /* @ignore */ - (id)_dragHitTest:(CGPoint)aPoint pasteboard:(CPPasteboard)aPasteboard { var levels = _windowLevels, layers = _windowLayers, levelCount = levels.length; while (levelCount--) { // Skip any windows above or at the dragging level. if (levels[levelCount] >= CPDraggingWindowLevel) continue; var windows = [layers objectForKey:levels[levelCount]]._windows, windowCount = windows.length; while (windowCount--) { var theWindow = windows[windowCount]; if ([theWindow _sharesChromeWithPlatformWindow]) return [theWindow _dragHitTest:aPoint pasteboard:aPasteboard]; if ([theWindow containsPoint:aPoint]) return [theWindow _dragHitTest:aPoint pasteboard:aPasteboard]; } } return nil; } /* @ignore */ - (void)_propagateCurrentDOMEvent:(BOOL)aFlag { StopDOMEventPropagation = !aFlag; } - (BOOL)_willPropagateCurrentDOMEvent { return !StopDOMEventPropagation; } - (void)_propagateContextMenuDOMEvent:(BOOL)aFlag { if (aFlag && CPBrowserIsEngine(CPGeckoBrowserEngine)) StopDOMEventPropagation = !aFlag; StopContextMenuDOMEventPropagation = !aFlag; } - (BOOL)_willPropagateContextMenuDOMEvent { return StopContextMenuDOMEventPropagation; } - (CPWindow)_mouseHitTest:(CGPoint)location { return [self _hitTest:location withTest:@selector(_isValidMousePoint:)] } - (CPWindow)hitTest:(CGPoint)location { return [self _hitTest:location withTest:@selector(containsPoint:)] } - (CPWindow)_hitTest:(CGPoint)location withTest:(SEL)aTest { if (self._only) return self._only; var levels = _windowLevels, layers = _windowLayers, levelCount = levels.length, theWindow = nil; while (levelCount-- && !theWindow) { var windows = [layers objectForKey:levels[levelCount]]._windows, windowCount = windows.length; while (windowCount-- && !theWindow) { var candidateWindow = windows[windowCount]; if (!candidateWindow._ignoresMouseEvents && [candidateWindow performSelector:aTest withObject:location]) theWindow = candidateWindow; } } return theWindow; } /*! @ignore Return the selected text in the DOM window if known. */ - (CPString)_selectedText { if (_DOMWindow.getSelection) return "" + _DOMWindow.getSelection(); else if (_DOMWindow.document.getSelection) return "" + _DOMWindow.document.getSelection(); else if (_DOMWindow.selection) return "" + _DOMWindow.selection.createRange().text; else return nil; } /*! Set the text selection range to the given range within the given element, which must be a child of this DOM window. */ - (void)setSelectedRange:(CPRange)aRange inElement:(DOMElement)anElement { if (_DOMWindow.getSelection()) { var domRange = _DOMWindow.document.createRange(); domRange.setStart(anElement.childNodes[0], aRange.location); domRange.setEnd(anElement.childNodes[0], CPMaxRange(aRange)); _DOMWindow.getSelection().removeAllRanges(); _DOMWindow.getSelection().addRange(domRange); } else if (_DOMWindow.document.selection) { var domRange = _DOMWindow.document.body.createTextRange(); domRange.moveToElementText(anElement); domRange.collapse(true); domRange.moveStart('character', aRange.location); domRange.moveEnd('character', aRange.length); domRange.select(); } } /*! When using command (mac) or control (windows), keys are propagated to the browser by default. To prevent a character key from propagating (to prevent its default action, and instead use it in your own application), use these methods. These methods are additive -- the list builds until you clear it. @param characters a list of characters to stop propagating keypresses to the browser. */ + (void)preventCharacterKeysFromPropagating:(CPArray)characters { for (var i = characters.length; i > 0; i--) CharacterKeysToPrevent["" + characters[i - 1].toLowerCase()] = YES; } /*! @param character a character to stop propagating keypresses to the browser. */ + (void)preventCharacterKeyFromPropagating:(CPString)character { CharacterKeysToPrevent[character.toLowerCase()] = YES; } /*! Clear the list of characters for which we are not sending keypresses to the browser. */ + (void)clearCharacterKeysToPreventFromPropagating { CharacterKeysToPrevent = {}; } /*! Prevent these keyCodes from sending their keypresses to the browser. @param keyCodes an array of keycodes to prevent propagation. */ + (void)preventKeyCodesFromPropagating:(CPArray)keyCodes { for (var i = keyCodes.length; i > 0; i--) KeyCodesToPrevent[keyCodes[i - 1]] = YES; } /*! Prevent this keyCode from sending its key events to the browser. @param keyCode a keycode to prevent propagation. */ + (void)preventKeyCodeFromPropagating:(CPString)keyCode { KeyCodesToPrevent[keyCode] = YES; } /*! Clear the list of keyCodes for which we are not sending keypresses to the browser. */ + (void)clearKeyCodesToPreventFromPropagating { KeyCodesToPrevent = {}; } - (void)_notifyPlatformWindowWillClose { [[CPNotificationCenter defaultCenter] postNotificationName:_CPPlatformWindowWillCloseNotification object:self userInfo:nil]; } @end #endif var CPEventClass = [CPEvent class]; var _CPEventFromNativeMouseEvent = function(aNativeEvent, anEventType, aPoint, modifierFlags, aTimestamp, aWindowNumber, aGraphicsContext, anEventNumber, aClickCount, aPressure, aMouseDragStart) { aNativeEvent.isa = CPEventClass; aNativeEvent._type = anEventType; aNativeEvent._location = aPoint; aNativeEvent._modifierFlags = modifierFlags; aNativeEvent._timestamp = aTimestamp; aNativeEvent._windowNumber = aWindowNumber; aNativeEvent._window = nil; aNativeEvent._context = aGraphicsContext; aNativeEvent._eventNumber = anEventNumber; aNativeEvent._clickCount = aClickCount; aNativeEvent._pressure = aPressure; if ((anEventType == CPLeftMouseDragged) || (anEventType == CPRightMouseDragged) || (anEventType == CPMouseMoved)) { aNativeEvent._deltaX = aPoint.x - aMouseDragStart.x; aNativeEvent._deltaY = aPoint.y - aMouseDragStart.y; } else { aNativeEvent._deltaX = 0; aNativeEvent._deltaY = 0; } return aNativeEvent; }; var CLICK_SPACE_DELTA = 5.0, CLICK_TIME_DELTA = (typeof document != "undefined" && document.addEventListener) ? 0.55 : 1.0; var CPDOMEventGetClickCount = function(aComparisonEvent, aTimestamp, aLocation) { if (!aComparisonEvent) return 1; var comparisonLocation = [aComparisonEvent locationInWindow]; return (aTimestamp - [aComparisonEvent timestamp] < CLICK_TIME_DELTA && ABS(comparisonLocation.x - aLocation.x) < CLICK_SPACE_DELTA && ABS(comparisonLocation.y - aLocation.y) < CLICK_SPACE_DELTA) ? [aComparisonEvent clickCount] + 1 : 1; }; // Global. _CPDOMEventStop = function(aDOMEvent, aPlatformWindow) { if (aDOMEvent.preventDefault) // W3C Model aDOMEvent.preventDefault(); else // IE Model aDOMEvent.returnValue = false; if (aDOMEvent.stopPropagation) // W3C Model aDOMEvent.stopPropagation(); else // IE Model aDOMEvent.cancelBubble = true; }; function CPWindowObjectList() { var platformWindows = [CPPlatformWindow visiblePlatformWindows], platformWindowEnumerator = [platformWindows objectEnumerator], platformWindow = nil, windowObjects = []; while ((platformWindow = [platformWindowEnumerator nextObject]) !== nil) { var levels = platformWindow._windowLevels, layers = platformWindow._windowLayers, levelCount = levels.length; while (levelCount--) { var windows = [layers objectForKey:levels[levelCount]]._windows, windowCount = windows.length; while (windowCount--) windowObjects.push(windows[windowCount]); } } return windowObjects; } function CPWindowList() { var windowObjectList = CPWindowObjectList(); return [windowObjectList arrayByApplyingBlock:function(windowObject) { return [windowObject windowNumber]; }]; }