@jielga/react-popup-window 0.1.1 → 0.2.0
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/README.md +39 -3
- package/dist/copyStyles.d.ts +11 -0
- package/dist/copyStyles.d.ts.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +143 -75
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +18 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/usePopupWindow.d.ts.map +1 -1
- package/dist/whenStylesheetsLoad.d.ts +13 -0
- package/dist/whenStylesheetsLoad.d.ts.map +1 -0
- package/package.json +2 -2
- package/skills/getting-started/SKILL.md +28 -10
- package/skills/popup-content/SKILL.md +10 -4
package/README.md
CHANGED
|
@@ -88,10 +88,35 @@ return (
|
|
|
88
88
|
`isOpen` also updates when the user closes the window directly, so the
|
|
89
89
|
inline branch is restored in every case.
|
|
90
90
|
|
|
91
|
+
### Show your own URL in the address bar
|
|
92
|
+
|
|
93
|
+
By default, the popup loads `about:blank`, and its address bar shows that.
|
|
94
|
+
To show your own domain instead, set `url` to an empty page served on the same origin.
|
|
95
|
+
For a Vite app, add `public/popup.html`:
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<!doctype html>
|
|
99
|
+
<html lang="en">
|
|
100
|
+
<head>
|
|
101
|
+
<meta charset="utf-8" />
|
|
102
|
+
<title>Loading</title>
|
|
103
|
+
</head>
|
|
104
|
+
<body></body>
|
|
105
|
+
</html>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
const { open, Popup } = usePopupWindow({ title: 'Panel', url: '/popup.html' })
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`isOpen` turns `true` as soon as the window opens; `Popup` renders once the page has loaded.
|
|
113
|
+
Important: do not point `url` at a route of the app, as that starts a second copy of the app inside the popup.
|
|
114
|
+
If the page redirects to another origin, the popup closes and `onBlocked` is called.
|
|
115
|
+
|
|
91
116
|
## How it works
|
|
92
117
|
|
|
93
|
-
The hook opens a same-origin `about:blank`
|
|
94
|
-
element in its body. The `Popup` component renders its children with
|
|
118
|
+
The hook opens a same-origin window, `about:blank` by default or the page
|
|
119
|
+
set with `url`, and creates a container element in its body. The `Popup` component renders its children with
|
|
95
120
|
`createPortal` into that container. A portal changes where DOM output is
|
|
96
121
|
placed, not where the components sit in the tree, so popup content
|
|
97
122
|
participates in the calling tree's state, context, and event system. React
|
|
@@ -115,13 +140,15 @@ interface UsePopupWindowOptions {
|
|
|
115
140
|
title?: string
|
|
116
141
|
/** window.open target name. The same name reuses the window. Default: '_blank'. */
|
|
117
142
|
name?: string
|
|
143
|
+
/** Same-origin URL to load instead of about:blank, such as an empty /popup.html. Default: 'about:blank'. */
|
|
144
|
+
url?: string
|
|
118
145
|
/** window.open features, merged over { popup: true, width: 640, height: 480 }. */
|
|
119
146
|
features?: PopupWindowFeatures
|
|
120
147
|
/** Center the popup over the opener when no left/top feature is given. Default: true. */
|
|
121
148
|
center?: boolean
|
|
122
149
|
/** Mirror and synchronize stylesheets into the popup. Default: true. */
|
|
123
150
|
copyStyles?: boolean
|
|
124
|
-
/** Called
|
|
151
|
+
/** Called when Popup starts rendering into the popup window. */
|
|
125
152
|
onOpen?: (popupWindow: Window) => void
|
|
126
153
|
/** Called when the popup closes: close(), user close, or opener unload. */
|
|
127
154
|
onClose?: () => void
|
|
@@ -164,6 +191,12 @@ document and kept current:
|
|
|
164
191
|
Theme systems keyed on root attributes propagate to the popup.
|
|
165
192
|
- `document.adoptedStyleSheets`.
|
|
166
193
|
|
|
194
|
+
The popup loads each copied `<link>` stylesheet again.
|
|
195
|
+
`Popup` renders once they have loaded or failed, so the first frame shown is styled.
|
|
196
|
+
Alternate stylesheets and stylesheets whose `media` does not match are not waited for.
|
|
197
|
+
If a stylesheet takes longer than 3 seconds, `Popup` renders without it.
|
|
198
|
+
Note that a `<link>` added to the opener while the popup is open loads in the popup after the content is shown.
|
|
199
|
+
|
|
167
200
|
Set `copyStyles: false` to disable.
|
|
168
201
|
|
|
169
202
|
## Communication
|
|
@@ -279,6 +312,9 @@ useEffect(() => {
|
|
|
279
312
|
|
|
280
313
|
- Browsers do not allow hiding the address bar entirely. `popup: true`
|
|
281
314
|
(the default) requests the minimal window chrome the platform provides.
|
|
315
|
+
By default, the bar shows `about:blank`; `url` replaces that with a page
|
|
316
|
+
of your own, see
|
|
317
|
+
[Show your own URL in the address bar](#show-your-own-url-in-the-address-bar).
|
|
282
318
|
- `open()` must be called from a user gesture; otherwise the browser's
|
|
283
319
|
popup blocker intervenes and `open()` returns `null`.
|
|
284
320
|
- Popup content unmounts and remounts when it moves between windows. State
|
package/dist/copyStyles.d.ts
CHANGED
|
@@ -15,4 +15,15 @@
|
|
|
15
15
|
* Returns a function that stops observing.
|
|
16
16
|
*/
|
|
17
17
|
export declare function copyStyles(source: Document, target: Document, watch?: boolean): () => void;
|
|
18
|
+
export interface StyleMirror {
|
|
19
|
+
/** Stops observing the source document. */
|
|
20
|
+
stop: () => void;
|
|
21
|
+
/**
|
|
22
|
+
* The `<link>` elements created in `target` by the initial copy. The target
|
|
23
|
+
* fetches each of them asynchronously.
|
|
24
|
+
*/
|
|
25
|
+
links: HTMLLinkElement[];
|
|
26
|
+
}
|
|
27
|
+
/** {@link copyStyles}, plus the `<link>` elements the initial copy created. */
|
|
28
|
+
export declare function mirrorStyles(source: Document, target: Document, watch?: boolean): StyleMirror;
|
|
18
29
|
//# sourceMappingURL=copyStyles.d.ts.map
|
package/dist/copyStyles.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"copyStyles.d.ts","sourceRoot":"","sources":["../src/copyStyles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,UAAO,GAAG,MAAM,IAAI,
|
|
1
|
+
{"version":3,"file":"copyStyles.d.ts","sourceRoot":"","sources":["../src/copyStyles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,UAAO,GAAG,MAAM,IAAI,CAEvF;AAKD,MAAM,WAAW,WAAW;IAC1B,2CAA2C;IAC3C,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB;;;OAGG;IACH,KAAK,EAAE,eAAe,EAAE,CAAA;CACzB;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,UAAO,GAAG,WAAW,CAiK1F"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("react"),t=require("react-dom");function n(e,t,n=!0){
|
|
2
|
-
`)}catch{}return e.textContent??``},
|
|
3
|
-
`)),t})}catch{}};for(let t of e.querySelectorAll(`style, link[rel~="stylesheet" i]`))
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("react"),t=require("react-dom");function n(e,t,n=!0){return i(e,t,n).stop}var r=[`rel`,`media`,`title`,`crossorigin`,`referrerpolicy`,`integrity`];function i(e,t,n=!0){let i=new Map,a=e=>{if(e.nodeType!==Node.ELEMENT_NODE)return!1;let t=e;return t.tagName===`STYLE`||t.tagName===`LINK`&&(t.getAttribute(`rel`)??``).toLowerCase().includes(`stylesheet`)},o=e=>{let t=e.sheet;if(t)try{return Array.from(t.cssRules).map(e=>e.cssText).join(`
|
|
2
|
+
`)}catch{}return e.textContent??``},s=e=>{if(i.has(e))return;let n;if(e.tagName===`LINK`){let i=t.createElement(`link`);for(let t of r){let n=e.getAttribute(t);n!==null&&i.setAttribute(t,n)}i.href=e.href,n=i}else{let r=t.createElement(`style`);r.textContent=o(e);let i=e.getAttribute(`media`);i&&r.setAttribute(`media`,i),n=r}t.head.appendChild(n),i.set(e,n)},c=e=>{let t=i.get(e);t&&(t.textContent=o(e))},l=e=>{for(let[t,n]of i)(e===t||e.nodeType===Node.ELEMENT_NODE&&e.contains(t))&&(n.remove(),i.delete(t))},u=[`class`,`style`],d=()=>{let n=[[e.documentElement,t.documentElement],[e.body,t.body]];for(let[e,t]of n)if(!(!e||!t)){for(let n of e.attributes)(u.includes(n.name)||n.name.startsWith(`data-`))&&t.setAttribute(n.name,n.value);for(let n of Array.from(t.attributes))(u.includes(n.name)||n.name.startsWith(`data-`))&&!e.hasAttribute(n.name)&&t.removeAttribute(n.name)}},f=()=>{let n=t.defaultView;if(!(!n?.CSSStyleSheet||!(`adoptedStyleSheets`in e)))try{t.adoptedStyleSheets=e.adoptedStyleSheets.map(e=>{let t=new n.CSSStyleSheet;return t.replaceSync(Array.from(e.cssRules).map(e=>e.cssText).join(`
|
|
3
|
+
`)),t})}catch{}};for(let t of e.querySelectorAll(`style, link[rel~="stylesheet" i]`))s(t);let p=Array.from(i.values()).filter(e=>e.tagName===`LINK`);if(d(),f(),!n||typeof MutationObserver>`u`)return{stop:()=>{},links:p};let m=new MutationObserver(e=>{for(let t of e){if(t.type===`characterData`){let e=t.target.parentElement;e?.tagName===`STYLE`&&c(e);continue}a(t.target)&&t.target.tagName===`STYLE`&&c(t.target);for(let e of t.addedNodes)if(a(e))s(e);else if(e.nodeType===Node.ELEMENT_NODE)for(let t of e.querySelectorAll(`style, link[rel~="stylesheet" i]`))s(t);for(let e of t.removedNodes)l(e)}});m.observe(e.head,{childList:!0,subtree:!0,characterData:!0});let h=new MutationObserver(()=>d());return h.observe(e.documentElement,{attributes:!0}),e.body&&h.observe(e.body,{attributes:!0}),{stop:()=>{m.disconnect(),h.disconnect()},links:p}}function a(e,t,n){let r=e.filter(e=>e.sheet===null&&o(e));if(r.length===0)return n(),()=>{};let i=r.length,a=()=>{i--,i===0&&c()},s=()=>{window.clearTimeout(l);for(let e of r)e.removeEventListener(`load`,a),e.removeEventListener(`error`,a)},c=()=>{s(),n()};for(let e of r)e.addEventListener(`load`,a),e.addEventListener(`error`,a);let l=window.setTimeout(c,t);return s}function o(e){if(/\balternate\b/i.test(e.rel))return!1;let t=e.ownerDocument.defaultView;return!e.media||!t?.matchMedia||t.matchMedia(e.media).matches}var s={popupWindow:null,container:null,blocked:!1},c=class{state=s;listeners=new Set;subscribe=e=>(this.listeners.add(e),()=>this.listeners.delete(e));getSnapshot=()=>this.state;getServerSnapshot=()=>s;setState(e){this.state={...this.state,...e};for(let e of this.listeners)e()}},l={popup:!0,width:640,height:480};function u(e){let t={...l,...e.features};return e.center!==!1&&t.left===void 0&&t.top===void 0&&typeof t.width==`number`&&typeof t.height==`number`&&(t.left=Math.max(0,Math.round(window.screenX+(window.outerWidth-t.width)/2)),t.top=Math.max(0,Math.round(window.screenY+(window.outerHeight-t.height)/2))),Object.entries(t).filter(([,e])=>e!==void 0&&e!==!1).map(([e,t])=>t===!0?`${e}=yes`:`${e}=${t}`).join(`,`)}var d=`about:blank`,f=20,p=3e3;function m(e){let t=e.document;return t.URL===d||t.readyState===`loading`?null:t}function h(n){function r({children:r}){let{container:i}=(0,e.useSyncExternalStore)(n.subscribe,n.getSnapshot,n.getServerSnapshot);return i?(0,t.createPortal)(r,i):null}return r.displayName=`PopupWindow.Popup`,r}function g(t={}){let[n]=(0,e.useState)(()=>new c),[r]=(0,e.useState)(()=>h(n)),o=(0,e.useRef)(t);o.current=t;let s=(0,e.useRef)(null),l=(0,e.useSyncExternalStore)(n.subscribe,n.getSnapshot,n.getServerSnapshot),g=(0,e.useCallback)(e=>{let{popupWindow:t}=n.getSnapshot();s.current?.(),s.current=null,t&&(!e&&!t.closed&&t.close(),n.setState({popupWindow:null,container:null}),o.current.onClose?.())},[n]),_=(0,e.useCallback)(()=>{if(typeof window>`u`)return null;let e=n.getSnapshot();if(e.popupWindow&&!e.popupWindow.closed)return e.popupWindow.focus(),e.popupWindow;let t=o.current,r=t.url??d,c=window.open(r,t.name??`_blank`,u(t));if(!c)return n.setState({blocked:!0}),t.onBlocked?.(),null;let l;try{l=c.document}catch{return c.close(),n.setState({blocked:!0}),t.onBlocked?.(),null}let h=()=>g(!0),_=()=>{setTimeout(()=>{c.closed&&h()},0)},v=window.setInterval(()=>{c.closed&&h()},250),y=()=>c.close();window.addEventListener(`pagehide`,y);let b,x=!1,S,C;s.current=()=>{window.clearInterval(v),window.clearInterval(b),window.removeEventListener(`pagehide`,y),x&&c.removeEventListener(`pagehide`,_),S?.(),C?.()};let w=e=>{x=!0,c.addEventListener(`pagehide`,_),e.title=t.title??document.title;let r=[];if(t.copyStyles!==!1){let t=i(document,e);S=t.stop,r=t.links}let o=e.createElement(`div`);o.setAttribute(`data-popup-window-root`,``),e.body.appendChild(o),C=a(r,p,()=>{n.setState({container:o}),t.onOpen?.(c)})};return n.setState({popupWindow:c,container:null,blocked:!1}),r===d?w(l):b=window.setInterval(()=>{if(c.closed){h();return}let e;try{e=m(c)}catch{g(!1),n.setState({blocked:!0}),t.onBlocked?.();return}e&&(window.clearInterval(b),w(e))},f),c},[n,g]),v=(0,e.useCallback)(()=>g(!1),[g]),y=(0,e.useCallback)(()=>{n.getSnapshot().popupWindow?g(!1):_()},[n,g,_]),b=(0,e.useCallback)(()=>{let{popupWindow:e}=n.getSnapshot();e&&!e.closed&&e.focus()},[n]);return(0,e.useEffect)(()=>()=>g(!1),[g]),{open:_,close:v,toggle:y,focus:b,isOpen:l.popupWindow!==null,isBlocked:l.blocked,popupWindow:l.popupWindow,Popup:r}}exports.copyStyles=n,exports.usePopupWindow=g;
|
|
4
4
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/copyStyles.ts","../src/usePopupWindow.ts"],"sourcesContent":["/**\n * Copy all stylesheets from `source` into `target` and (optionally) keep them\n * in sync while the popup is open:\n *\n * - `<link rel=\"stylesheet\">` and `<style>` elements are mirrored into the\n * target `<head>`. `<style>` contents are serialized from the CSSOM when\n * possible, so rules injected via `insertRule` (CSS-in-JS \"speedy\" mode)\n * are included.\n * - Additions/removals/edits of style nodes in the source `<head>` are\n * observed (covers Vite HMR, lazily loaded chunk CSS, styled-components).\n * - `class`/`style`/`data-*` attributes on `<html>` and `<body>` are mirrored\n * and kept in sync, so theme switching (e.g. a `dark` class) propagates.\n * - `document.adoptedStyleSheets` are re-constructed in the target document.\n *\n * Returns a function that stops observing.\n */\nexport function copyStyles(source: Document, target: Document, watch = true): () => void {\n const mirrors = new Map<Element, Element>()\n\n const isStyleNode = (node: Node): node is HTMLStyleElement | HTMLLinkElement => {\n if (node.nodeType !== Node.ELEMENT_NODE) return false\n const el = node as Element\n return (\n el.tagName === 'STYLE' ||\n (el.tagName === 'LINK' && (el.getAttribute('rel') ?? '').toLowerCase().includes('stylesheet'))\n )\n }\n\n const serializeStyle = (el: HTMLStyleElement): string => {\n const sheet = el.sheet\n if (sheet) {\n try {\n return Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n')\n } catch {\n // Inaccessible cssRules — fall back to the raw text below.\n }\n }\n return el.textContent ?? ''\n }\n\n const mirror = (el: HTMLStyleElement | HTMLLinkElement): void => {\n if (mirrors.has(el)) return\n let clone: Element\n if (el.tagName === 'LINK') {\n const link = target.createElement('link')\n link.rel = 'stylesheet'\n // .href resolves to an absolute URL, so relative hrefs keep working\n // from the popup's `about:blank` document.\n link.href = (el as HTMLLinkElement).href\n const media = el.getAttribute('media')\n if (media) link.media = media\n clone = link\n } else {\n const style = target.createElement('style')\n style.textContent = serializeStyle(el as HTMLStyleElement)\n const media = el.getAttribute('media')\n if (media) style.setAttribute('media', media)\n clone = style\n }\n target.head.appendChild(clone)\n mirrors.set(el, clone)\n }\n\n const refresh = (el: HTMLStyleElement): void => {\n const clone = mirrors.get(el)\n if (clone) clone.textContent = serializeStyle(el)\n }\n\n const unmirrorSubtree = (removed: Node): void => {\n for (const [src, clone] of mirrors) {\n if (removed === src || (removed.nodeType === Node.ELEMENT_NODE && removed.contains(src))) {\n clone.remove()\n mirrors.delete(src)\n }\n }\n }\n\n const ROOT_ATTRS = ['class', 'style']\n const syncRootAttrs = (): void => {\n const pairs: Array<[Element | null, Element | null]> = [\n [source.documentElement, target.documentElement],\n [source.body, target.body],\n ]\n for (const [from, to] of pairs) {\n if (!from || !to) continue\n for (const attr of from.attributes) {\n if (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) {\n to.setAttribute(attr.name, attr.value)\n }\n }\n for (const attr of Array.from(to.attributes)) {\n if (\n (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) &&\n !from.hasAttribute(attr.name)\n ) {\n to.removeAttribute(attr.name)\n }\n }\n }\n }\n\n const syncAdoptedSheets = (): void => {\n const targetWindow = target.defaultView as (Window & { CSSStyleSheet?: typeof CSSStyleSheet }) | null\n if (!targetWindow?.CSSStyleSheet || !('adoptedStyleSheets' in source)) return\n try {\n target.adoptedStyleSheets = source.adoptedStyleSheets.map((sheet) => {\n const copy = new targetWindow.CSSStyleSheet!()\n copy.replaceSync(\n Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n'),\n )\n return copy\n })\n } catch {\n // Best effort — older browsers or cross-origin rules.\n }\n }\n\n for (const el of source.querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n syncRootAttrs()\n syncAdoptedSheets()\n\n if (!watch || typeof MutationObserver === 'undefined') {\n return () => {}\n }\n\n const headObserver = new MutationObserver((records) => {\n for (const record of records) {\n if (record.type === 'characterData') {\n const parent = record.target.parentElement\n if (parent?.tagName === 'STYLE') refresh(parent as HTMLStyleElement)\n continue\n }\n // childList\n if (isStyleNode(record.target) && record.target.tagName === 'STYLE') {\n // textContent assignment replaces the text node → childList on <style>\n refresh(record.target as HTMLStyleElement)\n }\n for (const added of record.addedNodes) {\n if (isStyleNode(added)) {\n mirror(added)\n } else if (added.nodeType === Node.ELEMENT_NODE) {\n for (const el of (added as Element).querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n }\n }\n for (const removed of record.removedNodes) {\n unmirrorSubtree(removed)\n }\n }\n })\n headObserver.observe(source.head, { childList: true, subtree: true, characterData: true })\n\n const rootObserver = new MutationObserver(() => syncRootAttrs())\n rootObserver.observe(source.documentElement, { attributes: true })\n if (source.body) rootObserver.observe(source.body, { attributes: true })\n\n return () => {\n headObserver.disconnect()\n rootObserver.disconnect()\n }\n}\n","import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport type { FC } from 'react'\nimport { createPortal } from 'react-dom'\nimport { copyStyles } from './copyStyles'\nimport type {\n PopupProps,\n PopupWindowApi,\n PopupWindowFeatures,\n UsePopupWindowOptions,\n} from './types'\n\ninterface PopupState {\n popupWindow: Window | null\n container: HTMLElement | null\n blocked: boolean\n}\n\nconst INITIAL_STATE: PopupState = { popupWindow: null, container: null, blocked: false }\n\nclass PopupStore {\n state: PopupState = INITIAL_STATE\n private listeners = new Set<() => void>()\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n getSnapshot = (): PopupState => this.state\n\n getServerSnapshot = (): PopupState => INITIAL_STATE\n\n setState(partial: Partial<PopupState>): void {\n this.state = { ...this.state, ...partial }\n for (const listener of this.listeners) listener()\n }\n}\n\nconst DEFAULT_FEATURES: PopupWindowFeatures = { popup: true, width: 640, height: 480 }\n\nfunction buildFeatures(options: UsePopupWindowOptions): string {\n const merged: PopupWindowFeatures = { ...DEFAULT_FEATURES, ...options.features }\n if (\n options.center !== false &&\n merged.left === undefined &&\n merged.top === undefined &&\n typeof merged.width === 'number' &&\n typeof merged.height === 'number'\n ) {\n merged.left = Math.max(0, Math.round(window.screenX + (window.outerWidth - merged.width) / 2))\n merged.top = Math.max(0, Math.round(window.screenY + (window.outerHeight - merged.height) / 2))\n }\n return Object.entries(merged)\n .filter(([, value]) => value !== undefined && value !== false)\n .map(([key, value]) => (value === true ? `${key}=yes` : `${key}=${value}`))\n .join(',')\n}\n\nfunction createPopupComponent(store: PopupStore): FC<PopupProps> {\n function Popup({ children }: PopupProps) {\n const { container } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getServerSnapshot,\n )\n return container ? createPortal(children, container) : null\n }\n Popup.displayName = 'PopupWindow.Popup'\n return Popup\n}\n\n/**\n * Open part of your React tree in a separate browser window.\n *\n * The popup content is rendered with a portal into the popup's document, so\n * it stays part of your component tree: state, context and event handlers\n * all keep working across windows.\n *\n * ```tsx\n * const { open, close, isOpen, Popup } = usePopupWindow({ title: 'Panel' })\n *\n * return (\n * <>\n * <button onClick={open}>Open panel</button>\n * <Popup>\n * <MyPanel />\n * </Popup>\n * </>\n * )\n * ```\n */\nexport function usePopupWindow(options: UsePopupWindowOptions = {}): PopupWindowApi {\n const [store] = useState(() => new PopupStore())\n const [Popup] = useState(() => createPopupComponent(store))\n\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n const cleanupRef = useRef<(() => void) | null>(null)\n\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)\n\n // `userClosed` is true when the window was closed outside our control\n // (user hit the close button, opener unloaded) — then we must not touch it.\n const closePopup = useCallback(\n (userClosed: boolean) => {\n const { popupWindow } = store.getSnapshot()\n cleanupRef.current?.()\n cleanupRef.current = null\n if (!popupWindow) return\n if (!userClosed && !popupWindow.closed) popupWindow.close()\n store.setState({ popupWindow: null, container: null })\n optionsRef.current.onClose?.()\n },\n [store],\n )\n\n const open = useCallback((): Window | null => {\n if (typeof window === 'undefined') return null\n const current = store.getSnapshot()\n if (current.popupWindow && !current.popupWindow.closed) {\n current.popupWindow.focus()\n return current.popupWindow\n }\n\n const opts = optionsRef.current\n const popupWindow = window.open('about:blank', opts.name ?? '_blank', buildFeatures(opts))\n if (!popupWindow) {\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n\n // Sandboxed embedders (VS Code's built-in browser, CodeSandbox/StackBlitz\n // previews, iframes sandboxed without popup-escape) open the popup with an\n // opaque origin, so reading its document throws a SecurityError. Treat\n // that as blocked instead of stranding a blank window the portal can\n // never reach.\n let doc: Document\n try {\n doc = popupWindow.document\n } catch {\n popupWindow.close()\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n doc.title = opts.title ?? document.title\n\n let stopStyleSync: (() => void) | undefined\n if (opts.copyStyles !== false) {\n stopStyleSync = copyStyles(document, doc)\n }\n\n const container = doc.createElement('div')\n container.setAttribute('data-popup-window-root', '')\n doc.body.appendChild(container)\n\n const handleExternalClose = () => closePopup(true)\n const onPopupPagehide = () => {\n // pagehide also fires on navigation; only treat it as a close when the\n // window really is gone a tick later.\n setTimeout(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 0)\n }\n popupWindow.addEventListener('pagehide', onPopupPagehide)\n\n // Belt and braces: some browsers don't fire pagehide reliably for popups.\n const closePoll = window.setInterval(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 250)\n\n const onOpenerPagehide = () => popupWindow.close()\n window.addEventListener('pagehide', onOpenerPagehide)\n\n cleanupRef.current = () => {\n window.clearInterval(closePoll)\n window.removeEventListener('pagehide', onOpenerPagehide)\n popupWindow.removeEventListener('pagehide', onPopupPagehide)\n stopStyleSync?.()\n }\n\n store.setState({ popupWindow, container, blocked: false })\n opts.onOpen?.(popupWindow)\n return popupWindow\n }, [store, closePopup])\n\n const close = useCallback(() => closePopup(false), [closePopup])\n\n const toggle = useCallback(() => {\n if (store.getSnapshot().popupWindow) {\n closePopup(false)\n } else {\n open()\n }\n }, [store, closePopup, open])\n\n const focus = useCallback(() => {\n const { popupWindow } = store.getSnapshot()\n if (popupWindow && !popupWindow.closed) popupWindow.focus()\n }, [store])\n\n // Close the popup when the owning component unmounts — its portal content\n // would unmount anyway, leaving an empty window behind.\n useEffect(() => {\n return () => closePopup(false)\n }, [closePopup])\n\n return {\n open,\n close,\n toggle,\n focus,\n isOpen: state.popupWindow !== null,\n isBlocked: state.blocked,\n popupWindow: state.popupWindow,\n Popup,\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/copyStyles.ts","../src/whenStylesheetsLoad.ts","../src/usePopupWindow.ts"],"sourcesContent":["/**\n * Copy all stylesheets from `source` into `target` and (optionally) keep them\n * in sync while the popup is open:\n *\n * - `<link rel=\"stylesheet\">` and `<style>` elements are mirrored into the\n * target `<head>`. `<style>` contents are serialized from the CSSOM when\n * possible, so rules injected via `insertRule` (CSS-in-JS \"speedy\" mode)\n * are included.\n * - Additions/removals/edits of style nodes in the source `<head>` are\n * observed (covers Vite HMR, lazily loaded chunk CSS, styled-components).\n * - `class`/`style`/`data-*` attributes on `<html>` and `<body>` are mirrored\n * and kept in sync, so theme switching (e.g. a `dark` class) propagates.\n * - `document.adoptedStyleSheets` are re-constructed in the target document.\n *\n * Returns a function that stops observing.\n */\nexport function copyStyles(source: Document, target: Document, watch = true): () => void {\n return mirrorStyles(source, target, watch).stop\n}\n\n/** Attributes copied from a source `<link>` onto its mirror. */\nconst LINK_ATTRS = ['rel', 'media', 'title', 'crossorigin', 'referrerpolicy', 'integrity']\n\nexport interface StyleMirror {\n /** Stops observing the source document. */\n stop: () => void\n /**\n * The `<link>` elements created in `target` by the initial copy. The target\n * fetches each of them asynchronously.\n */\n links: HTMLLinkElement[]\n}\n\n/** {@link copyStyles}, plus the `<link>` elements the initial copy created. */\nexport function mirrorStyles(source: Document, target: Document, watch = true): StyleMirror {\n const mirrors = new Map<Element, Element>()\n\n const isStyleNode = (node: Node): node is HTMLStyleElement | HTMLLinkElement => {\n if (node.nodeType !== Node.ELEMENT_NODE) return false\n const el = node as Element\n return (\n el.tagName === 'STYLE' ||\n (el.tagName === 'LINK' && (el.getAttribute('rel') ?? '').toLowerCase().includes('stylesheet'))\n )\n }\n\n const serializeStyle = (el: HTMLStyleElement): string => {\n const sheet = el.sheet\n if (sheet) {\n try {\n return Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n')\n } catch {\n // Inaccessible cssRules — fall back to the raw text below.\n }\n }\n return el.textContent ?? ''\n }\n\n const mirror = (el: HTMLStyleElement | HTMLLinkElement): void => {\n if (mirrors.has(el)) return\n let clone: Element\n if (el.tagName === 'LINK') {\n const link = target.createElement('link')\n // `rel` is copied as is, so an alternate stylesheet stays disabled.\n // `crossorigin` and `referrerpolicy` make the popup send the same request\n // as the opener, so it can reuse the cached response.\n for (const name of LINK_ATTRS) {\n const value = el.getAttribute(name)\n if (value !== null) link.setAttribute(name, value)\n }\n // .href resolves to an absolute URL, so relative hrefs keep working\n // from the popup's `about:blank` document.\n link.href = (el as HTMLLinkElement).href\n clone = link\n } else {\n const style = target.createElement('style')\n style.textContent = serializeStyle(el as HTMLStyleElement)\n const media = el.getAttribute('media')\n if (media) style.setAttribute('media', media)\n clone = style\n }\n target.head.appendChild(clone)\n mirrors.set(el, clone)\n }\n\n const refresh = (el: HTMLStyleElement): void => {\n const clone = mirrors.get(el)\n if (clone) clone.textContent = serializeStyle(el)\n }\n\n const unmirrorSubtree = (removed: Node): void => {\n for (const [src, clone] of mirrors) {\n if (removed === src || (removed.nodeType === Node.ELEMENT_NODE && removed.contains(src))) {\n clone.remove()\n mirrors.delete(src)\n }\n }\n }\n\n const ROOT_ATTRS = ['class', 'style']\n const syncRootAttrs = (): void => {\n const pairs: Array<[Element | null, Element | null]> = [\n [source.documentElement, target.documentElement],\n [source.body, target.body],\n ]\n for (const [from, to] of pairs) {\n if (!from || !to) continue\n for (const attr of from.attributes) {\n if (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) {\n to.setAttribute(attr.name, attr.value)\n }\n }\n for (const attr of Array.from(to.attributes)) {\n if (\n (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) &&\n !from.hasAttribute(attr.name)\n ) {\n to.removeAttribute(attr.name)\n }\n }\n }\n }\n\n const syncAdoptedSheets = (): void => {\n const targetWindow = target.defaultView as (Window & { CSSStyleSheet?: typeof CSSStyleSheet }) | null\n if (!targetWindow?.CSSStyleSheet || !('adoptedStyleSheets' in source)) return\n try {\n target.adoptedStyleSheets = source.adoptedStyleSheets.map((sheet) => {\n const copy = new targetWindow.CSSStyleSheet!()\n copy.replaceSync(\n Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n'),\n )\n return copy\n })\n } catch {\n // Best effort — older browsers or cross-origin rules.\n }\n }\n\n for (const el of source.querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n const links = Array.from(mirrors.values()).filter(\n (clone): clone is HTMLLinkElement => clone.tagName === 'LINK',\n )\n syncRootAttrs()\n syncAdoptedSheets()\n\n if (!watch || typeof MutationObserver === 'undefined') {\n return { stop: () => {}, links }\n }\n\n const headObserver = new MutationObserver((records) => {\n for (const record of records) {\n if (record.type === 'characterData') {\n const parent = record.target.parentElement\n if (parent?.tagName === 'STYLE') refresh(parent as HTMLStyleElement)\n continue\n }\n // childList\n if (isStyleNode(record.target) && record.target.tagName === 'STYLE') {\n // textContent assignment replaces the text node → childList on <style>\n refresh(record.target as HTMLStyleElement)\n }\n for (const added of record.addedNodes) {\n if (isStyleNode(added)) {\n mirror(added)\n } else if (added.nodeType === Node.ELEMENT_NODE) {\n for (const el of (added as Element).querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n }\n }\n for (const removed of record.removedNodes) {\n unmirrorSubtree(removed)\n }\n }\n })\n headObserver.observe(source.head, { childList: true, subtree: true, characterData: true })\n\n const rootObserver = new MutationObserver(() => syncRootAttrs())\n rootObserver.observe(source.documentElement, { attributes: true })\n if (source.body) rootObserver.observe(source.body, { attributes: true })\n\n return {\n stop: () => {\n headObserver.disconnect()\n rootObserver.disconnect()\n },\n links,\n }\n}\n","/**\n * Calls `callback` once every stylesheet in `links` that applies has loaded\n * or failed, or after `timeoutMs`, whichever comes first.\n *\n * When none of them is still loading, `callback` runs synchronously, before\n * this function returns. Links that do not apply are not waited for: an\n * alternate stylesheet, or a `media` query that does not match, such as\n * `print`. Browsers fetch those at the lowest priority.\n *\n * Returns a function that stops waiting without calling `callback`.\n */\nexport function whenStylesheetsLoad(\n links: readonly HTMLLinkElement[],\n timeoutMs: number,\n callback: () => void,\n): () => void {\n const pending = links.filter((link) => link.sheet === null && applies(link))\n if (pending.length === 0) {\n callback()\n return () => {}\n }\n\n let remaining = pending.length\n const settle = () => {\n remaining--\n if (remaining === 0) finish()\n }\n const stop = () => {\n window.clearTimeout(timeout)\n for (const link of pending) {\n link.removeEventListener('load', settle)\n link.removeEventListener('error', settle)\n }\n }\n const finish = () => {\n stop()\n callback()\n }\n\n for (const link of pending) {\n link.addEventListener('load', settle)\n // A broken stylesheet must not keep the popup blank.\n link.addEventListener('error', settle)\n }\n const timeout = window.setTimeout(finish, timeoutMs)\n return stop\n}\n\nfunction applies(link: HTMLLinkElement): boolean {\n if (/\\balternate\\b/i.test(link.rel)) return false\n const view = link.ownerDocument.defaultView\n return !link.media || !view?.matchMedia || view.matchMedia(link.media).matches\n}\n","import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport type { FC } from 'react'\nimport { createPortal } from 'react-dom'\nimport { mirrorStyles } from './copyStyles'\nimport type {\n PopupProps,\n PopupWindowApi,\n PopupWindowFeatures,\n UsePopupWindowOptions,\n} from './types'\nimport { whenStylesheetsLoad } from './whenStylesheetsLoad'\n\ninterface PopupState {\n popupWindow: Window | null\n container: HTMLElement | null\n blocked: boolean\n}\n\nconst INITIAL_STATE: PopupState = { popupWindow: null, container: null, blocked: false }\n\nclass PopupStore {\n state: PopupState = INITIAL_STATE\n private listeners = new Set<() => void>()\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n getSnapshot = (): PopupState => this.state\n\n getServerSnapshot = (): PopupState => INITIAL_STATE\n\n setState(partial: Partial<PopupState>): void {\n this.state = { ...this.state, ...partial }\n for (const listener of this.listeners) listener()\n }\n}\n\nconst DEFAULT_FEATURES: PopupWindowFeatures = { popup: true, width: 640, height: 480 }\n\nfunction buildFeatures(options: UsePopupWindowOptions): string {\n const merged: PopupWindowFeatures = { ...DEFAULT_FEATURES, ...options.features }\n if (\n options.center !== false &&\n merged.left === undefined &&\n merged.top === undefined &&\n typeof merged.width === 'number' &&\n typeof merged.height === 'number'\n ) {\n merged.left = Math.max(0, Math.round(window.screenX + (window.outerWidth - merged.width) / 2))\n merged.top = Math.max(0, Math.round(window.screenY + (window.outerHeight - merged.height) / 2))\n }\n return Object.entries(merged)\n .filter(([, value]) => value !== undefined && value !== false)\n .map(([key, value]) => (value === true ? `${key}=yes` : `${key}=${value}`))\n .join(',')\n}\n\nconst ABOUT_BLANK = 'about:blank'\n/** Interval for checking whether the page given by `url` has loaded. */\nconst LOAD_POLL_MS = 20\n/** Longest time `Popup` waits for copied stylesheets before rendering anyway. */\nconst STYLES_TIMEOUT_MS = 3000\n\n/**\n * The popup's document once the page given by `url` has replaced the initial\n * `about:blank` document and finished parsing, otherwise `null`. Throws when\n * the document is not scriptable.\n */\nfunction loadedDocument(popupWindow: Window): Document | null {\n const doc = popupWindow.document\n if (doc.URL === ABOUT_BLANK || doc.readyState === 'loading') return null\n return doc\n}\n\nfunction createPopupComponent(store: PopupStore): FC<PopupProps> {\n function Popup({ children }: PopupProps) {\n const { container } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getServerSnapshot,\n )\n return container ? createPortal(children, container) : null\n }\n Popup.displayName = 'PopupWindow.Popup'\n return Popup\n}\n\n/**\n * Open part of your React tree in a separate browser window.\n *\n * The popup content is rendered with a portal into the popup's document, so\n * it stays part of your component tree: state, context and event handlers\n * all keep working across windows.\n *\n * ```tsx\n * const { open, close, isOpen, Popup } = usePopupWindow({ title: 'Panel' })\n *\n * return (\n * <>\n * <button onClick={open}>Open panel</button>\n * <Popup>\n * <MyPanel />\n * </Popup>\n * </>\n * )\n * ```\n */\nexport function usePopupWindow(options: UsePopupWindowOptions = {}): PopupWindowApi {\n const [store] = useState(() => new PopupStore())\n const [Popup] = useState(() => createPopupComponent(store))\n\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n const cleanupRef = useRef<(() => void) | null>(null)\n\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)\n\n // `userClosed` is true when the window was closed outside our control\n // (user hit the close button, opener unloaded) — then we must not touch it.\n const closePopup = useCallback(\n (userClosed: boolean) => {\n const { popupWindow } = store.getSnapshot()\n cleanupRef.current?.()\n cleanupRef.current = null\n if (!popupWindow) return\n if (!userClosed && !popupWindow.closed) popupWindow.close()\n store.setState({ popupWindow: null, container: null })\n optionsRef.current.onClose?.()\n },\n [store],\n )\n\n const open = useCallback((): Window | null => {\n if (typeof window === 'undefined') return null\n const current = store.getSnapshot()\n if (current.popupWindow && !current.popupWindow.closed) {\n current.popupWindow.focus()\n return current.popupWindow\n }\n\n const opts = optionsRef.current\n const url = opts.url ?? ABOUT_BLANK\n const popupWindow = window.open(url, opts.name ?? '_blank', buildFeatures(opts))\n if (!popupWindow) {\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n\n // Sandboxed embedders (VS Code's built-in browser, CodeSandbox/StackBlitz\n // previews, iframes sandboxed without popup-escape) open the popup with an\n // opaque origin, so reading its document throws a SecurityError. Treat\n // that as blocked instead of stranding a blank window the portal can\n // never reach.\n let initialDoc: Document\n try {\n initialDoc = popupWindow.document\n } catch {\n popupWindow.close()\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n\n const handleExternalClose = () => closePopup(true)\n const onPopupPagehide = () => {\n // pagehide also fires on navigation; only treat it as a close when the\n // window really is gone a tick later.\n setTimeout(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 0)\n }\n\n // Belt and braces: some browsers don't fire pagehide reliably for popups.\n const closePoll = window.setInterval(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 250)\n\n const onOpenerPagehide = () => popupWindow.close()\n window.addEventListener('pagehide', onOpenerPagehide)\n\n let loadPoll: number | undefined\n let prepared = false\n let stopStyleSync: (() => void) | undefined\n let stopStylesWait: (() => void) | undefined\n\n cleanupRef.current = () => {\n window.clearInterval(closePoll)\n window.clearInterval(loadPoll)\n window.removeEventListener('pagehide', onOpenerPagehide)\n if (prepared) popupWindow.removeEventListener('pagehide', onPopupPagehide)\n stopStyleSync?.()\n stopStylesWait?.()\n }\n\n // Title, styles and the portal container go into the document that\n // stays: `about:blank` itself, or the page `url` loads in its place.\n const prepare = (doc: Document) => {\n prepared = true\n popupWindow.addEventListener('pagehide', onPopupPagehide)\n doc.title = opts.title ?? document.title\n let links: HTMLLinkElement[] = []\n if (opts.copyStyles !== false) {\n const styles = mirrorStyles(document, doc)\n stopStyleSync = styles.stop\n links = styles.links\n }\n const container = doc.createElement('div')\n container.setAttribute('data-popup-window-root', '')\n doc.body.appendChild(container)\n // The popup fetches copied <link> stylesheets asynchronously. Render\n // only once they have loaded, so the first paint is not unstyled.\n stopStylesWait = whenStylesheetsLoad(links, STYLES_TIMEOUT_MS, () => {\n store.setState({ container })\n opts.onOpen?.(popupWindow)\n })\n }\n\n store.setState({ popupWindow, container: null, blocked: false })\n\n if (url === ABOUT_BLANK) {\n prepare(initialDoc)\n } else {\n // The window starts on an initial about:blank document and then\n // navigates to `url`. Wait until that page has replaced it and parsed.\n loadPoll = window.setInterval(() => {\n if (popupWindow.closed) {\n handleExternalClose()\n return\n }\n let doc: Document | null\n try {\n doc = loadedDocument(popupWindow)\n } catch {\n // The page went somewhere the opener cannot script, such as a\n // cross-origin redirect: nothing can ever render there.\n closePopup(false)\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return\n }\n if (!doc) return\n window.clearInterval(loadPoll)\n prepare(doc)\n }, LOAD_POLL_MS)\n }\n\n return popupWindow\n }, [store, closePopup])\n\n const close = useCallback(() => closePopup(false), [closePopup])\n\n const toggle = useCallback(() => {\n if (store.getSnapshot().popupWindow) {\n closePopup(false)\n } else {\n open()\n }\n }, [store, closePopup, open])\n\n const focus = useCallback(() => {\n const { popupWindow } = store.getSnapshot()\n if (popupWindow && !popupWindow.closed) popupWindow.focus()\n }, [store])\n\n // Close the popup when the owning component unmounts — its portal content\n // would unmount anyway, leaving an empty window behind.\n useEffect(() => {\n return () => closePopup(false)\n }, [closePopup])\n\n return {\n open,\n close,\n toggle,\n focus,\n isOpen: state.popupWindow !== null,\n isBlocked: state.blocked,\n popupWindow: state.popupWindow,\n Popup,\n }\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,17 @@ import { useCallback as e, useEffect as t, useRef as n, useState as r, useSyncEx
|
|
|
2
2
|
import { createPortal as a } from "react-dom";
|
|
3
3
|
//#region src/copyStyles.ts
|
|
4
4
|
function o(e, t, n = !0) {
|
|
5
|
+
return c(e, t, n).stop;
|
|
6
|
+
}
|
|
7
|
+
var s = [
|
|
8
|
+
"rel",
|
|
9
|
+
"media",
|
|
10
|
+
"title",
|
|
11
|
+
"crossorigin",
|
|
12
|
+
"referrerpolicy",
|
|
13
|
+
"integrity"
|
|
14
|
+
];
|
|
15
|
+
function c(e, t, n = !0) {
|
|
5
16
|
let r = /* @__PURE__ */ new Map(), i = (e) => {
|
|
6
17
|
if (e.nodeType !== Node.ELEMENT_NODE) return !1;
|
|
7
18
|
let t = e;
|
|
@@ -17,9 +28,11 @@ function o(e, t, n = !0) {
|
|
|
17
28
|
let n;
|
|
18
29
|
if (e.tagName === "LINK") {
|
|
19
30
|
let r = t.createElement("link");
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
for (let t of s) {
|
|
32
|
+
let n = e.getAttribute(t);
|
|
33
|
+
n !== null && r.setAttribute(t, n);
|
|
34
|
+
}
|
|
35
|
+
r.href = e.href, n = r;
|
|
23
36
|
} else {
|
|
24
37
|
let r = t.createElement("style");
|
|
25
38
|
r.textContent = a(e);
|
|
@@ -27,18 +40,18 @@ function o(e, t, n = !0) {
|
|
|
27
40
|
i && r.setAttribute("media", i), n = r;
|
|
28
41
|
}
|
|
29
42
|
t.head.appendChild(n), r.set(e, n);
|
|
30
|
-
},
|
|
43
|
+
}, c = (e) => {
|
|
31
44
|
let t = r.get(e);
|
|
32
45
|
t && (t.textContent = a(e));
|
|
33
|
-
},
|
|
46
|
+
}, l = (e) => {
|
|
34
47
|
for (let [t, n] of r) (e === t || e.nodeType === Node.ELEMENT_NODE && e.contains(t)) && (n.remove(), r.delete(t));
|
|
35
|
-
},
|
|
48
|
+
}, u = ["class", "style"], d = () => {
|
|
36
49
|
let n = [[e.documentElement, t.documentElement], [e.body, t.body]];
|
|
37
50
|
for (let [e, t] of n) if (!(!e || !t)) {
|
|
38
|
-
for (let n of e.attributes) (
|
|
39
|
-
for (let n of Array.from(t.attributes)) (
|
|
51
|
+
for (let n of e.attributes) (u.includes(n.name) || n.name.startsWith("data-")) && t.setAttribute(n.name, n.value);
|
|
52
|
+
for (let n of Array.from(t.attributes)) (u.includes(n.name) || n.name.startsWith("data-")) && !e.hasAttribute(n.name) && t.removeAttribute(n.name);
|
|
40
53
|
}
|
|
41
|
-
},
|
|
54
|
+
}, f = () => {
|
|
42
55
|
let n = t.defaultView;
|
|
43
56
|
if (!(!n?.CSSStyleSheet || !("adoptedStyleSheets" in e))) try {
|
|
44
57
|
t.adoptedStyleSheets = e.adoptedStyleSheets.map((e) => {
|
|
@@ -48,42 +61,71 @@ function o(e, t, n = !0) {
|
|
|
48
61
|
} catch {}
|
|
49
62
|
};
|
|
50
63
|
for (let t of e.querySelectorAll("style, link[rel~=\"stylesheet\" i]")) o(t);
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
let p = Array.from(r.values()).filter((e) => e.tagName === "LINK");
|
|
65
|
+
if (d(), f(), !n || typeof MutationObserver > "u") return {
|
|
66
|
+
stop: () => {},
|
|
67
|
+
links: p
|
|
68
|
+
};
|
|
69
|
+
let m = new MutationObserver((e) => {
|
|
53
70
|
for (let t of e) {
|
|
54
71
|
if (t.type === "characterData") {
|
|
55
72
|
let e = t.target.parentElement;
|
|
56
|
-
e?.tagName === "STYLE" &&
|
|
73
|
+
e?.tagName === "STYLE" && c(e);
|
|
57
74
|
continue;
|
|
58
75
|
}
|
|
59
|
-
i(t.target) && t.target.tagName === "STYLE" &&
|
|
76
|
+
i(t.target) && t.target.tagName === "STYLE" && c(t.target);
|
|
60
77
|
for (let e of t.addedNodes) if (i(e)) o(e);
|
|
61
78
|
else if (e.nodeType === Node.ELEMENT_NODE) for (let t of e.querySelectorAll("style, link[rel~=\"stylesheet\" i]")) o(t);
|
|
62
|
-
for (let e of t.removedNodes)
|
|
79
|
+
for (let e of t.removedNodes) l(e);
|
|
63
80
|
}
|
|
64
81
|
});
|
|
65
|
-
|
|
82
|
+
m.observe(e.head, {
|
|
66
83
|
childList: !0,
|
|
67
84
|
subtree: !0,
|
|
68
85
|
characterData: !0
|
|
69
86
|
});
|
|
70
|
-
let
|
|
71
|
-
return
|
|
72
|
-
|
|
87
|
+
let h = new MutationObserver(() => d());
|
|
88
|
+
return h.observe(e.documentElement, { attributes: !0 }), e.body && h.observe(e.body, { attributes: !0 }), {
|
|
89
|
+
stop: () => {
|
|
90
|
+
m.disconnect(), h.disconnect();
|
|
91
|
+
},
|
|
92
|
+
links: p
|
|
73
93
|
};
|
|
74
94
|
}
|
|
75
95
|
//#endregion
|
|
96
|
+
//#region src/whenStylesheetsLoad.ts
|
|
97
|
+
function l(e, t, n) {
|
|
98
|
+
let r = e.filter((e) => e.sheet === null && u(e));
|
|
99
|
+
if (r.length === 0) return n(), () => {};
|
|
100
|
+
let i = r.length, a = () => {
|
|
101
|
+
i--, i === 0 && s();
|
|
102
|
+
}, o = () => {
|
|
103
|
+
window.clearTimeout(c);
|
|
104
|
+
for (let e of r) e.removeEventListener("load", a), e.removeEventListener("error", a);
|
|
105
|
+
}, s = () => {
|
|
106
|
+
o(), n();
|
|
107
|
+
};
|
|
108
|
+
for (let e of r) e.addEventListener("load", a), e.addEventListener("error", a);
|
|
109
|
+
let c = window.setTimeout(s, t);
|
|
110
|
+
return o;
|
|
111
|
+
}
|
|
112
|
+
function u(e) {
|
|
113
|
+
if (/\balternate\b/i.test(e.rel)) return !1;
|
|
114
|
+
let t = e.ownerDocument.defaultView;
|
|
115
|
+
return !e.media || !t?.matchMedia || t.matchMedia(e.media).matches;
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
76
118
|
//#region src/usePopupWindow.ts
|
|
77
|
-
var
|
|
119
|
+
var d = {
|
|
78
120
|
popupWindow: null,
|
|
79
121
|
container: null,
|
|
80
122
|
blocked: !1
|
|
81
|
-
},
|
|
82
|
-
state =
|
|
123
|
+
}, f = class {
|
|
124
|
+
state = d;
|
|
83
125
|
listeners = /* @__PURE__ */ new Set();
|
|
84
126
|
subscribe = (e) => (this.listeners.add(e), () => this.listeners.delete(e));
|
|
85
127
|
getSnapshot = () => this.state;
|
|
86
|
-
getServerSnapshot = () =>
|
|
128
|
+
getServerSnapshot = () => d;
|
|
87
129
|
setState(e) {
|
|
88
130
|
this.state = {
|
|
89
131
|
...this.state,
|
|
@@ -91,89 +133,115 @@ var s = {
|
|
|
91
133
|
};
|
|
92
134
|
for (let e of this.listeners) e();
|
|
93
135
|
}
|
|
94
|
-
},
|
|
136
|
+
}, p = {
|
|
95
137
|
popup: !0,
|
|
96
138
|
width: 640,
|
|
97
139
|
height: 480
|
|
98
140
|
};
|
|
99
|
-
function
|
|
141
|
+
function m(e) {
|
|
100
142
|
let t = {
|
|
101
|
-
...
|
|
143
|
+
...p,
|
|
102
144
|
...e.features
|
|
103
145
|
};
|
|
104
146
|
return e.center !== !1 && t.left === void 0 && t.top === void 0 && typeof t.width == "number" && typeof t.height == "number" && (t.left = Math.max(0, Math.round(window.screenX + (window.outerWidth - t.width) / 2)), t.top = Math.max(0, Math.round(window.screenY + (window.outerHeight - t.height) / 2))), Object.entries(t).filter(([, e]) => e !== void 0 && e !== !1).map(([e, t]) => t === !0 ? `${e}=yes` : `${e}=${t}`).join(",");
|
|
105
147
|
}
|
|
106
|
-
|
|
148
|
+
var h = "about:blank", g = 20, _ = 3e3;
|
|
149
|
+
function v(e) {
|
|
150
|
+
let t = e.document;
|
|
151
|
+
return t.URL === h || t.readyState === "loading" ? null : t;
|
|
152
|
+
}
|
|
153
|
+
function y(e) {
|
|
107
154
|
function t({ children: t }) {
|
|
108
155
|
let { container: n } = i(e.subscribe, e.getSnapshot, e.getServerSnapshot);
|
|
109
156
|
return n ? a(t, n) : null;
|
|
110
157
|
}
|
|
111
158
|
return t.displayName = "PopupWindow.Popup", t;
|
|
112
159
|
}
|
|
113
|
-
function
|
|
114
|
-
let [
|
|
115
|
-
|
|
116
|
-
let
|
|
117
|
-
let { popupWindow: t } =
|
|
118
|
-
|
|
160
|
+
function b(a = {}) {
|
|
161
|
+
let [o] = r(() => new f()), [s] = r(() => y(o)), u = n(a);
|
|
162
|
+
u.current = a;
|
|
163
|
+
let d = n(null), p = i(o.subscribe, o.getSnapshot, o.getServerSnapshot), b = e((e) => {
|
|
164
|
+
let { popupWindow: t } = o.getSnapshot();
|
|
165
|
+
d.current?.(), d.current = null, t && (!e && !t.closed && t.close(), o.setState({
|
|
119
166
|
popupWindow: null,
|
|
120
167
|
container: null
|
|
121
|
-
}),
|
|
122
|
-
}, [
|
|
168
|
+
}), u.current.onClose?.());
|
|
169
|
+
}, [o]), x = e(() => {
|
|
123
170
|
if (typeof window > "u") return null;
|
|
124
|
-
let e =
|
|
171
|
+
let e = o.getSnapshot();
|
|
125
172
|
if (e.popupWindow && !e.popupWindow.closed) return e.popupWindow.focus(), e.popupWindow;
|
|
126
|
-
let t =
|
|
127
|
-
if (!
|
|
128
|
-
let
|
|
173
|
+
let t = u.current, n = t.url ?? h, r = window.open(n, t.name ?? "_blank", m(t));
|
|
174
|
+
if (!r) return o.setState({ blocked: !0 }), t.onBlocked?.(), null;
|
|
175
|
+
let i;
|
|
129
176
|
try {
|
|
130
|
-
|
|
177
|
+
i = r.document;
|
|
131
178
|
} catch {
|
|
132
|
-
return
|
|
179
|
+
return r.close(), o.setState({ blocked: !0 }), t.onBlocked?.(), null;
|
|
133
180
|
}
|
|
134
|
-
|
|
135
|
-
let i;
|
|
136
|
-
t.copyStyles !== !1 && (i = o(document, r));
|
|
137
|
-
let a = r.createElement("div");
|
|
138
|
-
a.setAttribute("data-popup-window-root", ""), r.body.appendChild(a);
|
|
139
|
-
let c = () => h(!0), l = () => {
|
|
181
|
+
let a = () => b(!0), s = () => {
|
|
140
182
|
setTimeout(() => {
|
|
141
|
-
|
|
183
|
+
r.closed && a();
|
|
142
184
|
}, 0);
|
|
185
|
+
}, f = window.setInterval(() => {
|
|
186
|
+
r.closed && a();
|
|
187
|
+
}, 250), p = () => r.close();
|
|
188
|
+
window.addEventListener("pagehide", p);
|
|
189
|
+
let y, x = !1, S, C;
|
|
190
|
+
d.current = () => {
|
|
191
|
+
window.clearInterval(f), window.clearInterval(y), window.removeEventListener("pagehide", p), x && r.removeEventListener("pagehide", s), S?.(), C?.();
|
|
143
192
|
};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
n
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
193
|
+
let w = (e) => {
|
|
194
|
+
x = !0, r.addEventListener("pagehide", s), e.title = t.title ?? document.title;
|
|
195
|
+
let n = [];
|
|
196
|
+
if (t.copyStyles !== !1) {
|
|
197
|
+
let t = c(document, e);
|
|
198
|
+
S = t.stop, n = t.links;
|
|
199
|
+
}
|
|
200
|
+
let i = e.createElement("div");
|
|
201
|
+
i.setAttribute("data-popup-window-root", ""), e.body.appendChild(i), C = l(n, _, () => {
|
|
202
|
+
o.setState({ container: i }), t.onOpen?.(r);
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
return o.setState({
|
|
206
|
+
popupWindow: r,
|
|
207
|
+
container: null,
|
|
153
208
|
blocked: !1
|
|
154
|
-
}),
|
|
155
|
-
|
|
156
|
-
|
|
209
|
+
}), n === h ? w(i) : y = window.setInterval(() => {
|
|
210
|
+
if (r.closed) {
|
|
211
|
+
a();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
let e;
|
|
215
|
+
try {
|
|
216
|
+
e = v(r);
|
|
217
|
+
} catch {
|
|
218
|
+
b(!1), o.setState({ blocked: !0 }), t.onBlocked?.();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
e && (window.clearInterval(y), w(e));
|
|
222
|
+
}, g), r;
|
|
223
|
+
}, [o, b]), S = e(() => b(!1), [b]), C = e(() => {
|
|
224
|
+
o.getSnapshot().popupWindow ? b(!1) : x();
|
|
157
225
|
}, [
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
]),
|
|
162
|
-
let { popupWindow: e } =
|
|
226
|
+
o,
|
|
227
|
+
b,
|
|
228
|
+
x
|
|
229
|
+
]), w = e(() => {
|
|
230
|
+
let { popupWindow: e } = o.getSnapshot();
|
|
163
231
|
e && !e.closed && e.focus();
|
|
164
|
-
}, [
|
|
165
|
-
return t(() => () =>
|
|
166
|
-
open:
|
|
167
|
-
close:
|
|
168
|
-
toggle:
|
|
169
|
-
focus:
|
|
170
|
-
isOpen:
|
|
171
|
-
isBlocked:
|
|
172
|
-
popupWindow:
|
|
173
|
-
Popup:
|
|
232
|
+
}, [o]);
|
|
233
|
+
return t(() => () => b(!1), [b]), {
|
|
234
|
+
open: x,
|
|
235
|
+
close: S,
|
|
236
|
+
toggle: C,
|
|
237
|
+
focus: w,
|
|
238
|
+
isOpen: p.popupWindow !== null,
|
|
239
|
+
isBlocked: p.blocked,
|
|
240
|
+
popupWindow: p.popupWindow,
|
|
241
|
+
Popup: s
|
|
174
242
|
};
|
|
175
243
|
}
|
|
176
244
|
//#endregion
|
|
177
|
-
export { o as copyStyles,
|
|
245
|
+
export { o as copyStyles, b as usePopupWindow };
|
|
178
246
|
|
|
179
247
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/copyStyles.ts","../src/usePopupWindow.ts"],"sourcesContent":["/**\n * Copy all stylesheets from `source` into `target` and (optionally) keep them\n * in sync while the popup is open:\n *\n * - `<link rel=\"stylesheet\">` and `<style>` elements are mirrored into the\n * target `<head>`. `<style>` contents are serialized from the CSSOM when\n * possible, so rules injected via `insertRule` (CSS-in-JS \"speedy\" mode)\n * are included.\n * - Additions/removals/edits of style nodes in the source `<head>` are\n * observed (covers Vite HMR, lazily loaded chunk CSS, styled-components).\n * - `class`/`style`/`data-*` attributes on `<html>` and `<body>` are mirrored\n * and kept in sync, so theme switching (e.g. a `dark` class) propagates.\n * - `document.adoptedStyleSheets` are re-constructed in the target document.\n *\n * Returns a function that stops observing.\n */\nexport function copyStyles(source: Document, target: Document, watch = true): () => void {\n const mirrors = new Map<Element, Element>()\n\n const isStyleNode = (node: Node): node is HTMLStyleElement | HTMLLinkElement => {\n if (node.nodeType !== Node.ELEMENT_NODE) return false\n const el = node as Element\n return (\n el.tagName === 'STYLE' ||\n (el.tagName === 'LINK' && (el.getAttribute('rel') ?? '').toLowerCase().includes('stylesheet'))\n )\n }\n\n const serializeStyle = (el: HTMLStyleElement): string => {\n const sheet = el.sheet\n if (sheet) {\n try {\n return Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n')\n } catch {\n // Inaccessible cssRules — fall back to the raw text below.\n }\n }\n return el.textContent ?? ''\n }\n\n const mirror = (el: HTMLStyleElement | HTMLLinkElement): void => {\n if (mirrors.has(el)) return\n let clone: Element\n if (el.tagName === 'LINK') {\n const link = target.createElement('link')\n link.rel = 'stylesheet'\n // .href resolves to an absolute URL, so relative hrefs keep working\n // from the popup's `about:blank` document.\n link.href = (el as HTMLLinkElement).href\n const media = el.getAttribute('media')\n if (media) link.media = media\n clone = link\n } else {\n const style = target.createElement('style')\n style.textContent = serializeStyle(el as HTMLStyleElement)\n const media = el.getAttribute('media')\n if (media) style.setAttribute('media', media)\n clone = style\n }\n target.head.appendChild(clone)\n mirrors.set(el, clone)\n }\n\n const refresh = (el: HTMLStyleElement): void => {\n const clone = mirrors.get(el)\n if (clone) clone.textContent = serializeStyle(el)\n }\n\n const unmirrorSubtree = (removed: Node): void => {\n for (const [src, clone] of mirrors) {\n if (removed === src || (removed.nodeType === Node.ELEMENT_NODE && removed.contains(src))) {\n clone.remove()\n mirrors.delete(src)\n }\n }\n }\n\n const ROOT_ATTRS = ['class', 'style']\n const syncRootAttrs = (): void => {\n const pairs: Array<[Element | null, Element | null]> = [\n [source.documentElement, target.documentElement],\n [source.body, target.body],\n ]\n for (const [from, to] of pairs) {\n if (!from || !to) continue\n for (const attr of from.attributes) {\n if (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) {\n to.setAttribute(attr.name, attr.value)\n }\n }\n for (const attr of Array.from(to.attributes)) {\n if (\n (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) &&\n !from.hasAttribute(attr.name)\n ) {\n to.removeAttribute(attr.name)\n }\n }\n }\n }\n\n const syncAdoptedSheets = (): void => {\n const targetWindow = target.defaultView as (Window & { CSSStyleSheet?: typeof CSSStyleSheet }) | null\n if (!targetWindow?.CSSStyleSheet || !('adoptedStyleSheets' in source)) return\n try {\n target.adoptedStyleSheets = source.adoptedStyleSheets.map((sheet) => {\n const copy = new targetWindow.CSSStyleSheet!()\n copy.replaceSync(\n Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n'),\n )\n return copy\n })\n } catch {\n // Best effort — older browsers or cross-origin rules.\n }\n }\n\n for (const el of source.querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n syncRootAttrs()\n syncAdoptedSheets()\n\n if (!watch || typeof MutationObserver === 'undefined') {\n return () => {}\n }\n\n const headObserver = new MutationObserver((records) => {\n for (const record of records) {\n if (record.type === 'characterData') {\n const parent = record.target.parentElement\n if (parent?.tagName === 'STYLE') refresh(parent as HTMLStyleElement)\n continue\n }\n // childList\n if (isStyleNode(record.target) && record.target.tagName === 'STYLE') {\n // textContent assignment replaces the text node → childList on <style>\n refresh(record.target as HTMLStyleElement)\n }\n for (const added of record.addedNodes) {\n if (isStyleNode(added)) {\n mirror(added)\n } else if (added.nodeType === Node.ELEMENT_NODE) {\n for (const el of (added as Element).querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n }\n }\n for (const removed of record.removedNodes) {\n unmirrorSubtree(removed)\n }\n }\n })\n headObserver.observe(source.head, { childList: true, subtree: true, characterData: true })\n\n const rootObserver = new MutationObserver(() => syncRootAttrs())\n rootObserver.observe(source.documentElement, { attributes: true })\n if (source.body) rootObserver.observe(source.body, { attributes: true })\n\n return () => {\n headObserver.disconnect()\n rootObserver.disconnect()\n }\n}\n","import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport type { FC } from 'react'\nimport { createPortal } from 'react-dom'\nimport { copyStyles } from './copyStyles'\nimport type {\n PopupProps,\n PopupWindowApi,\n PopupWindowFeatures,\n UsePopupWindowOptions,\n} from './types'\n\ninterface PopupState {\n popupWindow: Window | null\n container: HTMLElement | null\n blocked: boolean\n}\n\nconst INITIAL_STATE: PopupState = { popupWindow: null, container: null, blocked: false }\n\nclass PopupStore {\n state: PopupState = INITIAL_STATE\n private listeners = new Set<() => void>()\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n getSnapshot = (): PopupState => this.state\n\n getServerSnapshot = (): PopupState => INITIAL_STATE\n\n setState(partial: Partial<PopupState>): void {\n this.state = { ...this.state, ...partial }\n for (const listener of this.listeners) listener()\n }\n}\n\nconst DEFAULT_FEATURES: PopupWindowFeatures = { popup: true, width: 640, height: 480 }\n\nfunction buildFeatures(options: UsePopupWindowOptions): string {\n const merged: PopupWindowFeatures = { ...DEFAULT_FEATURES, ...options.features }\n if (\n options.center !== false &&\n merged.left === undefined &&\n merged.top === undefined &&\n typeof merged.width === 'number' &&\n typeof merged.height === 'number'\n ) {\n merged.left = Math.max(0, Math.round(window.screenX + (window.outerWidth - merged.width) / 2))\n merged.top = Math.max(0, Math.round(window.screenY + (window.outerHeight - merged.height) / 2))\n }\n return Object.entries(merged)\n .filter(([, value]) => value !== undefined && value !== false)\n .map(([key, value]) => (value === true ? `${key}=yes` : `${key}=${value}`))\n .join(',')\n}\n\nfunction createPopupComponent(store: PopupStore): FC<PopupProps> {\n function Popup({ children }: PopupProps) {\n const { container } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getServerSnapshot,\n )\n return container ? createPortal(children, container) : null\n }\n Popup.displayName = 'PopupWindow.Popup'\n return Popup\n}\n\n/**\n * Open part of your React tree in a separate browser window.\n *\n * The popup content is rendered with a portal into the popup's document, so\n * it stays part of your component tree: state, context and event handlers\n * all keep working across windows.\n *\n * ```tsx\n * const { open, close, isOpen, Popup } = usePopupWindow({ title: 'Panel' })\n *\n * return (\n * <>\n * <button onClick={open}>Open panel</button>\n * <Popup>\n * <MyPanel />\n * </Popup>\n * </>\n * )\n * ```\n */\nexport function usePopupWindow(options: UsePopupWindowOptions = {}): PopupWindowApi {\n const [store] = useState(() => new PopupStore())\n const [Popup] = useState(() => createPopupComponent(store))\n\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n const cleanupRef = useRef<(() => void) | null>(null)\n\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)\n\n // `userClosed` is true when the window was closed outside our control\n // (user hit the close button, opener unloaded) — then we must not touch it.\n const closePopup = useCallback(\n (userClosed: boolean) => {\n const { popupWindow } = store.getSnapshot()\n cleanupRef.current?.()\n cleanupRef.current = null\n if (!popupWindow) return\n if (!userClosed && !popupWindow.closed) popupWindow.close()\n store.setState({ popupWindow: null, container: null })\n optionsRef.current.onClose?.()\n },\n [store],\n )\n\n const open = useCallback((): Window | null => {\n if (typeof window === 'undefined') return null\n const current = store.getSnapshot()\n if (current.popupWindow && !current.popupWindow.closed) {\n current.popupWindow.focus()\n return current.popupWindow\n }\n\n const opts = optionsRef.current\n const popupWindow = window.open('about:blank', opts.name ?? '_blank', buildFeatures(opts))\n if (!popupWindow) {\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n\n // Sandboxed embedders (VS Code's built-in browser, CodeSandbox/StackBlitz\n // previews, iframes sandboxed without popup-escape) open the popup with an\n // opaque origin, so reading its document throws a SecurityError. Treat\n // that as blocked instead of stranding a blank window the portal can\n // never reach.\n let doc: Document\n try {\n doc = popupWindow.document\n } catch {\n popupWindow.close()\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n doc.title = opts.title ?? document.title\n\n let stopStyleSync: (() => void) | undefined\n if (opts.copyStyles !== false) {\n stopStyleSync = copyStyles(document, doc)\n }\n\n const container = doc.createElement('div')\n container.setAttribute('data-popup-window-root', '')\n doc.body.appendChild(container)\n\n const handleExternalClose = () => closePopup(true)\n const onPopupPagehide = () => {\n // pagehide also fires on navigation; only treat it as a close when the\n // window really is gone a tick later.\n setTimeout(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 0)\n }\n popupWindow.addEventListener('pagehide', onPopupPagehide)\n\n // Belt and braces: some browsers don't fire pagehide reliably for popups.\n const closePoll = window.setInterval(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 250)\n\n const onOpenerPagehide = () => popupWindow.close()\n window.addEventListener('pagehide', onOpenerPagehide)\n\n cleanupRef.current = () => {\n window.clearInterval(closePoll)\n window.removeEventListener('pagehide', onOpenerPagehide)\n popupWindow.removeEventListener('pagehide', onPopupPagehide)\n stopStyleSync?.()\n }\n\n store.setState({ popupWindow, container, blocked: false })\n opts.onOpen?.(popupWindow)\n return popupWindow\n }, [store, closePopup])\n\n const close = useCallback(() => closePopup(false), [closePopup])\n\n const toggle = useCallback(() => {\n if (store.getSnapshot().popupWindow) {\n closePopup(false)\n } else {\n open()\n }\n }, [store, closePopup, open])\n\n const focus = useCallback(() => {\n const { popupWindow } = store.getSnapshot()\n if (popupWindow && !popupWindow.closed) popupWindow.focus()\n }, [store])\n\n // Close the popup when the owning component unmounts — its portal content\n // would unmount anyway, leaving an empty window behind.\n useEffect(() => {\n return () => closePopup(false)\n }, [closePopup])\n\n return {\n open,\n close,\n toggle,\n focus,\n isOpen: state.popupWindow !== null,\n isBlocked: state.blocked,\n popupWindow: state.popupWindow,\n Popup,\n }\n}\n"],"mappings":";;;AAgBA,SAAgB,EAAW,GAAkB,GAAkB,IAAQ,IAAkB;CACvF,IAAM,oBAAU,IAAI,IAAsB,GAEpC,KAAe,MAA2D;EAC9E,IAAI,EAAK,aAAa,KAAK,cAAc,OAAO;EAChD,IAAM,IAAK;EACX,OACE,EAAG,YAAY,WACd,EAAG,YAAY,WAAW,EAAG,aAAa,KAAK,KAAK,GAAA,CAAI,YAAY,CAAC,CAAC,SAAS,YAAY;CAEhG,GAEM,KAAkB,MAAiC;EACvD,IAAM,IAAQ,EAAG;EACjB,IAAI,GACF,IAAI;GACF,OAAO,MAAM,KAAK,EAAM,QAAQ,CAAC,CAC9B,KAAK,MAAS,EAAK,OAAO,CAAC,CAC3B,KAAK,IAAI;EACd,QAAQ,CAER;EAEF,OAAO,EAAG,eAAe;CAC3B,GAEM,KAAU,MAAiD;EAC/D,IAAI,EAAQ,IAAI,CAAE,GAAG;EACrB,IAAI;EACJ,IAAI,EAAG,YAAY,QAAQ;GACzB,IAAM,IAAO,EAAO,cAAc,MAAM;GAIxC,AAHA,EAAK,MAAM,cAGX,EAAK,OAAQ,EAAuB;GACpC,IAAM,IAAQ,EAAG,aAAa,OAAO;GAErC,AADI,MAAO,EAAK,QAAQ,IACxB,IAAQ;EACV,OAAO;GACL,IAAM,IAAQ,EAAO,cAAc,OAAO;GAC1C,EAAM,cAAc,EAAe,CAAsB;GACzD,IAAM,IAAQ,EAAG,aAAa,OAAO;GAErC,AADI,KAAO,EAAM,aAAa,SAAS,CAAK,GAC5C,IAAQ;EACV;EAEA,AADA,EAAO,KAAK,YAAY,CAAK,GAC7B,EAAQ,IAAI,GAAI,CAAK;CACvB,GAEM,KAAW,MAA+B;EAC9C,IAAM,IAAQ,EAAQ,IAAI,CAAE;EAC5B,AAAI,MAAO,EAAM,cAAc,EAAe,CAAE;CAClD,GAEM,KAAmB,MAAwB;EAC/C,KAAK,IAAM,CAAC,GAAK,MAAU,GACzB,CAAI,MAAY,KAAQ,EAAQ,aAAa,KAAK,gBAAgB,EAAQ,SAAS,CAAG,OACpF,EAAM,OAAO,GACb,EAAQ,OAAO,CAAG;CAGxB,GAEM,IAAa,CAAC,SAAS,OAAO,GAC9B,UAA4B;EAChC,IAAM,IAAiD,CACrD,CAAC,EAAO,iBAAiB,EAAO,eAAe,GAC/C,CAAC,EAAO,MAAM,EAAO,IAAI,CAC3B;EACA,KAAK,IAAM,CAAC,GAAM,MAAO,GACnB,OAAC,KAAQ,CAAC,IACd;QAAK,IAAM,KAAQ,EAAK,YACtB,CAAI,EAAW,SAAS,EAAK,IAAI,KAAK,EAAK,KAAK,WAAW,OAAO,MAChE,EAAG,aAAa,EAAK,MAAM,EAAK,KAAK;GAGzC,KAAK,IAAM,KAAQ,MAAM,KAAK,EAAG,UAAU,GACzC,CACG,EAAW,SAAS,EAAK,IAAI,KAAK,EAAK,KAAK,WAAW,OAAO,MAC/D,CAAC,EAAK,aAAa,EAAK,IAAI,KAE5B,EAAG,gBAAgB,EAAK,IAAI;EARS;CAY7C,GAEM,UAAgC;EACpC,IAAM,IAAe,EAAO;EACxB,OAAC,GAAc,iBAAiB,EAAE,wBAAwB,KAC9D,IAAI;GACF,EAAO,qBAAqB,EAAO,mBAAmB,KAAK,MAAU;IACnE,IAAM,IAAO,IAAI,EAAa,cAAe;IAM7C,OALA,EAAK,YACH,MAAM,KAAK,EAAM,QAAQ,CAAC,CACvB,KAAK,MAAS,EAAK,OAAO,CAAC,CAC3B,KAAK,IAAI,CACd,GACO;GACT,CAAC;EACH,QAAQ,CAER;CACF;CAEA,KAAK,IAAM,KAAM,EAAO,iBAAiB,oCAAkC,GACzE,EAAO,CAAwC;CAKjD,IAHA,EAAc,GACd,EAAkB,GAEd,CAAC,KAAS,OAAO,mBAAqB,KACxC,aAAa,CAAC;CAGhB,IAAM,IAAe,IAAI,kBAAkB,MAAY;EACrD,KAAK,IAAM,KAAU,GAAS;GAC5B,IAAI,EAAO,SAAS,iBAAiB;IACnC,IAAM,IAAS,EAAO,OAAO;IAC7B,AAAI,GAAQ,YAAY,WAAS,EAAQ,CAA0B;IACnE;GACF;GAEA,AAAI,EAAY,EAAO,MAAM,KAAK,EAAO,OAAO,YAAY,WAE1D,EAAQ,EAAO,MAA0B;GAE3C,KAAK,IAAM,KAAS,EAAO,YACzB,IAAI,EAAY,CAAK,GACnB,EAAO,CAAK;QACP,IAAI,EAAM,aAAa,KAAK,cACjC,KAAK,IAAM,KAAO,EAAkB,iBAAiB,oCAAkC,GACrF,EAAO,CAAwC;GAIrD,KAAK,IAAM,KAAW,EAAO,cAC3B,EAAgB,CAAO;EAE3B;CACF,CAAC;CACD,EAAa,QAAQ,EAAO,MAAM;EAAE,WAAW;EAAM,SAAS;EAAM,eAAe;CAAK,CAAC;CAEzF,IAAM,IAAe,IAAI,uBAAuB,EAAc,CAAC;CAI/D,OAHA,EAAa,QAAQ,EAAO,iBAAiB,EAAE,YAAY,GAAK,CAAC,GAC7D,EAAO,QAAM,EAAa,QAAQ,EAAO,MAAM,EAAE,YAAY,GAAK,CAAC,SAE1D;EAEX,AADA,EAAa,WAAW,GACxB,EAAa,WAAW;CAC1B;AACF;;;ACtJA,IAAM,IAA4B;CAAE,aAAa;CAAM,WAAW;CAAM,SAAS;AAAM,GAEjF,IAAN,MAAiB;CACf,QAAoB;CACpB,4BAAoB,IAAI,IAAgB;CAExC,aAAa,OACX,KAAK,UAAU,IAAI,CAAQ,SACd,KAAK,UAAU,OAAO,CAAQ;CAG7C,oBAAgC,KAAK;CAErC,0BAAsC;CAEtC,SAAS,GAAoC;EAC3C,KAAK,QAAQ;GAAE,GAAG,KAAK;GAAO,GAAG;EAAQ;EACzC,KAAK,IAAM,KAAY,KAAK,WAAW,EAAS;CAClD;AACF,GAEM,IAAwC;CAAE,OAAO;CAAM,OAAO;CAAK,QAAQ;AAAI;AAErF,SAAS,EAAc,GAAwC;CAC7D,IAAM,IAA8B;EAAE,GAAG;EAAkB,GAAG,EAAQ;CAAS;CAW/E,OATE,EAAQ,WAAW,MACnB,EAAO,SAAS,KAAA,KAChB,EAAO,QAAQ,KAAA,KACf,OAAO,EAAO,SAAU,YACxB,OAAO,EAAO,UAAW,aAEzB,EAAO,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,WAAW,OAAO,aAAa,EAAO,SAAS,CAAC,CAAC,GAC7F,EAAO,MAAM,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,WAAW,OAAO,cAAc,EAAO,UAAU,CAAC,CAAC,IAEzF,OAAO,QAAQ,CAAM,CAAC,CAC1B,QAAQ,GAAG,OAAW,MAAU,KAAA,KAAa,MAAU,EAAK,CAAC,CAC7D,KAAK,CAAC,GAAK,OAAY,MAAU,KAAO,GAAG,EAAI,QAAQ,GAAG,EAAI,GAAG,GAAQ,CAAC,CAC1E,KAAK,GAAG;AACb;AAEA,SAAS,EAAqB,GAAmC;CAC/D,SAAS,EAAM,EAAE,eAAwB;EACvC,IAAM,EAAE,iBAAc,EACpB,EAAM,WACN,EAAM,aACN,EAAM,iBACR;EACA,OAAO,IAAY,EAAa,GAAU,CAAS,IAAI;CACzD;CAEA,OADA,EAAM,cAAc,qBACb;AACT;AAsBA,SAAgB,EAAe,IAAiC,CAAC,GAAmB;CAClF,IAAM,CAAC,KAAS,QAAe,IAAI,EAAW,CAAC,GACzC,CAAC,KAAS,QAAe,EAAqB,CAAK,CAAC,GAEpD,IAAa,EAAO,CAAO;CACjC,EAAW,UAAU;CAErB,IAAM,IAAa,EAA4B,IAAI,GAE7C,IAAQ,EAAqB,EAAM,WAAW,EAAM,aAAa,EAAM,iBAAiB,GAIxF,IAAa,GAChB,MAAwB;EACvB,IAAM,EAAE,mBAAgB,EAAM,YAAY;EAC1C,EAAW,UAAU,GACrB,EAAW,UAAU,MAChB,MACD,CAAC,KAAc,CAAC,EAAY,UAAQ,EAAY,MAAM,GAC1D,EAAM,SAAS;GAAE,aAAa;GAAM,WAAW;EAAK,CAAC,GACrD,EAAW,QAAQ,UAAU;CAC/B,GACA,CAAC,CAAK,CACR,GAEM,IAAO,QAAiC;EAC5C,IAAI,OAAO,SAAW,KAAa,OAAO;EAC1C,IAAM,IAAU,EAAM,YAAY;EAClC,IAAI,EAAQ,eAAe,CAAC,EAAQ,YAAY,QAE9C,OADA,EAAQ,YAAY,MAAM,GACnB,EAAQ;EAGjB,IAAM,IAAO,EAAW,SAClB,IAAc,OAAO,KAAK,eAAe,EAAK,QAAQ,UAAU,EAAc,CAAI,CAAC;EACzF,IAAI,CAAC,GAGH,OAFA,EAAM,SAAS,EAAE,SAAS,GAAK,CAAC,GAChC,EAAK,YAAY,GACV;EAQT,IAAI;EACJ,IAAI;GACF,IAAM,EAAY;EACpB,QAAQ;GAIN,OAHA,EAAY,MAAM,GAClB,EAAM,SAAS,EAAE,SAAS,GAAK,CAAC,GAChC,EAAK,YAAY,GACV;EACT;EACA,EAAI,QAAQ,EAAK,SAAS,SAAS;EAEnC,IAAI;EACJ,AAAI,EAAK,eAAe,OACtB,IAAgB,EAAW,UAAU,CAAG;EAG1C,IAAM,IAAY,EAAI,cAAc,KAAK;EAEzC,AADA,EAAU,aAAa,0BAA0B,EAAE,GACnD,EAAI,KAAK,YAAY,CAAS;EAE9B,IAAM,UAA4B,EAAW,EAAI,GAC3C,UAAwB;GAG5B,iBAAiB;IACf,AAAI,EAAY,UAAQ,EAAoB;GAC9C,GAAG,CAAC;EACN;EACA,EAAY,iBAAiB,YAAY,CAAe;EAGxD,IAAM,IAAY,OAAO,kBAAkB;GACzC,AAAI,EAAY,UAAQ,EAAoB;EAC9C,GAAG,GAAG,GAEA,UAAyB,EAAY,MAAM;EAYjD,OAXA,OAAO,iBAAiB,YAAY,CAAgB,GAEpD,EAAW,gBAAgB;GAIzB,AAHA,OAAO,cAAc,CAAS,GAC9B,OAAO,oBAAoB,YAAY,CAAgB,GACvD,EAAY,oBAAoB,YAAY,CAAe,GAC3D,IAAgB;EAClB,GAEA,EAAM,SAAS;GAAE;GAAa;GAAW,SAAS;EAAM,CAAC,GACzD,EAAK,SAAS,CAAW,GAClB;CACT,GAAG,CAAC,GAAO,CAAU,CAAC,GAEhB,IAAQ,QAAkB,EAAW,EAAK,GAAG,CAAC,CAAU,CAAC,GAEzD,IAAS,QAAkB;EAC/B,AAAI,EAAM,YAAY,CAAC,CAAC,cACtB,EAAW,EAAK,IAEhB,EAAK;CAET,GAAG;EAAC;EAAO;EAAY;CAAI,CAAC,GAEtB,IAAQ,QAAkB;EAC9B,IAAM,EAAE,mBAAgB,EAAM,YAAY;EAC1C,AAAI,KAAe,CAAC,EAAY,UAAQ,EAAY,MAAM;CAC5D,GAAG,CAAC,CAAK,CAAC;CAQV,OAJA,cACe,EAAW,EAAK,GAC5B,CAAC,CAAU,CAAC,GAER;EACL;EACA;EACA;EACA;EACA,QAAQ,EAAM,gBAAgB;EAC9B,WAAW,EAAM;EACjB,aAAa,EAAM;EACnB;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/copyStyles.ts","../src/whenStylesheetsLoad.ts","../src/usePopupWindow.ts"],"sourcesContent":["/**\n * Copy all stylesheets from `source` into `target` and (optionally) keep them\n * in sync while the popup is open:\n *\n * - `<link rel=\"stylesheet\">` and `<style>` elements are mirrored into the\n * target `<head>`. `<style>` contents are serialized from the CSSOM when\n * possible, so rules injected via `insertRule` (CSS-in-JS \"speedy\" mode)\n * are included.\n * - Additions/removals/edits of style nodes in the source `<head>` are\n * observed (covers Vite HMR, lazily loaded chunk CSS, styled-components).\n * - `class`/`style`/`data-*` attributes on `<html>` and `<body>` are mirrored\n * and kept in sync, so theme switching (e.g. a `dark` class) propagates.\n * - `document.adoptedStyleSheets` are re-constructed in the target document.\n *\n * Returns a function that stops observing.\n */\nexport function copyStyles(source: Document, target: Document, watch = true): () => void {\n return mirrorStyles(source, target, watch).stop\n}\n\n/** Attributes copied from a source `<link>` onto its mirror. */\nconst LINK_ATTRS = ['rel', 'media', 'title', 'crossorigin', 'referrerpolicy', 'integrity']\n\nexport interface StyleMirror {\n /** Stops observing the source document. */\n stop: () => void\n /**\n * The `<link>` elements created in `target` by the initial copy. The target\n * fetches each of them asynchronously.\n */\n links: HTMLLinkElement[]\n}\n\n/** {@link copyStyles}, plus the `<link>` elements the initial copy created. */\nexport function mirrorStyles(source: Document, target: Document, watch = true): StyleMirror {\n const mirrors = new Map<Element, Element>()\n\n const isStyleNode = (node: Node): node is HTMLStyleElement | HTMLLinkElement => {\n if (node.nodeType !== Node.ELEMENT_NODE) return false\n const el = node as Element\n return (\n el.tagName === 'STYLE' ||\n (el.tagName === 'LINK' && (el.getAttribute('rel') ?? '').toLowerCase().includes('stylesheet'))\n )\n }\n\n const serializeStyle = (el: HTMLStyleElement): string => {\n const sheet = el.sheet\n if (sheet) {\n try {\n return Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n')\n } catch {\n // Inaccessible cssRules — fall back to the raw text below.\n }\n }\n return el.textContent ?? ''\n }\n\n const mirror = (el: HTMLStyleElement | HTMLLinkElement): void => {\n if (mirrors.has(el)) return\n let clone: Element\n if (el.tagName === 'LINK') {\n const link = target.createElement('link')\n // `rel` is copied as is, so an alternate stylesheet stays disabled.\n // `crossorigin` and `referrerpolicy` make the popup send the same request\n // as the opener, so it can reuse the cached response.\n for (const name of LINK_ATTRS) {\n const value = el.getAttribute(name)\n if (value !== null) link.setAttribute(name, value)\n }\n // .href resolves to an absolute URL, so relative hrefs keep working\n // from the popup's `about:blank` document.\n link.href = (el as HTMLLinkElement).href\n clone = link\n } else {\n const style = target.createElement('style')\n style.textContent = serializeStyle(el as HTMLStyleElement)\n const media = el.getAttribute('media')\n if (media) style.setAttribute('media', media)\n clone = style\n }\n target.head.appendChild(clone)\n mirrors.set(el, clone)\n }\n\n const refresh = (el: HTMLStyleElement): void => {\n const clone = mirrors.get(el)\n if (clone) clone.textContent = serializeStyle(el)\n }\n\n const unmirrorSubtree = (removed: Node): void => {\n for (const [src, clone] of mirrors) {\n if (removed === src || (removed.nodeType === Node.ELEMENT_NODE && removed.contains(src))) {\n clone.remove()\n mirrors.delete(src)\n }\n }\n }\n\n const ROOT_ATTRS = ['class', 'style']\n const syncRootAttrs = (): void => {\n const pairs: Array<[Element | null, Element | null]> = [\n [source.documentElement, target.documentElement],\n [source.body, target.body],\n ]\n for (const [from, to] of pairs) {\n if (!from || !to) continue\n for (const attr of from.attributes) {\n if (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) {\n to.setAttribute(attr.name, attr.value)\n }\n }\n for (const attr of Array.from(to.attributes)) {\n if (\n (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) &&\n !from.hasAttribute(attr.name)\n ) {\n to.removeAttribute(attr.name)\n }\n }\n }\n }\n\n const syncAdoptedSheets = (): void => {\n const targetWindow = target.defaultView as (Window & { CSSStyleSheet?: typeof CSSStyleSheet }) | null\n if (!targetWindow?.CSSStyleSheet || !('adoptedStyleSheets' in source)) return\n try {\n target.adoptedStyleSheets = source.adoptedStyleSheets.map((sheet) => {\n const copy = new targetWindow.CSSStyleSheet!()\n copy.replaceSync(\n Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n'),\n )\n return copy\n })\n } catch {\n // Best effort — older browsers or cross-origin rules.\n }\n }\n\n for (const el of source.querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n const links = Array.from(mirrors.values()).filter(\n (clone): clone is HTMLLinkElement => clone.tagName === 'LINK',\n )\n syncRootAttrs()\n syncAdoptedSheets()\n\n if (!watch || typeof MutationObserver === 'undefined') {\n return { stop: () => {}, links }\n }\n\n const headObserver = new MutationObserver((records) => {\n for (const record of records) {\n if (record.type === 'characterData') {\n const parent = record.target.parentElement\n if (parent?.tagName === 'STYLE') refresh(parent as HTMLStyleElement)\n continue\n }\n // childList\n if (isStyleNode(record.target) && record.target.tagName === 'STYLE') {\n // textContent assignment replaces the text node → childList on <style>\n refresh(record.target as HTMLStyleElement)\n }\n for (const added of record.addedNodes) {\n if (isStyleNode(added)) {\n mirror(added)\n } else if (added.nodeType === Node.ELEMENT_NODE) {\n for (const el of (added as Element).querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n }\n }\n for (const removed of record.removedNodes) {\n unmirrorSubtree(removed)\n }\n }\n })\n headObserver.observe(source.head, { childList: true, subtree: true, characterData: true })\n\n const rootObserver = new MutationObserver(() => syncRootAttrs())\n rootObserver.observe(source.documentElement, { attributes: true })\n if (source.body) rootObserver.observe(source.body, { attributes: true })\n\n return {\n stop: () => {\n headObserver.disconnect()\n rootObserver.disconnect()\n },\n links,\n }\n}\n","/**\n * Calls `callback` once every stylesheet in `links` that applies has loaded\n * or failed, or after `timeoutMs`, whichever comes first.\n *\n * When none of them is still loading, `callback` runs synchronously, before\n * this function returns. Links that do not apply are not waited for: an\n * alternate stylesheet, or a `media` query that does not match, such as\n * `print`. Browsers fetch those at the lowest priority.\n *\n * Returns a function that stops waiting without calling `callback`.\n */\nexport function whenStylesheetsLoad(\n links: readonly HTMLLinkElement[],\n timeoutMs: number,\n callback: () => void,\n): () => void {\n const pending = links.filter((link) => link.sheet === null && applies(link))\n if (pending.length === 0) {\n callback()\n return () => {}\n }\n\n let remaining = pending.length\n const settle = () => {\n remaining--\n if (remaining === 0) finish()\n }\n const stop = () => {\n window.clearTimeout(timeout)\n for (const link of pending) {\n link.removeEventListener('load', settle)\n link.removeEventListener('error', settle)\n }\n }\n const finish = () => {\n stop()\n callback()\n }\n\n for (const link of pending) {\n link.addEventListener('load', settle)\n // A broken stylesheet must not keep the popup blank.\n link.addEventListener('error', settle)\n }\n const timeout = window.setTimeout(finish, timeoutMs)\n return stop\n}\n\nfunction applies(link: HTMLLinkElement): boolean {\n if (/\\balternate\\b/i.test(link.rel)) return false\n const view = link.ownerDocument.defaultView\n return !link.media || !view?.matchMedia || view.matchMedia(link.media).matches\n}\n","import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport type { FC } from 'react'\nimport { createPortal } from 'react-dom'\nimport { mirrorStyles } from './copyStyles'\nimport type {\n PopupProps,\n PopupWindowApi,\n PopupWindowFeatures,\n UsePopupWindowOptions,\n} from './types'\nimport { whenStylesheetsLoad } from './whenStylesheetsLoad'\n\ninterface PopupState {\n popupWindow: Window | null\n container: HTMLElement | null\n blocked: boolean\n}\n\nconst INITIAL_STATE: PopupState = { popupWindow: null, container: null, blocked: false }\n\nclass PopupStore {\n state: PopupState = INITIAL_STATE\n private listeners = new Set<() => void>()\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n getSnapshot = (): PopupState => this.state\n\n getServerSnapshot = (): PopupState => INITIAL_STATE\n\n setState(partial: Partial<PopupState>): void {\n this.state = { ...this.state, ...partial }\n for (const listener of this.listeners) listener()\n }\n}\n\nconst DEFAULT_FEATURES: PopupWindowFeatures = { popup: true, width: 640, height: 480 }\n\nfunction buildFeatures(options: UsePopupWindowOptions): string {\n const merged: PopupWindowFeatures = { ...DEFAULT_FEATURES, ...options.features }\n if (\n options.center !== false &&\n merged.left === undefined &&\n merged.top === undefined &&\n typeof merged.width === 'number' &&\n typeof merged.height === 'number'\n ) {\n merged.left = Math.max(0, Math.round(window.screenX + (window.outerWidth - merged.width) / 2))\n merged.top = Math.max(0, Math.round(window.screenY + (window.outerHeight - merged.height) / 2))\n }\n return Object.entries(merged)\n .filter(([, value]) => value !== undefined && value !== false)\n .map(([key, value]) => (value === true ? `${key}=yes` : `${key}=${value}`))\n .join(',')\n}\n\nconst ABOUT_BLANK = 'about:blank'\n/** Interval for checking whether the page given by `url` has loaded. */\nconst LOAD_POLL_MS = 20\n/** Longest time `Popup` waits for copied stylesheets before rendering anyway. */\nconst STYLES_TIMEOUT_MS = 3000\n\n/**\n * The popup's document once the page given by `url` has replaced the initial\n * `about:blank` document and finished parsing, otherwise `null`. Throws when\n * the document is not scriptable.\n */\nfunction loadedDocument(popupWindow: Window): Document | null {\n const doc = popupWindow.document\n if (doc.URL === ABOUT_BLANK || doc.readyState === 'loading') return null\n return doc\n}\n\nfunction createPopupComponent(store: PopupStore): FC<PopupProps> {\n function Popup({ children }: PopupProps) {\n const { container } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getServerSnapshot,\n )\n return container ? createPortal(children, container) : null\n }\n Popup.displayName = 'PopupWindow.Popup'\n return Popup\n}\n\n/**\n * Open part of your React tree in a separate browser window.\n *\n * The popup content is rendered with a portal into the popup's document, so\n * it stays part of your component tree: state, context and event handlers\n * all keep working across windows.\n *\n * ```tsx\n * const { open, close, isOpen, Popup } = usePopupWindow({ title: 'Panel' })\n *\n * return (\n * <>\n * <button onClick={open}>Open panel</button>\n * <Popup>\n * <MyPanel />\n * </Popup>\n * </>\n * )\n * ```\n */\nexport function usePopupWindow(options: UsePopupWindowOptions = {}): PopupWindowApi {\n const [store] = useState(() => new PopupStore())\n const [Popup] = useState(() => createPopupComponent(store))\n\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n const cleanupRef = useRef<(() => void) | null>(null)\n\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)\n\n // `userClosed` is true when the window was closed outside our control\n // (user hit the close button, opener unloaded) — then we must not touch it.\n const closePopup = useCallback(\n (userClosed: boolean) => {\n const { popupWindow } = store.getSnapshot()\n cleanupRef.current?.()\n cleanupRef.current = null\n if (!popupWindow) return\n if (!userClosed && !popupWindow.closed) popupWindow.close()\n store.setState({ popupWindow: null, container: null })\n optionsRef.current.onClose?.()\n },\n [store],\n )\n\n const open = useCallback((): Window | null => {\n if (typeof window === 'undefined') return null\n const current = store.getSnapshot()\n if (current.popupWindow && !current.popupWindow.closed) {\n current.popupWindow.focus()\n return current.popupWindow\n }\n\n const opts = optionsRef.current\n const url = opts.url ?? ABOUT_BLANK\n const popupWindow = window.open(url, opts.name ?? '_blank', buildFeatures(opts))\n if (!popupWindow) {\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n\n // Sandboxed embedders (VS Code's built-in browser, CodeSandbox/StackBlitz\n // previews, iframes sandboxed without popup-escape) open the popup with an\n // opaque origin, so reading its document throws a SecurityError. Treat\n // that as blocked instead of stranding a blank window the portal can\n // never reach.\n let initialDoc: Document\n try {\n initialDoc = popupWindow.document\n } catch {\n popupWindow.close()\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n\n const handleExternalClose = () => closePopup(true)\n const onPopupPagehide = () => {\n // pagehide also fires on navigation; only treat it as a close when the\n // window really is gone a tick later.\n setTimeout(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 0)\n }\n\n // Belt and braces: some browsers don't fire pagehide reliably for popups.\n const closePoll = window.setInterval(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 250)\n\n const onOpenerPagehide = () => popupWindow.close()\n window.addEventListener('pagehide', onOpenerPagehide)\n\n let loadPoll: number | undefined\n let prepared = false\n let stopStyleSync: (() => void) | undefined\n let stopStylesWait: (() => void) | undefined\n\n cleanupRef.current = () => {\n window.clearInterval(closePoll)\n window.clearInterval(loadPoll)\n window.removeEventListener('pagehide', onOpenerPagehide)\n if (prepared) popupWindow.removeEventListener('pagehide', onPopupPagehide)\n stopStyleSync?.()\n stopStylesWait?.()\n }\n\n // Title, styles and the portal container go into the document that\n // stays: `about:blank` itself, or the page `url` loads in its place.\n const prepare = (doc: Document) => {\n prepared = true\n popupWindow.addEventListener('pagehide', onPopupPagehide)\n doc.title = opts.title ?? document.title\n let links: HTMLLinkElement[] = []\n if (opts.copyStyles !== false) {\n const styles = mirrorStyles(document, doc)\n stopStyleSync = styles.stop\n links = styles.links\n }\n const container = doc.createElement('div')\n container.setAttribute('data-popup-window-root', '')\n doc.body.appendChild(container)\n // The popup fetches copied <link> stylesheets asynchronously. Render\n // only once they have loaded, so the first paint is not unstyled.\n stopStylesWait = whenStylesheetsLoad(links, STYLES_TIMEOUT_MS, () => {\n store.setState({ container })\n opts.onOpen?.(popupWindow)\n })\n }\n\n store.setState({ popupWindow, container: null, blocked: false })\n\n if (url === ABOUT_BLANK) {\n prepare(initialDoc)\n } else {\n // The window starts on an initial about:blank document and then\n // navigates to `url`. Wait until that page has replaced it and parsed.\n loadPoll = window.setInterval(() => {\n if (popupWindow.closed) {\n handleExternalClose()\n return\n }\n let doc: Document | null\n try {\n doc = loadedDocument(popupWindow)\n } catch {\n // The page went somewhere the opener cannot script, such as a\n // cross-origin redirect: nothing can ever render there.\n closePopup(false)\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return\n }\n if (!doc) return\n window.clearInterval(loadPoll)\n prepare(doc)\n }, LOAD_POLL_MS)\n }\n\n return popupWindow\n }, [store, closePopup])\n\n const close = useCallback(() => closePopup(false), [closePopup])\n\n const toggle = useCallback(() => {\n if (store.getSnapshot().popupWindow) {\n closePopup(false)\n } else {\n open()\n }\n }, [store, closePopup, open])\n\n const focus = useCallback(() => {\n const { popupWindow } = store.getSnapshot()\n if (popupWindow && !popupWindow.closed) popupWindow.focus()\n }, [store])\n\n // Close the popup when the owning component unmounts — its portal content\n // would unmount anyway, leaving an empty window behind.\n useEffect(() => {\n return () => closePopup(false)\n }, [closePopup])\n\n return {\n open,\n close,\n toggle,\n focus,\n isOpen: state.popupWindow !== null,\n isBlocked: state.blocked,\n popupWindow: state.popupWindow,\n Popup,\n }\n}\n"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -18,6 +18,14 @@ export interface UsePopupWindowOptions {
|
|
|
18
18
|
* window. Defaults to `_blank` (a fresh window every time).
|
|
19
19
|
*/
|
|
20
20
|
name?: string;
|
|
21
|
+
/**
|
|
22
|
+
* URL the popup loads instead of `about:blank`: an empty page served on the
|
|
23
|
+
* same origin, such as `/popup.html`. The popup's address bar shows that
|
|
24
|
+
* URL. `isOpen` turns `true` at once; `Popup` renders once the page has
|
|
25
|
+
* loaded. If the page redirects to another origin, the popup closes and is
|
|
26
|
+
* reported through `onBlocked`. Defaults to `about:blank`.
|
|
27
|
+
*/
|
|
28
|
+
url?: string;
|
|
21
29
|
/** Extra `window.open` features. Defaults to `{ popup: true, width: 640, height: 480 }` merged with what you pass. */
|
|
22
30
|
features?: PopupWindowFeatures;
|
|
23
31
|
/**
|
|
@@ -28,17 +36,24 @@ export interface UsePopupWindowOptions {
|
|
|
28
36
|
/**
|
|
29
37
|
* Copy the opener document's stylesheets into the popup and keep them in
|
|
30
38
|
* sync (new/changed `<style>`/`<link>` tags, `class`/`style` attributes on
|
|
31
|
-
* `<html>`/`<body>`, adopted stylesheets).
|
|
39
|
+
* `<html>`/`<body>`, adopted stylesheets). `Popup` renders once the copied
|
|
40
|
+
* `<link>` stylesheets have loaded, or after 3 seconds, so the first frame
|
|
41
|
+
* shown is styled. Defaults to `true`.
|
|
32
42
|
*/
|
|
33
43
|
copyStyles?: boolean;
|
|
34
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Called when `Popup` starts rendering into the popup: once the window has
|
|
46
|
+
* opened, the `url` page has loaded, and copied `<link>` stylesheets have
|
|
47
|
+
* loaded.
|
|
48
|
+
*/
|
|
35
49
|
onOpen?: (popupWindow: Window) => void;
|
|
36
50
|
/** Called when the popup closes — via `close()`, the user, or the opener unloading. */
|
|
37
51
|
onClose?: () => void;
|
|
38
52
|
/**
|
|
39
53
|
* Called when the popup cannot be used: `window.open` returned `null`
|
|
40
54
|
* (blocked by the browser), or the popup's document is not scriptable
|
|
41
|
-
* because a sandboxed embedder gave it an opaque origin
|
|
55
|
+
* because a sandboxed embedder gave it an opaque origin or `url` led to
|
|
56
|
+
* another origin.
|
|
42
57
|
*/
|
|
43
58
|
onBlocked?: () => void;
|
|
44
59
|
}
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAE1C;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;CACzD;AAED,MAAM,WAAW,qBAAqB;IACpC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sHAAsH;IACtH,QAAQ,CAAC,EAAE,mBAAmB,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAE1C;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;CACzD;AAED,MAAM,WAAW,qBAAqB;IACpC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,sHAAsH;IACtH,QAAQ,CAAC,EAAE,mBAAmB,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,uFAAuF;IACvF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,IAAI,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IACzB,kDAAkD;IAClD,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,qCAAqC;IACrC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,yDAAyD;IACzD,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,kDAAkD;IAClD,MAAM,EAAE,OAAO,CAAA;IACf;;;OAGG;IACH,SAAS,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B;;;OAGG;IACH,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,CAAA;CACtB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePopupWindow.d.ts","sourceRoot":"","sources":["../src/usePopupWindow.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,cAAc,EAEd,qBAAqB,EACtB,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"usePopupWindow.d.ts","sourceRoot":"","sources":["../src/usePopupWindow.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,cAAc,EAEd,qBAAqB,EACtB,MAAM,SAAS,CAAA;AAgFhB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,cAAc,CA+KlF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calls `callback` once every stylesheet in `links` that applies has loaded
|
|
3
|
+
* or failed, or after `timeoutMs`, whichever comes first.
|
|
4
|
+
*
|
|
5
|
+
* When none of them is still loading, `callback` runs synchronously, before
|
|
6
|
+
* this function returns. Links that do not apply are not waited for: an
|
|
7
|
+
* alternate stylesheet, or a `media` query that does not match, such as
|
|
8
|
+
* `print`. Browsers fetch those at the lowest priority.
|
|
9
|
+
*
|
|
10
|
+
* Returns a function that stops waiting without calling `callback`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function whenStylesheetsLoad(links: readonly HTMLLinkElement[], timeoutMs: number, callback: () => void): () => void;
|
|
13
|
+
//# sourceMappingURL=whenStylesheetsLoad.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whenStylesheetsLoad.d.ts","sourceRoot":"","sources":["../src/whenStylesheetsLoad.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,SAAS,eAAe,EAAE,EACjC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,IAAI,GACnB,MAAM,IAAI,CA+BZ"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jielga/react-popup-window",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "React hook to open part of your UI in a separate browser popup window — live portal rendering, style syncing and cross-window messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
57
57
|
"skills:validate": "node node_modules/@tanstack/intent/dist/cli.mjs validate",
|
|
58
58
|
"changeset": "changeset",
|
|
59
|
-
"version-packages": "changeset version && node scripts/sync-skill-version.mjs",
|
|
59
|
+
"version-packages": "changeset version && npm install --package-lock-only --no-audit --no-fund && node scripts/sync-skill-version.mjs",
|
|
60
60
|
"release": "changeset publish",
|
|
61
61
|
"prepublishOnly": "npm run build"
|
|
62
62
|
},
|
|
@@ -3,15 +3,15 @@ name: getting-started
|
|
|
3
3
|
description: >
|
|
4
4
|
Set up @jielga/react-popup-window: the usePopupWindow hook, the Popup portal
|
|
5
5
|
component, window controls (open, close, toggle, focus), reactive state
|
|
6
|
-
(isOpen, isBlocked, popupWindow), and hook options (title, name,
|
|
7
|
-
center, copyStyles, onOpen, onClose, onBlocked). Load when opening
|
|
8
|
-
React tree in a separate browser window, building an "open in new
|
|
9
|
-
detachable panel, or when a popup is blocked, closes unexpectedly,
|
|
10
|
-
state.
|
|
6
|
+
(isOpen, isBlocked, popupWindow), and hook options (title, name, url,
|
|
7
|
+
features, center, copyStyles, onOpen, onClose, onBlocked). Load when opening
|
|
8
|
+
part of a React tree in a separate browser window, building an "open in new
|
|
9
|
+
window" or detachable panel, or when a popup is blocked, closes unexpectedly,
|
|
10
|
+
loses state, or shows about:blank in its address bar.
|
|
11
11
|
metadata:
|
|
12
12
|
type: core
|
|
13
13
|
library: '@jielga/react-popup-window'
|
|
14
|
-
library_version: '0.
|
|
14
|
+
library_version: '0.2.0'
|
|
15
15
|
sources:
|
|
16
16
|
- 'Jielga/react-popup-window:README.md'
|
|
17
17
|
- 'Jielga/react-popup-window:src/usePopupWindow.ts'
|
|
@@ -21,9 +21,10 @@ sources:
|
|
|
21
21
|
# react-popup-window — Getting started
|
|
22
22
|
|
|
23
23
|
`usePopupWindow` opens part of a React tree in a separate browser window.
|
|
24
|
-
Content is rendered with a portal into the popup's `about:blank
|
|
25
|
-
so it remains part of the calling
|
|
26
|
-
|
|
24
|
+
Content is rendered with a portal into the popup's document (`about:blank`,
|
|
25
|
+
or the empty page set with `url`), so it remains part of the calling
|
|
26
|
+
component tree: state, context, and event handlers work across windows
|
|
27
|
+
without bridging. The popup document runs no
|
|
27
28
|
JavaScript of its own; the opener's React instance renders into it.
|
|
28
29
|
|
|
29
30
|
## Setup
|
|
@@ -90,16 +91,33 @@ const { open, isBlocked } = usePopupWindow({ onBlocked: () => notifyUser() })
|
|
|
90
91
|
// open() returns Window | null; null means blocked (or non-browser environment)
|
|
91
92
|
```
|
|
92
93
|
|
|
94
|
+
### Show the app's URL in the address bar
|
|
95
|
+
|
|
96
|
+
By default, the popup loads `about:blank`, and its address bar shows that.
|
|
97
|
+
Set `url` to an empty page served on the same origin; the bar then shows it:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
// public/popup.html:
|
|
101
|
+
// <!doctype html><html><head><meta charset="utf-8" /></head><body></body></html>
|
|
102
|
+
const { open, Popup } = usePopupWindow({ title: 'Panel', url: '/popup.html' })
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`isOpen` turns `true` as soon as the window opens; `Popup` renders once the
|
|
106
|
+
page has loaded. Important: do not point `url` at a route of the app, as that
|
|
107
|
+
starts a second copy of the app inside the popup. If the page redirects to
|
|
108
|
+
another origin, the popup closes and `onBlocked` is called.
|
|
109
|
+
|
|
93
110
|
### Options
|
|
94
111
|
|
|
95
112
|
```ts
|
|
96
113
|
interface UsePopupWindowOptions {
|
|
97
114
|
title?: string // popup document title; defaults to the opener's title
|
|
98
115
|
name?: string // window.open target name; same name reuses the window
|
|
116
|
+
url?: string // same-origin empty page to load instead of about:blank
|
|
99
117
|
features?: PopupWindowFeatures // merged over { popup: true, width: 640, height: 480 }
|
|
100
118
|
center?: boolean // center over the opener window; default true
|
|
101
119
|
copyStyles?: boolean // mirror and live-sync stylesheets; default true
|
|
102
|
-
onOpen?: (popupWindow: Window) => void
|
|
120
|
+
onOpen?: (popupWindow: Window) => void // Popup starts rendering into the popup
|
|
103
121
|
onClose?: () => void // close(), user close, or opener unload
|
|
104
122
|
onBlocked?: () => void
|
|
105
123
|
}
|
|
@@ -12,7 +12,7 @@ description: >
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: sub-skill
|
|
14
14
|
library: '@jielga/react-popup-window'
|
|
15
|
-
library_version: '0.
|
|
15
|
+
library_version: '0.2.0'
|
|
16
16
|
requires:
|
|
17
17
|
- '@jielga/react-popup-window/getting-started'
|
|
18
18
|
sources:
|
|
@@ -26,9 +26,9 @@ sources:
|
|
|
26
26
|
This skill builds on getting-started. Read it first for the portal model
|
|
27
27
|
and hook API.
|
|
28
28
|
|
|
29
|
-
The popup document starts as an unstyled `about:blank
|
|
30
|
-
open, the library mirrors the opener's
|
|
31
|
-
current:
|
|
29
|
+
The popup document starts as an unstyled page: `about:blank`, or the empty
|
|
30
|
+
page set with `url`. While it is open, the library mirrors the opener's
|
|
31
|
+
styling into it and keeps the mirror current:
|
|
32
32
|
|
|
33
33
|
- `<style>` and `<link rel="stylesheet">` elements are copied into the
|
|
34
34
|
popup `<head>`. `<style>` contents are serialized from the CSSOM, so
|
|
@@ -42,6 +42,12 @@ current:
|
|
|
42
42
|
automatically.
|
|
43
43
|
- `document.adoptedStyleSheets` are reconstructed in the popup document.
|
|
44
44
|
|
|
45
|
+
The popup loads each copied `<link>` stylesheet again. `Popup` renders once
|
|
46
|
+
they have loaded or failed (at most 3 seconds), so the first frame shown is
|
|
47
|
+
styled. Alternate stylesheets and non-matching `media` are not waited for.
|
|
48
|
+
A `<link>` added to the opener while the popup is open loads in the popup
|
|
49
|
+
after the content is shown.
|
|
50
|
+
|
|
45
51
|
`copyStyles: false` disables all of it. The mechanism is also exported
|
|
46
52
|
standalone as `copyStyles(source, target, watch?)`, returning a `stop`
|
|
47
53
|
function, for windows managed outside the hook.
|