diff --git a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts index 7e763d6760..3ca70dff65 100644 --- a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts @@ -10,6 +10,9 @@ const sharedTemplateElemForParsing = document.createElement('template'); const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g'); const preventDefaultEvents: { [eventType: string]: boolean } = { submit: true }; const rootComponentsPendingFirstRender: { [componentId: number]: LogicalElement } = {}; +const internalAttributeNamePrefix = '__internal_'; +const eventPreventDefaultAttributeNamePrefix = 'preventDefault_'; +const eventStopBubblingAttributeNamePrefix = 'stopBubbling_'; export class BrowserRenderer { private eventDelegator: EventDelegator; @@ -310,8 +313,30 @@ export class BrowserRenderer { return this.tryApplyValueProperty(batch, element, attributeFrame); case 'checked': return this.tryApplyCheckedProperty(batch, element, attributeFrame); - default: + default: { + if (attributeName.startsWith(internalAttributeNamePrefix)) { + this.applyInternalAttribute(batch, element, attributeName.substring(internalAttributeNamePrefix.length), attributeFrame); + return true; + } return false; + } + } + } + + private applyInternalAttribute(batch: RenderBatch, element: Element, internalAttributeName: string, attributeFrame: RenderTreeFrame | null) { + const attributeValue = attributeFrame ? batch.frameReader.attributeValue(attributeFrame) : null; + + if (internalAttributeName.startsWith(eventStopBubblingAttributeNamePrefix)) { + // Stop bubbling + const eventName = internalAttributeName.substring(eventStopBubblingAttributeNamePrefix.length); + this.eventDelegator.setStopBubbling(element, eventName, attributeValue !== null); + } else if (internalAttributeName.startsWith(eventPreventDefaultAttributeNamePrefix)) { + // Prevent default + const eventName = internalAttributeName.substring(eventPreventDefaultAttributeNamePrefix.length); + this.eventDelegator.setPreventDefault(element, eventName, attributeValue !== null); + } else { + // The prefix makes this attribute name reserved, so any other usage is disallowed + throw new Error(`Unsupported internal attribute '${internalAttributeName}'`); } } diff --git a/src/Components/Web.JS/src/Rendering/EventDelegator.ts b/src/Components/Web.JS/src/Rendering/EventDelegator.ts index fbede2f2b1..10888b7a70 100644 --- a/src/Components/Web.JS/src/Rendering/EventDelegator.ts +++ b/src/Components/Web.JS/src/Rendering/EventDelegator.ts @@ -74,6 +74,16 @@ export class EventDelegator { } } + public setStopBubbling(element: Element, eventName: string, value: boolean) { + const infoForElement = this.getEventHandlerInfosForElement(element, true)!; + infoForElement.stopBubbling(eventName, value); + } + + public setPreventDefault(element: Element, eventName: string, value: boolean) { + const infoForElement = this.getEventHandlerInfosForElement(element, true)!; + infoForElement.preventDefault(eventName, value); + } + private onGlobalEvent(evt: Event) { if (!(evt.target instanceof Element)) { return; @@ -188,6 +198,8 @@ class EventHandlerInfosForElement { // that name at any one time. // So to keep things simple, only track one EventHandlerInfo per (element, eventName) private handlers: { [eventName: string]: EventHandlerInfo } = {}; + private preventDefaultFlags: { [eventName: string]: boolean } | null = null; + private stopBubblingFlags: { [eventName: string]: boolean } | null = null; public getHandler(eventName: string): EventHandlerInfo | null { return this.handlers.hasOwnProperty(eventName) ? this.handlers[eventName] : null; @@ -201,8 +213,28 @@ class EventHandlerInfosForElement { delete this.handlers[eventName]; } + public preventDefault(eventName: string, setValue: boolean | null): boolean { + if (setValue !== null) { + this.preventDefaultFlags = this.preventDefaultFlags || {}; + this.preventDefaultFlags[eventName] = setValue; + } + + return this.preventDefaultFlags ? this.preventDefaultFlags[eventName] : false; + } + + public stopBubbling(eventName: string, setValue: boolean | null): boolean { + if (setValue !== null) { + this.stopBubblingFlags = this.stopBubblingFlags || {}; + this.stopBubblingFlags[eventName] = setValue; + } + + return this.stopBubblingFlags ? this.stopBubblingFlags[eventName] : false; + } + public isEmpty(): boolean { - return Object.getOwnPropertyNames(this.handlers).length === 0; + return Object.getOwnPropertyNames(this.handlers).length === 0 + && (!this.preventDefaultFlags || Object.getOwnPropertyNames(this.preventDefaultFlags).length === 0) + && (!this.stopBubblingFlags || Object.getOwnPropertyNames(this.stopBubblingFlags).length === 0); } }