Skip to content

Implement event modifiers #1819

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Oct 28, 2018
70 changes: 70 additions & 0 deletions src/compile/nodes/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import mapChildren from './shared/mapChildren';
import { dimensions } from '../../utils/patterns';
import fuzzymatch from '../validate/utils/fuzzymatch';
import Ref from './Ref';
import list from '../../utils/list';

const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;

Expand Down Expand Up @@ -56,6 +57,22 @@ const a11yRequiredContent = new Set([

const invisibleElements = new Set(['meta', 'html', 'script', 'style']);

const validModifiers = new Set([
'preventDefault',
'stopPropagation',
'capture',
'once',
'passive'
]);

const passiveEvents = new Set([
'wheel',
'touchstart',
'touchmove',
'touchend',
'touchcancel'
]);

export default class Element extends Node {
type: 'Element';
name: string;
Expand Down Expand Up @@ -228,6 +245,7 @@ export default class Element extends Node {
this.validateAttributes();
this.validateBindings();
this.validateContent();
this.validateEventHandlers();
}

validateAttributes() {
Expand Down Expand Up @@ -563,6 +581,58 @@ export default class Element extends Node {
}
}

validateEventHandlers() {
const { component } = this;

this.handlers.forEach(handler => {
if (handler.modifiers.has('passive') && handler.modifiers.has('preventDefault')) {
component.error(handler, {
code: 'invalid-event-modifier',
message: `The 'passive' and 'preventDefault' modifiers cannot be used together`
});
}

handler.modifiers.forEach(modifier => {
if (!validModifiers.has(modifier)) {
component.error(handler, {
code: 'invalid-event-modifier',
message: `Valid event modifiers are ${list([...validModifiers])}`
});
}

if (modifier === 'passive') {
if (passiveEvents.has(handler.name)) {
if (!handler.usesEventObject) {
component.warn(handler, {
code: 'redundant-event-modifier',
message: `Touch event handlers that don't use the 'event' object are passive by default`
});
}
} else {
component.warn(handler, {
code: 'redundant-event-modifier',
message: `The passive modifier only works with wheel and touch events`
});
}
}

if (component.options.legacy && (modifier === 'once' || modifier === 'passive')) {
// TODO this could be supported, but it would need a few changes to
// how event listeners work
component.error(handler, {
code: 'invalid-event-modifier',
message: `The '${modifier}' modifier cannot be used in legacy mode`
});
}
});

if (passiveEvents.has(handler.name) && !handler.usesEventObject && !handler.modifiers.has('preventDefault')) {
// touch/wheel events should be passive by default
handler.modifiers.add('passive');
}
});
}

getStaticAttributeValue(name: string) {
const attribute = this.attributes.find(
(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
Expand Down
7 changes: 7 additions & 0 deletions src/compile/nodes/EventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ const validBuiltins = new Set(['set', 'fire', 'destroy']);

export default class EventHandler extends Node {
name: string;
modifiers: Set<string>;
dependencies: Set<string>;
expression: Node;
callee: any; // TODO

usesComponent: boolean;
usesContext: boolean;
usesEventObject: boolean;
isCustomEvent: boolean;
shouldHoist: boolean;

Expand All @@ -26,6 +28,8 @@ export default class EventHandler extends Node {
super(component, parent, scope, info);

this.name = info.name;
this.modifiers = new Set(info.modifiers);

component.used.events.add(this.name);

this.dependencies = new Set();
Expand All @@ -39,11 +43,13 @@ export default class EventHandler extends Node {

this.usesComponent = !validCalleeObjects.has(this.callee.name);
this.usesContext = false;
this.usesEventObject = this.callee.name === 'event';

this.args = info.expression.arguments.map(param => {
const expression = new Expression(component, this, scope, param);
addToSet(this.dependencies, expression.dependencies);
if (expression.usesContext) this.usesContext = true;
if (expression.usesEvent) this.usesEventObject = true;
return expression;
});

Expand All @@ -55,6 +61,7 @@ export default class EventHandler extends Node {
this.args = null;
this.usesComponent = true;
this.usesContext = false;
this.usesEventObject = true;

this.snippet = null; // TODO handle shorthand events here?
}
Expand Down
14 changes: 9 additions & 5 deletions src/compile/nodes/shared/Expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ export default class Expression {
component: Component;
node: any;
snippet: string;

usesContext: boolean;
references: Set<string>;
dependencies: Set<string>;

usesContext = false;
usesEvent = false;

thisReferences: Array<{ start: number, end: number }>;

constructor(component, parent, scope, info) {
Expand All @@ -77,8 +78,6 @@ export default class Expression {

this.snippet = `[✂${info.start}-${info.end}✂]`;

this.usesContext = false;

const dependencies = new Set();

const { code, helpers } = component;
Expand Down Expand Up @@ -109,7 +108,12 @@ export default class Expression {
if (isReference(node, parent)) {
const { name, nodes } = flattenReference(node);

if (currentScope.has(name) || (name === 'event' && isEventHandler)) return;
if (name === 'event' && isEventHandler) {
expression.usesEvent = true;
return;
}

if (currentScope.has(name)) return;

if (component.helpers.has(name)) {
let object = node;
Expand Down
32 changes: 26 additions & 6 deletions src/compile/render-dom/wrappers/Element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,8 +649,13 @@ export default class ElementWrapper extends Wrapper {
${handlerName}.destroy();
`);
} else {
const modifiers = [];
if (handler.modifiers.has('preventDefault')) modifiers.push('event.preventDefault();');
if (handler.modifiers.has('stopPropagation')) modifiers.push('event.stopPropagation();');

const handlerFunction = deindent`
function ${handlerName}(event) {
${modifiers}
${handlerBody}
}
`;
Expand All @@ -661,13 +666,28 @@ export default class ElementWrapper extends Wrapper {
block.builders.init.addBlock(handlerFunction);
}

block.builders.hydrate.addLine(
`@addListener(${this.var}, "${handler.name}", ${handlerName});`
);
const opts = ['passive', 'once', 'capture'].filter(mod => handler.modifiers.has(mod));
if (opts.length) {
const optString = (opts.length === 1 && opts[0] === 'capture')
? 'true'
: `{ ${opts.map(opt => `${opt}: true`).join(', ')} }`;

block.builders.destroy.addLine(
`@removeListener(${this.var}, "${handler.name}", ${handlerName});`
);
block.builders.hydrate.addLine(
`@addListener(${this.var}, "${handler.name}", ${handlerName}, ${optString});`
);

block.builders.destroy.addLine(
`@removeListener(${this.var}, "${handler.name}", ${handlerName}, ${optString});`
);
} else {
block.builders.hydrate.addLine(
`@addListener(${this.var}, "${handler.name}", ${handlerName});`
);

block.builders.destroy.addLine(
`@removeListener(${this.var}, "${handler.name}", ${handlerName});`
);
}
}
});
}
Expand Down
62 changes: 0 additions & 62 deletions src/compile/render-dom/wrappers/shared/EventHandler.ts

This file was deleted.

5 changes: 3 additions & 2 deletions src/parse/read/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ const DIRECTIVES: Record<string, {

EventHandler: {
names: ['on'],
attribute(start, end, type, name, expression) {
return { start, end, type, name, expression };
attribute(start, end, type, lhs, expression) {
const [name, ...modifiers] = lhs.split('|');
return { start, end, type, name, modifiers, expression };
},
allowedExpressionTypes: ['CallExpression'],
error: 'Expected a method call'
Expand Down
8 changes: 4 additions & 4 deletions src/shared/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ export function createComment() {
return document.createComment('');
}

export function addListener(node, event, handler) {
node.addEventListener(event, handler, false);
export function addListener(node, event, handler, options) {
node.addEventListener(event, handler, options);
}

export function removeListener(node, event, handler) {
node.removeEventListener(event, handler, false);
export function removeListener(node, event, handler, options) {
node.removeEventListener(event, handler, options);
}

export function setAttribute(node, attribute, value) {
Expand Down
Loading