toggle-event-source-polyfill 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/index.d.ts +3 -0
- package/index.js +2 -0
- package/package.json +68 -0
- package/toggle-event-source.d.ts +12 -0
- package/toggle-event-source.js +357 -0
- package/toggle-event-source.min.js +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeroen Zwartepoorte
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# ToggleEvent.source Polyfill
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jpzwarte/toggle-event-source-polyfill/actions/workflows/test.yml)
|
|
4
|
+
|
|
5
|
+
This polyfills [`ToggleEvent.source`](https://developer.mozilla.org/en-US/docs/Web/API/ToggleEvent/source):
|
|
6
|
+
the element which caused a popover or `<dialog>` to be shown or hidden.
|
|
7
|
+
|
|
8
|
+
```html
|
|
9
|
+
<button commandfor="my-popover" command="toggle-popover">Toggle</button>
|
|
10
|
+
<div popover id="my-popover">I'm a popover!</div>
|
|
11
|
+
<script>
|
|
12
|
+
document.getElementById("my-popover").addEventListener("toggle", (event) => {
|
|
13
|
+
// The button which opened or closed the popover, or null if it was
|
|
14
|
+
// toggled programmatically.
|
|
15
|
+
console.log(event.source);
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`source` landed well after popovers, dialogs and invoker commands themselves
|
|
21
|
+
(Chrome 140, Firefox 145, Safari 26.5), so it is missing in plenty of browsers
|
|
22
|
+
which otherwise support everything you would use it with.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### With npm
|
|
27
|
+
|
|
28
|
+
If you're using npm, you only need to import the package, like so:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import "toggle-event-source-polyfill";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This will automatically apply the polyfill if required.
|
|
35
|
+
|
|
36
|
+
If you'd like to manually apply the polyfill, you can instead import the
|
|
37
|
+
`isSupported` and `apply` functions directly from the `./toggle-event-source.js`
|
|
38
|
+
file, which is mapped to `/fn`:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { isSupported, apply } from "toggle-event-source-polyfill/fn";
|
|
42
|
+
if (!isSupported()) apply();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
An `isPolyfilled` function is also available, to detect if it has been polyfilled:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { isSupported, isPolyfilled, apply } from "toggle-event-source-polyfill/fn";
|
|
49
|
+
if (!isSupported() && !isPolyfilled()) apply();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Alternatively, if you're not using a package manager, you can use the `unpkg` script:
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<!-- polyfill automatically -->
|
|
56
|
+
<script
|
|
57
|
+
type="module"
|
|
58
|
+
async
|
|
59
|
+
src="https://unpkg.com/toggle-event-source-polyfill@latest/toggle-event-source.min.js"
|
|
60
|
+
></script>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
A source is reported for popovers and dialogs toggled by:
|
|
66
|
+
|
|
67
|
+
- a [`command`/`commandfor`](https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API) button
|
|
68
|
+
- a [`popovertarget`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#popovertarget) button
|
|
69
|
+
- `showPopover({ source })` and `togglePopover({ source })`
|
|
70
|
+
|
|
71
|
+
As per spec, `hidePopover()` and the `<dialog>` `show()`, `showModal()`,
|
|
72
|
+
`close()` and `requestClose()` methods have no source, and neither does a
|
|
73
|
+
popover closed by light dismiss or by a close request - in all of those cases
|
|
74
|
+
`event.source` is `null`.
|
|
75
|
+
|
|
76
|
+
The polyfill also makes `new ToggleEvent(type, { source })` accept a source.
|
|
77
|
+
|
|
78
|
+
### Together with a `command`/`commandfor` polyfill
|
|
79
|
+
|
|
80
|
+
The polyfill reads `commandForElement`, `command`, `popoverTargetElement` and
|
|
81
|
+
`popoverTargetAction` off the button, so it works both with native invoker
|
|
82
|
+
commands and with a polyfill such as
|
|
83
|
+
[invokers-polyfill](https://github.com/keithamus/invokers-polyfill) providing
|
|
84
|
+
them. No cooperation between the two is needed, but **apply this polyfill
|
|
85
|
+
first**:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import "toggle-event-source-polyfill";
|
|
89
|
+
import "invokers-polyfill";
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
A `command`/`commandfor` polyfill hides popovers and opens dialogs by calling
|
|
93
|
+
`hidePopover()`, `showModal()`, `close()` and `requestClose()`, none of which
|
|
94
|
+
carry a source of their own. This polyfill recognises those calls as belonging
|
|
95
|
+
to the click it is handling, which requires its click listener to run first. In
|
|
96
|
+
the other order the `toggle` event still reports the right source, but
|
|
97
|
+
`beforetoggle` - which those methods fire synchronously, before this polyfill
|
|
98
|
+
has seen the click - reports `null`.
|
|
99
|
+
|
|
100
|
+
## Limitations
|
|
101
|
+
|
|
102
|
+
- The polyfill hands the source to `toggle`/`beforetoggle` listeners from a
|
|
103
|
+
capture phase listener on the popover's root node, so a capture phase
|
|
104
|
+
listener registered on that root _before_ the polyfill is applied will see
|
|
105
|
+
`event.source` as `null`. Apply the polyfill as early as possible.
|
|
106
|
+
- Setting a source does not make that element the popover's
|
|
107
|
+
[implicit anchor element](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning),
|
|
108
|
+
which cannot be polyfilled.
|
|
109
|
+
- Buttons inside a closed shadow root that was created before the polyfill was
|
|
110
|
+
applied are not detected as sources.
|
|
111
|
+
- If a popover polyfill is used, it must be loaded _before_ this polyfill, as it
|
|
112
|
+
replaces the popover methods this polyfill wraps.
|
|
113
|
+
- If an invoker's activation is cancelled (by calling `preventDefault()` on its
|
|
114
|
+
`command` event, for example) and the same popover or dialog is later changed
|
|
115
|
+
in the same direction without a source, that earlier invoker may be reported
|
|
116
|
+
as the source.
|
|
117
|
+
|
|
118
|
+
## Demo
|
|
119
|
+
|
|
120
|
+
`npm start` opens the demo in Playwright's WebKit 26.0 - a real browser without
|
|
121
|
+
`ToggleEvent.source` - so you can try the polyfill in a browser which actually
|
|
122
|
+
needs it. Closing the browser window stops the dev server. `npm run dev` serves
|
|
123
|
+
the same page in your own browser at http://localhost:5173/ instead.
|
|
124
|
+
|
|
125
|
+
The page logs the source of every toggle event, and the button in its header
|
|
126
|
+
reports whether the polyfill is loaded:
|
|
127
|
+
|
|
128
|
+
- **Apply polyfill** - this browser has no `ToggleEvent.source`. Clicking
|
|
129
|
+
reloads the page with `?polyfill`, which applies it.
|
|
130
|
+
- **Polyfill applied** - loaded with `?polyfill`. Clicking drops the parameter
|
|
131
|
+
again, so you can see the same buttons with and without it.
|
|
132
|
+
- **No polyfill needed** - disabled, because the browser implements
|
|
133
|
+
`ToggleEvent.source` itself.
|
|
134
|
+
|
|
135
|
+
The button reloads rather than applying the polyfill in place, because the
|
|
136
|
+
polyfill hands sources to listeners from a capture phase listener on the
|
|
137
|
+
popover's root node: anything registered before it applies would see a null
|
|
138
|
+
source. Reloading with a query parameter keeps it first on the page.
|
|
139
|
+
|
|
140
|
+
## Testing
|
|
141
|
+
|
|
142
|
+
The suite runs in [Playwright](https://playwright.dev) and drives `index.html` -
|
|
143
|
+
the demo's own buttons are clicked, and the source each `toggle` and
|
|
144
|
+
`beforetoggle` reports is checked against the spec. Cases with no button of
|
|
145
|
+
their own - the `ToggleEvent` constructor, the popover and dialog methods,
|
|
146
|
+
shadow DOM retargeting - run as scripts inside the same page.
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
npm install
|
|
150
|
+
npx playwright install
|
|
151
|
+
npm test
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
There are two projects:
|
|
155
|
+
|
|
156
|
+
- **WebKit 26.0**, the newest build Playwright ships which has popovers, dialogs
|
|
157
|
+
and `command`/`commandfor` but _not_ `ToggleEvent.source`. The polyfill is the
|
|
158
|
+
only missing piece there, so this is where it is exercised for real.
|
|
159
|
+
- **Chromium**, which implements `ToggleEvent.source` itself. As well as running
|
|
160
|
+
the polyfill suites, it runs `native.spec.js`: the same expectations against
|
|
161
|
+
the browser's own implementation. That is what keeps the suite honest, rather
|
|
162
|
+
than only proving the polyfill agrees with itself.
|
|
163
|
+
|
|
164
|
+
`@playwright/test` is pinned to an exact version on purpose: Playwright 1.59 and
|
|
165
|
+
later bundle WebKit 26.4, which has `ToggleEvent.source` natively and would
|
|
166
|
+
leave the polyfill untested. `support.spec.js` asserts what each project is
|
|
167
|
+
expected to support, so an accidental upgrade fails loudly instead of quietly
|
|
168
|
+
testing nothing.
|
|
169
|
+
|
|
170
|
+
`npm run test:ui` opens Playwright's UI mode.
|
|
171
|
+
|
|
172
|
+
## Releasing
|
|
173
|
+
|
|
174
|
+
`0.0.1` has to be published by hand, because npm can only be told to trust a
|
|
175
|
+
publisher for a package which already exists:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
npm login
|
|
179
|
+
npm version 0.0.1 --git-tag-version=false
|
|
180
|
+
npm publish --access public # prompts for your 2FA one-time password
|
|
181
|
+
git checkout package.json package-lock.json
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`prepublishOnly` builds `toggle-event-source.min.js`, and the version is
|
|
185
|
+
reverted afterwards because the repository keeps a placeholder version in git -
|
|
186
|
+
releases take their version from the tag instead.
|
|
187
|
+
|
|
188
|
+
Then set up trusted publishing, so CI never needs a token. On npmjs.com, under
|
|
189
|
+
the package's **Settings → Trusted Publisher**, choose GitHub Actions and enter:
|
|
190
|
+
|
|
191
|
+
| Field | Value |
|
|
192
|
+
| -------------------- | ------------------------------ |
|
|
193
|
+
| Organization or user | `jpzwarte` |
|
|
194
|
+
| Repository | `toggle-event-source-polyfill` |
|
|
195
|
+
| Workflow filename | `publish.yml` |
|
|
196
|
+
| Environment | leave empty |
|
|
197
|
+
|
|
198
|
+
Every release after that is automatic: create a GitHub release tagged `v0.0.2`,
|
|
199
|
+
`v0.1.0` and so on, and `.github/workflows/publish.yml` runs the tests, builds
|
|
200
|
+
the minified bundle, sets the version from the tag, and publishes with
|
|
201
|
+
provenance.
|
|
202
|
+
|
|
203
|
+
## Acknowledgements
|
|
204
|
+
|
|
205
|
+
Extracted from, and designed to work alongside,
|
|
206
|
+
[invokers-polyfill](https://github.com/keithamus/invokers-polyfill) by Keith
|
|
207
|
+
Cirkel, which polyfills the `command`/`commandfor` attributes themselves and is
|
|
208
|
+
MIT licensed.
|
package/index.d.ts
ADDED
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "toggle-event-source-polyfill",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "This polyfills `ToggleEvent.source`, the element which caused a popover or `<dialog>` to be shown or hidden.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"command",
|
|
7
|
+
"commandfor",
|
|
8
|
+
"dialog",
|
|
9
|
+
"invokers",
|
|
10
|
+
"polyfill",
|
|
11
|
+
"popover",
|
|
12
|
+
"toggleevent"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/jpzwarte/toggle-event-source-polyfill#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/jpzwarte/toggle-event-source-polyfill/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Jeroen Zwartepoorte",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/jpzwarte/toggle-event-source-polyfill.git"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"index.js",
|
|
26
|
+
"index.d.ts",
|
|
27
|
+
"toggle-event-source.js",
|
|
28
|
+
"toggle-event-source.d.ts",
|
|
29
|
+
"toggle-event-source.min.js"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"main": "index.js",
|
|
33
|
+
"types": "index.d.ts",
|
|
34
|
+
"typesVersions": {
|
|
35
|
+
"*": {
|
|
36
|
+
"fn": [
|
|
37
|
+
"./toggle-event-source.d.ts"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"exports": {
|
|
42
|
+
".": {
|
|
43
|
+
"types": "./index.d.ts",
|
|
44
|
+
"import": "./index.js",
|
|
45
|
+
"browser": "./toggle-event-source.min.js",
|
|
46
|
+
"require": "./toggle-event-source.min.js"
|
|
47
|
+
},
|
|
48
|
+
"./fn": {
|
|
49
|
+
"types": "./toggle-event-source.d.ts",
|
|
50
|
+
"import": "./toggle-event-source.js"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"start": "node scripts/start.js",
|
|
55
|
+
"dev": "vite",
|
|
56
|
+
"minify": "esbuild --bundle --minify index.js > toggle-event-source.min.js",
|
|
57
|
+
"prepublishOnly": "npm run minify",
|
|
58
|
+
"format": "oxfmt .",
|
|
59
|
+
"test": "playwright test",
|
|
60
|
+
"test:ui": "playwright test --ui"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@playwright/test": "1.58.2",
|
|
64
|
+
"esbuild": "^0.28.2",
|
|
65
|
+
"oxfmt": "^0.64.0",
|
|
66
|
+
"vite": "^8.2.2"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function isSupported(): boolean;
|
|
2
|
+
export declare function isPolyfilled(): boolean;
|
|
3
|
+
export declare function apply(): void;
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface ToggleEvent {
|
|
7
|
+
readonly source: Element | null;
|
|
8
|
+
}
|
|
9
|
+
interface ToggleEventInit {
|
|
10
|
+
source?: Element | null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// Polyfill for `ToggleEvent.source`: the element which caused a popover or
|
|
2
|
+
// `<dialog>` to be shown or hidden.
|
|
3
|
+
//
|
|
4
|
+
// The `toggle`/`beforetoggle` events themselves are dispatched by the browser,
|
|
5
|
+
// so this polyfill records the element responsible for a state change right
|
|
6
|
+
// before the change happens, then hands it to the event during the capture
|
|
7
|
+
// phase - which runs before any listener on the popover/dialog itself.
|
|
8
|
+
|
|
9
|
+
const ELEMENT_NODE = 1;
|
|
10
|
+
|
|
11
|
+
const NativeToggleEvent = globalThis.ToggleEvent;
|
|
12
|
+
|
|
13
|
+
// event -> source element (or null)
|
|
14
|
+
const eventSources = new WeakMap();
|
|
15
|
+
// popover/dialog -> { source, newState, activation } for the state change which
|
|
16
|
+
// is about to happen
|
|
17
|
+
const pendingSources = new WeakMap();
|
|
18
|
+
const observedRoots = new WeakSet();
|
|
19
|
+
|
|
20
|
+
let applied = false;
|
|
21
|
+
// The click event currently being dispatched, if it activated an invoker.
|
|
22
|
+
let clickActivation = null;
|
|
23
|
+
|
|
24
|
+
function isElement(node) {
|
|
25
|
+
return Boolean(node) && node.nodeType === ELEMENT_NODE;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getRootNode(node) {
|
|
29
|
+
if (node && typeof node.getRootNode === "function") {
|
|
30
|
+
return node.getRootNode();
|
|
31
|
+
}
|
|
32
|
+
if (node && node.parentNode) return getRootNode(node.parentNode);
|
|
33
|
+
return node;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Node returning IDL attributes are retargeted against the tree the event is
|
|
37
|
+
// being observed in, so a source inside a shadow tree is reported as its host.
|
|
38
|
+
function retarget(source, event) {
|
|
39
|
+
if (!isElement(source)) return null;
|
|
40
|
+
const sourceRoot = getRootNode(source);
|
|
41
|
+
if (sourceRoot !== getRootNode(event.target || document)) {
|
|
42
|
+
return sourceRoot.host || null;
|
|
43
|
+
}
|
|
44
|
+
return source;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isPopoverOpen(element) {
|
|
48
|
+
try {
|
|
49
|
+
return element.matches(":popover-open");
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isDialogOpen(element) {
|
|
56
|
+
return element.localName === "dialog" && element.hasAttribute("open");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class ToggleEvent extends (NativeToggleEvent || Event) {
|
|
60
|
+
constructor(type, toggleEventInit = {}) {
|
|
61
|
+
super(type, toggleEventInit);
|
|
62
|
+
const { source } = toggleEventInit;
|
|
63
|
+
if (source != null && !isElement(source)) {
|
|
64
|
+
throw new TypeError(`source must be an element`);
|
|
65
|
+
}
|
|
66
|
+
eventSources.set(this, source || null);
|
|
67
|
+
if (!NativeToggleEvent) {
|
|
68
|
+
const { oldState = "", newState = "" } = toggleEventInit;
|
|
69
|
+
Object.defineProperties(this, {
|
|
70
|
+
oldState: { value: String(oldState), enumerable: true },
|
|
71
|
+
newState: { value: String(newState), enumerable: true },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get [Symbol.toStringTag]() {
|
|
77
|
+
return "ToggleEvent";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Events dispatched by the browser are instances of the native class, so
|
|
82
|
+
// `instanceof` has to keep working for those too.
|
|
83
|
+
if (NativeToggleEvent) {
|
|
84
|
+
Object.defineProperty(ToggleEvent, Symbol.hasInstance, {
|
|
85
|
+
configurable: true,
|
|
86
|
+
value: (value) => value instanceof NativeToggleEvent,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function setEventSource(event, source) {
|
|
91
|
+
eventSources.set(event, source);
|
|
92
|
+
// Events which aren't instances of the `ToggleEvent` we patched (for example
|
|
93
|
+
// ones dispatched by a popover polyfill) don't inherit the `source` getter.
|
|
94
|
+
if (!("source" in event)) {
|
|
95
|
+
Object.defineProperty(event, "source", {
|
|
96
|
+
enumerable: true,
|
|
97
|
+
configurable: true,
|
|
98
|
+
get() {
|
|
99
|
+
return retarget(eventSources.get(event) || null, event);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function handleToggleEvent(event) {
|
|
106
|
+
const pending = pendingSources.get(event.target);
|
|
107
|
+
if (!pending) return;
|
|
108
|
+
// A state change we didn't predict isn't the one we recorded a source for.
|
|
109
|
+
if (event.newState !== pending.newState) return;
|
|
110
|
+
if (event.type === "toggle") pendingSources.delete(event.target);
|
|
111
|
+
if (!pending.source) return;
|
|
112
|
+
setEventSource(event, pending.source);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function observeRootOf(element) {
|
|
116
|
+
const root = getRootNode(element);
|
|
117
|
+
if (!root || observedRoots.has(root)) return;
|
|
118
|
+
if (typeof root.addEventListener !== "function") return;
|
|
119
|
+
observedRoots.add(root);
|
|
120
|
+
root.addEventListener("beforetoggle", handleToggleEvent, true);
|
|
121
|
+
root.addEventListener("toggle", handleToggleEvent, true);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function setPendingSource(element, source, newState) {
|
|
125
|
+
if (!isElement(element)) return () => {};
|
|
126
|
+
const existing = pendingSources.get(element);
|
|
127
|
+
// `hidePopover()` and the `<dialog>` methods carry no source of their own.
|
|
128
|
+
// When one of them is called while an invoker click is being dispatched -
|
|
129
|
+
// which is how a `command`/`commandfor` polyfill drives them - it is that
|
|
130
|
+
// click, not the sourceless call, which describes the state change.
|
|
131
|
+
if (
|
|
132
|
+
!isElement(source) &&
|
|
133
|
+
existing &&
|
|
134
|
+
existing.source &&
|
|
135
|
+
existing.newState === newState &&
|
|
136
|
+
clickActivation !== null &&
|
|
137
|
+
existing.activation === clickActivation
|
|
138
|
+
) {
|
|
139
|
+
return () => {};
|
|
140
|
+
}
|
|
141
|
+
const pending = {
|
|
142
|
+
source: isElement(source) ? source : null,
|
|
143
|
+
newState,
|
|
144
|
+
activation: clickActivation,
|
|
145
|
+
};
|
|
146
|
+
pendingSources.set(element, pending);
|
|
147
|
+
observeRootOf(element);
|
|
148
|
+
return () => {
|
|
149
|
+
if (pendingSources.get(element) === pending) pendingSources.delete(element);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function patchMethod(prototype, name, getPending) {
|
|
154
|
+
const method = prototype && prototype[name];
|
|
155
|
+
if (typeof method !== "function") return;
|
|
156
|
+
Object.defineProperty(prototype, name, {
|
|
157
|
+
...Object.getOwnPropertyDescriptor(prototype, name),
|
|
158
|
+
value: function (...args) {
|
|
159
|
+
const { source, newState } = getPending.call(this, args);
|
|
160
|
+
const undo = setPendingSource(this, source, newState);
|
|
161
|
+
try {
|
|
162
|
+
return method.apply(this, args);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
undo();
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function patchPopoverMethods() {
|
|
172
|
+
const prototype = globalThis.HTMLElement && HTMLElement.prototype;
|
|
173
|
+
if (!prototype || typeof prototype.showPopover !== "function") return;
|
|
174
|
+
|
|
175
|
+
patchMethod(prototype, "showPopover", function ([options]) {
|
|
176
|
+
return { source: options && options.source, newState: "open" };
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// `hidePopover()` is specified to always hide with a null source.
|
|
180
|
+
patchMethod(prototype, "hidePopover", function () {
|
|
181
|
+
return { source: null, newState: "closed" };
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
patchMethod(prototype, "togglePopover", function ([options]) {
|
|
185
|
+
const dictionary = typeof options === "object" && options ? options : null;
|
|
186
|
+
const force = typeof options === "boolean" ? options : dictionary?.force;
|
|
187
|
+
const willShow = force == null ? !isPopoverOpen(this) : Boolean(force);
|
|
188
|
+
return {
|
|
189
|
+
// Only the showing branch of `togglePopover()` carries a source.
|
|
190
|
+
source: willShow && dictionary ? dictionary.source : null,
|
|
191
|
+
newState: willShow ? "open" : "closed",
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function patchDialogMethods() {
|
|
197
|
+
const prototype = globalThis.HTMLDialogElement && HTMLDialogElement.prototype;
|
|
198
|
+
if (!prototype) return;
|
|
199
|
+
// None of these take a source; they are patched so a state change they cause
|
|
200
|
+
// isn't attributed to an invoker which is no longer responsible for it.
|
|
201
|
+
for (const [name, newState] of [
|
|
202
|
+
["show", "open"],
|
|
203
|
+
["showModal", "open"],
|
|
204
|
+
["close", "closed"],
|
|
205
|
+
["requestClose", "closed"],
|
|
206
|
+
]) {
|
|
207
|
+
patchMethod(prototype, name, () => ({ source: null, newState }));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function recordActivationSource(element, source, newState, possible) {
|
|
212
|
+
const pending = pendingSources.get(element);
|
|
213
|
+
if (pending) {
|
|
214
|
+
// Something already performed the state change during this click - most
|
|
215
|
+
// likely a `command`/`commandfor` or popover polyfill acting on the button
|
|
216
|
+
// before this listener ran. It is recorded, it just has no source yet.
|
|
217
|
+
if (!pending.source) {
|
|
218
|
+
pending.source = source;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Or it recorded this same source itself, via `showPopover({ source })`.
|
|
222
|
+
if (pending.source === source) return;
|
|
223
|
+
}
|
|
224
|
+
if (possible) setPendingSource(element, source, newState);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function recordCommandSource(target, command, source) {
|
|
228
|
+
switch (command) {
|
|
229
|
+
case "show-popover":
|
|
230
|
+
if (target.popover) {
|
|
231
|
+
recordActivationSource(target, source, "open", !isPopoverOpen(target));
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
case "hide-popover":
|
|
235
|
+
if (target.popover) {
|
|
236
|
+
recordActivationSource(target, source, "closed", isPopoverOpen(target));
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
case "toggle-popover":
|
|
240
|
+
if (target.popover) {
|
|
241
|
+
const open = isPopoverOpen(target);
|
|
242
|
+
recordActivationSource(target, source, open ? "closed" : "open", true);
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
case "show-modal":
|
|
246
|
+
if (target.localName === "dialog") {
|
|
247
|
+
recordActivationSource(target, source, "open", !isDialogOpen(target));
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
case "close":
|
|
251
|
+
case "request-close":
|
|
252
|
+
if (target.localName === "dialog") {
|
|
253
|
+
recordActivationSource(target, source, "closed", isDialogOpen(target));
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function recordInvokerSource(node) {
|
|
260
|
+
if (node.localName !== "button" && node.localName !== "input") return false;
|
|
261
|
+
|
|
262
|
+
const commandFor = node.commandForElement;
|
|
263
|
+
const command = typeof node.command === "string" ? node.command : "";
|
|
264
|
+
if (commandFor && command) {
|
|
265
|
+
recordCommandSource(commandFor, command.toLowerCase(), node);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const popover = node.popoverTargetElement;
|
|
270
|
+
if (popover) {
|
|
271
|
+
const action = String(node.popoverTargetAction || "toggle").toLowerCase();
|
|
272
|
+
const open = isPopoverOpen(popover);
|
|
273
|
+
const possible = (action !== "show" || !open) && (action !== "hide" || open);
|
|
274
|
+
recordActivationSource(popover, node, open ? "closed" : "open", possible);
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Both `command`/`commandfor` and `popovertarget` buttons act on their target
|
|
282
|
+
// after the click event has finished dispatching, so recording the source
|
|
283
|
+
// during the capture phase is early enough.
|
|
284
|
+
function handleClick(event) {
|
|
285
|
+
if (event.type !== "click" || event.defaultPrevented) return;
|
|
286
|
+
const path = typeof event.composedPath === "function" ? event.composedPath() : [event.target];
|
|
287
|
+
clickActivation = event;
|
|
288
|
+
let activated = false;
|
|
289
|
+
for (const node of path) {
|
|
290
|
+
// Only the innermost button/input can be the activation target.
|
|
291
|
+
if (isElement(node) && recordInvokerSource(node)) {
|
|
292
|
+
activated = true;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!activated) {
|
|
297
|
+
clickActivation = null;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// A polyfill driving this click acts on the target synchronously, so the
|
|
301
|
+
// activation only needs to outlive the current task.
|
|
302
|
+
setTimeout(() => {
|
|
303
|
+
if (clickActivation === event) clickActivation = null;
|
|
304
|
+
}, 0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function observeShadowRoots(ElementClass, callback) {
|
|
308
|
+
const attachShadow = ElementClass.prototype.attachShadow;
|
|
309
|
+
ElementClass.prototype.attachShadow = function (init) {
|
|
310
|
+
const shadow = attachShadow.call(this, init);
|
|
311
|
+
callback(shadow);
|
|
312
|
+
return shadow;
|
|
313
|
+
};
|
|
314
|
+
const attachInternals = ElementClass.prototype.attachInternals;
|
|
315
|
+
if (typeof attachInternals !== "function") return;
|
|
316
|
+
ElementClass.prototype.attachInternals = function () {
|
|
317
|
+
const internals = attachInternals.call(this);
|
|
318
|
+
if (internals.shadowRoot) callback(internals.shadowRoot);
|
|
319
|
+
return internals;
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function isSupported() {
|
|
324
|
+
return (
|
|
325
|
+
typeof globalThis.ToggleEvent !== "undefined" && "source" in globalThis.ToggleEvent.prototype
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function isPolyfilled() {
|
|
330
|
+
return Boolean(globalThis.ToggleEvent) && !/native code/i.test(String(globalThis.ToggleEvent));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function apply() {
|
|
334
|
+
if (applied) return;
|
|
335
|
+
applied = true;
|
|
336
|
+
|
|
337
|
+
Object.defineProperty((NativeToggleEvent || ToggleEvent).prototype, "source", {
|
|
338
|
+
enumerable: true,
|
|
339
|
+
configurable: true,
|
|
340
|
+
get() {
|
|
341
|
+
return retarget(eventSources.get(this) || null, this);
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
patchPopoverMethods();
|
|
346
|
+
patchDialogMethods();
|
|
347
|
+
|
|
348
|
+
document.addEventListener("click", handleClick, true);
|
|
349
|
+
observeRootOf(document);
|
|
350
|
+
|
|
351
|
+
observeShadowRoots(HTMLElement, (shadow) => {
|
|
352
|
+
shadow.addEventListener("click", handleClick, true);
|
|
353
|
+
observeRootOf(shadow);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
Object.assign(globalThis, { ToggleEvent });
|
|
357
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(()=>{var f=globalThis.ToggleEvent,h=new WeakMap,i=new WeakMap,w=new WeakSet,m=!1,u=null;function l(e){return!!e&&e.nodeType===1}function g(e){return e&&typeof e.getRootNode=="function"?e.getRootNode():e&&e.parentNode?g(e.parentNode):e}function E(e,t){if(!l(e))return null;let o=g(e);return o!==g(t.target||document)?o.host||null:e}function a(e){try{return e.matches(":popover-open")}catch{return!1}}function S(e){return e.localName==="dialog"&&e.hasAttribute("open")}var p=class extends(f||Event){constructor(t,o={}){super(t,o);let{source:n}=o;if(n!=null&&!l(n))throw new TypeError("source must be an element");if(h.set(this,n||null),!f){let{oldState:r="",newState:c=""}=o;Object.defineProperties(this,{oldState:{value:String(r),enumerable:!0},newState:{value:String(c),enumerable:!0}})}}get[Symbol.toStringTag](){return"ToggleEvent"}};f&&Object.defineProperty(p,Symbol.hasInstance,{configurable:!0,value:e=>e instanceof f});function O(e,t){h.set(e,t),"source"in e||Object.defineProperty(e,"source",{enumerable:!0,configurable:!0,get(){return E(h.get(e)||null,e)}})}function v(e){let t=i.get(e.target);t&&e.newState===t.newState&&(e.type==="toggle"&&i.delete(e.target),t.source&&O(e,t.source))}function b(e){let t=g(e);!t||w.has(t)||typeof t.addEventListener=="function"&&(w.add(t),t.addEventListener("beforetoggle",v,!0),t.addEventListener("toggle",v,!0))}function P(e,t,o){if(!l(e))return()=>{};let n=i.get(e);if(!l(t)&&n&&n.source&&n.newState===o&&u!==null&&n.activation===u)return()=>{};let r={source:l(t)?t:null,newState:o,activation:u};return i.set(e,r),b(e),()=>{i.get(e)===r&&i.delete(e)}}function d(e,t,o){let n=e&&e[t];typeof n=="function"&&Object.defineProperty(e,t,{...Object.getOwnPropertyDescriptor(e,t),value:function(...r){let{source:c,newState:y}=o.call(this,r),L=P(this,c,y);try{return n.apply(this,r)}catch(M){throw L(),M}}})}function R(){let e=globalThis.HTMLElement&&HTMLElement.prototype;!e||typeof e.showPopover!="function"||(d(e,"showPopover",function([t]){return{source:t&&t.source,newState:"open"}}),d(e,"hidePopover",function(){return{source:null,newState:"closed"}}),d(e,"togglePopover",function([t]){let o=typeof t=="object"&&t?t:null,n=typeof t=="boolean"?t:o?.force,r=n==null?!a(this):!!n;return{source:r&&o?o.source:null,newState:r?"open":"closed"}}))}function j(){let e=globalThis.HTMLDialogElement&&HTMLDialogElement.prototype;if(e)for(let[t,o]of[["show","open"],["showModal","open"],["close","closed"],["requestClose","closed"]])d(e,t,()=>({source:null,newState:o}))}function s(e,t,o,n){let r=i.get(e);if(r){if(!r.source){r.source=t;return}if(r.source===t)return}n&&P(e,t,o)}function D(e,t,o){switch(t){case"show-popover":e.popover&&s(e,o,"open",!a(e));break;case"hide-popover":e.popover&&s(e,o,"closed",a(e));break;case"toggle-popover":if(e.popover){let n=a(e);s(e,o,n?"closed":"open",!0)}break;case"show-modal":e.localName==="dialog"&&s(e,o,"open",!S(e));break;case"close":case"request-close":e.localName==="dialog"&&s(e,o,"closed",S(e));break}}function x(e){if(e.localName!=="button"&&e.localName!=="input")return!1;let t=e.commandForElement,o=typeof e.command=="string"?e.command:"";if(t&&o)return D(t,o.toLowerCase(),e),!0;let n=e.popoverTargetElement;if(n){let r=String(e.popoverTargetAction||"toggle").toLowerCase(),c=a(n);return s(n,e,c?"closed":"open",(r!=="show"||!c)&&(r!=="hide"||c)),!0}return!1}function T(e){if(e.type!=="click"||e.defaultPrevented)return;let t=typeof e.composedPath=="function"?e.composedPath():[e.target];u=e;let o=!1;for(let n of t)if(l(n)&&x(n)){o=!0;break}if(!o){u=null;return}setTimeout(()=>{u===e&&(u=null)},0)}function H(e,t){let o=e.prototype.attachShadow;e.prototype.attachShadow=function(r){let c=o.call(this,r);return t(c),c};let n=e.prototype.attachInternals;typeof n=="function"&&(e.prototype.attachInternals=function(){let r=n.call(this);return r.shadowRoot&&t(r.shadowRoot),r})}function N(){return typeof globalThis.ToggleEvent<"u"&&"source"in globalThis.ToggleEvent.prototype}function k(){m||(m=!0,Object.defineProperty((f||p).prototype,"source",{enumerable:!0,configurable:!0,get(){return E(h.get(this)||null,this)}}),R(),j(),document.addEventListener("click",T,!0),b(document),H(HTMLElement,e=>{e.addEventListener("click",T,!0),b(e)}),Object.assign(globalThis,{ToggleEvent:p}))}N()||k();})();
|