From feed0fce0fc083eacfd949045f5dc95d6428502b Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Mon, 7 May 2012 19:11:23 -0700 Subject: [PATCH] pass tests in IE 6-10, FF/S/C (lots of cleanup needed) --- packages/liveui/liveevents.js | 87 ++++++++++++----- packages/liveui/liveevents_nonw3c.js | 110 +++++++++++++++++----- packages/liveui/liveui_tests.js | 6 +- packages/liveui/package.js | 2 +- packages/test-helpers/event_simulation.js | 6 +- 5 files changed, 159 insertions(+), 52 deletions(-) diff --git a/packages/liveui/liveevents.js b/packages/liveui/liveevents.js index 5f9f70f2c0..ba7e001d42 100644 --- a/packages/liveui/liveevents.js +++ b/packages/liveui/liveevents.js @@ -47,16 +47,29 @@ Meteor.ui = Meteor.ui || {}; return; } + var SIMULATE_FOCUS_BLUR = 1; + var SIMULATE_FOCUSIN_FOCUSOUT = 2; + + // If we have focusin/focusout, use them to simulate focus/blur. + // This has the nice effect of making focus/blur synchronous in IE 9+. + // It doesn't work in Firefox, which lacks focusin/focusout completely + // as of v11.0. This style of event support testing ('onfoo' in div) + // doesn't work in Firefox 3.6, but does in IE and WebKit. + var focusBlurMode = ('onfocusin' in document.createElement("DIV")) ? + SIMULATE_FOCUS_BLUR : SIMULATE_FOCUSIN_FOCUSOUT; + var prefix = '_liveevents_'; var universalCapturer = function(event) { var type = event.type; var bubbles = event.bubbles; var target = event.target; - console.log("captured ", type, " on ", target.nodeName); target.addEventListener(type, universalHandler, false); + // according to the DOM event spec, ancestors for bubbling + // purposes are determined at dispatch time (and ignore changes + // to the DOM after that) var ancestors; if (bubbles) { ancestors = []; @@ -83,22 +96,58 @@ Meteor.ui = Meteor.ui || {}; target.dispatchEvent(event); }; - var universalHandler = function(event) { - if (event.type === 'focus') - console.log(event.target.nodeName); - // fire synthetic focusin/focusout on blur/focus + var doHandleHacks = function(event) { + // fire synthetic focusin/focusout on blur/focus or vice versa if (event.currentTarget === event.target) { - if (event.type === 'focus') - sendUIEvent('focusin', event.target, true); - else if (event.type === 'blur') - sendUIEvent('focusout', event.target, true); + if (focusBlurMode === SIMULATE_FOCUS_BLUR) { + if (event.type === 'focusin') + sendUIEvent('focus', event.target, false); + else if (event.type === 'focusout') + sendUIEvent('blur', event.target, false); + } else { // SIMULATE_FOCUSIN_FOCUSOUT + if (event.type === 'focus') + sendUIEvent('focusin', event.target, true); + else if (event.type === 'blur') + sendUIEvent('focusout', event.target, true); + } } - // only respond to synthetic focusin/focusout - if (event.type === 'focusin' || event.type === 'focusout') { - if (! event.synthetic) - return; + // only respond to synthetic events of the types we are faking + if (focusBlurMode === SIMULATE_FOCUS_BLUR) { + if (event.type === 'focus' || event.type === 'blur') { + if (! event.synthetic) + return false; + } + } else { // SIMULATE_FOCUSIN_FOCUSOUT + if (event.type === 'focusin' || event.type === 'focusout') { + if (! event.synthetic) + return false; + } } + return true; + }; + + var doInstallHacks = function(node, eventType) { + // install handlers for the events used to fake events of this type, + // in addition to handlers for the real type + if (focusBlurMode === SIMULATE_FOCUS_BLUR) { + if (eventType === 'focus') + Meteor.ui._installLiveHandler(node, 'focusin'); + else if (eventType === 'blur') + Meteor.ui._installLiveHandler(node, 'focusout'); + } else { // SIMULATE_FOCUSIN_FOCUSOUT + if (eventType === 'focusin') + Meteor.ui._installLiveHandler(node, 'focus'); + else if (eventType === 'focusout') + Meteor.ui._installLiveHandler(node, 'blur'); + } + + }; + + var universalHandler = function(event) { + if (doHandleHacks(event) === false) + return; + var curNode = event.currentTarget; if (! curNode) return; @@ -151,12 +200,7 @@ Meteor.ui = Meteor.ui || {}; }; Meteor.ui._installLiveHandler = function(node, eventType) { - // we completely fake focusin / focusout in the focus/blur handler - // because Firefox doesn't support them natively. - if (eventType === 'focusin') - Meteor.ui._installLiveHandler(node, 'focus'); - else if (eventType === 'focusout') - Meteor.ui._installLiveHandler(node, 'blur'); + doInstallHacks(node, eventType); var propName = prefix + eventType; if (! document[propName]) { @@ -165,11 +209,6 @@ Meteor.ui = Meteor.ui || {}; document.addEventListener(eventType, universalCapturer, true); } - // XXXX - //_.each(document.getElementsByTagName("*"), function(elem) { - //elem.addEventListener('focus', universalHandler, false); - //elem.addEventListener('blur', universalHandler, false); - //}); }; ////// WHAT WE SHOULD ACTUALLY DO diff --git a/packages/liveui/liveevents_nonw3c.js b/packages/liveui/liveevents_nonw3c.js index 1d3b2a4e19..e6c682571c 100644 --- a/packages/liveui/liveevents_nonw3c.js +++ b/packages/liveui/liveevents_nonw3c.js @@ -3,28 +3,38 @@ Meteor.ui._loadNonW3CEvents = function() { - var prefix = '_liveevents_'; + var installOne = function(node, prop) { + // install handlers for faking focus/blur if necessary + if (prop === 'onfocus') + installOne(node, 'onfocusin'); + else if (prop === 'onblur') + installOne(node, 'onfocusout'); + // install handlers for faking bubbling change/submit + else if (prop === 'onchange') { + installOne(node, 'oncellchange'); + if (node.nodeName === 'INPUT' && + (node.type === 'checkbox' || node.type === 'radio')) { + installOne(node, 'onpropertychange'); + return; + } + } else if (prop === 'onsubmit') + installOne(node, 'ondatasetcomplete'); - // use object property value so it doesn't show up in innerHTML - var TRUE = {}; - - var installOneHandler = function(node, eventType) { - var propName = prefix + eventType; - if (! node[propName]) { - // only bind one event listener per type per node - node[propName] = TRUE; - node.attachEvent('on'+eventType, universalHandler); - } + node[prop] = universalHandler; }; Meteor.ui._installLiveHandler = function(node, eventType) { + // use old-school event binding, so that we can + // access the currentTarget as `this` in the handler. + var prop = 'on'+eventType; + if (node.nodeType === 1) { // ELEMENT - installOneHandler(node, eventType); + installOne(node, prop); var descendents = node.getElementsByTagName('*'); for(var i=0, N = descendents.length; i'; + }); + }, 0); + var LOG = function(str) { + document.getElementById('mylog').innerHTML += '
'+str; + };*/ + + var sendEvent = function(ontype, target) { + var e = document.createEventObject(); + e.synthetic = true; + target.fireEvent(ontype, e); + return e.returnValue; + }; + var universalHandler = function() { var event = window.event; var type = event.type; - event.target = event.srcElement || document; + var target = event.srcElement || document; + event.target = target; if (this.nodeType !== 1) return; // sanity check that we have a real target (always an element) event.currentTarget = this; var curNode = this; + // simulate focus/blur so that they are synchronous + if (curNode === target && ! event.synthetic) { + if (type === 'focusin') + sendEvent('onfocus', curNode); + else if (type === 'focusout') + sendEvent('onblur', curNode); + else if (type === 'change') + sendEvent('oncellchange', curNode); + else if (type === 'propertychange') { + if (event.propertyName === 'checked') + sendEvent('oncellchange', curNode); + } else if (type === 'submit') { + sendEvent('ondatasetcomplete', curNode); + } + } + // ignore non-simulated events of types we simulate + if ((type === 'focus' || event.type === 'blur' || event.type === 'change' || + event.type === 'submit') && ! event.synthetic) { + if (event.type === 'submit') + event.returnValue = false; // block all native submits, we will submit + return; + } + + if (type === 'cellchange' && event.synthetic) { + type = event.type = 'change'; + } + if (type === 'datasetcomplete' && event.synthetic) { + type = event.type = 'submit'; + } + var innerRange = Meteor.ui._LiveRange.findRange(Meteor.ui._tag, curNode); if (! innerRange) return; @@ -127,16 +186,9 @@ Meteor.ui._loadNonW3CEvents = function() { }; var resetOne = function(node) { - for(var k in node) { - if (! node[k]) - continue; - if (k.substring(0, prefix.length) !== prefix) - continue; - - var type = k.substring(prefix.length); - - node.detachEvent('on'+type, universalHandler); - } + for(var k in node) + if (node[k] === universalHandler) + node[k] = null; }; Meteor.ui._resetEvents = function(node) { @@ -150,4 +202,14 @@ Meteor.ui._loadNonW3CEvents = function() { } }; + // submit forms that aren't preventDefaulted + document.attachEvent('ondatasetcomplete', function() { + var evt = window.event; + var target = evt && evt.srcElement; + if (evt.synthetic && target && + target.nodeName === 'FORM' && + evt.returnValue !== false) + target.submit(); + }); + }; diff --git a/packages/liveui/liveui_tests.js b/packages/liveui/liveui_tests.js index 5d332e6c36..82c9db8145 100644 --- a/packages/liveui/liveui_tests.js +++ b/packages/liveui/liveui_tests.js @@ -1369,6 +1369,8 @@ testAsyncMulti("liveui - tricky events", [ return render_func(); }, { events: events, event_data: buf })); div.node().style.display = "block"; // make visible + div.node().style.height = 0; + div.node().style.overflow = 'hidden'; var getbuf = function() { var ret = buf.slice(); @@ -1433,7 +1435,7 @@ testAsyncMulti("liveui - tricky events", [ var focusBuf = tester.focus(); var blurBuf = tester.blur(); - tester.kill(); + tricky_events_kill_later(tester); return [focusBuf, blurBuf]; }; @@ -1442,12 +1444,12 @@ testAsyncMulti("liveui - tricky events", [ test.equal(focus_blur(textLevel1, 'focus input'), [['focus input'], []]); + // focus on second-level input // issue #108 test.equal(focus_blur(textLevel2,'focus input'), [['focus input'], []]); - // focusin test.equal(focus_blur(textLevel1, 'focusin input'), [['focusin input'], []]); diff --git a/packages/liveui/package.js b/packages/liveui/package.js index 01414ab4d9..5bc6b6c712 100644 --- a/packages/liveui/package.js +++ b/packages/liveui/package.js @@ -14,7 +14,7 @@ Package.on_use(function (api) { api.use('jquery'); api.add_files(['liverange.js', 'liveui.js', 'innerhtml.js', 'smartpatch.js', - 'liveevents.js', 'liveevents_nonw3c.js'], + 'liveevents_nonw3c.js', 'liveevents.js'], 'client'); }); diff --git a/packages/test-helpers/event_simulation.js b/packages/test-helpers/event_simulation.js index 376dada67c..da9a8d6610 100644 --- a/packages/test-helpers/event_simulation.js +++ b/packages/test-helpers/event_simulation.js @@ -14,8 +14,12 @@ var simulateEvent = function (node, event, args) { }; var focusElement = function(elem) { + // This sequence is for benefit of IE 8 and 9; + // test there before changing. + window.focus(); elem.focus(); - elem.focus(); // IE 8 seems to need a second call! + elem.focus(); + // focus() should set document.activeElement if (document.activeElement !== elem) throw new Error("focus() didn't set activeElement");