@jielga/react-popup-window 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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` window and creates a container
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 after the popup window is opened and prepared. */
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
@@ -158,12 +185,24 @@ document and kept current:
158
185
  - `<style>` and `<link rel="stylesheet">` elements. `<style>` contents are
159
186
  serialized from the CSSOM, so rules injected with `insertRule` are
160
187
  included.
188
+ - Rules added to or removed from a `<style>` sheet with `insertRule` and
189
+ `deleteRule`, applied to the popup as they happen. CSS-in-JS libraries
190
+ such as Emotion and styled-components add rules this way in production
191
+ builds.
161
192
  - Additions, removals, and text edits of style nodes in `<head>`. This
162
- covers Vite HMR, lazily loaded chunk CSS, and CSS-in-JS libraries.
193
+ covers Vite HMR, lazily loaded chunk CSS, and CSS-in-JS libraries in
194
+ development builds.
163
195
  - `class`, `style`, and `data-*` attributes on `<html>` and `<body>`.
164
196
  Theme systems keyed on root attributes propagate to the popup.
165
197
  - `document.adoptedStyleSheets`.
166
198
 
199
+ The popup loads each copied `<link>` stylesheet again.
200
+ `Popup` renders once they have loaded or failed, so the first frame shown is styled.
201
+ Alternate stylesheets and stylesheets whose `media` does not match are not waited for.
202
+ If a stylesheet takes longer than 3 seconds, `Popup` renders without it.
203
+ Note that a `<link>` added to the opener while the popup is open loads in the popup after the content is shown.
204
+ Edits inside an existing rule, rules inserted into a nested rule such as `@media`, and changes to `document.adoptedStyleSheets` after the popup opened are not mirrored.
205
+
167
206
  Set `copyStyles: false` to disable.
168
207
 
169
208
  ## Communication
@@ -279,6 +318,9 @@ useEffect(() => {
279
318
 
280
319
  - Browsers do not allow hiding the address bar entirely. `popup: true`
281
320
  (the default) requests the minimal window chrome the platform provides.
321
+ By default, the bar shows `about:blank`; `url` replaces that with a page
322
+ of your own, see
323
+ [Show your own URL in the address bar](#show-your-own-url-in-the-address-bar).
282
324
  - `open()` must be called from a user gesture; otherwise the browser's
283
325
  popup blocker intervenes and `open()` returns `null`.
284
326
  - Popup content unmounts and remounts when it moves between windows. State
@@ -4,10 +4,14 @@
4
4
  *
5
5
  * - `<link rel="stylesheet">` and `<style>` elements are mirrored into the
6
6
  * target `<head>`. `<style>` contents are serialized from the CSSOM when
7
- * possible, so rules injected via `insertRule` (CSS-in-JS "speedy" mode)
8
- * are included.
7
+ * possible, so rules injected via `insertRule` are included.
8
+ * - Rules added to or removed from a `<style>` sheet later with
9
+ * `insertRule`/`deleteRule` are applied to its mirror as they happen. This
10
+ * is how CSS-in-JS libraries (Emotion, styled-components) add rules in
11
+ * production builds, and it changes no DOM.
9
12
  * - Additions/removals/edits of style nodes in the source `<head>` are
10
- * observed (covers Vite HMR, lazily loaded chunk CSS, styled-components).
13
+ * observed (covers Vite HMR, lazily loaded chunk CSS, CSS-in-JS in
14
+ * development builds).
11
15
  * - `class`/`style`/`data-*` attributes on `<html>` and `<body>` are mirrored
12
16
  * and kept in sync, so theme switching (e.g. a `dark` class) propagates.
13
17
  * - `document.adoptedStyleSheets` are re-constructed in the target document.
@@ -15,4 +19,15 @@
15
19
  * Returns a function that stops observing.
16
20
  */
17
21
  export declare function copyStyles(source: Document, target: Document, watch?: boolean): () => void;
22
+ export interface StyleMirror {
23
+ /** Stops observing the source document. */
24
+ stop: () => void;
25
+ /**
26
+ * The `<link>` elements created in `target` by the initial copy. The target
27
+ * fetches each of them asynchronously.
28
+ */
29
+ links: HTMLLinkElement[];
30
+ }
31
+ /** {@link copyStyles}, plus the `<link>` elements the initial copy created. */
32
+ export declare function mirrorStyles(source: Document, target: Document, watch?: boolean): StyleMirror;
18
33
  //# sourceMappingURL=copyStyles.d.ts.map
@@ -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,CAuJvF"}
1
+ {"version":3,"file":"copyStyles.d.ts","sourceRoot":"","sources":["../src/copyStyles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,UAAO,GAAG,MAAM,IAAI,CAEvF;AA6CD,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,CA2O1F"}
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){let r=new Map,i=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`)},a=e=>{let t=e.sheet;if(t)try{return Array.from(t.cssRules).map(e=>e.cssText).join(`
2
- `)}catch{}return e.textContent??``},o=e=>{if(r.has(e))return;let n;if(e.tagName===`LINK`){let r=t.createElement(`link`);r.rel=`stylesheet`,r.href=e.href;let i=e.getAttribute(`media`);i&&(r.media=i),n=r}else{let r=t.createElement(`style`);r.textContent=a(e);let i=e.getAttribute(`media`);i&&r.setAttribute(`media`,i),n=r}t.head.appendChild(n),r.set(e,n)},s=e=>{let t=r.get(e);t&&(t.textContent=a(e))},c=e=>{for(let[t,n]of r)(e===t||e.nodeType===Node.ELEMENT_NODE&&e.contains(t))&&(n.remove(),r.delete(t))},l=[`class`,`style`],u=()=>{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)(l.includes(n.name)||n.name.startsWith(`data-`))&&t.setAttribute(n.name,n.value);for(let n of Array.from(t.attributes))(l.includes(n.name)||n.name.startsWith(`data-`))&&!e.hasAttribute(n.name)&&t.removeAttribute(n.name)}},d=()=>{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]`))o(t);if(u(),d(),!n||typeof MutationObserver>`u`)return()=>{};let f=new MutationObserver(e=>{for(let t of e){if(t.type===`characterData`){let e=t.target.parentElement;e?.tagName===`STYLE`&&s(e);continue}i(t.target)&&t.target.tagName===`STYLE`&&s(t.target);for(let e of t.addedNodes)if(i(e))o(e);else if(e.nodeType===Node.ELEMENT_NODE)for(let t of e.querySelectorAll(`style, link[rel~="stylesheet" i]`))o(t);for(let e of t.removedNodes)c(e)}});f.observe(e.head,{childList:!0,subtree:!0,characterData:!0});let p=new MutationObserver(()=>u());return p.observe(e.documentElement,{attributes:!0}),e.body&&p.observe(e.body,{attributes:!0}),()=>{f.disconnect(),p.disconnect()}}var r={popupWindow:null,container:null,blocked:!1},i=class{state=r;listeners=new Set;subscribe=e=>(this.listeners.add(e),()=>this.listeners.delete(e));getSnapshot=()=>this.state;getServerSnapshot=()=>r;setState(e){this.state={...this.state,...e};for(let e of this.listeners)e()}},a={popup:!0,width:640,height:480};function o(e){let t={...a,...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(`,`)}function s(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 c(t={}){let[r]=(0,e.useState)(()=>new i),[a]=(0,e.useState)(()=>s(r)),c=(0,e.useRef)(t);c.current=t;let l=(0,e.useRef)(null),u=(0,e.useSyncExternalStore)(r.subscribe,r.getSnapshot,r.getServerSnapshot),d=(0,e.useCallback)(e=>{let{popupWindow:t}=r.getSnapshot();l.current?.(),l.current=null,t&&(!e&&!t.closed&&t.close(),r.setState({popupWindow:null,container:null}),c.current.onClose?.())},[r]),f=(0,e.useCallback)(()=>{if(typeof window>`u`)return null;let e=r.getSnapshot();if(e.popupWindow&&!e.popupWindow.closed)return e.popupWindow.focus(),e.popupWindow;let t=c.current,i=window.open(`about:blank`,t.name??`_blank`,o(t));if(!i)return r.setState({blocked:!0}),t.onBlocked?.(),null;let a;try{a=i.document}catch{return i.close(),r.setState({blocked:!0}),t.onBlocked?.(),null}a.title=t.title??document.title;let s;t.copyStyles!==!1&&(s=n(document,a));let u=a.createElement(`div`);u.setAttribute(`data-popup-window-root`,``),a.body.appendChild(u);let f=()=>d(!0),p=()=>{setTimeout(()=>{i.closed&&f()},0)};i.addEventListener(`pagehide`,p);let m=window.setInterval(()=>{i.closed&&f()},250),h=()=>i.close();return window.addEventListener(`pagehide`,h),l.current=()=>{window.clearInterval(m),window.removeEventListener(`pagehide`,h),i.removeEventListener(`pagehide`,p),s?.()},r.setState({popupWindow:i,container:u,blocked:!1}),t.onOpen?.(i),i},[r,d]),p=(0,e.useCallback)(()=>d(!1),[d]),m=(0,e.useCallback)(()=>{r.getSnapshot().popupWindow?d(!1):f()},[r,d,f]),h=(0,e.useCallback)(()=>{let{popupWindow:e}=r.getSnapshot();e&&!e.closed&&e.focus()},[r]);return(0,e.useEffect)(()=>()=>d(!1),[d]),{open:f,close:p,toggle:m,focus:h,isOpen:u.popupWindow!==null,isBlocked:u.blocked,popupWindow:u.popupWindow,Popup:a}}exports.copyStyles=n,exports.usePopupWindow=c;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("react"),t=require("react-dom");function n(e,t,n=!0){return o(e,t,n).stop}var r=new WeakMap;function i(e,t){let n=r.get(e);if(!n){let t=new Set;n=t,r.set(e,t);let i=Object.getPrototypeOf(e);e.insertRule=function(e,n){let r=i.insertRule.call(this,e,n);for(let n of[...t])n({type:`insert`,rule:e,index:r});return r},e.deleteRule=function(e){i.deleteRule.call(this,e);for(let n of[...t])n({type:`delete`,index:e})}}let i=n;return i.add(t),()=>{if(i.delete(t),i.size>0)return;r.delete(e);let n=e;delete n.insertRule,delete n.deleteRule}}var a=[`rel`,`media`,`title`,`crossorigin`,`referrerpolicy`,`integrity`];function o(e,t,n=!0){let r=new Map,o=n&&typeof MutationObserver<`u`,s=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`)},c=e=>{let t=e.sheet;if(t)try{return Array.from(t.cssRules).map(e=>e.cssText).join(`
2
+ `)}catch{}return e.textContent??``},l=new Map,u=(e,t)=>{let n=e.sheet,r=l.get(e);if(r&&r.sheet===n){r.count=n.cssRules.length;return}if(r?.stop(),l.delete(e),!o||!n)return;let a={sheet:n,count:n.cssRules.length,stop:i(n,r=>{let i=t.sheet;try{if(i&&(r.type===`insert`?i.insertRule(r.rule,r.index):i.deleteRule(r.index),i.cssRules.length===n.cssRules.length)){a.count=n.cssRules.length;return}}catch{}p(e)})};l.set(e,a)},d=e=>{l.get(e)?.stop(),l.delete(e)},f=e=>{if(r.has(e))return;let n;if(e.tagName===`LINK`){let r=t.createElement(`link`);for(let t of a){let n=e.getAttribute(t);n!==null&&r.setAttribute(t,n)}r.href=e.href,n=r}else{let r=t.createElement(`style`);r.textContent=c(e);let i=e.getAttribute(`media`);i&&r.setAttribute(`media`,i),n=r}t.head.appendChild(n),r.set(e,n),e.tagName===`STYLE`&&u(e,n)},p=e=>{let t=r.get(e);t&&(t.textContent=c(e),u(e,t))},m=e=>{for(let[t,n]of r)(e===t||e.nodeType===Node.ELEMENT_NODE&&e.contains(t))&&(n.remove(),r.delete(t),d(t))},h=[`class`,`style`],g=()=>{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)(h.includes(n.name)||n.name.startsWith(`data-`))&&t.setAttribute(n.name,n.value);for(let n of Array.from(t.attributes))(h.includes(n.name)||n.name.startsWith(`data-`))&&!e.hasAttribute(n.name)&&t.removeAttribute(n.name)}},_=()=>{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]`))f(t);let v=Array.from(r.values()).filter(e=>e.tagName===`LINK`);if(g(),_(),!o)return{stop:()=>{},links:v};let y=new MutationObserver(e=>{for(let t of e){if(t.type===`characterData`){let e=t.target.parentElement;e?.tagName===`STYLE`&&p(e);continue}s(t.target)&&t.target.tagName===`STYLE`&&p(t.target);for(let e of t.addedNodes)if(s(e))f(e);else if(e.nodeType===Node.ELEMENT_NODE)for(let t of e.querySelectorAll(`style, link[rel~="stylesheet" i]`))f(t);for(let e of t.removedNodes)m(e)}});y.observe(e.head,{childList:!0,subtree:!0,characterData:!0});let b=new MutationObserver(()=>g());b.observe(e.documentElement,{attributes:!0}),e.body&&b.observe(e.body,{attributes:!0});let x=t.defaultView,S,C=()=>{for(let[e,t]of l)e.sheet===t.sheet&&t.sheet.cssRules.length!==t.count&&p(e);S=x?.requestAnimationFrame(C)};return x&&typeof x.requestAnimationFrame==`function`&&(S=x.requestAnimationFrame(C)),{stop:()=>{y.disconnect(),b.disconnect(),S!==void 0&&x?.cancelAnimationFrame(S);for(let e of l.values())e.stop();l.clear()},links:v}}function s(e,t,n){let r=e.filter(e=>e.sheet===null&&c(e));if(r.length===0)return n(),()=>{};let i=r.length,a=()=>{i--,i===0&&s()},o=()=>{window.clearTimeout(l);for(let e of r)e.removeEventListener(`load`,a),e.removeEventListener(`error`,a)},s=()=>{o(),n()};for(let e of r)e.addEventListener(`load`,a),e.addEventListener(`error`,a);let l=window.setTimeout(s,t);return o}function c(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 l={popupWindow:null,container:null,blocked:!1},u=class{state=l;listeners=new Set;subscribe=e=>(this.listeners.add(e),()=>this.listeners.delete(e));getSnapshot=()=>this.state;getServerSnapshot=()=>l;setState(e){this.state={...this.state,...e};for(let e of this.listeners)e()}},d={popup:!0,width:640,height:480};function f(e){let t={...d,...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 p=`about:blank`,m=20,h=3e3;function g(e){let t=e.document;return t.URL===p||t.readyState===`loading`?null:t}function _(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 v(t={}){let[n]=(0,e.useState)(()=>new u),[r]=(0,e.useState)(()=>_(n)),i=(0,e.useRef)(t);i.current=t;let a=(0,e.useRef)(null),c=(0,e.useSyncExternalStore)(n.subscribe,n.getSnapshot,n.getServerSnapshot),l=(0,e.useCallback)(e=>{let{popupWindow:t}=n.getSnapshot();a.current?.(),a.current=null,t&&(!e&&!t.closed&&t.close(),n.setState({popupWindow:null,container:null}),i.current.onClose?.())},[n]),d=(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=i.current,r=t.url??p,c=window.open(r,t.name??`_blank`,f(t));if(!c)return n.setState({blocked:!0}),t.onBlocked?.(),null;let u;try{u=c.document}catch{return c.close(),n.setState({blocked:!0}),t.onBlocked?.(),null}let d=()=>l(!0),_=()=>{setTimeout(()=>{c.closed&&d()},0)},v=window.setInterval(()=>{c.closed&&d()},250),y=()=>c.close();window.addEventListener(`pagehide`,y);let b,x=!1,S,C;a.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=o(document,e);S=t.stop,r=t.links}let i=e.createElement(`div`);i.setAttribute(`data-popup-window-root`,``),e.body.appendChild(i),C=s(r,h,()=>{n.setState({container:i}),t.onOpen?.(c)})};return n.setState({popupWindow:c,container:null,blocked:!1}),r===p?w(u):b=window.setInterval(()=>{if(c.closed){d();return}let e;try{e=g(c)}catch{l(!1),n.setState({blocked:!0}),t.onBlocked?.();return}e&&(window.clearInterval(b),w(e))},m),c},[n,l]),v=(0,e.useCallback)(()=>l(!1),[l]),y=(0,e.useCallback)(()=>{n.getSnapshot().popupWindow?l(!1):d()},[n,l,d]),b=(0,e.useCallback)(()=>{let{popupWindow:e}=n.getSnapshot();e&&!e.closed&&e.focus()},[n]);return(0,e.useEffect)(()=>()=>l(!1),[l]),{open:d,close:v,toggle:y,focus:b,isOpen:c.popupWindow!==null,isBlocked:c.blocked,popupWindow:c.popupWindow,Popup:r}}exports.copyStyles=n,exports.usePopupWindow=v;
4
4
  //# sourceMappingURL=index.cjs.map
@@ -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":"iHAgBA,SAAgB,EAAW,EAAkB,EAAkB,EAAQ,GAAkB,CACvF,IAAM,EAAU,IAAI,IAEd,EAAe,GAA2D,CAC9E,GAAI,EAAK,WAAa,KAAK,aAAc,MAAO,GAChD,IAAM,EAAK,EACX,OACE,EAAG,UAAY,SACd,EAAG,UAAY,SAAW,EAAG,aAAa,KAAK,GAAK,GAAA,CAAI,YAAY,CAAC,CAAC,SAAS,YAAY,CAEhG,EAEM,EAAkB,GAAiC,CACvD,IAAM,EAAQ,EAAG,MACjB,GAAI,EACF,GAAI,CACF,OAAO,MAAM,KAAK,EAAM,QAAQ,CAAC,CAC9B,IAAK,GAAS,EAAK,OAAO,CAAC,CAC3B,KAAK;CAAI,CACd,MAAQ,CAER,CAEF,OAAO,EAAG,aAAe,EAC3B,EAEM,EAAU,GAAiD,CAC/D,GAAI,EAAQ,IAAI,CAAE,EAAG,OACrB,IAAI,EACJ,GAAI,EAAG,UAAY,OAAQ,CACzB,IAAM,EAAO,EAAO,cAAc,MAAM,EACxC,EAAK,IAAM,aAGX,EAAK,KAAQ,EAAuB,KACpC,IAAM,EAAQ,EAAG,aAAa,OAAO,EACjC,IAAO,EAAK,MAAQ,GACxB,EAAQ,CACV,KAAO,CACL,IAAM,EAAQ,EAAO,cAAc,OAAO,EAC1C,EAAM,YAAc,EAAe,CAAsB,EACzD,IAAM,EAAQ,EAAG,aAAa,OAAO,EACjC,GAAO,EAAM,aAAa,QAAS,CAAK,EAC5C,EAAQ,CACV,CACA,EAAO,KAAK,YAAY,CAAK,EAC7B,EAAQ,IAAI,EAAI,CAAK,CACvB,EAEM,EAAW,GAA+B,CAC9C,IAAM,EAAQ,EAAQ,IAAI,CAAE,EACxB,IAAO,EAAM,YAAc,EAAe,CAAE,EAClD,EAEM,EAAmB,GAAwB,CAC/C,IAAK,GAAM,CAAC,EAAK,KAAU,GACrB,IAAY,GAAQ,EAAQ,WAAa,KAAK,cAAgB,EAAQ,SAAS,CAAG,KACpF,EAAM,OAAO,EACb,EAAQ,OAAO,CAAG,EAGxB,EAEM,EAAa,CAAC,QAAS,OAAO,EAC9B,MAA4B,CAChC,IAAM,EAAiD,CACrD,CAAC,EAAO,gBAAiB,EAAO,eAAe,EAC/C,CAAC,EAAO,KAAM,EAAO,IAAI,CAC3B,EACA,IAAK,GAAM,CAAC,EAAM,KAAO,EACnB,MAAC,GAAQ,CAAC,GACd,KAAK,IAAM,KAAQ,EAAK,YAClB,EAAW,SAAS,EAAK,IAAI,GAAK,EAAK,KAAK,WAAW,OAAO,IAChE,EAAG,aAAa,EAAK,KAAM,EAAK,KAAK,EAGzC,IAAK,IAAM,KAAQ,MAAM,KAAK,EAAG,UAAU,GAEtC,EAAW,SAAS,EAAK,IAAI,GAAK,EAAK,KAAK,WAAW,OAAO,IAC/D,CAAC,EAAK,aAAa,EAAK,IAAI,GAE5B,EAAG,gBAAgB,EAAK,IAAI,CARS,CAY7C,EAEM,MAAgC,CACpC,IAAM,EAAe,EAAO,YACxB,MAAC,GAAc,eAAiB,EAAE,uBAAwB,IAC9D,GAAI,CACF,EAAO,mBAAqB,EAAO,mBAAmB,IAAK,GAAU,CACnE,IAAM,EAAO,IAAI,EAAa,cAM9B,OALA,EAAK,YACH,MAAM,KAAK,EAAM,QAAQ,CAAC,CACvB,IAAK,GAAS,EAAK,OAAO,CAAC,CAC3B,KAAK;CAAI,CACd,EACO,CACT,CAAC,CACH,MAAQ,CAER,CACF,EAEA,IAAK,IAAM,KAAM,EAAO,iBAAiB,kCAAkC,EACzE,EAAO,CAAwC,EAKjD,GAHA,EAAc,EACd,EAAkB,EAEd,CAAC,GAAS,OAAO,iBAAqB,IACxC,UAAa,CAAC,EAGhB,IAAM,EAAe,IAAI,iBAAkB,GAAY,CACrD,IAAK,IAAM,KAAU,EAAS,CAC5B,GAAI,EAAO,OAAS,gBAAiB,CACnC,IAAM,EAAS,EAAO,OAAO,cACzB,GAAQ,UAAY,SAAS,EAAQ,CAA0B,EACnE,QACF,CAEI,EAAY,EAAO,MAAM,GAAK,EAAO,OAAO,UAAY,SAE1D,EAAQ,EAAO,MAA0B,EAE3C,IAAK,IAAM,KAAS,EAAO,WACzB,GAAI,EAAY,CAAK,EACnB,EAAO,CAAK,OACP,GAAI,EAAM,WAAa,KAAK,aACjC,IAAK,IAAM,KAAO,EAAkB,iBAAiB,kCAAkC,EACrF,EAAO,CAAwC,EAIrD,IAAK,IAAM,KAAW,EAAO,aAC3B,EAAgB,CAAO,CAE3B,CACF,CAAC,EACD,EAAa,QAAQ,EAAO,KAAM,CAAE,UAAW,GAAM,QAAS,GAAM,cAAe,EAAK,CAAC,EAEzF,IAAM,EAAe,IAAI,qBAAuB,EAAc,CAAC,EAI/D,OAHA,EAAa,QAAQ,EAAO,gBAAiB,CAAE,WAAY,EAAK,CAAC,EAC7D,EAAO,MAAM,EAAa,QAAQ,EAAO,KAAM,CAAE,WAAY,EAAK,CAAC,MAE1D,CACX,EAAa,WAAW,EACxB,EAAa,WAAW,CAC1B,CACF,CCtJA,IAAM,EAA4B,CAAE,YAAa,KAAM,UAAW,KAAM,QAAS,EAAM,EAEjF,EAAN,KAAiB,CACf,MAAoB,EACpB,UAAoB,IAAI,IAExB,UAAa,IACX,KAAK,UAAU,IAAI,CAAQ,MACd,KAAK,UAAU,OAAO,CAAQ,GAG7C,gBAAgC,KAAK,MAErC,sBAAsC,EAEtC,SAAS,EAAoC,CAC3C,KAAK,MAAQ,CAAE,GAAG,KAAK,MAAO,GAAG,CAAQ,EACzC,IAAK,IAAM,KAAY,KAAK,UAAW,EAAS,CAClD,CACF,EAEM,EAAwC,CAAE,MAAO,GAAM,MAAO,IAAK,OAAQ,GAAI,EAErF,SAAS,EAAc,EAAwC,CAC7D,IAAM,EAA8B,CAAE,GAAG,EAAkB,GAAG,EAAQ,QAAS,EAW/E,OATE,EAAQ,SAAW,IACnB,EAAO,OAAS,IAAA,IAChB,EAAO,MAAQ,IAAA,IACf,OAAO,EAAO,OAAU,UACxB,OAAO,EAAO,QAAW,WAEzB,EAAO,KAAO,KAAK,IAAI,EAAG,KAAK,MAAM,OAAO,SAAW,OAAO,WAAa,EAAO,OAAS,CAAC,CAAC,EAC7F,EAAO,IAAM,KAAK,IAAI,EAAG,KAAK,MAAM,OAAO,SAAW,OAAO,YAAc,EAAO,QAAU,CAAC,CAAC,GAEzF,OAAO,QAAQ,CAAM,CAAC,CAC1B,QAAQ,EAAG,KAAW,IAAU,IAAA,IAAa,IAAU,EAAK,CAAC,CAC7D,KAAK,CAAC,EAAK,KAAY,IAAU,GAAO,GAAG,EAAI,MAAQ,GAAG,EAAI,GAAG,GAAQ,CAAC,CAC1E,KAAK,GAAG,CACb,CAEA,SAAS,EAAqB,EAAmC,CAC/D,SAAS,EAAM,CAAE,YAAwB,CACvC,GAAM,CAAE,cAAA,EAAc,EAAA,qBAAA,CACpB,EAAM,UACN,EAAM,YACN,EAAM,iBACR,EACA,OAAO,GAAA,EAAY,EAAA,aAAA,CAAa,EAAU,CAAS,EAAI,IACzD,CAEA,MADA,GAAM,YAAc,oBACb,CACT,CAsBA,SAAgB,EAAe,EAAiC,CAAC,EAAmB,CAClF,GAAM,CAAC,IAAA,EAAS,EAAA,SAAA,KAAe,IAAI,CAAY,EACzC,CAAC,IAAA,EAAS,EAAA,SAAA,KAAe,EAAqB,CAAK,CAAC,EAEpD,GAAA,EAAa,EAAA,OAAA,CAAO,CAAO,EACjC,EAAW,QAAU,EAErB,IAAM,GAAA,EAAa,EAAA,OAAA,CAA4B,IAAI,EAE7C,GAAA,EAAQ,EAAA,qBAAA,CAAqB,EAAM,UAAW,EAAM,YAAa,EAAM,iBAAiB,EAIxF,GAAA,EAAa,EAAA,YAAA,CAChB,GAAwB,CACvB,GAAM,CAAE,eAAgB,EAAM,YAAY,EAC1C,EAAW,UAAU,EACrB,EAAW,QAAU,KAChB,IACD,CAAC,GAAc,CAAC,EAAY,QAAQ,EAAY,MAAM,EAC1D,EAAM,SAAS,CAAE,YAAa,KAAM,UAAW,IAAK,CAAC,EACrD,EAAW,QAAQ,UAAU,EAC/B,EACA,CAAC,CAAK,CACR,EAEM,GAAA,EAAO,EAAA,YAAA,KAAiC,CAC5C,GAAI,OAAO,OAAW,IAAa,OAAO,KAC1C,IAAM,EAAU,EAAM,YAAY,EAClC,GAAI,EAAQ,aAAe,CAAC,EAAQ,YAAY,OAE9C,OADA,EAAQ,YAAY,MAAM,EACnB,EAAQ,YAGjB,IAAM,EAAO,EAAW,QAClB,EAAc,OAAO,KAAK,cAAe,EAAK,MAAQ,SAAU,EAAc,CAAI,CAAC,EACzF,GAAI,CAAC,EAGH,OAFA,EAAM,SAAS,CAAE,QAAS,EAAK,CAAC,EAChC,EAAK,YAAY,EACV,KAQT,IAAI,EACJ,GAAI,CACF,EAAM,EAAY,QACpB,MAAQ,CAIN,OAHA,EAAY,MAAM,EAClB,EAAM,SAAS,CAAE,QAAS,EAAK,CAAC,EAChC,EAAK,YAAY,EACV,IACT,CACA,EAAI,MAAQ,EAAK,OAAS,SAAS,MAEnC,IAAI,EACA,EAAK,aAAe,KACtB,EAAgB,EAAW,SAAU,CAAG,GAG1C,IAAM,EAAY,EAAI,cAAc,KAAK,EACzC,EAAU,aAAa,yBAA0B,EAAE,EACnD,EAAI,KAAK,YAAY,CAAS,EAE9B,IAAM,MAA4B,EAAW,EAAI,EAC3C,MAAwB,CAG5B,eAAiB,CACX,EAAY,QAAQ,EAAoB,CAC9C,EAAG,CAAC,CACN,EACA,EAAY,iBAAiB,WAAY,CAAe,EAGxD,IAAM,EAAY,OAAO,gBAAkB,CACrC,EAAY,QAAQ,EAAoB,CAC9C,EAAG,GAAG,EAEA,MAAyB,EAAY,MAAM,EAYjD,OAXA,OAAO,iBAAiB,WAAY,CAAgB,EAEpD,EAAW,YAAgB,CACzB,OAAO,cAAc,CAAS,EAC9B,OAAO,oBAAoB,WAAY,CAAgB,EACvD,EAAY,oBAAoB,WAAY,CAAe,EAC3D,IAAgB,CAClB,EAEA,EAAM,SAAS,CAAE,cAAa,YAAW,QAAS,EAAM,CAAC,EACzD,EAAK,SAAS,CAAW,EAClB,CACT,EAAG,CAAC,EAAO,CAAU,CAAC,EAEhB,GAAA,EAAQ,EAAA,YAAA,KAAkB,EAAW,EAAK,EAAG,CAAC,CAAU,CAAC,EAEzD,GAAA,EAAS,EAAA,YAAA,KAAkB,CAC3B,EAAM,YAAY,CAAC,CAAC,YACtB,EAAW,EAAK,EAEhB,EAAK,CAET,EAAG,CAAC,EAAO,EAAY,CAAI,CAAC,EAEtB,GAAA,EAAQ,EAAA,YAAA,KAAkB,CAC9B,GAAM,CAAE,eAAgB,EAAM,YAAY,EACtC,GAAe,CAAC,EAAY,QAAQ,EAAY,MAAM,CAC5D,EAAG,CAAC,CAAK,CAAC,EAQV,OAJA,EAAA,EAAA,UAAA,SACe,EAAW,EAAK,EAC5B,CAAC,CAAU,CAAC,EAER,CACL,OACA,QACA,SACA,QACA,OAAQ,EAAM,cAAgB,KAC9B,UAAW,EAAM,QACjB,YAAa,EAAM,YACnB,OACF,CACF"}
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` are included.\n * - Rules added to or removed from a `<style>` sheet later with\n * `insertRule`/`deleteRule` are applied to its mirror as they happen. This\n * is how CSS-in-JS libraries (Emotion, styled-components) add rules in\n * production builds, and it changes no DOM.\n * - Additions/removals/edits of style nodes in the source `<head>` are\n * observed (covers Vite HMR, lazily loaded chunk CSS, CSS-in-JS in\n * development builds).\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\ntype RuleChange = { type: 'insert'; rule: string; index: number } | { type: 'delete'; index: number }\ntype RuleListener = (change: RuleChange) => void\n\nconst ruleListeners = new WeakMap<CSSStyleSheet, Set<RuleListener>>()\n\n/**\n * Calls `listener` after each `insertRule`/`deleteRule` call on `sheet`.\n * Wraps the two methods on this sheet object only, not on the prototype, and\n * restores them once the sheet's last listener is removed. Returns a function\n * that removes the listener.\n */\nfunction onRuleChange(sheet: CSSStyleSheet, listener: RuleListener): () => void {\n let listeners = ruleListeners.get(sheet)\n if (!listeners) {\n const all = new Set<RuleListener>()\n listeners = all\n ruleListeners.set(sheet, all)\n const proto = Object.getPrototypeOf(sheet) as CSSStyleSheet\n sheet.insertRule = function (this: CSSStyleSheet, rule: string, index?: number): number {\n const inserted = proto.insertRule.call(this, rule, index)\n for (const notify of [...all]) notify({ type: 'insert', rule, index: inserted })\n return inserted\n }\n sheet.deleteRule = function (this: CSSStyleSheet, index: number): void {\n proto.deleteRule.call(this, index)\n for (const notify of [...all]) notify({ type: 'delete', index })\n }\n }\n const own = listeners\n own.add(listener)\n return () => {\n own.delete(listener)\n if (own.size > 0) return\n ruleListeners.delete(sheet)\n const wrapped = sheet as { insertRule?: unknown; deleteRule?: unknown }\n delete wrapped.insertRule\n delete wrapped.deleteRule\n }\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 const observing = watch && typeof MutationObserver !== 'undefined'\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 interface RuleSync {\n sheet: CSSStyleSheet\n /** Rule count of `sheet` that the mirror reflects. */\n count: number\n stop: () => void\n }\n const ruleSyncs = new Map<HTMLStyleElement, RuleSync>()\n\n // CSS-in-JS libraries in production builds add rules with insertRule, which\n // changes no DOM. Apply each change to the mirror as it happens, so content\n // that renders in the same task, such as the popup's first render, is\n // already styled.\n const syncRules = (el: HTMLStyleElement, clone: HTMLStyleElement): void => {\n const sheet = el.sheet\n const current = ruleSyncs.get(el)\n if (current && current.sheet === sheet) {\n current.count = sheet.cssRules.length\n return\n }\n current?.stop()\n ruleSyncs.delete(el)\n if (!observing || !sheet) return\n const sync: RuleSync = {\n sheet,\n count: sheet.cssRules.length,\n stop: onRuleChange(sheet, (change) => {\n const copy = clone.sheet\n try {\n if (copy) {\n if (change.type === 'insert') copy.insertRule(change.rule, change.index)\n else copy.deleteRule(change.index)\n if (copy.cssRules.length === sheet.cssRules.length) {\n sync.count = sheet.cssRules.length\n return\n }\n }\n } catch {\n // Out of step with the source - copy the whole sheet again below.\n }\n refresh(el)\n }),\n }\n ruleSyncs.set(el, sync)\n }\n\n const unsyncRules = (el: Element): void => {\n ruleSyncs.get(el as HTMLStyleElement)?.stop()\n ruleSyncs.delete(el as HTMLStyleElement)\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 if (el.tagName === 'STYLE') syncRules(el as HTMLStyleElement, clone as HTMLStyleElement)\n }\n\n const refresh = (el: HTMLStyleElement): void => {\n const clone = mirrors.get(el)\n if (!clone) return\n clone.textContent = serializeStyle(el)\n // New text gives the source a new sheet object.\n syncRules(el, clone as HTMLStyleElement)\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 unsyncRules(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 (!observing) {\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 // Fallback for rule changes that bypass the sheet's own methods, such as\n // CSSStyleSheet.prototype.insertRule.call(sheet, ...): once per frame of the\n // target window, copy again any sheet whose rule count changed.\n const view = target.defaultView\n let frame: number | undefined\n const checkRuleCounts = (): void => {\n for (const [el, sync] of ruleSyncs) {\n if (el.sheet === sync.sheet && sync.sheet.cssRules.length !== sync.count) refresh(el)\n }\n frame = view?.requestAnimationFrame(checkRuleCounts)\n }\n if (view && typeof view.requestAnimationFrame === 'function') {\n frame = view.requestAnimationFrame(checkRuleCounts)\n }\n\n return {\n stop: () => {\n headObserver.disconnect()\n rootObserver.disconnect()\n if (frame !== undefined) view?.cancelAnimationFrame(frame)\n for (const sync of ruleSyncs.values()) sync.stop()\n ruleSyncs.clear()\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,43 +2,111 @@ 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
- let r = /* @__PURE__ */ new Map(), i = (e) => {
5
+ return u(e, t, n).stop;
6
+ }
7
+ var s = /* @__PURE__ */ new WeakMap();
8
+ function c(e, t) {
9
+ let n = s.get(e);
10
+ if (!n) {
11
+ let t = /* @__PURE__ */ new Set();
12
+ n = t, s.set(e, t);
13
+ let r = Object.getPrototypeOf(e);
14
+ e.insertRule = function(e, n) {
15
+ let i = r.insertRule.call(this, e, n);
16
+ for (let n of [...t]) n({
17
+ type: "insert",
18
+ rule: e,
19
+ index: i
20
+ });
21
+ return i;
22
+ }, e.deleteRule = function(e) {
23
+ r.deleteRule.call(this, e);
24
+ for (let n of [...t]) n({
25
+ type: "delete",
26
+ index: e
27
+ });
28
+ };
29
+ }
30
+ let r = n;
31
+ return r.add(t), () => {
32
+ if (r.delete(t), r.size > 0) return;
33
+ s.delete(e);
34
+ let n = e;
35
+ delete n.insertRule, delete n.deleteRule;
36
+ };
37
+ }
38
+ var l = [
39
+ "rel",
40
+ "media",
41
+ "title",
42
+ "crossorigin",
43
+ "referrerpolicy",
44
+ "integrity"
45
+ ];
46
+ function u(e, t, n = !0) {
47
+ let r = /* @__PURE__ */ new Map(), i = n && typeof MutationObserver < "u", a = (e) => {
6
48
  if (e.nodeType !== Node.ELEMENT_NODE) return !1;
7
49
  let t = e;
8
50
  return t.tagName === "STYLE" || t.tagName === "LINK" && (t.getAttribute("rel") ?? "").toLowerCase().includes("stylesheet");
9
- }, a = (e) => {
51
+ }, o = (e) => {
10
52
  let t = e.sheet;
11
53
  if (t) try {
12
54
  return Array.from(t.cssRules).map((e) => e.cssText).join("\n");
13
55
  } catch {}
14
56
  return e.textContent ?? "";
15
- }, o = (e) => {
57
+ }, s = /* @__PURE__ */ new Map(), u = (e, t) => {
58
+ let n = e.sheet, r = s.get(e);
59
+ if (r && r.sheet === n) {
60
+ r.count = n.cssRules.length;
61
+ return;
62
+ }
63
+ if (r?.stop(), s.delete(e), !i || !n) return;
64
+ let a = {
65
+ sheet: n,
66
+ count: n.cssRules.length,
67
+ stop: c(n, (r) => {
68
+ let i = t.sheet;
69
+ try {
70
+ if (i && (r.type === "insert" ? i.insertRule(r.rule, r.index) : i.deleteRule(r.index), i.cssRules.length === n.cssRules.length)) {
71
+ a.count = n.cssRules.length;
72
+ return;
73
+ }
74
+ } catch {}
75
+ p(e);
76
+ })
77
+ };
78
+ s.set(e, a);
79
+ }, d = (e) => {
80
+ s.get(e)?.stop(), s.delete(e);
81
+ }, f = (e) => {
16
82
  if (r.has(e)) return;
17
83
  let n;
18
84
  if (e.tagName === "LINK") {
19
85
  let r = t.createElement("link");
20
- r.rel = "stylesheet", r.href = e.href;
21
- let i = e.getAttribute("media");
22
- i && (r.media = i), n = r;
86
+ for (let t of l) {
87
+ let n = e.getAttribute(t);
88
+ n !== null && r.setAttribute(t, n);
89
+ }
90
+ r.href = e.href, n = r;
23
91
  } else {
24
92
  let r = t.createElement("style");
25
- r.textContent = a(e);
93
+ r.textContent = o(e);
26
94
  let i = e.getAttribute("media");
27
95
  i && r.setAttribute("media", i), n = r;
28
96
  }
29
- t.head.appendChild(n), r.set(e, n);
30
- }, s = (e) => {
97
+ t.head.appendChild(n), r.set(e, n), e.tagName === "STYLE" && u(e, n);
98
+ }, p = (e) => {
31
99
  let t = r.get(e);
32
- t && (t.textContent = a(e));
33
- }, c = (e) => {
34
- for (let [t, n] of r) (e === t || e.nodeType === Node.ELEMENT_NODE && e.contains(t)) && (n.remove(), r.delete(t));
35
- }, l = ["class", "style"], u = () => {
100
+ t && (t.textContent = o(e), u(e, t));
101
+ }, m = (e) => {
102
+ for (let [t, n] of r) (e === t || e.nodeType === Node.ELEMENT_NODE && e.contains(t)) && (n.remove(), r.delete(t), d(t));
103
+ }, h = ["class", "style"], g = () => {
36
104
  let n = [[e.documentElement, t.documentElement], [e.body, t.body]];
37
105
  for (let [e, t] of n) if (!(!e || !t)) {
38
- for (let n of e.attributes) (l.includes(n.name) || n.name.startsWith("data-")) && t.setAttribute(n.name, n.value);
39
- for (let n of Array.from(t.attributes)) (l.includes(n.name) || n.name.startsWith("data-")) && !e.hasAttribute(n.name) && t.removeAttribute(n.name);
106
+ for (let n of e.attributes) (h.includes(n.name) || n.name.startsWith("data-")) && t.setAttribute(n.name, n.value);
107
+ for (let n of Array.from(t.attributes)) (h.includes(n.name) || n.name.startsWith("data-")) && !e.hasAttribute(n.name) && t.removeAttribute(n.name);
40
108
  }
41
- }, d = () => {
109
+ }, _ = () => {
42
110
  let n = t.defaultView;
43
111
  if (!(!n?.CSSStyleSheet || !("adoptedStyleSheets" in e))) try {
44
112
  t.adoptedStyleSheets = e.adoptedStyleSheets.map((e) => {
@@ -47,43 +115,79 @@ function o(e, t, n = !0) {
47
115
  });
48
116
  } catch {}
49
117
  };
50
- for (let t of e.querySelectorAll("style, link[rel~=\"stylesheet\" i]")) o(t);
51
- if (u(), d(), !n || typeof MutationObserver > "u") return () => {};
52
- let f = new MutationObserver((e) => {
118
+ for (let t of e.querySelectorAll("style, link[rel~=\"stylesheet\" i]")) f(t);
119
+ let v = Array.from(r.values()).filter((e) => e.tagName === "LINK");
120
+ if (g(), _(), !i) return {
121
+ stop: () => {},
122
+ links: v
123
+ };
124
+ let y = new MutationObserver((e) => {
53
125
  for (let t of e) {
54
126
  if (t.type === "characterData") {
55
127
  let e = t.target.parentElement;
56
- e?.tagName === "STYLE" && s(e);
128
+ e?.tagName === "STYLE" && p(e);
57
129
  continue;
58
130
  }
59
- i(t.target) && t.target.tagName === "STYLE" && s(t.target);
60
- for (let e of t.addedNodes) if (i(e)) o(e);
61
- 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) c(e);
131
+ a(t.target) && t.target.tagName === "STYLE" && p(t.target);
132
+ for (let e of t.addedNodes) if (a(e)) f(e);
133
+ else if (e.nodeType === Node.ELEMENT_NODE) for (let t of e.querySelectorAll("style, link[rel~=\"stylesheet\" i]")) f(t);
134
+ for (let e of t.removedNodes) m(e);
63
135
  }
64
136
  });
65
- f.observe(e.head, {
137
+ y.observe(e.head, {
66
138
  childList: !0,
67
139
  subtree: !0,
68
140
  characterData: !0
69
141
  });
70
- let p = new MutationObserver(() => u());
71
- return p.observe(e.documentElement, { attributes: !0 }), e.body && p.observe(e.body, { attributes: !0 }), () => {
72
- f.disconnect(), p.disconnect();
142
+ let b = new MutationObserver(() => g());
143
+ b.observe(e.documentElement, { attributes: !0 }), e.body && b.observe(e.body, { attributes: !0 });
144
+ let x = t.defaultView, S, C = () => {
145
+ for (let [e, t] of s) e.sheet === t.sheet && t.sheet.cssRules.length !== t.count && p(e);
146
+ S = x?.requestAnimationFrame(C);
147
+ };
148
+ return x && typeof x.requestAnimationFrame == "function" && (S = x.requestAnimationFrame(C)), {
149
+ stop: () => {
150
+ y.disconnect(), b.disconnect(), S !== void 0 && x?.cancelAnimationFrame(S);
151
+ for (let e of s.values()) e.stop();
152
+ s.clear();
153
+ },
154
+ links: v
73
155
  };
74
156
  }
75
157
  //#endregion
158
+ //#region src/whenStylesheetsLoad.ts
159
+ function d(e, t, n) {
160
+ let r = e.filter((e) => e.sheet === null && f(e));
161
+ if (r.length === 0) return n(), () => {};
162
+ let i = r.length, a = () => {
163
+ i--, i === 0 && s();
164
+ }, o = () => {
165
+ window.clearTimeout(c);
166
+ for (let e of r) e.removeEventListener("load", a), e.removeEventListener("error", a);
167
+ }, s = () => {
168
+ o(), n();
169
+ };
170
+ for (let e of r) e.addEventListener("load", a), e.addEventListener("error", a);
171
+ let c = window.setTimeout(s, t);
172
+ return o;
173
+ }
174
+ function f(e) {
175
+ if (/\balternate\b/i.test(e.rel)) return !1;
176
+ let t = e.ownerDocument.defaultView;
177
+ return !e.media || !t?.matchMedia || t.matchMedia(e.media).matches;
178
+ }
179
+ //#endregion
76
180
  //#region src/usePopupWindow.ts
77
- var s = {
181
+ var p = {
78
182
  popupWindow: null,
79
183
  container: null,
80
184
  blocked: !1
81
- }, c = class {
82
- state = s;
185
+ }, m = class {
186
+ state = p;
83
187
  listeners = /* @__PURE__ */ new Set();
84
188
  subscribe = (e) => (this.listeners.add(e), () => this.listeners.delete(e));
85
189
  getSnapshot = () => this.state;
86
- getServerSnapshot = () => s;
190
+ getServerSnapshot = () => p;
87
191
  setState(e) {
88
192
  this.state = {
89
193
  ...this.state,
@@ -91,89 +195,115 @@ var s = {
91
195
  };
92
196
  for (let e of this.listeners) e();
93
197
  }
94
- }, l = {
198
+ }, h = {
95
199
  popup: !0,
96
200
  width: 640,
97
201
  height: 480
98
202
  };
99
- function u(e) {
203
+ function g(e) {
100
204
  let t = {
101
- ...l,
205
+ ...h,
102
206
  ...e.features
103
207
  };
104
208
  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
209
  }
106
- function d(e) {
210
+ var _ = "about:blank", v = 20, y = 3e3;
211
+ function b(e) {
212
+ let t = e.document;
213
+ return t.URL === _ || t.readyState === "loading" ? null : t;
214
+ }
215
+ function x(e) {
107
216
  function t({ children: t }) {
108
217
  let { container: n } = i(e.subscribe, e.getSnapshot, e.getServerSnapshot);
109
218
  return n ? a(t, n) : null;
110
219
  }
111
220
  return t.displayName = "PopupWindow.Popup", t;
112
221
  }
113
- function f(a = {}) {
114
- let [s] = r(() => new c()), [l] = r(() => d(s)), f = n(a);
115
- f.current = a;
116
- let p = n(null), m = i(s.subscribe, s.getSnapshot, s.getServerSnapshot), h = e((e) => {
117
- let { popupWindow: t } = s.getSnapshot();
118
- p.current?.(), p.current = null, t && (!e && !t.closed && t.close(), s.setState({
222
+ function S(a = {}) {
223
+ let [o] = r(() => new m()), [s] = r(() => x(o)), c = n(a);
224
+ c.current = a;
225
+ let l = n(null), f = i(o.subscribe, o.getSnapshot, o.getServerSnapshot), p = e((e) => {
226
+ let { popupWindow: t } = o.getSnapshot();
227
+ l.current?.(), l.current = null, t && (!e && !t.closed && t.close(), o.setState({
119
228
  popupWindow: null,
120
229
  container: null
121
- }), f.current.onClose?.());
122
- }, [s]), g = e(() => {
230
+ }), c.current.onClose?.());
231
+ }, [o]), h = e(() => {
123
232
  if (typeof window > "u") return null;
124
- let e = s.getSnapshot();
233
+ let e = o.getSnapshot();
125
234
  if (e.popupWindow && !e.popupWindow.closed) return e.popupWindow.focus(), e.popupWindow;
126
- let t = f.current, n = window.open("about:blank", t.name ?? "_blank", u(t));
127
- if (!n) return s.setState({ blocked: !0 }), t.onBlocked?.(), null;
128
- let r;
235
+ let t = c.current, n = t.url ?? _, r = window.open(n, t.name ?? "_blank", g(t));
236
+ if (!r) return o.setState({ blocked: !0 }), t.onBlocked?.(), null;
237
+ let i;
129
238
  try {
130
- r = n.document;
239
+ i = r.document;
131
240
  } catch {
132
- return n.close(), s.setState({ blocked: !0 }), t.onBlocked?.(), null;
241
+ return r.close(), o.setState({ blocked: !0 }), t.onBlocked?.(), null;
133
242
  }
134
- r.title = t.title ?? document.title;
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 = () => {
243
+ let a = () => p(!0), s = () => {
140
244
  setTimeout(() => {
141
- n.closed && c();
245
+ r.closed && a();
142
246
  }, 0);
247
+ }, f = window.setInterval(() => {
248
+ r.closed && a();
249
+ }, 250), m = () => r.close();
250
+ window.addEventListener("pagehide", m);
251
+ let h, x = !1, S, C;
252
+ l.current = () => {
253
+ window.clearInterval(f), window.clearInterval(h), window.removeEventListener("pagehide", m), x && r.removeEventListener("pagehide", s), S?.(), C?.();
254
+ };
255
+ let w = (e) => {
256
+ x = !0, r.addEventListener("pagehide", s), e.title = t.title ?? document.title;
257
+ let n = [];
258
+ if (t.copyStyles !== !1) {
259
+ let t = u(document, e);
260
+ S = t.stop, n = t.links;
261
+ }
262
+ let i = e.createElement("div");
263
+ i.setAttribute("data-popup-window-root", ""), e.body.appendChild(i), C = d(n, y, () => {
264
+ o.setState({ container: i }), t.onOpen?.(r);
265
+ });
143
266
  };
144
- n.addEventListener("pagehide", l);
145
- let d = window.setInterval(() => {
146
- n.closed && c();
147
- }, 250), m = () => n.close();
148
- return window.addEventListener("pagehide", m), p.current = () => {
149
- window.clearInterval(d), window.removeEventListener("pagehide", m), n.removeEventListener("pagehide", l), i?.();
150
- }, s.setState({
151
- popupWindow: n,
152
- container: a,
267
+ return o.setState({
268
+ popupWindow: r,
269
+ container: null,
153
270
  blocked: !1
154
- }), t.onOpen?.(n), n;
155
- }, [s, h]), _ = e(() => h(!1), [h]), v = e(() => {
156
- s.getSnapshot().popupWindow ? h(!1) : g();
271
+ }), n === _ ? w(i) : h = window.setInterval(() => {
272
+ if (r.closed) {
273
+ a();
274
+ return;
275
+ }
276
+ let e;
277
+ try {
278
+ e = b(r);
279
+ } catch {
280
+ p(!1), o.setState({ blocked: !0 }), t.onBlocked?.();
281
+ return;
282
+ }
283
+ e && (window.clearInterval(h), w(e));
284
+ }, v), r;
285
+ }, [o, p]), S = e(() => p(!1), [p]), C = e(() => {
286
+ o.getSnapshot().popupWindow ? p(!1) : h();
157
287
  }, [
158
- s,
159
- h,
160
- g
161
- ]), y = e(() => {
162
- let { popupWindow: e } = s.getSnapshot();
288
+ o,
289
+ p,
290
+ h
291
+ ]), w = e(() => {
292
+ let { popupWindow: e } = o.getSnapshot();
163
293
  e && !e.closed && e.focus();
164
- }, [s]);
165
- return t(() => () => h(!1), [h]), {
166
- open: g,
167
- close: _,
168
- toggle: v,
169
- focus: y,
170
- isOpen: m.popupWindow !== null,
171
- isBlocked: m.blocked,
172
- popupWindow: m.popupWindow,
173
- Popup: l
294
+ }, [o]);
295
+ return t(() => () => p(!1), [p]), {
296
+ open: h,
297
+ close: S,
298
+ toggle: C,
299
+ focus: w,
300
+ isOpen: f.popupWindow !== null,
301
+ isBlocked: f.blocked,
302
+ popupWindow: f.popupWindow,
303
+ Popup: s
174
304
  };
175
305
  }
176
306
  //#endregion
177
- export { o as copyStyles, f as usePopupWindow };
307
+ export { o as copyStyles, S as usePopupWindow };
178
308
 
179
309
  //# 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":"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"}
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` are included.\n * - Rules added to or removed from a `<style>` sheet later with\n * `insertRule`/`deleteRule` are applied to its mirror as they happen. This\n * is how CSS-in-JS libraries (Emotion, styled-components) add rules in\n * production builds, and it changes no DOM.\n * - Additions/removals/edits of style nodes in the source `<head>` are\n * observed (covers Vite HMR, lazily loaded chunk CSS, CSS-in-JS in\n * development builds).\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\ntype RuleChange = { type: 'insert'; rule: string; index: number } | { type: 'delete'; index: number }\ntype RuleListener = (change: RuleChange) => void\n\nconst ruleListeners = new WeakMap<CSSStyleSheet, Set<RuleListener>>()\n\n/**\n * Calls `listener` after each `insertRule`/`deleteRule` call on `sheet`.\n * Wraps the two methods on this sheet object only, not on the prototype, and\n * restores them once the sheet's last listener is removed. Returns a function\n * that removes the listener.\n */\nfunction onRuleChange(sheet: CSSStyleSheet, listener: RuleListener): () => void {\n let listeners = ruleListeners.get(sheet)\n if (!listeners) {\n const all = new Set<RuleListener>()\n listeners = all\n ruleListeners.set(sheet, all)\n const proto = Object.getPrototypeOf(sheet) as CSSStyleSheet\n sheet.insertRule = function (this: CSSStyleSheet, rule: string, index?: number): number {\n const inserted = proto.insertRule.call(this, rule, index)\n for (const notify of [...all]) notify({ type: 'insert', rule, index: inserted })\n return inserted\n }\n sheet.deleteRule = function (this: CSSStyleSheet, index: number): void {\n proto.deleteRule.call(this, index)\n for (const notify of [...all]) notify({ type: 'delete', index })\n }\n }\n const own = listeners\n own.add(listener)\n return () => {\n own.delete(listener)\n if (own.size > 0) return\n ruleListeners.delete(sheet)\n const wrapped = sheet as { insertRule?: unknown; deleteRule?: unknown }\n delete wrapped.insertRule\n delete wrapped.deleteRule\n }\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 const observing = watch && typeof MutationObserver !== 'undefined'\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 interface RuleSync {\n sheet: CSSStyleSheet\n /** Rule count of `sheet` that the mirror reflects. */\n count: number\n stop: () => void\n }\n const ruleSyncs = new Map<HTMLStyleElement, RuleSync>()\n\n // CSS-in-JS libraries in production builds add rules with insertRule, which\n // changes no DOM. Apply each change to the mirror as it happens, so content\n // that renders in the same task, such as the popup's first render, is\n // already styled.\n const syncRules = (el: HTMLStyleElement, clone: HTMLStyleElement): void => {\n const sheet = el.sheet\n const current = ruleSyncs.get(el)\n if (current && current.sheet === sheet) {\n current.count = sheet.cssRules.length\n return\n }\n current?.stop()\n ruleSyncs.delete(el)\n if (!observing || !sheet) return\n const sync: RuleSync = {\n sheet,\n count: sheet.cssRules.length,\n stop: onRuleChange(sheet, (change) => {\n const copy = clone.sheet\n try {\n if (copy) {\n if (change.type === 'insert') copy.insertRule(change.rule, change.index)\n else copy.deleteRule(change.index)\n if (copy.cssRules.length === sheet.cssRules.length) {\n sync.count = sheet.cssRules.length\n return\n }\n }\n } catch {\n // Out of step with the source - copy the whole sheet again below.\n }\n refresh(el)\n }),\n }\n ruleSyncs.set(el, sync)\n }\n\n const unsyncRules = (el: Element): void => {\n ruleSyncs.get(el as HTMLStyleElement)?.stop()\n ruleSyncs.delete(el as HTMLStyleElement)\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 if (el.tagName === 'STYLE') syncRules(el as HTMLStyleElement, clone as HTMLStyleElement)\n }\n\n const refresh = (el: HTMLStyleElement): void => {\n const clone = mirrors.get(el)\n if (!clone) return\n clone.textContent = serializeStyle(el)\n // New text gives the source a new sheet object.\n syncRules(el, clone as HTMLStyleElement)\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 unsyncRules(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 (!observing) {\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 // Fallback for rule changes that bypass the sheet's own methods, such as\n // CSSStyleSheet.prototype.insertRule.call(sheet, ...): once per frame of the\n // target window, copy again any sheet whose rule count changed.\n const view = target.defaultView\n let frame: number | undefined\n const checkRuleCounts = (): void => {\n for (const [el, sync] of ruleSyncs) {\n if (el.sheet === sync.sheet && sync.sheet.cssRules.length !== sync.count) refresh(el)\n }\n frame = view?.requestAnimationFrame(checkRuleCounts)\n }\n if (view && typeof view.requestAnimationFrame === 'function') {\n frame = view.requestAnimationFrame(checkRuleCounts)\n }\n\n return {\n stop: () => {\n headObserver.disconnect()\n rootObserver.disconnect()\n if (frame !== undefined) view?.cancelAnimationFrame(frame)\n for (const sync of ruleSyncs.values()) sync.stop()\n ruleSyncs.clear()\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). Defaults to `true`.
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
- /** Called after the popup window has been opened and prepared. */
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
  }
@@ -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;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,kEAAkE;IAClE,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,uFAAuF;IACvF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB;;;;OAIG;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
+ {"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;AA8DhB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,cAAc,CAgIlF"}
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.1.1",
3
+ "version": "0.2.1",
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, features,
7
- center, copyStyles, onOpen, onClose, onBlocked). Load when opening part of a
8
- React tree in a separate browser window, building an "open in new window" or
9
- detachable panel, or when a popup is blocked, closes unexpectedly, or loses
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.1.1'
14
+ library_version: '0.2.1'
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` document,
25
- so it remains part of the calling component tree: state, context, and event
26
- handlers work across windows without bridging. The popup document runs no
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.1.1'
15
+ library_version: '0.2.1'
16
16
  requires:
17
17
  - '@jielga/react-popup-window/getting-started'
18
18
  sources:
@@ -26,13 +26,18 @@ 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` page. While it is
30
- open, the library mirrors the opener's styling into it and keeps the mirror
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
35
- rules injected with `insertRule` (CSS-in-JS) are included at copy time.
35
+ rules injected with `insertRule` are included.
36
+ - Rules added to or removed from a `<style>` sheet later with `insertRule`
37
+ and `deleteRule` are applied to the popup's copy as they happen. Emotion
38
+ (including MUI and `@mantine/emotion`) and styled-components add rules
39
+ this way in production builds, often while popup content renders for the
40
+ first time.
36
41
  - Additions, removals, and edits of style nodes in the opener's `<head>`
37
42
  are observed and re-mirrored. This covers Vite HMR and lazily loaded
38
43
  chunk CSS.
@@ -42,6 +47,12 @@ current:
42
47
  automatically.
43
48
  - `document.adoptedStyleSheets` are reconstructed in the popup document.
44
49
 
50
+ The popup loads each copied `<link>` stylesheet again. `Popup` renders once
51
+ they have loaded or failed (at most 3 seconds), so the first frame shown is
52
+ styled. Alternate stylesheets and non-matching `media` are not waited for.
53
+ A `<link>` added to the opener while the popup is open loads in the popup
54
+ after the content is shown.
55
+
45
56
  `copyStyles: false` disables all of it. The mechanism is also exported
46
57
  standalone as `copyStyles(source, target, watch?)`, returning a `stop`
47
58
  function, for windows managed outside the hook.
@@ -395,28 +406,28 @@ message — require listeners on the `popupWindow` object.
395
406
 
396
407
  Source: README.md (Communication)
397
408
 
398
- ### MEDIUM Expecting CSSOM-only rule changes to sync after open
409
+ ### MEDIUM Expecting in-place CSSOM edits to sync after open
399
410
 
400
411
  Wrong:
401
412
 
402
413
  ```tsx
403
414
  // after the popup is open
404
- someStyleSheet.insertRule('.late { color: red }') // no DOM mutation; not observed
415
+ rule.style.setProperty('color', 'red') // edits an existing rule; not mirrored
416
+ mediaRule.insertRule('.late { color: red }') // nested rule; not mirrored
405
417
  ```
406
418
 
407
419
  Correct:
408
420
 
409
421
  ```tsx
410
- // inject a new <style> element instead; node additions are observed
411
- const el = document.createElement('style')
412
- el.textContent = '.late { color: red }'
413
- document.head.appendChild(el)
422
+ // add a rule to the <style> sheet itself; insertRule on the sheet is mirrored
423
+ const sheet = styleElement.sheet!
424
+ sheet.insertRule('.late { color: red }', sheet.cssRules.length)
414
425
  ```
415
426
 
416
- Style synchronization serializes each sheet when it is copied and re-reads
417
- it when its DOM node changes. A rule inserted directly into an existing
418
- sheet's CSSOM after the popup opened produces no mutation and is not
419
- re-mirrored. Most CSS-in-JS libraries create or update style elements and
420
- are unaffected.
427
+ Style synchronization copies each sheet when the popup opens, applies
428
+ `insertRule`/`deleteRule` calls on a `<style>` sheet as they happen, and
429
+ copies a sheet again when its node changes. Edits inside an existing rule,
430
+ rules inserted into a nested rule such as `@media`, and changes to
431
+ `document.adoptedStyleSheets` after the popup opened are not mirrored.
421
432
 
422
433
  Source: src/copyStyles.ts