diff --git a/src/blockdom/config.ts b/src/blockdom/config.ts index fc18e286..9e918f70 100644 --- a/src/blockdom/config.ts +++ b/src/blockdom/config.ts @@ -16,12 +16,13 @@ export const config = { // this is the main event handler. Every event handler registered with blockdom // will go through this function, giving it the data registered in the block // and the event - mainEventHandler: (data: any, ev: Event) => { + mainEventHandler: (data: any, ev: Event, currentTarget?: EventTarget | null): boolean => { if (typeof data === "function") { data(ev); } else if (Array.isArray(data)) { data = filterOutModifiersFromData(data).data; data[0](data[1], ev); } + return false; }, }; diff --git a/src/blockdom/events.ts b/src/blockdom/events.ts index 95b4dd0b..a3eb5043 100644 --- a/src/blockdom/events.ts +++ b/src/blockdom/events.ts @@ -9,27 +9,32 @@ interface EventHandlerCreator { export function createEventHandler(rawEvent: string): EventHandlerCreator { const eventName = rawEvent.split(".")[0]; + const capture = rawEvent.includes(".capture"); if (rawEvent.includes(".synthetic")) { - return createSyntheticHandler(eventName); + return createSyntheticHandler(eventName, capture); } else { - return createElementHandler(eventName); + return createElementHandler(eventName, capture); } } // Native listener -function createElementHandler(evName: string): EventHandlerCreator { +function createElementHandler(evName: string, capture: boolean = false): EventHandlerCreator { let eventKey = `__event__${evName}`; + if (capture) { + eventKey = `${eventKey}_capture`; + } + function listener(ev: Event) { const currentTarget = ev.currentTarget; if (!currentTarget || !document.contains(currentTarget as HTMLElement)) return; const data = (currentTarget as any)[eventKey]; if (!data) return; - config.mainEventHandler(data, ev); + config.mainEventHandler(data, ev, currentTarget); } function setup(this: HTMLElement, data: any) { (this as any)[eventKey] = data; - this.addEventListener(evName, listener); + this.addEventListener(evName, listener, { capture }); } function update(this: HTMLElement, data: any) { @@ -41,9 +46,12 @@ function createElementHandler(evName: string): EventHandlerCreator { // Synthetic handler: a form of event delegation that allows placing only one // listener per event type. -function createSyntheticHandler(evName: string): EventHandlerCreator { +function createSyntheticHandler(evName: string, capture: boolean = false): EventHandlerCreator { let eventKey = `__event__synthetic_${evName}`; - setupSyntheticEvent(evName, eventKey); + if (capture) { + eventKey = `${eventKey}_capture`; + } + setupSyntheticEvent(evName, eventKey, capture); function setup(this: HTMLElement, data: any) { (this as any)[eventKey] = data; } @@ -55,7 +63,8 @@ function nativeToSyntheticEvent(eventKey: string, event: Event) { while (dom !== null) { const data = (dom as any)[eventKey]; if (data) { - config.mainEventHandler(data, event); + const stopped = config.mainEventHandler(data, event, dom); + if (stopped) return; } dom = (dom as any).parentNode; } @@ -63,10 +72,10 @@ function nativeToSyntheticEvent(eventKey: string, event: Event) { const CONFIGURED_SYNTHETIC_EVENTS: { [event: string]: boolean } = {}; -function setupSyntheticEvent(evName: string, eventKey: string) { +function setupSyntheticEvent(evName: string, eventKey: string, capture: boolean = false) { if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) { return; } - document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event)); + document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event), { capture }); CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true; } diff --git a/src/component/handler.ts b/src/component/handler.ts index 2fb8b5d1..c6acf616 100644 --- a/src/component/handler.ts +++ b/src/component/handler.ts @@ -1,13 +1,26 @@ import { filterOutModifiersFromData } from "../blockdom/config"; -export function mainEventHandler(data: any, ev: Event) { - if (typeof data === "function") { - data(ev); - } else { - data = filterOutModifiersFromData(data).data; - const ctx = data[0]; +export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarget|null) => { + const { data: _data, modifiers } = filterOutModifiersFromData(data); + data = _data; + let stopped = false; + if (modifiers.length) { + let selfMode = false; + const isSelf = ev.target === currentTarget; + for (const mod of modifiers) { + switch (mod) { + case "self": selfMode = true; if (isSelf) { continue; } else { return stopped; }; + case "prevent": if ((selfMode && isSelf) || (!selfMode)) ev.preventDefault(); continue; + case "stop": if ((selfMode && isSelf) || (!selfMode)) ev.stopPropagation() ; stopped = true; continue; + } + } + } + if (typeof data[0] === "function") { + data[0](ev); + } else if (data[0].__owl__) { const method = data[1]; const args = data[2] || []; - ctx.__owl__.component[method](...args, ev); + data[0].__owl__.component[method](...args, ev); } + return stopped; } diff --git a/src/qweb/compiler.ts b/src/qweb/compiler.ts index d74f6923..5c456b8c 100644 --- a/src/qweb/compiler.ts +++ b/src/qweb/compiler.ts @@ -468,12 +468,17 @@ export class QWebCompiler { } } - generateHandlerCode(handler: string, event: string = ""): string { + generateHandlerCode(rawEvent: string, handler: string): string { let args: string = ""; const name: string = handler.replace(/\(.*\)/, function (_args) { args = _args.slice(1, -1); return ""; }); + const modifiers = rawEvent.split(".").slice(1).map(m => `"${m}"`); + let modifiersCode = ""; + if (modifiers.length) { + modifiersCode = `${modifiers.join(",")}, `; + } const isMethodCall = name.match(FNAMEREGEXP); if (isMethodCall) { let handlerFn: string; @@ -484,15 +489,11 @@ export class QWebCompiler { } else { handlerFn = `'${name}'`; } - return `[${event ? `\`${event}\`` + ", " : ""}ctx, ${handlerFn!}]`; + return `[${modifiersCode}ctx, ${handlerFn!}]`; } else { let code = this.captureExpression(handler); code = `{const res = (() => { return ${code} })(); if (typeof res === 'function') { res(e) }}`; - let handlerFn = `(e) => ${code}`; - if (event) { - handlerFn = `[\`${event}\`, ${handlerFn}]`; - } - return handlerFn; + return `[${modifiersCode}(e) => ${code}]`; } } @@ -528,7 +529,7 @@ export class QWebCompiler { // event handlers for (let ev in ast.on) { - const name = this.generateHandlerCode(ast.on[ev]); + const name = this.generateHandlerCode(ev, ast.on[ev]); const idx = block!.insertData(name); attrs[`block-handler-${idx}`] = ev; } diff --git a/tests/blockdom/block_event_handling.test.ts b/tests/blockdom/block_event_handling.test.ts index 14827fc1..d95a8d42 100644 --- a/tests/blockdom/block_event_handling.test.ts +++ b/tests/blockdom/block_event_handling.test.ts @@ -61,6 +61,7 @@ test("simple event handling ", async () => { const [owner, method] = data; owner[method](); } + return false; }; const block = createBlock('
'); diff --git a/tests/components/__snapshots__/basics.test.ts.snap b/tests/components/__snapshots__/basics.test.ts.snap index 9af23e4c..a9216c3c 100644 --- a/tests/components/__snapshots__/basics.test.ts.snap +++ b/tests/components/__snapshots__/basics.test.ts.snap @@ -98,7 +98,7 @@ exports[`basics can be clicked on and updated 1`] = ` return function template(ctx, node, key = \\"\\") { let d1 = ctx['state'].counter; const v1 = ctx['state']; - let d2 = (e) => {const res = (() => { return v1.counter++ })(); if (typeof res === 'function') { res(e) }}; + let d2 = [(e) => {const res = (() => { return v1.counter++ })(); if (typeof res === 'function') { res(e) }}]; return block1([d1, d2]); } }" @@ -598,7 +598,7 @@ exports[`basics rerendering a widget with a sub widget 1`] = ` return function template(ctx, node, key = \\"\\") { let d1 = ctx['state'].counter; const v1 = ctx['state']; - let d2 = (e) => {const res = (() => { return v1.counter++ })(); if (typeof res === 'function') { res(e) }}; + let d2 = [(e) => {const res = (() => { return v1.counter++ })(); if (typeof res === 'function') { res(e) }}]; return block1([d1, d2]); } }" diff --git a/tests/components/__snapshots__/event_handling.test.ts.snap b/tests/components/__snapshots__/event_handling.test.ts.snap index eebd4334..1126700c 100644 --- a/tests/components/__snapshots__/event_handling.test.ts.snap +++ b/tests/components/__snapshots__/event_handling.test.ts.snap @@ -42,7 +42,7 @@ exports[`event handling support for callable expression in event handler 1`] = ` return function template(ctx, node, key = \\"\\") { let d1 = ctx['state'].value; const v1 = ctx['obj']; - let d2 = (e) => {const res = (() => { return v1.onInput })(); if (typeof res === 'function') { res(e) }}; + let d2 = [(e) => {const res = (() => { return v1.onInput })(); if (typeof res === 'function') { res(e) }}]; return block1([d1, d2]); } }" diff --git a/tests/components/__snapshots__/t_component.test.ts.snap b/tests/components/__snapshots__/t_component.test.ts.snap index ec4b3078..7116e703 100644 --- a/tests/components/__snapshots__/t_component.test.ts.snap +++ b/tests/components/__snapshots__/t_component.test.ts.snap @@ -137,7 +137,7 @@ exports[`t-component modifying a sub widget 1`] = ` return function template(ctx, node, key = \\"\\") { let d1 = ctx['state'].counter; const v1 = ctx['state']; - let d2 = (e) => {const res = (() => { return v1.counter++ })(); if (typeof res === 'function') { res(e) }}; + let d2 = [(e) => {const res = (() => { return v1.counter++ })(); if (typeof res === 'function') { res(e) }}]; return block1([d1, d2]); } }" diff --git a/tests/components/__snapshots__/t_on.test.ts.snap b/tests/components/__snapshots__/t_on.test.ts.snap index 03977e39..312a68cf 100644 --- a/tests/components/__snapshots__/t_on.test.ts.snap +++ b/tests/components/__snapshots__/t_on.test.ts.snap @@ -20,7 +20,7 @@ exports[`t-on t-on expression captured in t-foreach 1`] = ` let key1 = ctx['val']; const v1 = ctx['otherState']; const v2 = ctx['iter']; - let d1 = (e) => {const res = (() => { return v1.vals.push(v2+'_'+v2) })(); if (typeof res === 'function') { res(e) }}; + let d1 = [(e) => {const res = (() => { return v1.vals.push(v2+'_'+v2) })(); if (typeof res === 'function') { res(e) }}]; setContextValue(ctx, \\"iter\\", ctx['iter']+1); c_block2[i1] = withKey(block3([d1]), key1); } @@ -50,7 +50,7 @@ exports[`t-on t-on expression in t-foreach 1`] = ` let d2 = ctx['val']+''; const v1 = ctx['otherState']; const v2 = ctx['val']; - let d3 = (e) => {const res = (() => { return v1.vals.push(v2) })(); if (typeof res === 'function') { res(e) }}; + let d3 = [(e) => {const res = (() => { return v1.vals.push(v2) })(); if (typeof res === 'function') { res(e) }}]; c_block2[i1] = withKey(block3([d1, d2, d3]), key1); } let b2 = list(c_block2); @@ -84,7 +84,7 @@ exports[`t-on t-on expression in t-foreach with t-set 1`] = ` const v1 = ctx['otherState']; const v2 = ctx['val']; const v3 = ctx['bossa']; - let d3 = (e) => {const res = (() => { return v1.vals.push(v2+'_'+v3) })(); if (typeof res === 'function') { res(e) }}; + let d3 = [(e) => {const res = (() => { return v1.vals.push(v2+'_'+v3) })(); if (typeof res === 'function') { res(e) }}]; c_block2[i1] = withKey(block3([d1, d2, d3]), key1); } let b2 = list(c_block2); diff --git a/tests/qweb/__snapshots__/event_handling.test.ts.snap b/tests/qweb/__snapshots__/event_handling.test.ts.snap index bf6a05a9..ed328a01 100644 --- a/tests/qweb/__snapshots__/event_handling.test.ts.snap +++ b/tests/qweb/__snapshots__/event_handling.test.ts.snap @@ -256,6 +256,158 @@ exports[`t-on t-on modifiers (native listener) basic support for native listener }" `; +exports[`t-on t-on modifiers (native listener) t-on combined with t-esc 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = [ctx, 'onClick']; + let d2 = ctx['text']; + return block1([d1, d2]); + } +}" +`; + +exports[`t-on t-on modifiers (native listener) t-on combined with t-raw 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = [ctx, 'onClick']; + let b2 = html(ctx['html']); + return block1([d1], [b2]); + } +}" +`; + +exports[`t-on t-on modifiers (native listener) t-on with .capture modifier 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = [\\"capture\\", ctx, 'onCapture']; + let d2 = [ctx, 'doSomething']; + return block1([d1, d2]); + } +}" +`; + +exports[`t-on t-on modifiers (native listener) t-on with empty handler (only modifiers) 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = [\\"prevent\\", (e) => {const res = (() => { return })(); if (typeof res === 'function') { res(e) }}]; + return block1([d1]); + } +}" +`; + +exports[`t-on t-on modifiers (native listener) t-on with prevent and self modifiers (order matters) 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = [\\"prevent\\",\\"self\\", ctx, 'onClick']; + return block1([d1]); + } +}" +`; + +exports[`t-on t-on modifiers (native listener) t-on with prevent and/or stop modifiers 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = [\\"prevent\\", ctx, 'onClickPrevented']; + let d2 = [\\"stop\\", ctx, 'onClickStopped']; + let d3 = [\\"prevent\\",\\"stop\\", ctx, 'onClickPreventedAndStopped']; + return block1([d1, d2, d3]); + } +}" +`; + +exports[`t-on t-on modifiers (native listener) t-on with prevent modifier in t-foreach 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`