diff --git a/schemas/browserlib/extract-events.json b/schemas/browserlib/extract-events.json index 8d6dbfcf..2870718e 100644 --- a/schemas/browserlib/extract-events.json +++ b/schemas/browserlib/extract-events.json @@ -15,6 +15,7 @@ "items": { "$ref": "../common.json#/$defs/interface" } }, "bubbles": { "type": "boolean" }, + "cancelable": { "type": "boolean" }, "isExtension": { "type": "boolean" }, "href": { "$ref": "../common.json#/$defs/url" }, "src": { diff --git a/schemas/postprocessing/events.json b/schemas/postprocessing/events.json index 219192de..223e01f2 100644 --- a/schemas/postprocessing/events.json +++ b/schemas/postprocessing/events.json @@ -32,6 +32,7 @@ "href": { "$ref": "../common.json#/$defs/url" } } }, + "cancelable": { "type": "boolean" }, "extendedIn": { "type": "array", "items": { diff --git a/src/browserlib/extract-events.mjs b/src/browserlib/extract-events.mjs index 96064a7a..9838d7d2 100644 --- a/src/browserlib/extract-events.mjs +++ b/src/browserlib/extract-events.mjs @@ -3,10 +3,6 @@ import extractWebIdl from './extract-webidl.mjs'; import {parse} from "../../node_modules/webidl2/index.js"; import getAbsoluteUrl from './get-absolute-url.mjs'; -const isSameEvent = (e1, e2) => e1.type === e2.type && - ((e1.href && e1.href === e2.href ) || - (e1.targets?.sort()?.join("|") === e2.targets?.sort()?.join("|"))); - const singlePage = !document.querySelector('[data-reffy-page]'); const href = el => el?.getAttribute("id") ? getAbsoluteUrl(el, {singlePage}) : null; @@ -37,6 +33,15 @@ export default function (spec) { return acc; }, {}); + function isSameEvent(e1, e2) { + const res = e1.type === e2.type && + ((e1.href && e1.href === e2.href ) || + (e1.targets?.sort()?.join("|") === e2.targets?.sort()?.join("|"))); + if (res && e1.cancelable !== undefined && e2.cancelable !== undefined && e1.cancelable !== e2.cancelable) { + console.error(`[reffy] Found two occurrences of same event with different "cancelable" properties in ${spec.title}: type=${e1.type} targets=${e1.targets.join(', ')} href=${e1.href}`); + } + return res; + } function fromEventElementToTargetInterfaces(eventEl) { if (!eventEl) return; @@ -74,6 +79,8 @@ export default function (spec) { // Useful e.g. for pointerevents const bubblingInfoColumn = [...table.querySelectorAll("thead th")] .findIndex(n => n.textContent.trim().match(/^bubbl/i)); + const cancelableInfoColumn = [...table.querySelectorAll("thead th")] + .findIndex(n => n.textContent.trim().match(/^cancel/i)); const interfaceColumn = [...table.querySelectorAll("thead th")] .findIndex(n => n.textContent.trim().match(/^(dom )?interface/i)); const targetsColumn = [...table.querySelectorAll("thead th")] @@ -115,6 +122,9 @@ export default function (spec) { if (bubblingInfoColumn >= 0) { event.bubbles = tr.querySelector(`td:nth-child(${bubblingInfoColumn + 1})`)?.textContent?.trim() === "Yes"; } + if (cancelableInfoColumn >= 0) { + event.cancelable = !!tr.querySelector(`td:nth-child(${cancelableInfoColumn + 1})`)?.textContent?.trim().match(/(yes)|✓|(varies)/i); + } if (interfaceColumn >= 0) { event.interface = tr.querySelector(`td:nth-child(${interfaceColumn + 1}) a`)?.textContent ?? @@ -134,14 +144,17 @@ export default function (spec) { } const eventTypeRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim().match(/^type/i)); const bubblingInfoRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim() === "Bubbles"); + const cancelableInfoRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim() === "Cancelable"); const interfaceRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim().match(/^interface/i)); const eventName = table.querySelector(`tr:nth-child(${eventTypeRow + 1}) td:nth-child(2)`)?.textContent?.trim(); const bubblesCell = table.querySelector(`tr:nth-child(${bubblingInfoRow + 1}) td:nth-child(2)`); const bubbles = bubblesCell ? bubblesCell.textContent.trim() === "Yes" : null; + const cancelableCell = table.querySelector(`tr:nth-child(${cancelableInfoRow + 1}) td:nth-child(2)`); + const cancelable = cancelableCell ? cancelableCell.textContent.trim() === "Yes" : null; const iface = table.querySelector(`tr:nth-child(${interfaceRow + 1}) td:nth-child(2)`)?.textContent?.trim(); if (eventName) { events.push({ - type: eventName, interface: iface, bubbles, + type: eventName, interface: iface, bubbles, cancelable, src: { format: "css definition table", href: href(table.closest('*[id]')) }, href: href(table.closest('*[id]')) }); } @@ -208,6 +221,7 @@ export default function (spec) { phrasing = "fire functional event"; } } + if (phrasing) { const name = m.groups.eventName; let newEvent = true; @@ -263,6 +277,17 @@ export default function (spec) { } } } + if (event.bubbles === undefined && event.cancelable === undefined) { + if (parsedText.match(/bubbles and cancelable attributes/)) { + if (parsedText.match(/true/)) { + event.bubbles = true; + event.cancelable = true; + } else if (parsedText.match(/false/)) { + event.bubbles = false; + event.cancelable = false; + } + } + } if (event.bubbles === undefined) { if (parsedText.match(/bubbles attribute/)) { if (parsedText.match(/true/)) { @@ -276,6 +301,19 @@ export default function (spec) { event.bubbles = false; } } + if (event.cancelable === undefined) { + if (parsedText.match(/cancelable attribute/)) { + if (parsedText.match(/true/)) { + event.cancelable = true; + } else if (parsedText.match(/false/)) { + event.cancelable = false; + } + } else if (parsedText.match(/not cancelable/) || parsedText.match(/not be cancelable/)) { + event.cancelable = false; + } else if (parsedText.match(/cancelable/)) { + event.cancelable = true; + } + } if (newEvent) { events.push(event); } @@ -329,13 +367,18 @@ export default function (spec) { }; // CSS Animations & Transitions uses dt/dd to describe events // and uses a ul in the dd to describe bubbling behavior - let bubbles, iface; + let bubbles, iface, cancelable; if (container.tagName === "DT") { const bubbleItem = [...container.nextElementSibling.querySelectorAll("li")] .find(li => li.textContent.startsWith("Bubbles:")); if (bubbleItem) { bubbles = !!bubbleItem.textContent.match(/yes/i); } + const cancelableItem = [...container.nextElementSibling.querySelectorAll("li")] + .find(li => li.textContent.startsWith("Cancelable:")); + if (cancelableItem) { + cancelable = !!cancelableItem.textContent.match(/yes/i); + } // CSS Animation & Transitions document the event in the heading // of the section where the definitions are located let currentEl = container.parentNode; @@ -356,6 +399,7 @@ export default function (spec) { event.interface = iface; } event.bubbles = bubbles; + event.cancelable = cancelable; events.push(event); if (!iface) { console.error(`[reffy] No interface hint found for event definition ${event.type} in ${spec.title}`); @@ -370,6 +414,9 @@ export default function (spec) { if (bubbles !== undefined) { ev.bubbles = bubbles; } + if (cancelable !== undefined) { + ev.cancelable = cancelable; + } } }); return events diff --git a/tests/extract-events.js b/tests/extract-events.js index fec8fd4a..848f11e0 100644 --- a/tests/extract-events.js +++ b/tests/extract-events.js @@ -8,6 +8,7 @@ const defaultResults = (format, {successIface} = {successIface: "SuccessEvent"}) { type: "success", interface: successIface, + cancelable: true, targets: [ "Example" ], bubbles: true, href: "about:blank#success", @@ -19,6 +20,7 @@ const defaultResults = (format, {successIface} = {successIface: "SuccessEvent"}) { type: "error", interface: "ErrorEvent", + cancelable: false, targets: [ "Example" ], bubbles: false, href: "about:blank#error", @@ -39,11 +41,11 @@ const tests = [ title: "extracts events from a summary table with data spread across columns, completed by an IDL fragment", html: ` - + - - + +
Event typeInterfaceBubbles
Event typeInterfaceBubblesCancelable
successSuccessEventYes
errorErrorEventNo
successSuccessEventYes
errorErrorEventNoNo
${defaultIdl}`, res: defaultResults("summary table") }, @@ -54,6 +56,7 @@ const tests = [ Typesuccess BubblesYes +CancelableYes InterfaceSuccessEvent

error Event

@@ -61,6 +64,7 @@ const tests = [ Typeerror Bubblesno +Cancelableno InterfaceErrorEvent ${defaultIdl}`, @@ -73,24 +77,26 @@ ${defaultIdl}`,
success
error
`, res: defaultResults("dfn", {successIface: "ErrorEvent"}) }, { title: "extracts events from an event mentioned in a 'Fire an event' context, completed by an IDL fragment", - html: `

Fire an event named success using SuccessEvent with the bubbles attribute initialized to true

-

Fire an event named error using ErrorEvent with the bubbles attribute initialized to false

${defaultIdl}`, + html: `

Fire an event named success using SuccessEvent with the bubbles and cancelable attributes initialized to true

+

Fire an event named error using ErrorEvent with the bubbles attribute initialized to false and the cancelable attribute set to false

${defaultIdl}`, res: defaultResults("fire an event phrasing") }, { title: "extracts events from an event mentioned in a 'Fire Functional Event' context, completed by an IDL fragment", - html: `

Fire Functional Event success with the bubbles attribute initialized to true

-

Fire an event named error using ErrorEvent with the bubbles attribute initialized to false

${defaultIdl}`, + html: `

Fire Functional Event success with the bubbles attribute initialized to true and the cancelable attribute initialized to true

+

Fire an event named error using ErrorEvent with the bubbles and cancelable attributes initialized to false

${defaultIdl}`, res: defaultResults("fire an event phrasing", {successIface: "ExtendableEvent"}) }, { @@ -161,8 +167,8 @@ ${defaultIdl}`, { title: "does not get confused by asides", html: `

Fire an event - named successInfo using SuccessEvent with the bubbles attribute initialized to true.

-

Fire an event named error using ErrorEvent with the bubbles attribute initialized to false

+ named successInfo using SuccessEvent with the bubbles and cancelable attributes initialized to true.

+

Fire an event named error using ErrorEvent with the bubbles attribute initialized to false and must not be cancelable

${defaultIdl}`, res: defaultResults("fire an event phrasing") }