Merge pull request from sveltejs/gh-1088

Implement event modifiers
pull/1822/head
Rich Harris 7 years ago committed by GitHub
commit 54fe128cf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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)$/;
@ -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;
@ -228,6 +245,7 @@ export default class Element extends Node {
this.validateAttributes();
this.validateBindings();
this.validateContent();
this.validateEventHandlers();
}
validateAttributes() {
@ -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

@ -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;
@ -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();
@ -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;
});
@ -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?
}

@ -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) {
@ -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;

@ -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}
}
`;
@ -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});`
);
}
}
});
}

@ -1,62 +0,0 @@
import Renderer from '../../Renderer';
import Block from '../../Block';
import Wrapper from './Wrapper';
import EventHandler from '../../../nodes/EventHandler';
import validCalleeObjects from '../../../../utils/validCalleeObjects';
export default class EventHandlerWrapper extends Wrapper {
node: EventHandler;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: EventHandler,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
}
render(block: Block, parentNode: string, parentNodes: string) {
const { renderer } = this;
const { component } = renderer;
const hoisted = this.node.shouldHoist;
if (this.node.insertionPoint === null) return; // TODO handle shorthand events here?
if (!validCalleeObjects.has(this.node.callee.name)) {
const component_name = hoisted ? `component` : block.alias(`component`);
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
if (this.node.callee.name[0] === '$' && !component.methods.has(this.node.callee.name)) {
component.code.overwrite(
this.node.insertionPoint,
this.node.insertionPoint + 1,
`${component_name}.store.`
);
} else {
component.code.prependRight(
this.node.insertionPoint,
`${component_name}.`
);
}
}
if (this.node.isCustomEvent) {
this.node.args.forEach(arg => {
arg.overwriteThis(this.parent.var);
});
if (this.node.callee && this.node.callee.name === 'this') {
const node = this.node.callee.nodes[0];
component.code.overwrite(node.start, node.end, this.parent.var, {
storeName: true,
contentOnly: true
});
}
}
}
}

@ -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'

@ -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) {

@ -0,0 +1,91 @@
/* generated by Svelte vX.Y.Z */
import { addListener, append, assign, createElement, createText, detachNode, init, insert, noop, proto, removeListener } from "svelte/shared.js";
var methods = {
handleTouchstart() {
// ...
},
handleClick() {
// ...
}
};
function create_main_fragment(component, ctx) {
var div, button0, text1, button1, text3, button2;
function click_handler(event) {
event.preventDefault();
event.stopPropagation();
component.handleClick();
}
function click_handler_1(event) {
component.handleClick();
}
function click_handler_2(event) {
component.handleClick();
}
function touchstart_handler(event) {
component.handleTouchstart();
}
return {
c() {
div = createElement("div");
button0 = createElement("button");
button0.textContent = "click me";
text1 = createText("\n\t");
button1 = createElement("button");
button1.textContent = "or me";
text3 = createText("\n\t");
button2 = createElement("button");
button2.textContent = "or me!";
addListener(button0, "click", click_handler);
addListener(button1, "click", click_handler_1, { once: true, capture: true });
addListener(button2, "click", click_handler_2, true);
addListener(div, "touchstart", touchstart_handler, { passive: true });
},
m(target, anchor) {
insert(target, div, anchor);
append(div, button0);
append(div, text1);
append(div, button1);
append(div, text3);
append(div, button2);
},
p: noop,
d(detach) {
if (detach) {
detachNode(div);
}
removeListener(button0, "click", click_handler);
removeListener(button1, "click", click_handler_1, { once: true, capture: true });
removeListener(button2, "click", click_handler_2, true);
removeListener(div, "touchstart", touchstart_handler, { passive: true });
}
};
}
function SvelteComponent(options) {
init(this, options);
this._state = assign({}, options.data);
this._intro = true;
this._fragment = create_main_fragment(this, this._state);
if (options.target) {
this._fragment.c();
this._mount(options.target, options.anchor);
}
}
assign(SvelteComponent.prototype, proto);
assign(SvelteComponent.prototype, methods);
export default SvelteComponent;

@ -0,0 +1,19 @@
<div on:touchstart="handleTouchstart()">
<button on:click|stopPropagation|preventDefault="handleClick()">click me</button>
<button on:click|once|capture="handleClick()">or me</button>
<button on:click|capture="handleClick()">or me!</button>
</div>
<script>
export default {
methods: {
handleTouchstart() {
// ...
},
handleClick() {
// ...
}
}
};
</script>

@ -15,6 +15,7 @@
"end": 45,
"type": "EventHandler",
"name": "click",
"modifiers": [],
"expression": {
"type": "CallExpression",
"start": 18,

@ -32,6 +32,7 @@ describe("validate", () => {
warnings.push({ code, message, pos, start, end });
},
dev: config.dev,
legacy: config.legacy,
generate: false
});

@ -0,0 +1,15 @@
[{
"message": "The 'passive' and 'preventDefault' modifiers cannot be used together",
"code": "invalid-event-modifier",
"start": {
"line": 1,
"column": 5,
"character": 5
},
"end": {
"line": 1,
"column": 52,
"character": 52
},
"pos": 5
}]

@ -0,0 +1,3 @@
<div on:wheel|passive|preventDefault="handleWheel()">
oops
</div>

@ -0,0 +1,15 @@
[{
"message": "Valid event modifiers are preventDefault, stopPropagation, capture, once or passive",
"code": "invalid-event-modifier",
"start": {
"line": 1,
"column": 8,
"character": 8
},
"end": {
"line": 1,
"column": 36,
"character": 36
},
"pos": 8
}]

@ -0,0 +1 @@
<button on:click|stop|bad="doThat()"></button>

@ -0,0 +1,3 @@
export default {
legacy: true
};

@ -0,0 +1,15 @@
[{
"message": "The 'once' modifier cannot be used in legacy mode",
"code": "invalid-event-modifier",
"start": {
"line": 1,
"column": 8,
"character": 8
},
"end": {
"line": 1,
"column": 37,
"character": 37
},
"pos": 8
}]

@ -0,0 +1 @@
<button on:click|once="handleClick()"></button>

@ -0,0 +1,16 @@
<button on:click|passive="handleClick()"></button>
<div on:touchstart|passive="handleTouchstart()"></div>
<script>
export default {
methods: {
handleTouchstart() {
// ...
},
handleClick() {
// ...
}
}
};
</script>

@ -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
}
]
Loading…
Cancel
Save