Skip to content

Commit

Permalink
Merge pull request #328 from Polymer/event-bindings
Browse files Browse the repository at this point in the history
Event bindings
  • Loading branch information
dfreedm committed Oct 24, 2013
2 parents 773f0bb + 79d5379 commit b720085
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 227 deletions.
71 changes: 4 additions & 67 deletions src/declaration/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

// imports

var EVENT_PREFIX = scope.api.instance.events.EVENT_PREFIX;
var api = scope.api.instance.events;
var log = window.logFlags || {};

// polymer-element declarative api: events feature
Expand All @@ -24,57 +24,13 @@
// for each attribute
for (var i=0, a; a=this.attributes[i]; i++) {
// does it have magic marker identifying it as an event delegate?
if (hasEventPrefix(a.name)) {
if (api.hasEventPrefix(a.name)) {
// if so, add the info to delegates
delegates[removeEventPrefix(a.name)] = a.value;
delegates[api.removeEventPrefix(a.name)] = a.value.replace('{{', '')
.replace('}}', '').trim();
}
}
},
parseLocalEvents: function() {
// extract data from all templates into delegates
var t$ = this.querySelectorAll('template');
for (var i=0, l=t$.length, t; (i<l) && (t=t$[i]); i++) {
// store delegate information directly on template
t.delegates = {};
// acquire delegates from entire subtree at t
this.accumulateTemplatedEvents(t, t.delegates);
log.events && console.log('[%s] parseLocalEvents:', this.attributes.name.value, t.delegates);
};
},
accumulateTemplatedEvents: function(node, events) {
if (node.localName === 'template') {
var content = getTemplateContent(node);
if (content) {
this.accumulateChildEvents(content, events);
}
}
},
accumulateChildEvents: function(node, events) {
var n$ = node.childNodes;
for (var i=0, l=n$.length, n; (i<l) && (n=n$[i]); i++) {
this.accumulateEvents(n, events);
}
},
accumulateEvents: function(node, events) {
this.accumulateAttributeEvents(node, events);
this.accumulateChildEvents(node, events);
this.accumulateTemplatedEvents(node, events);
return events;
},
accumulateAttributeEvents: function(node, events) {
var a$ = node.attributes;
if (a$) {
for (var i=0, l=a$.length, a; (i<l) && (a=a$[i]); i++) {
if (hasEventPrefix(a.name)) {
this.accumulateEvent(removeEventPrefix(a.name), events);
}
}
}
},
accumulateEvent: function(name, events) {
name = this.event_translations[name] || name;
events[name] = events[name] || 1;
},
event_translations: {
webkitanimationstart: 'webkitAnimationStart',
webkitanimationend: 'webkitAnimationEnd',
Expand All @@ -84,25 +40,6 @@
}
};

var prefixLength = EVENT_PREFIX.length;

function hasEventPrefix(n) {
return n.slice(0, prefixLength) === EVENT_PREFIX;
}

function removeEventPrefix(n) {
return n.slice(prefixLength);
}

// TODO(sorvell): Currently in MDV, there is no way to get a template's
// effective content. A template can have a ref property
// that points to the template from which this one has been cloned.
// Remove this when the MDV api is improved
// (https://github.com/polymer-project/mdv/issues/15).
function getTemplateContent(template) {
return template.ref ? template.ref.content : template.content;
}

// exports

scope.api.declaration.events = events;
Expand Down
2 changes: 1 addition & 1 deletion src/declaration/properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
}
},
requireProperties: function(properties, prototype, base) {
// ensure a prototype value for each one
// ensure a prototype value for each property
for (var n in properties) {
if (prototype[n] === undefined && base[n] === undefined) {
prototype[n] = properties[n];
Expand Down
2 changes: 0 additions & 2 deletions src/declaration/prototype.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@
this.accumulateInstanceAttributes();
// parse on-* delegates declared on `this` element
this.parseHostEvents();
// parse on-* delegates declared in templates
this.parseLocalEvents();
// install external stylesheets as if they are inline
this.installSheets();
// TODO(sorvell): install a helper method this.resolvePath to aid in
Expand Down
25 changes: 2 additions & 23 deletions src/instance/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@
PolymerBase: true,
job: Polymer.job,
super: Polymer.super,
// TODO(sorvell): bc, created is currently a synonym for 'ready'.
// We should call this in createdCallback instead of at ready time or
// eliminate it.
// user entry point for element has had its createdCallback called
created: function() {
},
// user entry point for element has shadowRoot and is ready for
// api interaction
ready: function() {
},
createdCallback: function() {
this.created();
if (this.ownerDocument.defaultView || this.alwaysPrepare ||
preparingElements > 0) {
this.prepareElement();
Expand All @@ -45,8 +44,6 @@
preparingElements--;
// user entry point
this.ready();
// TODO(sorvell): bc
this.created();
},
enteredViewCallback: function() {
if (!this._elementPrepared) {
Expand All @@ -57,14 +54,6 @@
if (this.enteredView) {
this.enteredView();
}
// TODO(sorvell): bc
if (this.enteredDocument) {
this.enteredDocument();
}
},
// TODO(sorvell): bc
enteredDocumentCallback: function() {
this.enteredViewCallback();
},
leftViewCallback: function() {
if (!this.preventDispose) {
Expand All @@ -74,14 +63,6 @@
if (this.leftView) {
this.leftView();
}
// TODO(sorvell): bc
if (this.leftDocument) {
this.leftDocument();
}
},
// TODO(sorvell): bc
leftDocumentCallback: function() {
this.leftViewCallback();
},
// recursive ancestral <element> initialization, oldest first
parseDeclarations: function(p) {
Expand Down Expand Up @@ -147,8 +128,6 @@
shadowRootReady: function(root, template) {
// locate nodes with id and store references to them in this.$ hash
this.marshalNodeReferences(root);
// add local events of interest...
this.addInstanceListeners(root, template);
// set up pointer gestures
PointerGestures.register(root);
},
Expand Down
146 changes: 51 additions & 95 deletions src/instance/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,21 @@
// instance events api

var events = {
// magic words
// read-only
EVENT_PREFIX: EVENT_PREFIX,
// event name utilities
hasEventPrefix: function (n) {
return n && (n[0] === 'o') && (n[1] === 'n') && (n[2] === '-');
},
removeEventPrefix: function(n) {
return n.slice(prefixLength);
},
// event listeners on host
addHostListeners: function() {
var events = this.eventDelegates;
log.events && (Object.keys(events).length > 0) && console.log('[%s] addHostListeners:', this.localName, events);
this.addNodeListeners(this, events, this.hostEventListener);
},
// event listeners inside a shadow-root
addInstanceListeners: function(root, template) {
var events = template.delegates;
if (events) {
log.events && (Object.keys(events).length > 0) && console.log('[%s:root] addInstanceListeners:', this.localName, events);
this.addNodeListeners(root, events, this.instanceEventListener);
}
},
addNodeListeners: function(node, events, listener) {
// note: conditional inside loop as optimization
// for empty 'events' object
Expand Down Expand Up @@ -63,81 +62,58 @@
findEventDelegate: function(event) {
return this.eventDelegates[event.type];
},
// call 'methodName' method on 'node' with 'args', if the method exists
dispatchMethod: function(node, methodName, args) {
if (node) {
log.events && console.group('[%s] dispatch [%s]', node.localName, methodName);
var fn = this[methodName];
// call 'method' or function method on 'obj' with 'args', if the method exists
dispatchMethod: function(obj, method, args) {
if (obj) {
log.events && console.group('[%s] dispatch [%s]', obj.localName, method);
var fn = typeof method === 'function' ? method : obj[method];
if (fn) {
fn[args ? 'apply' : 'call'](this, args);
fn[args ? 'apply' : 'call'](obj, args);
}
log.events && console.groupEnd();
Platform.flush();
}
},
instanceEventListener: function(event) {
listenLocal(this, event);
}
};

// TODO(sjmiles): much of the below privatized only because of the vague
// notion this code is too fiddly and we need to revisit the core feature

function listenLocal(host, event) {
if (!event.cancelBubble) {
event.on = EVENT_PREFIX + event.type;
log.events && console.group("[%s]: listenLocal [%s]", host.localName, event.on);
if (!event.path) {
_listenLocalNoEventPath(host, event);
} else {
_listenLocal(host, event);
/*
Bind events via attributes of the form on-eventName.
This method hooks into the model syntax and does adds event listeners as
needed. By default, binding paths are always method names on the root
model, the custom element in which the node exists. Adding a '@' in the
path directs the event binding to use the model path as the event listener.
In both cases, the actual listener is attached to a generic method which
evaluates the bound path at event execution time.
*/
prepareBinding: function(path, name, node) {
// if lhs an event prefix,
if (events.hasEventPrefix(name)) {
// provide an event-binding callback
return function(model, name, node) {
log.events && console.log('event: [%s].%s => [%s].%s()"', node.localName, name, model.localName, path);
var listener = function(event) {
var ctrlr = findController(node);
if (ctrlr && ctrlr.dispatchMethod) {
var obj = ctrlr, method = path;
if (path[0] == '@') {
obj = model;
method = Path.get(path.slice(1)).getValueFrom(model);
}
ctrlr.dispatchMethod(obj, method, [event, event.detail, node]);
}
};
var eventName = events.removeEventPrefix(name);
node.addEventListener(eventName, listener, false);
return {
close: function() {
log.events && console.log('event.remove: [%s].%s => [%s].%s()"', node.localName, name, model.localName, path);
node.removeEventListener(eventName, listener, false);
}
}
};
}
log.events && console.groupEnd();
}
}
};

function _listenLocal(host, event) {
var c = null;
// use `some` to stop iterating after the first matching target
Array.prototype.some.call(event.path, function(t) {
// if we hit host, stop
if (t === host) {
return true;
}
// find a controller for target `t`, unless we already found `host`
// as a controller
c = (c === host) ? c : findController(t);
// if we have a controller, dispatch the event, return 'true' if
// handler returns true
if (c && handleEvent(c, t, event)) {
return true;
}
}, this);
}

// TODO(sorvell): remove when ShadowDOM polyfill supports event path.
// Note that findController will not return the expected
// controller when when the event target is a distributed node.
// This because we cannot traverse from a composed node to a node
// in shadowRoot.
// This will be addressed via an event path api
// https://www.w3.org/Bugs/Public/show_bug.cgi?id=21066
function _listenLocalNoEventPath(host, event) {
log.events && console.log('event.path() not supported for', event.type);
var t = event.target, c = null;
// if we hit dirt or host, stop
while (t && t != host) {
// find a controller for target `t`, unless we already found `host`
// as a controller
c = (c === host) ? c : findController(t);
// if we have a controller, dispatch the event, return 'true' if
// handler returns true
if (c && handleEvent(c, t, event)) {
return true;
}
t = t.parentNode;
}
}
var prefixLength = EVENT_PREFIX.length;

function findController(node) {
while (node.parentNode) {
Expand All @@ -146,26 +122,6 @@
return node.host;
};

function handleEvent(ctrlr, node, event) {
var h = node.getAttribute && node.getAttribute(event.on);
if (h && handleIfNotHandled(node, event)) {
log.events && console.log('[%s] found handler name [%s]', ctrlr.localName, h);
ctrlr.dispatchMethod(node, h, [event, event.detail, node]);
}
return event.cancelBubble;
};

function handleIfNotHandled(node, event) {
var list = event[HANDLED_LIST];
if (!list) {
list = event[HANDLED_LIST] = [];
}
if (list.indexOf(node) < 0) {
list.push(node);
return true;
}
}

// exports

scope.api.instance.events = events;
Expand Down
Loading

0 comments on commit b720085

Please sign in to comment.