diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index c060edc1f8..e05c587805 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -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)$/; @@ -228,6 +229,7 @@ export default class Element extends Node { this.validateAttributes(); this.validateBindings(); this.validateContent(); + this.validateEventHandlers(); } validateAttributes() { @@ -563,6 +565,58 @@ export default class Element extends Node { } } + validateEventHandlers() { + const { component } = this; + + const validModifiers = new Set([ + 'preventDefault', + 'stopPropagation', + 'capture', + 'once', + 'passive' + ]); + + const passiveEvents = new Set([ + 'wheel', + 'touchstart', + 'touchmove', + 'touchend', + 'touchcancel' + ]); + + this.handlers.forEach(handler => { + 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)) { + const usesEvent = ( + handler.callee.name === 'event' || + handler.args.some(x => x.usesEvent) + ); + + if (!usesEvent) { + 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` + }); + } + } + }); + }); + } + getStaticAttributeValue(name: string) { const attribute = this.attributes.find( (attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name diff --git a/src/compile/nodes/EventHandler.ts b/src/compile/nodes/EventHandler.ts index 05aa51d1da..00b24f21f5 100644 --- a/src/compile/nodes/EventHandler.ts +++ b/src/compile/nodes/EventHandler.ts @@ -9,6 +9,7 @@ const validBuiltins = new Set(['set', 'fire', 'destroy']); export default class EventHandler extends Node { name: string; + modifiers: Set; dependencies: Set; expression: Node; callee: any; // TODO @@ -26,6 +27,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(); diff --git a/src/compile/nodes/shared/Expression.ts b/src/compile/nodes/shared/Expression.ts index 6acb6282ec..dc6f77d82e 100644 --- a/src/compile/nodes/shared/Expression.ts +++ b/src/compile/nodes/shared/Expression.ts @@ -57,11 +57,12 @@ export default class Expression { component: Component; node: any; snippet: string; - - usesContext: boolean; references: Set; dependencies: Set; + usesContext = false; + usesEvent = false; + thisReferences: Array<{ start: number, end: number }>; constructor(component, parent, scope, info) { @@ -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; @@ -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; diff --git a/src/parse/read/directives.ts b/src/parse/read/directives.ts index f1955ffcbc..6993f3ebda 100644 --- a/src/parse/read/directives.ts +++ b/src/parse/read/directives.ts @@ -25,8 +25,9 @@ const DIRECTIVES: Record +
+ + \ No newline at end of file diff --git a/test/validator/samples/event-modifiers-redundant/warnings.json b/test/validator/samples/event-modifiers-redundant/warnings.json new file mode 100644 index 0000000000..7b6cae16d4 --- /dev/null +++ b/test/validator/samples/event-modifiers-redundant/warnings.json @@ -0,0 +1,32 @@ +[ + { + "message": "The passive modifier only works with wheel and touch events", + "code": "redundant-event-modifier", + "start": { + "line": 1, + "column": 8, + "character": 8 + }, + "end": { + "line": 1, + "column": 40, + "character": 40 + }, + "pos": 8 + }, + { + "message": "Touch event handlers that don't use the 'event' object are passive by default", + "code": "redundant-event-modifier", + "start": { + "line": 2, + "column": 5, + "character": 56 + }, + "end": { + "line": 2, + "column": 47, + "character": 98 + }, + "pos": 56 + } +]