From 82b1b75afe3eb0ef3922abe707c6acc3e2a6d188 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 28 Oct 2018 13:46:17 -0400 Subject: [PATCH] implement event modifiers --- src/compile/nodes/Element.ts | 12 +-- src/compile/nodes/EventHandler.ts | 4 + .../render-dom/wrappers/Element/index.ts | 32 +++++-- test/js/samples/event-modifiers/expected.js | 91 +++++++++++++++++++ test/js/samples/event-modifiers/input.html | 19 ++++ test/parser/samples/event-handler/output.json | 1 + 6 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 test/js/samples/event-modifiers/expected.js create mode 100644 test/js/samples/event-modifiers/input.html diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index fad73bce93..890b384f83 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -595,12 +595,7 @@ export default class Element extends Node { if (modifier === 'passive') { if (passiveEvents.has(handler.name)) { - const usesEvent = ( - handler.callee.name === 'event' || - handler.args.some(x => x.usesEvent) - ); - - if (!usesEvent) { + 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` @@ -623,6 +618,11 @@ export default class Element extends Node { }); } }); + + if (passiveEvents.has(handler.name) && !handler.usesEventObject) { + // touch/wheel events should be passive by default + handler.modifiers.add('passive'); + } }); } diff --git a/src/compile/nodes/EventHandler.ts b/src/compile/nodes/EventHandler.ts index 00b24f21f5..9676923d81 100644 --- a/src/compile/nodes/EventHandler.ts +++ b/src/compile/nodes/EventHandler.ts @@ -16,6 +16,7 @@ export default class EventHandler extends Node { usesComponent: boolean; usesContext: boolean; + usesEventObject: boolean; isCustomEvent: boolean; shouldHoist: boolean; @@ -42,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; }); @@ -58,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? } diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts index 58b215889b..47c1bc57f3 100644 --- a/src/compile/render-dom/wrappers/Element/index.ts +++ b/src/compile/render-dom/wrappers/Element/index.ts @@ -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});` + ); + } } }); } diff --git a/test/js/samples/event-modifiers/expected.js b/test/js/samples/event-modifiers/expected.js new file mode 100644 index 0000000000..e0b355f216 --- /dev/null +++ b/test/js/samples/event-modifiers/expected.js @@ -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; \ No newline at end of file diff --git a/test/js/samples/event-modifiers/input.html b/test/js/samples/event-modifiers/input.html new file mode 100644 index 0000000000..4726d96c70 --- /dev/null +++ b/test/js/samples/event-modifiers/input.html @@ -0,0 +1,19 @@ +
+ + + +
+ + \ No newline at end of file diff --git a/test/parser/samples/event-handler/output.json b/test/parser/samples/event-handler/output.json index c297572b16..617c2fde71 100644 --- a/test/parser/samples/event-handler/output.json +++ b/test/parser/samples/event-handler/output.json @@ -15,6 +15,7 @@ "end": 45, "type": "EventHandler", "name": "click", + "modifiers": [], "expression": { "type": "CallExpression", "start": 18,