Skip to content

Commit 54fe128

Browse files
authored
Merge pull request #1819 from sveltejs/gh-1088
Implement event modifiers
2 parents 0d881ee + f1d7044 commit 54fe128

File tree

20 files changed

+332
-79
lines changed

20 files changed

+332
-79
lines changed

src/compile/nodes/Element.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import mapChildren from './shared/mapChildren';
1414
import { dimensions } from '../../utils/patterns';
1515
import fuzzymatch from '../validate/utils/fuzzymatch';
1616
import Ref from './Ref';
17+
import list from '../../utils/list';
1718

1819
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)$/;
1920

@@ -56,6 +57,22 @@ const a11yRequiredContent = new Set([
5657

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

60+
const validModifiers = new Set([
61+
'preventDefault',
62+
'stopPropagation',
63+
'capture',
64+
'once',
65+
'passive'
66+
]);
67+
68+
const passiveEvents = new Set([
69+
'wheel',
70+
'touchstart',
71+
'touchmove',
72+
'touchend',
73+
'touchcancel'
74+
]);
75+
5976
export default class Element extends Node {
6077
type: 'Element';
6178
name: string;
@@ -228,6 +245,7 @@ export default class Element extends Node {
228245
this.validateAttributes();
229246
this.validateBindings();
230247
this.validateContent();
248+
this.validateEventHandlers();
231249
}
232250

233251
validateAttributes() {
@@ -563,6 +581,58 @@ export default class Element extends Node {
563581
}
564582
}
565583

584+
validateEventHandlers() {
585+
const { component } = this;
586+
587+
this.handlers.forEach(handler => {
588+
if (handler.modifiers.has('passive') && handler.modifiers.has('preventDefault')) {
589+
component.error(handler, {
590+
code: 'invalid-event-modifier',
591+
message: `The 'passive' and 'preventDefault' modifiers cannot be used together`
592+
});
593+
}
594+
595+
handler.modifiers.forEach(modifier => {
596+
if (!validModifiers.has(modifier)) {
597+
component.error(handler, {
598+
code: 'invalid-event-modifier',
599+
message: `Valid event modifiers are ${list([...validModifiers])}`
600+
});
601+
}
602+
603+
if (modifier === 'passive') {
604+
if (passiveEvents.has(handler.name)) {
605+
if (!handler.usesEventObject) {
606+
component.warn(handler, {
607+
code: 'redundant-event-modifier',
608+
message: `Touch event handlers that don't use the 'event' object are passive by default`
609+
});
610+
}
611+
} else {
612+
component.warn(handler, {
613+
code: 'redundant-event-modifier',
614+
message: `The passive modifier only works with wheel and touch events`
615+
});
616+
}
617+
}
618+
619+
if (component.options.legacy && (modifier === 'once' || modifier === 'passive')) {
620+
// TODO this could be supported, but it would need a few changes to
621+
// how event listeners work
622+
component.error(handler, {
623+
code: 'invalid-event-modifier',
624+
message: `The '${modifier}' modifier cannot be used in legacy mode`
625+
});
626+
}
627+
});
628+
629+
if (passiveEvents.has(handler.name) && !handler.usesEventObject && !handler.modifiers.has('preventDefault')) {
630+
// touch/wheel events should be passive by default
631+
handler.modifiers.add('passive');
632+
}
633+
});
634+
}
635+
566636
getStaticAttributeValue(name: string) {
567637
const attribute = this.attributes.find(
568638
(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name

src/compile/nodes/EventHandler.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ const validBuiltins = new Set(['set', 'fire', 'destroy']);
99

1010
export default class EventHandler extends Node {
1111
name: string;
12+
modifiers: Set<string>;
1213
dependencies: Set<string>;
1314
expression: Node;
1415
callee: any; // TODO
1516

1617
usesComponent: boolean;
1718
usesContext: boolean;
19+
usesEventObject: boolean;
1820
isCustomEvent: boolean;
1921
shouldHoist: boolean;
2022

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

2830
this.name = info.name;
31+
this.modifiers = new Set(info.modifiers);
32+
2933
component.used.events.add(this.name);
3034

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

4044
this.usesComponent = !validCalleeObjects.has(this.callee.name);
4145
this.usesContext = false;
46+
this.usesEventObject = this.callee.name === 'event';
4247

4348
this.args = info.expression.arguments.map(param => {
4449
const expression = new Expression(component, this, scope, param);
4550
addToSet(this.dependencies, expression.dependencies);
4651
if (expression.usesContext) this.usesContext = true;
52+
if (expression.usesEvent) this.usesEventObject = true;
4753
return expression;
4854
});
4955

@@ -55,6 +61,7 @@ export default class EventHandler extends Node {
5561
this.args = null;
5662
this.usesComponent = true;
5763
this.usesContext = false;
64+
this.usesEventObject = true;
5865

5966
this.snippet = null; // TODO handle shorthand events here?
6067
}

src/compile/nodes/shared/Expression.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,12 @@ export default class Expression {
5757
component: Component;
5858
node: any;
5959
snippet: string;
60-
61-
usesContext: boolean;
6260
references: Set<string>;
6361
dependencies: Set<string>;
6462

63+
usesContext = false;
64+
usesEvent = false;
65+
6566
thisReferences: Array<{ start: number, end: number }>;
6667

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

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

80-
this.usesContext = false;
81-
8281
const dependencies = new Set();
8382

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

112-
if (currentScope.has(name) || (name === 'event' && isEventHandler)) return;
111+
if (name === 'event' && isEventHandler) {
112+
expression.usesEvent = true;
113+
return;
114+
}
115+
116+
if (currentScope.has(name)) return;
113117

114118
if (component.helpers.has(name)) {
115119
let object = node;

src/compile/render-dom/wrappers/Element/index.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -649,8 +649,13 @@ export default class ElementWrapper extends Wrapper {
649649
${handlerName}.destroy();
650650
`);
651651
} else {
652+
const modifiers = [];
653+
if (handler.modifiers.has('preventDefault')) modifiers.push('event.preventDefault();');
654+
if (handler.modifiers.has('stopPropagation')) modifiers.push('event.stopPropagation();');
655+
652656
const handlerFunction = deindent`
653657
function ${handlerName}(event) {
658+
${modifiers}
654659
${handlerBody}
655660
}
656661
`;
@@ -661,13 +666,28 @@ export default class ElementWrapper extends Wrapper {
661666
block.builders.init.addBlock(handlerFunction);
662667
}
663668

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

668-
block.builders.destroy.addLine(
669-
`@removeListener(${this.var}, "${handler.name}", ${handlerName});`
670-
);
675+
block.builders.hydrate.addLine(
676+
`@addListener(${this.var}, "${handler.name}", ${handlerName}, ${optString});`
677+
);
678+
679+
block.builders.destroy.addLine(
680+
`@removeListener(${this.var}, "${handler.name}", ${handlerName}, ${optString});`
681+
);
682+
} else {
683+
block.builders.hydrate.addLine(
684+
`@addListener(${this.var}, "${handler.name}", ${handlerName});`
685+
);
686+
687+
block.builders.destroy.addLine(
688+
`@removeListener(${this.var}, "${handler.name}", ${handlerName});`
689+
);
690+
}
671691
}
672692
});
673693
}

src/compile/render-dom/wrappers/shared/EventHandler.ts

Lines changed: 0 additions & 62 deletions
This file was deleted.

src/parse/read/directives.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ const DIRECTIVES: Record<string, {
2525

2626
EventHandler: {
2727
names: ['on'],
28-
attribute(start, end, type, name, expression) {
29-
return { start, end, type, name, expression };
28+
attribute(start, end, type, lhs, expression) {
29+
const [name, ...modifiers] = lhs.split('|');
30+
return { start, end, type, name, modifiers, expression };
3031
},
3132
allowedExpressionTypes: ['CallExpression'],
3233
error: 'Expected a method call'

src/shared/dom.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,12 @@ export function createComment() {
7373
return document.createComment('');
7474
}
7575

76-
export function addListener(node, event, handler) {
77-
node.addEventListener(event, handler, false);
76+
export function addListener(node, event, handler, options) {
77+
node.addEventListener(event, handler, options);
7878
}
7979

80-
export function removeListener(node, event, handler) {
81-
node.removeEventListener(event, handler, false);
80+
export function removeListener(node, event, handler, options) {
81+
node.removeEventListener(event, handler, options);
8282
}
8383

8484
export function setAttribute(node, attribute, value) {

0 commit comments

Comments
 (0)