Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dirty-meals-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/core': minor
---

fix: don't trigger document and window events for normal events
20 changes: 13 additions & 7 deletions packages/qwik/src/core/client/vnode-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import { _OWNER, _PROPS_HANDLER } from '../shared/utils/constants';
import {
fromCamelToKebabCase,
getEventDataFromHtmlAttribute,
getLoaderScopedEventName,
getScopedEventName,
isHtmlAttributeAnEventName,
} from '../shared/utils/event-names';
import { getFileLocationFromJsx } from '../shared/utils/jsx-filename';
Expand Down Expand Up @@ -660,16 +662,18 @@ export const vnode_diff = (
if (isHtmlAttributeAnEventName(key)) {
const data = getEventDataFromHtmlAttribute(key);
if (data) {
const scope = data[0];
const eventName = data[1];
const [scope, eventName] = data;
const scopedEvent = getScopedEventName(scope, eventName);
const loaderScopedEvent = getLoaderScopedEventName(scope, scopedEvent);

if (eventName) {
vNewNode!.setProp(HANDLER_PREFIX + ':' + scope + ':' + eventName, value);
vNewNode!.setProp(HANDLER_PREFIX + ':' + scopedEvent, value);
if (scope) {
// window and document need attrs so qwik loader can find them
vNewNode!.setAttr(key, '', journal);
}
registerQwikLoaderEvent(eventName);
// register an event for qwik loader (window/document prefixed with '-')
registerQwikLoaderEvent(loaderScopedEvent);
}
}

Expand Down Expand Up @@ -932,9 +936,11 @@ export const vnode_diff = (
const data = getEventDataFromHtmlAttribute(key);
if (data) {
const [scope, eventName] = data;
record(':' + scope + ':' + eventName, value);
// register an event for qwik loader
registerQwikLoaderEvent(eventName);
const scopedEvent = getScopedEventName(scope, eventName);
const loaderScopedEvent = getLoaderScopedEventName(scope, scopedEvent);
record(':' + scopedEvent, value);
// register an event for qwik loader (window/document prefixed with '-')
registerQwikLoaderEvent(loaderScopedEvent);
patchEventDispatch = true;
}
};
Expand Down
9 changes: 9 additions & 0 deletions packages/qwik/src/core/shared/utils/event-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,12 @@ export const getEventDataFromHtmlAttribute = (htmlKey: string): [string, string]
}
return ['document', htmlKey.substring(12)];
};

export const getScopedEventName = (scope: string, eventName: string): string => {
const suffix = ':' + eventName;
return scope ? scope + suffix : suffix;
};

export const getLoaderScopedEventName = (scope: string, scopedEvent: string): string => {
return scope ? '-' + scopedEvent : scopedEvent;
};
32 changes: 10 additions & 22 deletions packages/qwik/src/core/ssr/ssr-render-jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { DEBUG_TYPE, VirtualType } from '../shared/types';
import { isAsyncGenerator } from '../shared/utils/async-generator';
import {
getEventDataFromHtmlAttribute,
getLoaderScopedEventName,
getScopedEventName,
isHtmlAttributeAnEventName,
isPreventDefault,
} from '../shared/utils/event-names';
Expand Down Expand Up @@ -167,13 +169,13 @@ function processJSXNode(

const innerHTML = ssr.openElement(
type,
varPropsToSsrAttrs(jsx.varProps, jsx.constProps, {
toSsrAttrs(jsx.varProps, {
serializationCtx: ssr.serializationCtx,
styleScopedId: options.styleScoped,
key: jsx.key,
toSort: jsx.toSort,
}),
constPropsToSsrAttrs(jsx.constProps, jsx.varProps, {
toSsrAttrs(jsx.constProps, {
serializationCtx: ssr.serializationCtx,
styleScopedId: options.styleScoped,
}),
Expand Down Expand Up @@ -325,22 +327,6 @@ interface SsrAttrsOptions {
toSort?: boolean;
}

export function varPropsToSsrAttrs(
varProps: Record<string, unknown>,
constProps: Record<string, unknown> | null,
options: SsrAttrsOptions
): SsrAttrs | null {
return toSsrAttrs(varProps, options);
}

export function constPropsToSsrAttrs(
constProps: Record<string, unknown> | null,
varProps: Record<string, unknown>,
options: SsrAttrsOptions
): SsrAttrs | null {
return toSsrAttrs(constProps, options);
}

export function toSsrAttrs(
record: Record<string, unknown> | null | undefined,
options: SsrAttrsOptions
Expand Down Expand Up @@ -451,8 +437,10 @@ function addQwikEventToSerializationContext(
// TODO extract window/document too so qwikloader can precisely listen
const data = getEventDataFromHtmlAttribute(key);
if (data) {
const eventName = data[1];
serializationCtx.$eventNames$.add(eventName);
const [scope, eventName] = data;
const scopedEvent = getScopedEventName(scope, eventName);
const loaderScopedEvent = getLoaderScopedEventName(scope, scopedEvent);
serializationCtx.$eventNames$.add(loaderScopedEvent);
serializationCtx.$eventQrls$.add(qrl);
}
}
Expand All @@ -461,8 +449,8 @@ function addPreventDefaultEventToSerializationContext(
serializationCtx: SerializationContext,
key: string
) {
// skip first 15 chars, this is length of the `preventdefault:`
const eventName = key.substring(15);
// skip first 15 chars, this is length of the `preventdefault`, leave the ":"
const eventName = key.substring(14);
if (eventName) {
serializationCtx.$eventNames$.add(eventName);
}
Expand Down
6 changes: 3 additions & 3 deletions packages/qwik/src/core/tests/container.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import type { QRLInternal } from '../shared/qrl/qrl-class';
import { _qrlSync } from '../shared/qrl/qrl.public';
import { TypeIds } from '../shared/serdes/constants';
import { hasClassAttr } from '../shared/utils/scoped-styles';
import { constPropsToSsrAttrs, varPropsToSsrAttrs } from '../ssr/ssr-render-jsx';
import { type SSRContainer } from '../ssr/ssr-types';
import { toSsrAttrs } from '../ssr/ssr-render-jsx';

vi.hoisted(() => {
vi.stubGlobal('QWIK_LOADER_DEFAULT_MINIFIED', 'min');
Expand Down Expand Up @@ -605,12 +605,12 @@ async function toHTML(jsx: JSXOutput): Promise<string> {
}
ssrContainer.openElement(
jsx.type,
varPropsToSsrAttrs(jsx.varProps as any, jsx.constProps, {
toSsrAttrs(jsx.varProps, {
serializationCtx: ssrContainer.serializationCtx,
styleScopedId: null,
key: jsx.key,
}),
constPropsToSsrAttrs(jsx.constProps as any, jsx.varProps, {
toSsrAttrs(jsx.constProps, {
serializationCtx: ssrContainer.serializationCtx,
styleScopedId: null,
})
Expand Down
5 changes: 4 additions & 1 deletion packages/qwik/src/core/tests/render-api.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ const ManyEventsComponent = component$(() => {
onClick$={inlinedQrl(() => {}, 's_click2')}
onBlur$={inlinedQrl(() => {}, 's_blur1')}
on-anotherCustom$={inlinedQrl(() => {}, 's_anotherCustom1')}
document:onFocus$={inlinedQrl(() => {}, 's_documentFocus1')}
window:onClick$={inlinedQrl(() => {}, 's_windowClick1')}
>
click
</button>
Expand Down Expand Up @@ -521,7 +523,8 @@ describe('render api', () => {
qwikLoader: 'module',
});
expect(result.html).toContain(
'(window.qwikevents||(window.qwikevents=[])).push("focus", "-my---custom", "click", "dblclick", "another-custom", "blur")'
'(window.qwikevents||(window.qwikevents=[])).push(' +
'":focus", ":-my---custom", ":click", ":dblclick", "-document:focus", ":another-custom", ":blur", "-window:click")'
);
});
});
Expand Down
49 changes: 39 additions & 10 deletions packages/qwik/src/qwikloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const win = window as unknown as qWindow;
const events = new Set<string>();
const roots = new Set<EventTarget & ParentNode>([doc]);
const symbols: Record<string, unknown> = {};
const windowPrefix = '-window';
const documentPrefix = '-document';

let hasInitialized: number;

Expand Down Expand Up @@ -104,12 +106,12 @@ const dispatch = async (
}
return;
}
const attrValue = element.getAttribute(attrName);
// </DELETE ME LATER>
const qDispatchEvent = (element as QElement).qDispatchEvent;
if (qDispatchEvent) {
return qDispatchEvent(ev, scope);
}
const attrValue = element.getAttribute(attrName);
if (attrValue) {
const container = element.closest(
'[q\\:container]:not([q\\:container=html]):not([q\\:container=text])'
Expand Down Expand Up @@ -207,11 +209,14 @@ const camelToKebab = (str: string) => str.replace(/([A-Z-])/g, (a) => '-' + a.to
*
* @param ev - Browser event.
*/
const processDocumentEvent = async (ev: Event) => {
const processDocumentEvent = async (ev: Event, scope: string) => {
// eslint-disable-next-line prefer-const
let type = camelToKebab(ev.type);
let element = ev.target as Element | null;
broadcast('-document', ev, type);
if (scope === documentPrefix) {
broadcast(documentPrefix, ev, type);
return;
}

while (element && element.getAttribute) {
const results = dispatch(element, '', ev, type);
Expand All @@ -227,7 +232,7 @@ const processDocumentEvent = async (ev: Event) => {
};

const processWindowEvent = (ev: Event) => {
broadcast('-window', ev, camelToKebab(ev.type));
broadcast(windowPrefix, ev, camelToKebab(ev.type));
};

const processReadyStateChange = () => {
Expand All @@ -241,7 +246,7 @@ const processReadyStateChange = () => {
const riC = win.requestIdleCallback ?? win.setTimeout;
riC.bind(win)(() => emitEvent('qidle'));

if (events.has('qvisible')) {
if (events.has(':qvisible')) {
const results = querySelectorAll('[on\\:qvisible]');
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
Expand All @@ -268,23 +273,47 @@ const addEventListener = (
// Keep in sync with ./qwikloader.unit.ts
const kebabToCamel = (eventName: string) => eventName.replace(/-./g, (a) => a[1].toUpperCase());

const processEventName = (event: string) => {
const i = event.indexOf(':');
let scope = '';
let eventName = event;
if (i >= 0) {
const s = event.substring(0, i);
if (s === '' || s === windowPrefix || s === documentPrefix) {
scope = s;
eventName = event.substring(i + 1);
}
}
return { scope, eventName: kebabToCamel(eventName) };
};

const processEventOrNode = (...eventNames: (string | (EventTarget & ParentNode))[]) => {
for (const eventNameOrNode of eventNames) {
if (typeof eventNameOrNode === 'string') {
// If it is string we just add the event to window and each of our roots.
if (!events.has(eventNameOrNode)) {
events.add(eventNameOrNode);
const eventName = kebabToCamel(eventNameOrNode);
roots.forEach((root) => addEventListener(root, eventName, processDocumentEvent, true));
const { scope, eventName } = processEventName(eventNameOrNode);

addEventListener(win, eventName, processWindowEvent, true);
if (scope === windowPrefix) {
addEventListener(win, eventName, processWindowEvent, true);
} else {
roots.forEach((root) =>
addEventListener(root, eventName, (ev) => processDocumentEvent(ev, scope), true)
);
}
}
} else {
// If it is a new root, we also need this root to catch up to all of the document events so far.
if (!roots.has(eventNameOrNode)) {
events.forEach((kebabEventName) => {
const eventName = kebabToCamel(kebabEventName);
addEventListener(eventNameOrNode, eventName, processDocumentEvent, true);
const { scope, eventName } = processEventName(kebabEventName);
addEventListener(
eventNameOrNode,
eventName,
(ev) => processDocumentEvent(ev, scope),
true
);
});

roots.add(eventNameOrNode);
Expand Down
6 changes: 3 additions & 3 deletions packages/qwik/src/qwikloader.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ test('qwikloader script', () => {
const compressed = compress(Buffer.from(qwikLoader), { mode: 1, quality: 11 });
expect([compressed.length, qwikLoader.length]).toMatchInlineSnapshot(`
[
1515,
3308,
1615,
3555,
]
`);

expect(qwikLoader).toMatchInlineSnapshot(
`"const t=document,e=window,n=new Set,o=new Set([t]),r={};let s;const i=(t,e)=>Array.from(t.querySelectorAll(e)),a=t=>{const e=[];return o.forEach(n=>e.push(...i(n,t))),e},c=t=>{w(t),i(t,"[q\\\\:shadowroot]").forEach(t=>{const e=t.shadowRoot;e&&c(e)})},l=t=>t&&"function"==typeof t.then,f=(t,e,n=e.type)=>{a("[on"+t+"\\\\:"+n+"]").forEach(o=>{b(o,t,e,n)})},p=e=>{if(void 0===e._qwikjson_){let n=(e===t.documentElement?t.body:e).lastElementChild;for(;n;){if("SCRIPT"===n.tagName&&"qwik/json"===n.getAttribute("type")){e._qwikjson_=JSON.parse(n.textContent.replace(/\\\\x3C(\\/?script)/gi,"<$1"));break}n=n.previousElementSibling}}},u=(t,e)=>new CustomEvent(t,{detail:e}),b=async(e,n,o,s=o.type)=>{const i="on"+n+":"+s;e.hasAttribute("preventdefault:"+s)&&o.preventDefault(),e.hasAttribute("stoppropagation:"+s)&&o.stopPropagation();const a=e._qc_,c=a&&a.li.filter(t=>t[0]===i);if(c&&c.length>0){for(const t of c){const n=t[1].getFn([e,o],()=>e.isConnected)(o,e),r=o.cancelBubble;l(n)&&await n,r&&o.stopPropagation()}return}const f=e.getAttribute(i),u=e.qDispatchEvent;if(u)return u(o,n);if(f){const n=e.closest("[q\\\\:container]:not([q\\\\:container=html]):not([q\\\\:container=text])"),s=n.getAttribute("q:base"),i=n.getAttribute("q:version")||"unknown",a=n.getAttribute("q:manifest-hash")||"dev",c=new URL(s,t.baseURI);for(const u of f.split("\\n")){const f=new URL(u,c),b=f.href,h=f.hash.replace(/^#?([^?[|]*).*$/,"$1")||"default",_=performance.now();let d,y,g;const m=u.startsWith("#"),w={qBase:s,qManifest:a,qVersion:i,href:b,symbol:h,element:e,reqTime:_};if(m){const e=n.getAttribute("q:instance");d=(t["qFuncs_"+e]||[])[Number.parseInt(h)],d||(y="sync",g=Error("sym:"+h))}else if(h in r)d=r[h];else{q("qsymbol",w);const t=f.href.split("#")[0];try{const e=import(t);p(n),d=(await e)[h],d?r[h]=d:(y="no-symbol",g=Error(\`\${h} not in \${t}\`))}catch(t){y||(y="async"),g=t}}if(!d){q("qerror",{importError:y,error:g,...w}),console.error(g);break}const v=t.__q_context__;if(e.isConnected)try{t.__q_context__=[e,o,f];const n=d(o,e);l(n)&&await n}catch(t){q("qerror",{error:t,...w})}finally{t.__q_context__=v}}}},q=(e,n)=>{t.dispatchEvent(u(e,n))},h=t=>t.replace(/([A-Z-])/g,t=>"-"+t.toLowerCase()),_=async t=>{let e=h(t.type),n=t.target;for(f("-document",t,e);n&&n.getAttribute;){const o=b(n,"",t,e);let r=t.cancelBubble;l(o)&&await o,r||(r=r||t.cancelBubble||n.hasAttribute("stoppropagation:"+t.type)),n=t.bubbles&&!0!==r?n.parentElement:null}},d=t=>{f("-window",t,h(t.type))},y=()=>{const r=t.readyState;if(!s&&("interactive"==r||"complete"==r)&&(o.forEach(c),s=1,q("qinit"),(e.requestIdleCallback??e.setTimeout).bind(e)(()=>q("qidle")),n.has("qvisible"))){const t=a("[on\\\\:qvisible]"),e=new IntersectionObserver(t=>{for(const n of t)n.isIntersecting&&(e.unobserve(n.target),b(n.target,"",u("qvisible",n)))});t.forEach(t=>e.observe(t))}},g=(t,e,n,o=!1)=>{t.addEventListener(e,n,{capture:o,passive:!1})},m=t=>t.replace(/-./g,t=>t[1].toUpperCase()),w=(...t)=>{for(const r of t)if("string"==typeof r){if(!n.has(r)){n.add(r);const t=m(r);o.forEach(e=>g(e,t,_,!0)),g(e,t,d,!0)}}else o.has(r)||(n.forEach(t=>{const e=m(t);g(r,e,_,!0)}),o.add(r))};if(!("__q_context__"in t)){t.__q_context__=0;const r=e.qwikevents;r&&(Array.isArray(r)?w(...r):w("click","input")),e.qwikevents={events:n,roots:o,push:w},g(t,"readystatechange",y),y()}"`
`"const t=document,e=window,n=new Set,o=new Set([t]),r={},s="-window",i="-document";let a;const c=(t,e)=>Array.from(t.querySelectorAll(e)),l=t=>{const e=[];return o.forEach(n=>e.push(...c(n,t))),e},f=t=>{A(t),c(t,"[q\\\\:shadowroot]").forEach(t=>{const e=t.shadowRoot;e&&f(e)})},p=t=>t&&"function"==typeof t.then,u=(t,e,n=e.type)=>{l("[on"+t+"\\\\:"+n+"]").forEach(o=>{h(o,t,e,n)})},b=e=>{if(void 0===e._qwikjson_){let n=(e===t.documentElement?t.body:e).lastElementChild;for(;n;){if("SCRIPT"===n.tagName&&"qwik/json"===n.getAttribute("type")){e._qwikjson_=JSON.parse(n.textContent.replace(/\\\\x3C(\\/?script)/gi,"<$1"));break}n=n.previousElementSibling}}},q=(t,e)=>new CustomEvent(t,{detail:e}),h=async(e,n,o,s=o.type)=>{const i="on"+n+":"+s;e.hasAttribute("preventdefault:"+s)&&o.preventDefault(),e.hasAttribute("stoppropagation:"+s)&&o.stopPropagation();const a=e._qc_,c=a&&a.li.filter(t=>t[0]===i);if(c&&c.length>0){for(const t of c){const n=t[1].getFn([e,o],()=>e.isConnected)(o,e),r=o.cancelBubble;p(n)&&await n,r&&o.stopPropagation()}return}const l=e.qDispatchEvent;if(l)return l(o,n);const f=e.getAttribute(i);if(f){const n=e.closest("[q\\\\:container]:not([q\\\\:container=html]):not([q\\\\:container=text])"),s=n.getAttribute("q:base"),i=n.getAttribute("q:version")||"unknown",a=n.getAttribute("q:manifest-hash")||"dev",c=new URL(s,t.baseURI);for(const l of f.split("\\n")){const f=new URL(l,c),u=f.href,q=f.hash.replace(/^#?([^?[|]*).*$/,"$1")||"default",h=performance.now();let d,m,g;const y=l.startsWith("#"),v={qBase:s,qManifest:a,qVersion:i,href:u,symbol:q,element:e,reqTime:h};if(y){const e=n.getAttribute("q:instance");d=(t["qFuncs_"+e]||[])[Number.parseInt(q)],d||(m="sync",g=Error("sym:"+q))}else if(q in r)d=r[q];else{_("qsymbol",v);const t=f.href.split("#")[0];try{const e=import(t);b(n),d=(await e)[q],d?r[q]=d:(m="no-symbol",g=Error(\`\${q} not in \${t}\`))}catch(t){m||(m="async"),g=t}}if(!d){_("qerror",{importError:m,error:g,...v}),console.error(g);break}const w=t.__q_context__;if(e.isConnected)try{t.__q_context__=[e,o,f];const n=d(o,e);p(n)&&await n}catch(t){_("qerror",{error:t,...v})}finally{t.__q_context__=w}}}},_=(e,n)=>{t.dispatchEvent(q(e,n))},d=t=>t.replace(/([A-Z-])/g,t=>"-"+t.toLowerCase()),m=async(t,e)=>{let n=d(t.type),o=t.target;if(e!==i)for(;o&&o.getAttribute;){const e=h(o,"",t,n);let r=t.cancelBubble;p(e)&&await e,r||(r=r||t.cancelBubble||o.hasAttribute("stoppropagation:"+t.type)),o=t.bubbles&&!0!==r?o.parentElement:null}else u(i,t,n)},g=t=>{u(s,t,d(t.type))},y=()=>{const r=t.readyState;if(!a&&("interactive"==r||"complete"==r)&&(o.forEach(f),a=1,_("qinit"),(e.requestIdleCallback??e.setTimeout).bind(e)(()=>_("qidle")),n.has(":qvisible"))){const t=l("[on\\\\:qvisible]"),e=new IntersectionObserver(t=>{for(const n of t)n.isIntersecting&&(e.unobserve(n.target),h(n.target,"",q("qvisible",n)))});t.forEach(t=>e.observe(t))}},v=(t,e,n,o=!1)=>{t.addEventListener(e,n,{capture:o,passive:!1})},w=t=>t.replace(/-./g,t=>t[1].toUpperCase()),E=t=>{const e=t.indexOf(":");let n="",o=t;if(e>=0){const r=t.substring(0,e);""!==r&&r!==s&&r!==i||(n=r,o=t.substring(e+1))}return{scope:n,eventName:w(o)}},A=(...t)=>{for(const r of t)if("string"==typeof r){if(!n.has(r)){n.add(r);const{scope:t,eventName:i}=E(r);t===s?v(e,i,g,!0):o.forEach(e=>v(e,i,e=>m(e,t),!0))}}else o.has(r)||(n.forEach(t=>{const{scope:e,eventName:n}=E(t);v(r,n,t=>m(t,e),!0)}),o.add(r))};if(!("__q_context__"in t)){t.__q_context__=0;const r=e.qwikevents;r&&(Array.isArray(r)?A(...r):A("click","input")),e.qwikevents={events:n,roots:o,push:A},v(t,"readystatechange",y),y()}"`
);
});

Expand Down