diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index 403b9cce8bee..7c42ab71e2f4 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -6,6 +6,7 @@ import validCalleeObjects from '../../utils/validCalleeObjects'; import reservedNames from '../../utils/reservedNames'; import fixAttributeCasing from '../../utils/fixAttributeCasing'; import { quoteNameIfNecessary, quotePropIfNecessary } from '../../utils/quoteIfNecessary'; +import getEventModifiers from '../../utils/getEventModifiers'; import Compiler from '../Compiler'; import Node from './shared/Node'; import Block from '../dom/Block'; @@ -21,7 +22,45 @@ import mapChildren from './shared/mapChildren'; import { dimensions } from '../../utils/patterns'; // source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7 -const booleanAttributes = new Set('async autocomplete autofocus autoplay border challenge checked compact contenteditable controls default defer disabled formnovalidate frameborder hidden indeterminate ismap loop multiple muted nohref noresize noshade novalidate nowrap open readonly required reversed scoped scrolling seamless selected sortable spellcheck translate'.split(' ')); +const booleanAttributes = new Set([ + 'async', + 'autocomplete', + 'autofocus', + 'autoplay', + 'border', + 'challenge', + 'checked', + 'compact', + 'contenteditable', + 'controls', + 'default', + 'defer', + 'disabled', + 'formnovalidate', + 'frameborder', + 'hidden', + 'indeterminate', + 'ismap', + 'loop', + 'multiple', + 'muted', + 'nohref', + 'noresize', + 'noshade', + 'novalidate', + 'nowrap', + 'open', + 'readonly', + 'required', + 'reversed', + 'scoped', + 'scrolling', + 'seamless', + 'selected', + 'sortable', + 'spellcheck', + 'translate' +]); export default class Element extends Node { type: 'Element'; @@ -612,14 +651,19 @@ export default class Element extends Node { const target = handler.shouldHoist ? 'this' : this.var; + // split by | to remove stop, prevent, pass, etc. + const eventName = handler.name.split('|')[0]; + // get a name for the event handler that is globally unique - // if hoisted, locally unique otherwise + // if hoisted, locally unique otherwise. const handlerName = (handler.shouldHoist ? compiler : block).getUniqueName( - `${handler.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler` + `${eventName.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler` ); const component = block.alias('component'); // can't use #component, might be hoisted + const { bodyModifiers, optionModifiers } = getEventModifiers(handler.name); + // create the handler body const handlerBody = deindent` ${handler.shouldHoist && ( @@ -627,7 +671,7 @@ export default class Element extends Node { ? `const { ${[handler.usesComponent && 'component', handler.usesContext && 'ctx'].filter(Boolean).join(', ')} } = ${target}._svelte;` : null )} - + ${bodyModifiers} ${handler.snippet ? handler.snippet : `${component}.fire("${handler.name}", event);`} @@ -659,11 +703,11 @@ export default class Element extends Node { } block.builders.hydrate.addLine( - `@addListener(${this.var}, "${handler.name}", ${handlerName});` + `@addListener(${this.var}, "${eventName}", ${handlerName}, ${JSON.stringify(optionModifiers)});` ); block.builders.destroy.addLine( - `@removeListener(${this.var}, "${handler.name}", ${handlerName});` + `@removeListener(${this.var}, "${eventName}", ${handlerName}, ${JSON.stringify(optionModifiers)});` ); } }); diff --git a/src/shared/dom.js b/src/shared/dom.js index 03aaf8aaeb39..0fb8f78f95fe 100644 --- a/src/shared/dom.js +++ b/src/shared/dom.js @@ -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) { diff --git a/src/utils/getEventModifiers.ts b/src/utils/getEventModifiers.ts new file mode 100644 index 000000000000..554a2c79af21 --- /dev/null +++ b/src/utils/getEventModifiers.ts @@ -0,0 +1,36 @@ +import EventHandler from '../compile/nodes/EventHandler'; +import deindent from '../utils/deindent'; + +export default function getEventModifiers(handlerName: String) { + // Ignore first element because it's the event name, i.e. click + let modifiers = handlerName.split('|').slice(1); + + let eventModifiers = modifiers.reduce((acc, m) => { + if (m === 'stopPropagation') + acc.bodyModifiers += 'event.stopPropagation();\n'; + else if (m === 'preventDefault') + acc.bodyModifiers += 'event.preventDefault();\n'; + else if (m === 'capture') + acc.optionModifiers[m] = true; + else if (m === 'once') + acc.optionModifiers[m] = true; + else if (m === 'passive') + acc.optionModifiers[m] = true; + + return acc; + }, { + bodyModifiers: '', + optionModifiers: { + capture: false, + once: false, + passive: false, + } + }); + + if (eventModifiers.bodyModifiers !== '') + eventModifiers.bodyModifiers = deindent` + ${eventModifiers.bodyModifiers} + `; + + return eventModifiers; +} \ No newline at end of file diff --git a/src/validate/html/validateEventHandler.ts b/src/validate/html/validateEventHandler.ts index df499a9fd595..f1df2f741c39 100644 --- a/src/validate/html/validateEventHandler.ts +++ b/src/validate/html/validateEventHandler.ts @@ -6,6 +6,8 @@ import { Node } from '../../interfaces'; const validBuiltins = new Set(['set', 'fire', 'destroy']); +const validModifiers = new Set(['stopPropagation', 'preventDefault', 'capture', 'once', 'passive']); + export default function validateEventHandlerCallee( validator: Validator, attribute: Node, @@ -22,6 +24,17 @@ export default function validateEventHandlerCallee( }); } + const modifiers = attribute.name.split('|').slice(1); + if ( + modifiers.length > 0 && + modifiers.filter(m => !validModifiers.has(m)).length > 0 + ) { + validator.error(attribute, { + code: 'invalid-event-modifiers', + message: `Valid event modifiers are ${[...validModifiers].join(', ')}.` + }); + } + const { name } = flattenReference(callee); if (validCalleeObjects.has(name) || name === 'options') return; diff --git a/test/js/samples/input-files/expected-bundle.js b/test/js/samples/input-files/expected-bundle.js index 097dc9e1b589..fa515f700cad 100644 --- a/test/js/samples/input-files/expected-bundle.js +++ b/test/js/samples/input-files/expected-bundle.js @@ -17,12 +17,12 @@ function createElement(name) { return document.createElement(name); } -function addListener(node, event, handler) { - node.addEventListener(event, handler, false); +function addListener(node, event, handler, options) { + node.addEventListener(event, handler, options); } -function removeListener(node, event, handler) { - node.removeEventListener(event, handler, false); +function removeListener(node, event, handler, options) { + node.removeEventListener(event, handler, options); } function setAttribute(node, attribute, value) { diff --git a/test/js/samples/input-range/expected-bundle.js b/test/js/samples/input-range/expected-bundle.js index 50ba725fa96a..b159277bf6c7 100644 --- a/test/js/samples/input-range/expected-bundle.js +++ b/test/js/samples/input-range/expected-bundle.js @@ -17,12 +17,12 @@ function createElement(name) { return document.createElement(name); } -function addListener(node, event, handler) { - node.addEventListener(event, handler, false); +function addListener(node, event, handler, options) { + node.addEventListener(event, handler, options); } -function removeListener(node, event, handler) { - node.removeEventListener(event, handler, false); +function removeListener(node, event, handler, options) { + node.removeEventListener(event, handler, options); } function setAttribute(node, attribute, value) { diff --git a/test/js/samples/input-without-blowback-guard/expected-bundle.js b/test/js/samples/input-without-blowback-guard/expected-bundle.js index 5bf43ec519c4..b50393ef21fc 100644 --- a/test/js/samples/input-without-blowback-guard/expected-bundle.js +++ b/test/js/samples/input-without-blowback-guard/expected-bundle.js @@ -17,12 +17,12 @@ function createElement(name) { return document.createElement(name); } -function addListener(node, event, handler) { - node.addEventListener(event, handler, false); +function addListener(node, event, handler, options) { + node.addEventListener(event, handler, options); } -function removeListener(node, event, handler) { - node.removeEventListener(event, handler, false); +function removeListener(node, event, handler, options) { + node.removeEventListener(event, handler, options); } function setAttribute(node, attribute, value) { diff --git a/test/js/samples/media-bindings/expected-bundle.js b/test/js/samples/media-bindings/expected-bundle.js index ec9116f544ca..17e820a067cf 100644 --- a/test/js/samples/media-bindings/expected-bundle.js +++ b/test/js/samples/media-bindings/expected-bundle.js @@ -17,12 +17,12 @@ function createElement(name) { return document.createElement(name); } -function addListener(node, event, handler) { - node.addEventListener(event, handler, false); +function addListener(node, event, handler, options) { + node.addEventListener(event, handler, options); } -function removeListener(node, event, handler) { - node.removeEventListener(event, handler, false); +function removeListener(node, event, handler, options) { + node.removeEventListener(event, handler, options); } function timeRangesToArray(ranges) { diff --git a/test/validator/samples/event-modifiers-invalid/errors.json b/test/validator/samples/event-modifiers-invalid/errors.json new file mode 100644 index 000000000000..af1cca83e46e --- /dev/null +++ b/test/validator/samples/event-modifiers-invalid/errors.json @@ -0,0 +1,15 @@ +[{ + "message": "Valid event modifiers are stopPropagation, preventDefault, capture, once, passive.", + "code": "invalid-event-modifiers", + "start": { + "line": 1, + "column": 8, + "character": 8 + }, + "end": { + "line": 1, + "column": 36, + "character": 36 + }, + "pos": 8 +}] \ No newline at end of file diff --git a/test/validator/samples/event-modifiers-invalid/input.html b/test/validator/samples/event-modifiers-invalid/input.html new file mode 100644 index 000000000000..27dacdbc2d3a --- /dev/null +++ b/test/validator/samples/event-modifiers-invalid/input.html @@ -0,0 +1 @@ + \ No newline at end of file