Created
May 20, 2014 20:12
-
-
Save jcmoore/38135abefad78dc41e79 to your computer and use it in GitHub Desktop.
polymer (over-)eager event triggering
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<script language="javascript" src="//cdnjs.cloudflare.com/ajax/libs/polymer/0.2.4/platform.js"></script> | |
<script language="javascript" src="//cdnjs.cloudflare.com/ajax/libs/polymer/0.2.4/polymer.js"></script> | |
<polymer-element name="mm-list"> | |
<template> | |
click this | |
</template> | |
<script> | |
Polymer('mm-list', { | |
ready: function() { | |
this.clickHandler = this.clickHandler.bind(this); | |
this.addEventListener("down", this.clickHandler); | |
}, | |
clickHandler: function(input) { | |
this.removeEventListener("down", this.clickHandler); | |
// clicking on the "click this" text should pop up the following confirm | |
// (affirmative input to which will result in an infinite loop) | |
if (confirm("loop?")) { | |
this.addEventListener("down", this.clickHandler); | |
} else { | |
setTimeout((function () { | |
this.addEventListener("down", this.clickHandler); | |
}).bind(this), 0); | |
}; | |
}, | |
}); | |
</script> | |
</polymer-element> | |
</head> | |
<body> | |
<mm-list></mm-list> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 The Polymer Authors. All rights reserved. | |
// Use of this source code is goverened by a BSD-style | |
// license that can be found in the LICENSE file. | |
(function(scope) { | |
'use strict'; | |
var forwardMethodsToWrapper = scope.forwardMethodsToWrapper; | |
var getTreeScope = scope.getTreeScope; | |
var mixin = scope.mixin; | |
var registerWrapper = scope.registerWrapper; | |
var unwrap = scope.unwrap; | |
var wrap = scope.wrap; | |
var wrappers = scope.wrappers; | |
var wrappedFuns = new WeakMap(); | |
var listenersTable = new WeakMap(); | |
var handledEventsTable = new WeakMap(); | |
var currentlyDispatchingEvents = new WeakMap(); | |
var targetTable = new WeakMap(); | |
var currentTargetTable = new WeakMap(); | |
var relatedTargetTable = new WeakMap(); | |
var eventPhaseTable = new WeakMap(); | |
var stopPropagationTable = new WeakMap(); | |
var stopImmediatePropagationTable = new WeakMap(); | |
var eventHandlersTable = new WeakMap(); | |
var eventPathTable = new WeakMap(); | |
function isShadowRoot(node) { | |
return node instanceof wrappers.ShadowRoot; | |
} | |
function rootOfNode(node) { | |
return getTreeScope(node).root; | |
} | |
// http://w3c.github.io/webcomponents/spec/shadow/#event-paths | |
function getEventPath(node, event) { | |
var path = []; | |
var current = node; | |
path.push(current); | |
while (current) { | |
// 4.1. | |
var destinationInsertionPoints = getDestinationInsertionPoints(current); | |
if (destinationInsertionPoints && destinationInsertionPoints.length > 0) { | |
// 4.1.1 | |
for (var i = 0; i < destinationInsertionPoints.length; i++) { | |
var insertionPoint = destinationInsertionPoints[i]; | |
// 4.1.1.1 | |
if (isShadowInsertionPoint(insertionPoint)) { | |
var shadowRoot = rootOfNode(insertionPoint); | |
// 4.1.1.1.2 | |
var olderShadowRoot = shadowRoot.olderShadowRoot; | |
if (olderShadowRoot) | |
path.push(olderShadowRoot); | |
} | |
// 4.1.1.2 | |
path.push(insertionPoint); | |
} | |
// 4.1.2 | |
current = destinationInsertionPoints[ | |
destinationInsertionPoints.length - 1]; | |
// 4.2 | |
} else { | |
if (isShadowRoot(current)) { | |
if (inSameTree(node, current) && eventMustBeStopped(event)) { | |
// Stop this algorithm | |
break; | |
} | |
current = current.host; | |
path.push(current); | |
// 4.2.2 | |
} else { | |
current = current.parentNode; | |
if (current) | |
path.push(current); | |
} | |
} | |
} | |
return path; | |
} | |
// http://w3c.github.io/webcomponents/spec/shadow/#dfn-events-always-stopped | |
function eventMustBeStopped(event) { | |
if (!event) | |
return false; | |
switch (event.type) { | |
case 'abort': | |
case 'error': | |
case 'select': | |
case 'change': | |
case 'load': | |
case 'reset': | |
case 'resize': | |
case 'scroll': | |
case 'selectstart': | |
return true; | |
} | |
return false; | |
} | |
// http://w3c.github.io/webcomponents/spec/shadow/#dfn-shadow-insertion-point | |
function isShadowInsertionPoint(node) { | |
return node instanceof HTMLShadowElement; | |
// and make sure that there are no shadow precing this? | |
// and that there is no content ancestor? | |
} | |
function getDestinationInsertionPoints(node) { | |
return scope.getDestinationInsertionPoints(node); | |
} | |
// http://w3c.github.io/webcomponents/spec/shadow/#event-retargeting | |
function eventRetargetting(path, currentTarget) { | |
if (path.length === 0) | |
return currentTarget; | |
// The currentTarget might be the window object. Use its document for the | |
// purpose of finding the retargetted node. | |
if (currentTarget instanceof wrappers.Window) | |
currentTarget = currentTarget.document; | |
var currentTargetTree = getTreeScope(currentTarget); | |
var originalTarget = path[0]; | |
var originalTargetTree = getTreeScope(originalTarget); | |
var relativeTargetTree = | |
lowestCommonInclusiveAncestor(currentTargetTree, originalTargetTree); | |
for (var i = 0; i < path.length; i++) { | |
var node = path[i]; | |
if (getTreeScope(node) === relativeTargetTree) | |
return node; | |
} | |
return path[path.length - 1]; | |
} | |
function getTreeScopeAncestors(treeScope) { | |
var ancestors = []; | |
for (;treeScope; treeScope = treeScope.parent) { | |
ancestors.push(treeScope); | |
} | |
return ancestors; | |
} | |
function lowestCommonInclusiveAncestor(tsA, tsB) { | |
var ancestorsA = getTreeScopeAncestors(tsA); | |
var ancestorsB = getTreeScopeAncestors(tsB); | |
var result = null; | |
while (ancestorsA.length > 0 && ancestorsB.length > 0) { | |
var a = ancestorsA.pop(); | |
var b = ancestorsB.pop(); | |
if (a === b) | |
result = a; | |
else | |
break; | |
} | |
return result; | |
} | |
function getTreeScopeRoot(ts) { | |
if (!ts.parent) | |
return ts; | |
return getTreeScopeRoot(ts.parent); | |
} | |
function relatedTargetResolution(event, currentTarget, relatedTarget) { | |
// In case the current target is a window use its document for the purpose | |
// of retargetting the related target. | |
if (currentTarget instanceof wrappers.Window) | |
currentTarget = currentTarget.document; | |
var currentTargetTree = getTreeScope(currentTarget); | |
var relatedTargetTree = getTreeScope(relatedTarget); | |
var relatedTargetEventPath = getEventPath(relatedTarget, event); | |
var lowestCommonAncestorTree; | |
// 4 | |
var lowestCommonAncestorTree = | |
lowestCommonInclusiveAncestor(currentTargetTree, relatedTargetTree); | |
// 5 | |
if (!lowestCommonAncestorTree) | |
lowestCommonAncestorTree = relatedTargetTree.root; | |
// 6 | |
for (var commonAncestorTree = lowestCommonAncestorTree; | |
commonAncestorTree; | |
commonAncestorTree = commonAncestorTree.parent) { | |
// 6.1 | |
var adjustedRelatedTarget; | |
for (var i = 0; i < relatedTargetEventPath.length; i++) { | |
var node = relatedTargetEventPath[i]; | |
if (getTreeScope(node) === commonAncestorTree) | |
return node; | |
} | |
} | |
return null; | |
} | |
function inSameTree(a, b) { | |
return getTreeScope(a) === getTreeScope(b); | |
} | |
var NONE = 0; | |
var CAPTURING_PHASE = 1; | |
var AT_TARGET = 2; | |
var BUBBLING_PHASE = 3; | |
// pendingError is used to rethrow the first error we got during an event | |
// dispatch. The browser actually reports all errors but to do that we would | |
// need to rethrow the error asynchronously. | |
var pendingError; | |
function dispatchOriginalEvent(originalEvent) { | |
// Make sure this event is only dispatched once. | |
if (handledEventsTable.get(originalEvent)) | |
return; | |
handledEventsTable.set(originalEvent, true); | |
dispatchEvent(wrap(originalEvent), wrap(originalEvent.target)); | |
if (pendingError) { | |
var err = pendingError; | |
pendingError = null; | |
throw err; | |
} | |
} | |
function dispatchEvent(event, originalWrapperTarget) { | |
if (currentlyDispatchingEvents.get(event)) | |
throw new Error('InvalidStateError'); | |
currentlyDispatchingEvents.set(event, true); | |
// Render to ensure that the event path is correct. | |
scope.renderAllPending(); | |
var eventPath; | |
// http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#events-and-the-window-object | |
// All events dispatched on Nodes with a default view, except load events, | |
// should propagate to the Window. | |
// http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#the-end | |
var overrideTarget; | |
var win; | |
var type = event.type; | |
// Should really be not cancelable too but since Firefox has a bug there | |
// we skip that check. | |
// https://bugzilla.mozilla.org/show_bug.cgi?id=999456 | |
if (type === 'load' && !event.bubbles) { | |
var doc = originalWrapperTarget; | |
if (doc instanceof wrappers.Document && (win = doc.defaultView)) { | |
overrideTarget = doc; | |
eventPath = []; | |
} | |
} | |
if (!eventPath) { | |
if (originalWrapperTarget instanceof wrappers.Window) { | |
win = originalWrapperTarget; | |
eventPath = []; | |
} else { | |
eventPath = getEventPath(originalWrapperTarget, event); | |
if (event.type !== 'load') { | |
var doc = eventPath[eventPath.length - 1]; | |
if (doc instanceof wrappers.Document) | |
win = doc.defaultView; | |
} | |
} | |
} | |
eventPathTable.set(event, eventPath); | |
if (dispatchCapturing(event, eventPath, win, overrideTarget)) { | |
if (dispatchAtTarget(event, eventPath, win, overrideTarget)) { | |
dispatchBubbling(event, eventPath, win, overrideTarget); | |
} | |
} | |
eventPhaseTable.set(event, NONE); | |
currentTargetTable.delete(event, null); | |
currentlyDispatchingEvents.delete(event); | |
return event.defaultPrevented; | |
} | |
function dispatchCapturing(event, eventPath, win, overrideTarget) { | |
var phase = CAPTURING_PHASE; | |
if (win) { | |
if (!invoke(win, event, phase, eventPath, overrideTarget)) | |
return false; | |
} | |
for (var i = eventPath.length - 1; i > 0; i--) { | |
if (!invoke(eventPath[i], event, phase, eventPath, overrideTarget)) | |
return false; | |
} | |
return true; | |
} | |
function dispatchAtTarget(event, eventPath, win, overrideTarget) { | |
var phase = AT_TARGET; | |
var currentTarget = eventPath[0] || win; | |
return invoke(currentTarget, event, phase, eventPath, overrideTarget); | |
} | |
function dispatchBubbling(event, eventPath, win, overrideTarget) { | |
var phase = BUBBLING_PHASE; | |
for (var i = 1; i < eventPath.length; i++) { | |
if (!invoke(eventPath[i], event, phase, eventPath, overrideTarget)) | |
return; | |
} | |
if (win && eventPath.length > 0) { | |
invoke(win, event, phase, eventPath, overrideTarget); | |
} | |
} | |
function invoke(currentTarget, event, phase, eventPath, overrideTarget) { | |
var listeners = listenersTable.get(currentTarget); | |
if (!listeners) | |
return true; | |
var target = overrideTarget || eventRetargetting(eventPath, currentTarget); | |
if (target === currentTarget) { | |
if (phase === CAPTURING_PHASE) | |
return true; | |
if (phase === BUBBLING_PHASE) | |
phase = AT_TARGET; | |
} else if (phase === BUBBLING_PHASE && !event.bubbles) { | |
return true; | |
} | |
if ('relatedTarget' in event) { | |
var originalEvent = unwrap(event); | |
var unwrappedRelatedTarget = originalEvent.relatedTarget; | |
// X-Tag sets relatedTarget on a CustomEvent. If they do that there is no | |
// way to have relatedTarget return the adjusted target but worse is that | |
// the originalEvent might not have a relatedTarget so we hit an assert | |
// when we try to wrap it. | |
if (unwrappedRelatedTarget) { | |
// In IE we can get objects that are not EventTargets at this point. | |
// Safari does not have an EventTarget interface so revert to checking | |
// for addEventListener as an approximation. | |
if (unwrappedRelatedTarget instanceof Object && | |
unwrappedRelatedTarget.addEventListener) { | |
var relatedTarget = wrap(unwrappedRelatedTarget); | |
var adjusted = | |
relatedTargetResolution(event, currentTarget, relatedTarget); | |
if (adjusted === target) | |
return true; | |
} else { | |
adjusted = null; | |
} | |
relatedTargetTable.set(event, adjusted); | |
} | |
} | |
eventPhaseTable.set(event, phase); | |
var type = event.type; | |
var anyRemoved = false; | |
// targetTable.set(event, target); | |
targetTable.set(event, target); | |
currentTargetTable.set(event, currentTarget); | |
// making the following substitution will prevent the unbounded looping behavior | |
//for (var i = 0; i < listeners.length; i++) { | |
for (var i = 0, limit = listeners.length; i < limit; i++) { | |
var listener = listeners[i]; | |
if (listener.removed) { | |
anyRemoved = true; | |
continue; | |
} | |
if (listener.type !== type || | |
!listener.capture && phase === CAPTURING_PHASE || | |
listener.capture && phase === BUBBLING_PHASE) { | |
continue; | |
} | |
try { | |
if (typeof listener.handler === 'function') | |
listener.handler.call(currentTarget, event); | |
else | |
listener.handler.handleEvent(event); | |
if (stopImmediatePropagationTable.get(event)) | |
return false; | |
} catch (ex) { | |
if (!pendingError) | |
pendingError = ex; | |
} | |
} | |
if (anyRemoved) { | |
var copy = listeners.slice(); | |
listeners.length = 0; | |
for (var i = 0; i < copy.length; i++) { | |
if (!copy[i].removed) | |
listeners.push(copy[i]); | |
} | |
} | |
return !stopPropagationTable.get(event); | |
} | |
function Listener(type, handler, capture) { | |
this.type = type; | |
this.handler = handler; | |
this.capture = Boolean(capture); | |
} | |
Listener.prototype = { | |
equals: function(that) { | |
return this.handler === that.handler && this.type === that.type && | |
this.capture === that.capture; | |
}, | |
get removed() { | |
return this.handler === null; | |
}, | |
remove: function() { | |
this.handler = null; | |
} | |
}; | |
var OriginalEvent = window.Event; | |
OriginalEvent.prototype.polymerBlackList_ = { | |
returnValue: true, | |
// TODO(arv): keyLocation is part of KeyboardEvent but Firefox does not | |
// support constructable KeyboardEvent so we keep it here for now. | |
keyLocation: true | |
}; | |
/** | |
* Creates a new Event wrapper or wraps an existin native Event object. | |
* @param {string|Event} type | |
* @param {Object=} options | |
* @constructor | |
*/ | |
function Event(type, options) { | |
if (type instanceof OriginalEvent) { | |
var impl = type; | |
if (!OriginalBeforeUnloadEvent && impl.type === 'beforeunload') | |
return new BeforeUnloadEvent(impl); | |
this.impl = impl; | |
} else { | |
return wrap(constructEvent(OriginalEvent, 'Event', type, options)); | |
} | |
} | |
Event.prototype = { | |
get target() { | |
return targetTable.get(this); | |
}, | |
get currentTarget() { | |
return currentTargetTable.get(this); | |
}, | |
get eventPhase() { | |
return eventPhaseTable.get(this); | |
}, | |
get path() { | |
var nodeList = new wrappers.NodeList(); | |
var eventPath = eventPathTable.get(this); | |
if (eventPath) { | |
var index = 0; | |
var lastIndex = eventPath.length - 1; | |
var baseRoot = getTreeScope(currentTargetTable.get(this)); | |
for (var i = 0; i <= lastIndex; i++) { | |
var currentTarget = eventPath[i]; | |
var currentRoot = getTreeScope(currentTarget); | |
if (currentRoot.contains(baseRoot) && | |
// Make sure we do not add Window to the path. | |
(i !== lastIndex || currentTarget instanceof wrappers.Node)) { | |
nodeList[index++] = currentTarget; | |
} | |
} | |
nodeList.length = index; | |
} | |
return nodeList; | |
}, | |
stopPropagation: function() { | |
stopPropagationTable.set(this, true); | |
}, | |
stopImmediatePropagation: function() { | |
stopPropagationTable.set(this, true); | |
stopImmediatePropagationTable.set(this, true); | |
} | |
}; | |
registerWrapper(OriginalEvent, Event, document.createEvent('Event')); | |
function unwrapOptions(options) { | |
if (!options || !options.relatedTarget) | |
return options; | |
return Object.create(options, { | |
relatedTarget: {value: unwrap(options.relatedTarget)} | |
}); | |
} | |
function registerGenericEvent(name, SuperEvent, prototype) { | |
var OriginalEvent = window[name]; | |
var GenericEvent = function(type, options) { | |
if (type instanceof OriginalEvent) | |
this.impl = type; | |
else | |
return wrap(constructEvent(OriginalEvent, name, type, options)); | |
}; | |
GenericEvent.prototype = Object.create(SuperEvent.prototype); | |
if (prototype) | |
mixin(GenericEvent.prototype, prototype); | |
if (OriginalEvent) { | |
// - Old versions of Safari fails on new FocusEvent (and others?). | |
// - IE does not support event constructors. | |
// - createEvent('FocusEvent') throws in Firefox. | |
// => Try the best practice solution first and fallback to the old way | |
// if needed. | |
try { | |
registerWrapper(OriginalEvent, GenericEvent, new OriginalEvent('temp')); | |
} catch (ex) { | |
registerWrapper(OriginalEvent, GenericEvent, | |
document.createEvent(name)); | |
} | |
} | |
return GenericEvent; | |
} | |
var UIEvent = registerGenericEvent('UIEvent', Event); | |
var CustomEvent = registerGenericEvent('CustomEvent', Event); | |
var relatedTargetProto = { | |
get relatedTarget() { | |
var relatedTarget = relatedTargetTable.get(this); | |
// relatedTarget can be null. | |
if (relatedTarget !== undefined) | |
return relatedTarget; | |
return wrap(unwrap(this).relatedTarget); | |
} | |
}; | |
function getInitFunction(name, relatedTargetIndex) { | |
return function() { | |
arguments[relatedTargetIndex] = unwrap(arguments[relatedTargetIndex]); | |
var impl = unwrap(this); | |
impl[name].apply(impl, arguments); | |
}; | |
} | |
var mouseEventProto = mixin({ | |
initMouseEvent: getInitFunction('initMouseEvent', 14) | |
}, relatedTargetProto); | |
var focusEventProto = mixin({ | |
initFocusEvent: getInitFunction('initFocusEvent', 5) | |
}, relatedTargetProto); | |
var MouseEvent = registerGenericEvent('MouseEvent', UIEvent, mouseEventProto); | |
var FocusEvent = registerGenericEvent('FocusEvent', UIEvent, focusEventProto); | |
// In case the browser does not support event constructors we polyfill that | |
// by calling `createEvent('Foo')` and `initFooEvent` where the arguments to | |
// `initFooEvent` are derived from the registered default event init dict. | |
var defaultInitDicts = Object.create(null); | |
var supportsEventConstructors = (function() { | |
try { | |
new window.FocusEvent('focus'); | |
} catch (ex) { | |
return false; | |
} | |
return true; | |
})(); | |
/** | |
* Constructs a new native event. | |
*/ | |
function constructEvent(OriginalEvent, name, type, options) { | |
if (supportsEventConstructors) | |
return new OriginalEvent(type, unwrapOptions(options)); | |
// Create the arguments from the default dictionary. | |
var event = unwrap(document.createEvent(name)); | |
var defaultDict = defaultInitDicts[name]; | |
var args = [type]; | |
Object.keys(defaultDict).forEach(function(key) { | |
var v = options != null && key in options ? | |
options[key] : defaultDict[key]; | |
if (key === 'relatedTarget') | |
v = unwrap(v); | |
args.push(v); | |
}); | |
event['init' + name].apply(event, args); | |
return event; | |
} | |
if (!supportsEventConstructors) { | |
var configureEventConstructor = function(name, initDict, superName) { | |
if (superName) { | |
var superDict = defaultInitDicts[superName]; | |
initDict = mixin(mixin({}, superDict), initDict); | |
} | |
defaultInitDicts[name] = initDict; | |
}; | |
// The order of the default event init dictionary keys is important, the | |
// arguments to initFooEvent is derived from that. | |
configureEventConstructor('Event', {bubbles: false, cancelable: false}); | |
configureEventConstructor('CustomEvent', {detail: null}, 'Event'); | |
configureEventConstructor('UIEvent', {view: null, detail: 0}, 'Event'); | |
configureEventConstructor('MouseEvent', { | |
screenX: 0, | |
screenY: 0, | |
clientX: 0, | |
clientY: 0, | |
ctrlKey: false, | |
altKey: false, | |
shiftKey: false, | |
metaKey: false, | |
button: 0, | |
relatedTarget: null | |
}, 'UIEvent'); | |
configureEventConstructor('FocusEvent', {relatedTarget: null}, 'UIEvent'); | |
} | |
// Safari 7 does not yet have BeforeUnloadEvent. | |
// https://bugs.webkit.org/show_bug.cgi?id=120849 | |
var OriginalBeforeUnloadEvent = window.BeforeUnloadEvent; | |
function BeforeUnloadEvent(impl) { | |
Event.call(this, impl); | |
} | |
BeforeUnloadEvent.prototype = Object.create(Event.prototype); | |
mixin(BeforeUnloadEvent.prototype, { | |
get returnValue() { | |
return this.impl.returnValue; | |
}, | |
set returnValue(v) { | |
this.impl.returnValue = v; | |
} | |
}); | |
if (OriginalBeforeUnloadEvent) | |
registerWrapper(OriginalBeforeUnloadEvent, BeforeUnloadEvent); | |
function isValidListener(fun) { | |
if (typeof fun === 'function') | |
return true; | |
return fun && fun.handleEvent; | |
} | |
function isMutationEvent(type) { | |
switch (type) { | |
case 'DOMAttrModified': | |
case 'DOMAttributeNameChanged': | |
case 'DOMCharacterDataModified': | |
case 'DOMElementNameChanged': | |
case 'DOMNodeInserted': | |
case 'DOMNodeInsertedIntoDocument': | |
case 'DOMNodeRemoved': | |
case 'DOMNodeRemovedFromDocument': | |
case 'DOMSubtreeModified': | |
return true; | |
} | |
return false; | |
} | |
var OriginalEventTarget = window.EventTarget; | |
/** | |
* This represents a wrapper for an EventTarget. | |
* @param {!EventTarget} impl The original event target. | |
* @constructor | |
*/ | |
function EventTarget(impl) { | |
this.impl = impl; | |
} | |
// Node and Window have different internal type checks in WebKit so we cannot | |
// use the same method as the original function. | |
var methodNames = [ | |
'addEventListener', | |
'removeEventListener', | |
'dispatchEvent' | |
]; | |
[Node, Window].forEach(function(constructor) { | |
var p = constructor.prototype; | |
methodNames.forEach(function(name) { | |
Object.defineProperty(p, name + '_', {value: p[name]}); | |
}); | |
}); | |
function getTargetToListenAt(wrapper) { | |
if (wrapper instanceof wrappers.ShadowRoot) | |
wrapper = wrapper.host; | |
return unwrap(wrapper); | |
} | |
EventTarget.prototype = { | |
addEventListener: function(type, fun, capture) { | |
if (!isValidListener(fun) || isMutationEvent(type)) | |
return; | |
var listener = new Listener(type, fun, capture); | |
var listeners = listenersTable.get(this); | |
if (!listeners) { | |
listeners = []; | |
listenersTable.set(this, listeners); | |
} else { | |
// Might have a duplicate. | |
for (var i = 0; i < listeners.length; i++) { | |
if (listener.equals(listeners[i])) | |
return; | |
} | |
} | |
listeners.push(listener); | |
var target = getTargetToListenAt(this); | |
target.addEventListener_(type, dispatchOriginalEvent, true); | |
}, | |
removeEventListener: function(type, fun, capture) { | |
capture = Boolean(capture); | |
var listeners = listenersTable.get(this); | |
if (!listeners) | |
return; | |
var count = 0, found = false; | |
for (var i = 0; i < listeners.length; i++) { | |
if (listeners[i].type === type && listeners[i].capture === capture) { | |
count++; | |
if (listeners[i].handler === fun) { | |
found = true; | |
listeners[i].remove(); | |
} | |
} | |
} | |
if (found && count === 1) { | |
var target = getTargetToListenAt(this); | |
target.removeEventListener_(type, dispatchOriginalEvent, true); | |
} | |
}, | |
dispatchEvent: function(event) { | |
// We want to use the native dispatchEvent because it triggers the default | |
// actions (like checking a checkbox). However, if there are no listeners | |
// in the composed tree then there are no events that will trigger and | |
// listeners in the non composed tree that are part of the event path are | |
// not notified. | |
// | |
// If we find out that there are no listeners in the composed tree we add | |
// a temporary listener to the target which makes us get called back even | |
// in that case. | |
var nativeEvent = unwrap(event); | |
var eventType = nativeEvent.type; | |
// Allow dispatching the same event again. This is safe because if user | |
// code calls this during an existing dispatch of the same event the | |
// native dispatchEvent throws (that is required by the spec). | |
handledEventsTable.set(nativeEvent, false); | |
// Force rendering since we prefer native dispatch and that works on the | |
// composed tree. | |
scope.renderAllPending(); | |
var tempListener; | |
if (!hasListenerInAncestors(this, eventType)) { | |
tempListener = function() {}; | |
this.addEventListener(eventType, tempListener, true); | |
} | |
try { | |
return unwrap(this).dispatchEvent_(nativeEvent); | |
} finally { | |
if (tempListener) | |
this.removeEventListener(eventType, tempListener, true); | |
} | |
} | |
}; | |
function hasListener(node, type) { | |
var listeners = listenersTable.get(node); | |
if (listeners) { | |
for (var i = 0; i < listeners.length; i++) { | |
if (!listeners[i].removed && listeners[i].type === type) | |
return true; | |
} | |
} | |
return false; | |
} | |
function hasListenerInAncestors(target, type) { | |
for (var node = unwrap(target); node; node = node.parentNode) { | |
if (hasListener(wrap(node), type)) | |
return true; | |
} | |
return false; | |
} | |
if (OriginalEventTarget) | |
registerWrapper(OriginalEventTarget, EventTarget); | |
function wrapEventTargetMethods(constructors) { | |
forwardMethodsToWrapper(constructors, methodNames); | |
} | |
var originalElementFromPoint = document.elementFromPoint; | |
function elementFromPoint(self, document, x, y) { | |
scope.renderAllPending(); | |
var element = wrap(originalElementFromPoint.call(document.impl, x, y)); | |
var path = getEventPath(element, null); | |
return eventRetargetting(path, self); | |
} | |
/** | |
* Returns a function that is to be used as a getter for `onfoo` properties. | |
* @param {string} name | |
* @return {Function} | |
*/ | |
function getEventHandlerGetter(name) { | |
return function() { | |
var inlineEventHandlers = eventHandlersTable.get(this); | |
return inlineEventHandlers && inlineEventHandlers[name] && | |
inlineEventHandlers[name].value || null; | |
}; | |
} | |
/** | |
* Returns a function that is to be used as a setter for `onfoo` properties. | |
* @param {string} name | |
* @return {Function} | |
*/ | |
function getEventHandlerSetter(name) { | |
var eventType = name.slice(2); | |
return function(value) { | |
var inlineEventHandlers = eventHandlersTable.get(this); | |
if (!inlineEventHandlers) { | |
inlineEventHandlers = Object.create(null); | |
eventHandlersTable.set(this, inlineEventHandlers); | |
} | |
var old = inlineEventHandlers[name]; | |
if (old) | |
this.removeEventListener(eventType, old.wrapped, false); | |
if (typeof value === 'function') { | |
var wrapped = function(e) { | |
var rv = value.call(this, e); | |
if (rv === false) | |
e.preventDefault(); | |
else if (name === 'onbeforeunload' && typeof rv === 'string') | |
e.returnValue = rv; | |
// mouseover uses true for preventDefault but preventDefault for | |
// mouseover is ignored by browsers these day. | |
}; | |
this.addEventListener(eventType, wrapped, false); | |
inlineEventHandlers[name] = { | |
value: value, | |
wrapped: wrapped | |
}; | |
} | |
}; | |
} | |
scope.elementFromPoint = elementFromPoint; | |
scope.getEventHandlerGetter = getEventHandlerGetter; | |
scope.getEventHandlerSetter = getEventHandlerSetter; | |
scope.wrapEventTargetMethods = wrapEventTargetMethods; | |
scope.wrappers.BeforeUnloadEvent = BeforeUnloadEvent; | |
scope.wrappers.CustomEvent = CustomEvent; | |
scope.wrappers.Event = Event; | |
scope.wrappers.EventTarget = EventTarget; | |
scope.wrappers.FocusEvent = FocusEvent; | |
scope.wrappers.MouseEvent = MouseEvent; | |
scope.wrappers.UIEvent = UIEvent; | |
})(window.ShadowDOMPolyfill); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment