@jielga/react-popup-window 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/dist/copyStyles.d.ts +10 -3
- package/dist/copyStyles.d.ts.map +1 -1
- package/dist/index.cjs +4 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +168 -84
- package/dist/index.js.map +1 -1
- package/dist/usePopupWindow.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/getting-started/SKILL.md +3 -2
- package/skills/popup-content/SKILL.md +22 -15
package/README.md
CHANGED
|
@@ -111,12 +111,15 @@ const { open, Popup } = usePopupWindow({ title: 'Panel', url: '/popup.html' })
|
|
|
111
111
|
|
|
112
112
|
`isOpen` turns `true` as soon as the window opens; `Popup` renders once the page has loaded.
|
|
113
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
|
+
Start the page with `<!doctype html>`: without it, the popup lays out in quirks mode.
|
|
114
115
|
If the page redirects to another origin, the popup closes and `onBlocked` is called.
|
|
115
116
|
|
|
116
117
|
## How it works
|
|
117
118
|
|
|
118
119
|
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
|
|
120
|
+
set with `url`, and creates a container element in its body. The
|
|
121
|
+
`about:blank` document is written with an HTML5 doctype first, so the popup
|
|
122
|
+
lays out in standards mode, like the opener. The `Popup` component renders its children with
|
|
120
123
|
`createPortal` into that container. A portal changes where DOM output is
|
|
121
124
|
placed, not where the components sit in the tree, so popup content
|
|
122
125
|
participates in the calling tree's state, context, and event system. React
|
|
@@ -185,8 +188,13 @@ document and kept current:
|
|
|
185
188
|
- `<style>` and `<link rel="stylesheet">` elements. `<style>` contents are
|
|
186
189
|
serialized from the CSSOM, so rules injected with `insertRule` are
|
|
187
190
|
included.
|
|
191
|
+
- Rules added to or removed from a `<style>` sheet with `insertRule` and
|
|
192
|
+
`deleteRule`, applied to the popup as they happen. CSS-in-JS libraries
|
|
193
|
+
such as Emotion and styled-components add rules this way in production
|
|
194
|
+
builds.
|
|
188
195
|
- Additions, removals, and text edits of style nodes in `<head>`. This
|
|
189
|
-
covers Vite HMR, lazily loaded chunk CSS, and CSS-in-JS libraries
|
|
196
|
+
covers Vite HMR, lazily loaded chunk CSS, and CSS-in-JS libraries in
|
|
197
|
+
development builds.
|
|
190
198
|
- `class`, `style`, and `data-*` attributes on `<html>` and `<body>`.
|
|
191
199
|
Theme systems keyed on root attributes propagate to the popup.
|
|
192
200
|
- `document.adoptedStyleSheets`.
|
|
@@ -195,7 +203,10 @@ The popup loads each copied `<link>` stylesheet again.
|
|
|
195
203
|
`Popup` renders once they have loaded or failed, so the first frame shown is styled.
|
|
196
204
|
Alternate stylesheets and stylesheets whose `media` does not match are not waited for.
|
|
197
205
|
If a stylesheet takes longer than 3 seconds, `Popup` renders without it.
|
|
198
|
-
|
|
206
|
+
A `<link>` added to the opener while the popup is open, such as lazily loaded chunk CSS, is loaded by the popup again.
|
|
207
|
+
Until that copy has loaded, the popup applies the rules the opener already loaded, so content rendered after the opener's load is styled.
|
|
208
|
+
Note that a cross-origin stylesheet served without CORS cannot be read that way, and its content is unstyled until the popup has loaded it.
|
|
209
|
+
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.
|
|
199
210
|
|
|
200
211
|
Set `copyStyles: false` to disable.
|
|
201
212
|
|
package/dist/copyStyles.d.ts
CHANGED
|
@@ -4,10 +4,17 @@
|
|
|
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`
|
|
8
|
-
*
|
|
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,
|
|
13
|
+
* observed (covers Vite HMR, lazily loaded chunk CSS, CSS-in-JS in
|
|
14
|
+
* development builds).
|
|
15
|
+
* - A `<link>` added while watching is loaded by the target again. Until
|
|
16
|
+
* that copy has loaded, the rules the source already loaded are applied
|
|
17
|
+
* to the target through a constructed stylesheet.
|
|
11
18
|
* - `class`/`style`/`data-*` attributes on `<html>` and `<body>` are mirrored
|
|
12
19
|
* and kept in sync, so theme switching (e.g. a `dark` class) propagates.
|
|
13
20
|
* - `document.adoptedStyleSheets` are re-constructed in the target document.
|
package/dist/copyStyles.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"copyStyles.d.ts","sourceRoot":"","sources":["../src/copyStyles.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"copyStyles.d.ts","sourceRoot":"","sources":["../src/copyStyles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;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,CAiS1F"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("react"),t=require("react-dom");function n(e,t,n=!0){return
|
|
2
|
-
`)}catch{}return e.textContent??``},
|
|
3
|
-
`)
|
|
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{}g(e)})};l.set(e,a)},d=e=>{l.get(e)?.stop(),l.delete(e)},f=new Map,p=(e,n)=>{let r=t.defaultView;if(!r||typeof r.CSSStyleSheet!=`function`||!(`adoptedStyleSheets`in t)||/\balternate\b/i.test(e.rel))return;let i=null,a=()=>{if(!(n.sheet!==null||!e.sheet))try{let n=Array.from(e.sheet.cssRules).map(e=>e.cssText).join(`
|
|
3
|
+
`),a=new r.CSSStyleSheet({baseURL:e.href,media:e.media||void 0});a.replaceSync(n),t.adoptedStyleSheets=[...t.adoptedStyleSheets,a],i=a}catch{}},o=()=>{e.removeEventListener(`load`,a),n.removeEventListener(`load`,o),n.removeEventListener(`error`,o),f.delete(e);let r=i;r&&(t.adoptedStyleSheets=t.adoptedStyleSheets.filter(e=>e!==r)),i=null};n.addEventListener(`load`,o),n.addEventListener(`error`,o),f.set(e,o),e.sheet?a():e.addEventListener(`load`,a)},m=!0,h=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):!m&&o&&p(e,n)},g=e=>{let t=r.get(e);t&&(t.textContent=c(e),u(e,t))},_=e=>{for(let[t,n]of r)(e===t||e.nodeType===Node.ELEMENT_NODE&&e.contains(t))&&(n.remove(),r.delete(t),d(t),f.get(t)?.())},v=[`class`,`style`],y=()=>{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)(v.includes(n.name)||n.name.startsWith(`data-`))&&t.setAttribute(n.name,n.value);for(let n of Array.from(t.attributes))(v.includes(n.name)||n.name.startsWith(`data-`))&&!e.hasAttribute(n.name)&&t.removeAttribute(n.name)}},b=()=>{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(`
|
|
4
|
+
`)),t})}catch{}};for(let t of e.querySelectorAll(`style, link[rel~="stylesheet" i]`))h(t);m=!1;let x=Array.from(r.values()).filter(e=>e.tagName===`LINK`);if(y(),b(),!o)return{stop:()=>{},links:x};let S=new MutationObserver(e=>{for(let t of e){if(t.type===`characterData`){let e=t.target.parentElement;e?.tagName===`STYLE`&&g(e);continue}s(t.target)&&t.target.tagName===`STYLE`&&g(t.target);for(let e of t.addedNodes)if(s(e))h(e);else if(e.nodeType===Node.ELEMENT_NODE)for(let t of e.querySelectorAll(`style, link[rel~="stylesheet" i]`))h(t);for(let e of t.removedNodes)_(e)}});S.observe(e.head,{childList:!0,subtree:!0,characterData:!0});let C=new MutationObserver(()=>y());C.observe(e.documentElement,{attributes:!0}),e.body&&C.observe(e.body,{attributes:!0});let w=t.defaultView,T,E=()=>{for(let[e,t]of l)e.sheet===t.sheet&&t.sheet.cssRules.length!==t.count&&g(e);T=w?.requestAnimationFrame(E)};return w&&typeof w.requestAnimationFrame==`function`&&(T=w.requestAnimationFrame(E)),{stop:()=>{S.disconnect(),C.disconnect(),T!==void 0&&w?.cancelAnimationFrame(T);for(let e of l.values())e.stop();for(let e of[...f.values()])e();l.clear()},links:x}}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){e.open(),e.write(`<!DOCTYPE html><html><head></head><body></body></html>`),e.close()}function _(e){let t=e.document;return t.URL===p||t.readyState===`loading`?null:t}function v(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 y(t={}){let[n]=(0,e.useState)(()=>new u),[r]=(0,e.useState)(()=>v(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),v=()=>{setTimeout(()=>{c.closed&&d()},0)},y=window.setInterval(()=>{c.closed&&d()},250),b=()=>c.close();window.addEventListener(`pagehide`,b);let x,S=!1,C,w;a.current=()=>{window.clearInterval(y),window.clearInterval(x),window.removeEventListener(`pagehide`,b),S&&c.removeEventListener(`pagehide`,v),C?.(),w?.()};let T=e=>{S=!0,c.addEventListener(`pagehide`,v),e.title=t.title??document.title;let r=[];if(t.copyStyles!==!1){let t=o(document,e);C=t.stop,r=t.links}let i=e.createElement(`div`);i.setAttribute(`data-popup-window-root`,``),e.body.appendChild(i),w=s(r,h,()=>{n.setState({container:i}),t.onOpen?.(c)})};return n.setState({popupWindow:c,container:null,blocked:!1}),r===p?(g(u),T(u)):x=window.setInterval(()=>{if(c.closed){d();return}let e;try{e=_(c)}catch{l(!1),n.setState({blocked:!0}),t.onBlocked?.();return}e&&(window.clearInterval(x),T(e))},m),c},[n,l]),y=(0,e.useCallback)(()=>l(!1),[l]),b=(0,e.useCallback)(()=>{n.getSnapshot().popupWindow?l(!1):d()},[n,l,d]),x=(0,e.useCallback)(()=>{let{popupWindow:e}=n.getSnapshot();e&&!e.closed&&e.focus()},[n]);return(0,e.useEffect)(()=>()=>l(!1),[l]),{open:d,close:y,toggle:b,focus:x,isOpen:c.popupWindow!==null,isBlocked:c.blocked,popupWindow:c.popupWindow,Popup:r}}exports.copyStyles=n,exports.usePopupWindow=y;
|
|
4
5
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/copyStyles.ts","../src/whenStylesheetsLoad.ts","../src/usePopupWindow.ts"],"sourcesContent":["/**\n * Copy all stylesheets from `source` into `target` and (optionally) keep them\n * in sync while the popup is open:\n *\n * - `<link rel=\"stylesheet\">` and `<style>` elements are mirrored into the\n * target `<head>`. `<style>` contents are serialized from the CSSOM when\n * possible, so rules injected via `insertRule` (CSS-in-JS \"speedy\" mode)\n * are included.\n * - Additions/removals/edits of style nodes in the source `<head>` are\n * observed (covers Vite HMR, lazily loaded chunk CSS, styled-components).\n * - `class`/`style`/`data-*` attributes on `<html>` and `<body>` are mirrored\n * and kept in sync, so theme switching (e.g. a `dark` class) propagates.\n * - `document.adoptedStyleSheets` are re-constructed in the target document.\n *\n * Returns a function that stops observing.\n */\nexport function copyStyles(source: Document, target: Document, watch = true): () => void {\n return mirrorStyles(source, target, watch).stop\n}\n\n/** Attributes copied from a source `<link>` onto its mirror. */\nconst LINK_ATTRS = ['rel', 'media', 'title', 'crossorigin', 'referrerpolicy', 'integrity']\n\nexport interface StyleMirror {\n /** Stops observing the source document. */\n stop: () => void\n /**\n * The `<link>` elements created in `target` by the initial copy. The target\n * fetches each of them asynchronously.\n */\n links: HTMLLinkElement[]\n}\n\n/** {@link copyStyles}, plus the `<link>` elements the initial copy created. */\nexport function mirrorStyles(source: Document, target: Document, watch = true): StyleMirror {\n const mirrors = new Map<Element, Element>()\n\n const isStyleNode = (node: Node): node is HTMLStyleElement | HTMLLinkElement => {\n if (node.nodeType !== Node.ELEMENT_NODE) return false\n const el = node as Element\n return (\n el.tagName === 'STYLE' ||\n (el.tagName === 'LINK' && (el.getAttribute('rel') ?? '').toLowerCase().includes('stylesheet'))\n )\n }\n\n const serializeStyle = (el: HTMLStyleElement): string => {\n const sheet = el.sheet\n if (sheet) {\n try {\n return Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n')\n } catch {\n // Inaccessible cssRules — fall back to the raw text below.\n }\n }\n return el.textContent ?? ''\n }\n\n const mirror = (el: HTMLStyleElement | HTMLLinkElement): void => {\n if (mirrors.has(el)) return\n let clone: Element\n if (el.tagName === 'LINK') {\n const link = target.createElement('link')\n // `rel` is copied as is, so an alternate stylesheet stays disabled.\n // `crossorigin` and `referrerpolicy` make the popup send the same request\n // as the opener, so it can reuse the cached response.\n for (const name of LINK_ATTRS) {\n const value = el.getAttribute(name)\n if (value !== null) link.setAttribute(name, value)\n }\n // .href resolves to an absolute URL, so relative hrefs keep working\n // from the popup's `about:blank` document.\n link.href = (el as HTMLLinkElement).href\n clone = link\n } else {\n const style = target.createElement('style')\n style.textContent = serializeStyle(el as HTMLStyleElement)\n const media = el.getAttribute('media')\n if (media) style.setAttribute('media', media)\n clone = style\n }\n target.head.appendChild(clone)\n mirrors.set(el, clone)\n }\n\n const refresh = (el: HTMLStyleElement): void => {\n const clone = mirrors.get(el)\n if (clone) clone.textContent = serializeStyle(el)\n }\n\n const unmirrorSubtree = (removed: Node): void => {\n for (const [src, clone] of mirrors) {\n if (removed === src || (removed.nodeType === Node.ELEMENT_NODE && removed.contains(src))) {\n clone.remove()\n mirrors.delete(src)\n }\n }\n }\n\n const ROOT_ATTRS = ['class', 'style']\n const syncRootAttrs = (): void => {\n const pairs: Array<[Element | null, Element | null]> = [\n [source.documentElement, target.documentElement],\n [source.body, target.body],\n ]\n for (const [from, to] of pairs) {\n if (!from || !to) continue\n for (const attr of from.attributes) {\n if (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) {\n to.setAttribute(attr.name, attr.value)\n }\n }\n for (const attr of Array.from(to.attributes)) {\n if (\n (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) &&\n !from.hasAttribute(attr.name)\n ) {\n to.removeAttribute(attr.name)\n }\n }\n }\n }\n\n const syncAdoptedSheets = (): void => {\n const targetWindow = target.defaultView as (Window & { CSSStyleSheet?: typeof CSSStyleSheet }) | null\n if (!targetWindow?.CSSStyleSheet || !('adoptedStyleSheets' in source)) return\n try {\n target.adoptedStyleSheets = source.adoptedStyleSheets.map((sheet) => {\n const copy = new targetWindow.CSSStyleSheet!()\n copy.replaceSync(\n Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n'),\n )\n return copy\n })\n } catch {\n // Best effort — older browsers or cross-origin rules.\n }\n }\n\n for (const el of source.querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n const links = Array.from(mirrors.values()).filter(\n (clone): clone is HTMLLinkElement => clone.tagName === 'LINK',\n )\n syncRootAttrs()\n syncAdoptedSheets()\n\n if (!watch || typeof MutationObserver === 'undefined') {\n return { stop: () => {}, links }\n }\n\n const headObserver = new MutationObserver((records) => {\n for (const record of records) {\n if (record.type === 'characterData') {\n const parent = record.target.parentElement\n if (parent?.tagName === 'STYLE') refresh(parent as HTMLStyleElement)\n continue\n }\n // childList\n if (isStyleNode(record.target) && record.target.tagName === 'STYLE') {\n // textContent assignment replaces the text node → childList on <style>\n refresh(record.target as HTMLStyleElement)\n }\n for (const added of record.addedNodes) {\n if (isStyleNode(added)) {\n mirror(added)\n } else if (added.nodeType === Node.ELEMENT_NODE) {\n for (const el of (added as Element).querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n }\n }\n for (const removed of record.removedNodes) {\n unmirrorSubtree(removed)\n }\n }\n })\n headObserver.observe(source.head, { childList: true, subtree: true, characterData: true })\n\n const rootObserver = new MutationObserver(() => syncRootAttrs())\n rootObserver.observe(source.documentElement, { attributes: true })\n if (source.body) rootObserver.observe(source.body, { attributes: true })\n\n return {\n stop: () => {\n headObserver.disconnect()\n rootObserver.disconnect()\n },\n links,\n }\n}\n","/**\n * Calls `callback` once every stylesheet in `links` that applies has loaded\n * or failed, or after `timeoutMs`, whichever comes first.\n *\n * When none of them is still loading, `callback` runs synchronously, before\n * this function returns. Links that do not apply are not waited for: an\n * alternate stylesheet, or a `media` query that does not match, such as\n * `print`. Browsers fetch those at the lowest priority.\n *\n * Returns a function that stops waiting without calling `callback`.\n */\nexport function whenStylesheetsLoad(\n links: readonly HTMLLinkElement[],\n timeoutMs: number,\n callback: () => void,\n): () => void {\n const pending = links.filter((link) => link.sheet === null && applies(link))\n if (pending.length === 0) {\n callback()\n return () => {}\n }\n\n let remaining = pending.length\n const settle = () => {\n remaining--\n if (remaining === 0) finish()\n }\n const stop = () => {\n window.clearTimeout(timeout)\n for (const link of pending) {\n link.removeEventListener('load', settle)\n link.removeEventListener('error', settle)\n }\n }\n const finish = () => {\n stop()\n callback()\n }\n\n for (const link of pending) {\n link.addEventListener('load', settle)\n // A broken stylesheet must not keep the popup blank.\n link.addEventListener('error', settle)\n }\n const timeout = window.setTimeout(finish, timeoutMs)\n return stop\n}\n\nfunction applies(link: HTMLLinkElement): boolean {\n if (/\\balternate\\b/i.test(link.rel)) return false\n const view = link.ownerDocument.defaultView\n return !link.media || !view?.matchMedia || view.matchMedia(link.media).matches\n}\n","import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport type { FC } from 'react'\nimport { createPortal } from 'react-dom'\nimport { mirrorStyles } from './copyStyles'\nimport type {\n PopupProps,\n PopupWindowApi,\n PopupWindowFeatures,\n UsePopupWindowOptions,\n} from './types'\nimport { whenStylesheetsLoad } from './whenStylesheetsLoad'\n\ninterface PopupState {\n popupWindow: Window | null\n container: HTMLElement | null\n blocked: boolean\n}\n\nconst INITIAL_STATE: PopupState = { popupWindow: null, container: null, blocked: false }\n\nclass PopupStore {\n state: PopupState = INITIAL_STATE\n private listeners = new Set<() => void>()\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n getSnapshot = (): PopupState => this.state\n\n getServerSnapshot = (): PopupState => INITIAL_STATE\n\n setState(partial: Partial<PopupState>): void {\n this.state = { ...this.state, ...partial }\n for (const listener of this.listeners) listener()\n }\n}\n\nconst DEFAULT_FEATURES: PopupWindowFeatures = { popup: true, width: 640, height: 480 }\n\nfunction buildFeatures(options: UsePopupWindowOptions): string {\n const merged: PopupWindowFeatures = { ...DEFAULT_FEATURES, ...options.features }\n if (\n options.center !== false &&\n merged.left === undefined &&\n merged.top === undefined &&\n typeof merged.width === 'number' &&\n typeof merged.height === 'number'\n ) {\n merged.left = Math.max(0, Math.round(window.screenX + (window.outerWidth - merged.width) / 2))\n merged.top = Math.max(0, Math.round(window.screenY + (window.outerHeight - merged.height) / 2))\n }\n return Object.entries(merged)\n .filter(([, value]) => value !== undefined && value !== false)\n .map(([key, value]) => (value === true ? `${key}=yes` : `${key}=${value}`))\n .join(',')\n}\n\nconst ABOUT_BLANK = 'about:blank'\n/** Interval for checking whether the page given by `url` has loaded. */\nconst LOAD_POLL_MS = 20\n/** Longest time `Popup` waits for copied stylesheets before rendering anyway. */\nconst STYLES_TIMEOUT_MS = 3000\n\n/**\n * The popup's document once the page given by `url` has replaced the initial\n * `about:blank` document and finished parsing, otherwise `null`. Throws when\n * the document is not scriptable.\n */\nfunction loadedDocument(popupWindow: Window): Document | null {\n const doc = popupWindow.document\n if (doc.URL === ABOUT_BLANK || doc.readyState === 'loading') return null\n return doc\n}\n\nfunction createPopupComponent(store: PopupStore): FC<PopupProps> {\n function Popup({ children }: PopupProps) {\n const { container } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getServerSnapshot,\n )\n return container ? createPortal(children, container) : null\n }\n Popup.displayName = 'PopupWindow.Popup'\n return Popup\n}\n\n/**\n * Open part of your React tree in a separate browser window.\n *\n * The popup content is rendered with a portal into the popup's document, so\n * it stays part of your component tree: state, context and event handlers\n * all keep working across windows.\n *\n * ```tsx\n * const { open, close, isOpen, Popup } = usePopupWindow({ title: 'Panel' })\n *\n * return (\n * <>\n * <button onClick={open}>Open panel</button>\n * <Popup>\n * <MyPanel />\n * </Popup>\n * </>\n * )\n * ```\n */\nexport function usePopupWindow(options: UsePopupWindowOptions = {}): PopupWindowApi {\n const [store] = useState(() => new PopupStore())\n const [Popup] = useState(() => createPopupComponent(store))\n\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n const cleanupRef = useRef<(() => void) | null>(null)\n\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)\n\n // `userClosed` is true when the window was closed outside our control\n // (user hit the close button, opener unloaded) — then we must not touch it.\n const closePopup = useCallback(\n (userClosed: boolean) => {\n const { popupWindow } = store.getSnapshot()\n cleanupRef.current?.()\n cleanupRef.current = null\n if (!popupWindow) return\n if (!userClosed && !popupWindow.closed) popupWindow.close()\n store.setState({ popupWindow: null, container: null })\n optionsRef.current.onClose?.()\n },\n [store],\n )\n\n const open = useCallback((): Window | null => {\n if (typeof window === 'undefined') return null\n const current = store.getSnapshot()\n if (current.popupWindow && !current.popupWindow.closed) {\n current.popupWindow.focus()\n return current.popupWindow\n }\n\n const opts = optionsRef.current\n const url = opts.url ?? ABOUT_BLANK\n const popupWindow = window.open(url, opts.name ?? '_blank', buildFeatures(opts))\n if (!popupWindow) {\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n\n // Sandboxed embedders (VS Code's built-in browser, CodeSandbox/StackBlitz\n // previews, iframes sandboxed without popup-escape) open the popup with an\n // opaque origin, so reading its document throws a SecurityError. Treat\n // that as blocked instead of stranding a blank window the portal can\n // never reach.\n let initialDoc: Document\n try {\n initialDoc = popupWindow.document\n } catch {\n popupWindow.close()\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n\n const handleExternalClose = () => closePopup(true)\n const onPopupPagehide = () => {\n // pagehide also fires on navigation; only treat it as a close when the\n // window really is gone a tick later.\n setTimeout(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 0)\n }\n\n // Belt and braces: some browsers don't fire pagehide reliably for popups.\n const closePoll = window.setInterval(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 250)\n\n const onOpenerPagehide = () => popupWindow.close()\n window.addEventListener('pagehide', onOpenerPagehide)\n\n let loadPoll: number | undefined\n let prepared = false\n let stopStyleSync: (() => void) | undefined\n let stopStylesWait: (() => void) | undefined\n\n cleanupRef.current = () => {\n window.clearInterval(closePoll)\n window.clearInterval(loadPoll)\n window.removeEventListener('pagehide', onOpenerPagehide)\n if (prepared) popupWindow.removeEventListener('pagehide', onPopupPagehide)\n stopStyleSync?.()\n stopStylesWait?.()\n }\n\n // Title, styles and the portal container go into the document that\n // stays: `about:blank` itself, or the page `url` loads in its place.\n const prepare = (doc: Document) => {\n prepared = true\n popupWindow.addEventListener('pagehide', onPopupPagehide)\n doc.title = opts.title ?? document.title\n let links: HTMLLinkElement[] = []\n if (opts.copyStyles !== false) {\n const styles = mirrorStyles(document, doc)\n stopStyleSync = styles.stop\n links = styles.links\n }\n const container = doc.createElement('div')\n container.setAttribute('data-popup-window-root', '')\n doc.body.appendChild(container)\n // The popup fetches copied <link> stylesheets asynchronously. Render\n // only once they have loaded, so the first paint is not unstyled.\n stopStylesWait = whenStylesheetsLoad(links, STYLES_TIMEOUT_MS, () => {\n store.setState({ container })\n opts.onOpen?.(popupWindow)\n })\n }\n\n store.setState({ popupWindow, container: null, blocked: false })\n\n if (url === ABOUT_BLANK) {\n prepare(initialDoc)\n } else {\n // The window starts on an initial about:blank document and then\n // navigates to `url`. Wait until that page has replaced it and parsed.\n loadPoll = window.setInterval(() => {\n if (popupWindow.closed) {\n handleExternalClose()\n return\n }\n let doc: Document | null\n try {\n doc = loadedDocument(popupWindow)\n } catch {\n // The page went somewhere the opener cannot script, such as a\n // cross-origin redirect: nothing can ever render there.\n closePopup(false)\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return\n }\n if (!doc) return\n window.clearInterval(loadPoll)\n prepare(doc)\n }, LOAD_POLL_MS)\n }\n\n return popupWindow\n }, [store, closePopup])\n\n const close = useCallback(() => closePopup(false), [closePopup])\n\n const toggle = useCallback(() => {\n if (store.getSnapshot().popupWindow) {\n closePopup(false)\n } else {\n open()\n }\n }, [store, closePopup, open])\n\n const focus = useCallback(() => {\n const { popupWindow } = store.getSnapshot()\n if (popupWindow && !popupWindow.closed) popupWindow.focus()\n }, [store])\n\n // Close the popup when the owning component unmounts — its portal content\n // would unmount anyway, leaving an empty window behind.\n useEffect(() => {\n return () => closePopup(false)\n }, [closePopup])\n\n return {\n open,\n close,\n toggle,\n focus,\n isOpen: state.popupWindow !== null,\n isBlocked: state.blocked,\n popupWindow: state.popupWindow,\n Popup,\n }\n}\n"],"mappings":"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"}
|
|
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 * - A `<link>` added while watching is loaded by the target again. Until\n * that copy has loaded, the rules the source already loaded are applied\n * to the target through a constructed stylesheet.\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 // A <link> added while watching, such as lazily loaded chunk CSS: the\n // target loads its copy separately, and content that waits only for the\n // source's copy, as bundlers do, can render before the target's has\n // loaded. Until it has, apply the rules the source already parsed through a\n // constructed sheet. `baseURL` keeps relative url()s resolving against the\n // stylesheet. Adopted sheets come last in the cascade, which can change\n // precedence for that short time. Unreadable cross-origin sheets are left\n // to load normally.\n const unbridgeLinks = new Map<Element, () => void>()\n const bridgeLink = (el: HTMLLinkElement, clone: HTMLLinkElement): void => {\n const view = target.defaultView as (Window & typeof globalThis) | null\n if (!view || typeof view.CSSStyleSheet !== 'function' || !('adoptedStyleSheets' in target)) {\n return\n }\n if (/\\balternate\\b/i.test(el.rel)) return\n let bridge: CSSStyleSheet | null = null\n const add = (): void => {\n if (clone.sheet !== null || !el.sheet) return\n try {\n const text = Array.from(el.sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n')\n const sheet = new view.CSSStyleSheet({ baseURL: el.href, media: el.media || undefined })\n sheet.replaceSync(text)\n target.adoptedStyleSheets = [...target.adoptedStyleSheets, sheet]\n bridge = sheet\n } catch {\n // Cross-origin rules or no constructable stylesheets - wait for the copy.\n }\n }\n const remove = (): void => {\n el.removeEventListener('load', add)\n clone.removeEventListener('load', remove)\n clone.removeEventListener('error', remove)\n unbridgeLinks.delete(el)\n const sheet = bridge\n if (sheet) target.adoptedStyleSheets = target.adoptedStyleSheets.filter((s) => s !== sheet)\n bridge = null\n }\n clone.addEventListener('load', remove)\n clone.addEventListener('error', remove)\n unbridgeLinks.set(el, remove)\n if (el.sheet) add()\n else el.addEventListener('load', add)\n }\n\n // True during the initial copy. `usePopupWindow` waits for those links\n // itself before rendering.\n let copying = true\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 else if (!copying && observing) bridgeLink(el as HTMLLinkElement, clone as HTMLLinkElement)\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 unbridgeLinks.get(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 copying = false\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 for (const remove of [...unbridgeLinks.values()]) remove()\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 * Gives the initial `about:blank` document an HTML5 doctype, so the popup\n * lays out in standards mode like the opener instead of in quirks mode. Only\n * the parser can set the mode, so the document is written again. In Chromium\n * its `URL` then reports the opener's URL; a reload still loads `about:blank`.\n */\nfunction writeStandardsDocument(doc: Document): void {\n doc.open()\n doc.write('<!DOCTYPE html><html><head></head><body></body></html>')\n doc.close()\n}\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 // Before prepare(): writing the document removes event listeners on the\n // popup window.\n writeStandardsDocument(initialDoc)\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,9 +2,40 @@ import { useCallback as e, useEffect as t, useRef as n, useState as r, useSyncEx
|
|
|
2
2
|
import { createPortal as a } from "react-dom";
|
|
3
3
|
//#region src/copyStyles.ts
|
|
4
4
|
function o(e, t, n = !0) {
|
|
5
|
-
return
|
|
5
|
+
return u(e, t, n).stop;
|
|
6
6
|
}
|
|
7
|
-
var s =
|
|
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 = [
|
|
8
39
|
"rel",
|
|
9
40
|
"media",
|
|
10
41
|
"title",
|
|
@@ -12,46 +43,87 @@ var s = [
|
|
|
12
43
|
"referrerpolicy",
|
|
13
44
|
"integrity"
|
|
14
45
|
];
|
|
15
|
-
function
|
|
16
|
-
let r = /* @__PURE__ */ new Map(), i = (e) => {
|
|
46
|
+
function u(e, t, n = !0) {
|
|
47
|
+
let r = /* @__PURE__ */ new Map(), i = n && typeof MutationObserver < "u", a = (e) => {
|
|
17
48
|
if (e.nodeType !== Node.ELEMENT_NODE) return !1;
|
|
18
49
|
let t = e;
|
|
19
50
|
return t.tagName === "STYLE" || t.tagName === "LINK" && (t.getAttribute("rel") ?? "").toLowerCase().includes("stylesheet");
|
|
20
|
-
},
|
|
51
|
+
}, o = (e) => {
|
|
21
52
|
let t = e.sheet;
|
|
22
53
|
if (t) try {
|
|
23
54
|
return Array.from(t.cssRules).map((e) => e.cssText).join("\n");
|
|
24
55
|
} catch {}
|
|
25
56
|
return e.textContent ?? "";
|
|
26
|
-
},
|
|
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
|
+
g(e);
|
|
76
|
+
})
|
|
77
|
+
};
|
|
78
|
+
s.set(e, a);
|
|
79
|
+
}, d = (e) => {
|
|
80
|
+
s.get(e)?.stop(), s.delete(e);
|
|
81
|
+
}, f = /* @__PURE__ */ new Map(), p = (e, n) => {
|
|
82
|
+
let r = t.defaultView;
|
|
83
|
+
if (!r || typeof r.CSSStyleSheet != "function" || !("adoptedStyleSheets" in t) || /\balternate\b/i.test(e.rel)) return;
|
|
84
|
+
let i = null, a = () => {
|
|
85
|
+
if (!(n.sheet !== null || !e.sheet)) try {
|
|
86
|
+
let n = Array.from(e.sheet.cssRules).map((e) => e.cssText).join("\n"), a = new r.CSSStyleSheet({
|
|
87
|
+
baseURL: e.href,
|
|
88
|
+
media: e.media || void 0
|
|
89
|
+
});
|
|
90
|
+
a.replaceSync(n), t.adoptedStyleSheets = [...t.adoptedStyleSheets, a], i = a;
|
|
91
|
+
} catch {}
|
|
92
|
+
}, o = () => {
|
|
93
|
+
e.removeEventListener("load", a), n.removeEventListener("load", o), n.removeEventListener("error", o), f.delete(e);
|
|
94
|
+
let r = i;
|
|
95
|
+
r && (t.adoptedStyleSheets = t.adoptedStyleSheets.filter((e) => e !== r)), i = null;
|
|
96
|
+
};
|
|
97
|
+
n.addEventListener("load", o), n.addEventListener("error", o), f.set(e, o), e.sheet ? a() : e.addEventListener("load", a);
|
|
98
|
+
}, m = !0, h = (e) => {
|
|
27
99
|
if (r.has(e)) return;
|
|
28
100
|
let n;
|
|
29
101
|
if (e.tagName === "LINK") {
|
|
30
102
|
let r = t.createElement("link");
|
|
31
|
-
for (let t of
|
|
103
|
+
for (let t of l) {
|
|
32
104
|
let n = e.getAttribute(t);
|
|
33
105
|
n !== null && r.setAttribute(t, n);
|
|
34
106
|
}
|
|
35
107
|
r.href = e.href, n = r;
|
|
36
108
|
} else {
|
|
37
109
|
let r = t.createElement("style");
|
|
38
|
-
r.textContent =
|
|
110
|
+
r.textContent = o(e);
|
|
39
111
|
let i = e.getAttribute("media");
|
|
40
112
|
i && r.setAttribute("media", i), n = r;
|
|
41
113
|
}
|
|
42
|
-
t.head.appendChild(n), r.set(e, n);
|
|
43
|
-
},
|
|
114
|
+
t.head.appendChild(n), r.set(e, n), e.tagName === "STYLE" ? u(e, n) : !m && i && p(e, n);
|
|
115
|
+
}, g = (e) => {
|
|
44
116
|
let t = r.get(e);
|
|
45
|
-
t && (t.textContent =
|
|
46
|
-
},
|
|
47
|
-
for (let [t, n] of r) (e === t || e.nodeType === Node.ELEMENT_NODE && e.contains(t)) && (n.remove(), r.delete(t));
|
|
48
|
-
},
|
|
117
|
+
t && (t.textContent = o(e), u(e, t));
|
|
118
|
+
}, _ = (e) => {
|
|
119
|
+
for (let [t, n] of r) (e === t || e.nodeType === Node.ELEMENT_NODE && e.contains(t)) && (n.remove(), r.delete(t), d(t), f.get(t)?.());
|
|
120
|
+
}, v = ["class", "style"], y = () => {
|
|
49
121
|
let n = [[e.documentElement, t.documentElement], [e.body, t.body]];
|
|
50
122
|
for (let [e, t] of n) if (!(!e || !t)) {
|
|
51
|
-
for (let n of e.attributes) (
|
|
52
|
-
for (let n of Array.from(t.attributes)) (
|
|
123
|
+
for (let n of e.attributes) (v.includes(n.name) || n.name.startsWith("data-")) && t.setAttribute(n.name, n.value);
|
|
124
|
+
for (let n of Array.from(t.attributes)) (v.includes(n.name) || n.name.startsWith("data-")) && !e.hasAttribute(n.name) && t.removeAttribute(n.name);
|
|
53
125
|
}
|
|
54
|
-
},
|
|
126
|
+
}, b = () => {
|
|
55
127
|
let n = t.defaultView;
|
|
56
128
|
if (!(!n?.CSSStyleSheet || !("adoptedStyleSheets" in e))) try {
|
|
57
129
|
t.adoptedStyleSheets = e.adoptedStyleSheets.map((e) => {
|
|
@@ -60,42 +132,51 @@ function c(e, t, n = !0) {
|
|
|
60
132
|
});
|
|
61
133
|
} catch {}
|
|
62
134
|
};
|
|
63
|
-
for (let t of e.querySelectorAll("style, link[rel~=\"stylesheet\" i]"))
|
|
64
|
-
|
|
65
|
-
|
|
135
|
+
for (let t of e.querySelectorAll("style, link[rel~=\"stylesheet\" i]")) h(t);
|
|
136
|
+
m = !1;
|
|
137
|
+
let x = Array.from(r.values()).filter((e) => e.tagName === "LINK");
|
|
138
|
+
if (y(), b(), !i) return {
|
|
66
139
|
stop: () => {},
|
|
67
|
-
links:
|
|
140
|
+
links: x
|
|
68
141
|
};
|
|
69
|
-
let
|
|
142
|
+
let S = new MutationObserver((e) => {
|
|
70
143
|
for (let t of e) {
|
|
71
144
|
if (t.type === "characterData") {
|
|
72
145
|
let e = t.target.parentElement;
|
|
73
|
-
e?.tagName === "STYLE" &&
|
|
146
|
+
e?.tagName === "STYLE" && g(e);
|
|
74
147
|
continue;
|
|
75
148
|
}
|
|
76
|
-
|
|
77
|
-
for (let e of t.addedNodes) if (
|
|
78
|
-
else if (e.nodeType === Node.ELEMENT_NODE) for (let t of e.querySelectorAll("style, link[rel~=\"stylesheet\" i]"))
|
|
79
|
-
for (let e of t.removedNodes)
|
|
149
|
+
a(t.target) && t.target.tagName === "STYLE" && g(t.target);
|
|
150
|
+
for (let e of t.addedNodes) if (a(e)) h(e);
|
|
151
|
+
else if (e.nodeType === Node.ELEMENT_NODE) for (let t of e.querySelectorAll("style, link[rel~=\"stylesheet\" i]")) h(t);
|
|
152
|
+
for (let e of t.removedNodes) _(e);
|
|
80
153
|
}
|
|
81
154
|
});
|
|
82
|
-
|
|
155
|
+
S.observe(e.head, {
|
|
83
156
|
childList: !0,
|
|
84
157
|
subtree: !0,
|
|
85
158
|
characterData: !0
|
|
86
159
|
});
|
|
87
|
-
let
|
|
88
|
-
|
|
160
|
+
let C = new MutationObserver(() => y());
|
|
161
|
+
C.observe(e.documentElement, { attributes: !0 }), e.body && C.observe(e.body, { attributes: !0 });
|
|
162
|
+
let w = t.defaultView, T, E = () => {
|
|
163
|
+
for (let [e, t] of s) e.sheet === t.sheet && t.sheet.cssRules.length !== t.count && g(e);
|
|
164
|
+
T = w?.requestAnimationFrame(E);
|
|
165
|
+
};
|
|
166
|
+
return w && typeof w.requestAnimationFrame == "function" && (T = w.requestAnimationFrame(E)), {
|
|
89
167
|
stop: () => {
|
|
90
|
-
|
|
168
|
+
S.disconnect(), C.disconnect(), T !== void 0 && w?.cancelAnimationFrame(T);
|
|
169
|
+
for (let e of s.values()) e.stop();
|
|
170
|
+
for (let e of [...f.values()]) e();
|
|
171
|
+
s.clear();
|
|
91
172
|
},
|
|
92
|
-
links:
|
|
173
|
+
links: x
|
|
93
174
|
};
|
|
94
175
|
}
|
|
95
176
|
//#endregion
|
|
96
177
|
//#region src/whenStylesheetsLoad.ts
|
|
97
|
-
function
|
|
98
|
-
let r = e.filter((e) => e.sheet === null &&
|
|
178
|
+
function d(e, t, n) {
|
|
179
|
+
let r = e.filter((e) => e.sheet === null && f(e));
|
|
99
180
|
if (r.length === 0) return n(), () => {};
|
|
100
181
|
let i = r.length, a = () => {
|
|
101
182
|
i--, i === 0 && s();
|
|
@@ -109,23 +190,23 @@ function l(e, t, n) {
|
|
|
109
190
|
let c = window.setTimeout(s, t);
|
|
110
191
|
return o;
|
|
111
192
|
}
|
|
112
|
-
function
|
|
193
|
+
function f(e) {
|
|
113
194
|
if (/\balternate\b/i.test(e.rel)) return !1;
|
|
114
195
|
let t = e.ownerDocument.defaultView;
|
|
115
196
|
return !e.media || !t?.matchMedia || t.matchMedia(e.media).matches;
|
|
116
197
|
}
|
|
117
198
|
//#endregion
|
|
118
199
|
//#region src/usePopupWindow.ts
|
|
119
|
-
var
|
|
200
|
+
var p = {
|
|
120
201
|
popupWindow: null,
|
|
121
202
|
container: null,
|
|
122
203
|
blocked: !1
|
|
123
|
-
},
|
|
124
|
-
state =
|
|
204
|
+
}, m = class {
|
|
205
|
+
state = p;
|
|
125
206
|
listeners = /* @__PURE__ */ new Set();
|
|
126
207
|
subscribe = (e) => (this.listeners.add(e), () => this.listeners.delete(e));
|
|
127
208
|
getSnapshot = () => this.state;
|
|
128
|
-
getServerSnapshot = () =>
|
|
209
|
+
getServerSnapshot = () => p;
|
|
129
210
|
setState(e) {
|
|
130
211
|
this.state = {
|
|
131
212
|
...this.state,
|
|
@@ -133,44 +214,47 @@ var d = {
|
|
|
133
214
|
};
|
|
134
215
|
for (let e of this.listeners) e();
|
|
135
216
|
}
|
|
136
|
-
},
|
|
217
|
+
}, h = {
|
|
137
218
|
popup: !0,
|
|
138
219
|
width: 640,
|
|
139
220
|
height: 480
|
|
140
221
|
};
|
|
141
|
-
function
|
|
222
|
+
function g(e) {
|
|
142
223
|
let t = {
|
|
143
|
-
...
|
|
224
|
+
...h,
|
|
144
225
|
...e.features
|
|
145
226
|
};
|
|
146
227
|
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(",");
|
|
147
228
|
}
|
|
148
|
-
var
|
|
149
|
-
function
|
|
229
|
+
var _ = "about:blank", v = 20, y = 3e3;
|
|
230
|
+
function b(e) {
|
|
231
|
+
e.open(), e.write("<!DOCTYPE html><html><head></head><body></body></html>"), e.close();
|
|
232
|
+
}
|
|
233
|
+
function x(e) {
|
|
150
234
|
let t = e.document;
|
|
151
|
-
return t.URL ===
|
|
235
|
+
return t.URL === _ || t.readyState === "loading" ? null : t;
|
|
152
236
|
}
|
|
153
|
-
function
|
|
237
|
+
function S(e) {
|
|
154
238
|
function t({ children: t }) {
|
|
155
239
|
let { container: n } = i(e.subscribe, e.getSnapshot, e.getServerSnapshot);
|
|
156
240
|
return n ? a(t, n) : null;
|
|
157
241
|
}
|
|
158
242
|
return t.displayName = "PopupWindow.Popup", t;
|
|
159
243
|
}
|
|
160
|
-
function
|
|
161
|
-
let [o] = r(() => new
|
|
162
|
-
|
|
163
|
-
let
|
|
244
|
+
function C(a = {}) {
|
|
245
|
+
let [o] = r(() => new m()), [s] = r(() => S(o)), c = n(a);
|
|
246
|
+
c.current = a;
|
|
247
|
+
let l = n(null), f = i(o.subscribe, o.getSnapshot, o.getServerSnapshot), p = e((e) => {
|
|
164
248
|
let { popupWindow: t } = o.getSnapshot();
|
|
165
|
-
|
|
249
|
+
l.current?.(), l.current = null, t && (!e && !t.closed && t.close(), o.setState({
|
|
166
250
|
popupWindow: null,
|
|
167
251
|
container: null
|
|
168
|
-
}),
|
|
169
|
-
}, [o]),
|
|
252
|
+
}), c.current.onClose?.());
|
|
253
|
+
}, [o]), h = e(() => {
|
|
170
254
|
if (typeof window > "u") return null;
|
|
171
255
|
let e = o.getSnapshot();
|
|
172
256
|
if (e.popupWindow && !e.popupWindow.closed) return e.popupWindow.focus(), e.popupWindow;
|
|
173
|
-
let t =
|
|
257
|
+
let t = c.current, n = t.url ?? _, r = window.open(n, t.name ?? "_blank", g(t));
|
|
174
258
|
if (!r) return o.setState({ blocked: !0 }), t.onBlocked?.(), null;
|
|
175
259
|
let i;
|
|
176
260
|
try {
|
|
@@ -178,27 +262,27 @@ function b(a = {}) {
|
|
|
178
262
|
} catch {
|
|
179
263
|
return r.close(), o.setState({ blocked: !0 }), t.onBlocked?.(), null;
|
|
180
264
|
}
|
|
181
|
-
let a = () =>
|
|
265
|
+
let a = () => p(!0), s = () => {
|
|
182
266
|
setTimeout(() => {
|
|
183
267
|
r.closed && a();
|
|
184
268
|
}, 0);
|
|
185
269
|
}, f = window.setInterval(() => {
|
|
186
270
|
r.closed && a();
|
|
187
|
-
}, 250),
|
|
188
|
-
window.addEventListener("pagehide",
|
|
189
|
-
let
|
|
190
|
-
|
|
191
|
-
window.clearInterval(f), window.clearInterval(
|
|
271
|
+
}, 250), m = () => r.close();
|
|
272
|
+
window.addEventListener("pagehide", m);
|
|
273
|
+
let h, S = !1, C, w;
|
|
274
|
+
l.current = () => {
|
|
275
|
+
window.clearInterval(f), window.clearInterval(h), window.removeEventListener("pagehide", m), S && r.removeEventListener("pagehide", s), C?.(), w?.();
|
|
192
276
|
};
|
|
193
|
-
let
|
|
194
|
-
|
|
277
|
+
let T = (e) => {
|
|
278
|
+
S = !0, r.addEventListener("pagehide", s), e.title = t.title ?? document.title;
|
|
195
279
|
let n = [];
|
|
196
280
|
if (t.copyStyles !== !1) {
|
|
197
|
-
let t =
|
|
198
|
-
|
|
281
|
+
let t = u(document, e);
|
|
282
|
+
C = t.stop, n = t.links;
|
|
199
283
|
}
|
|
200
284
|
let i = e.createElement("div");
|
|
201
|
-
i.setAttribute("data-popup-window-root", ""), e.body.appendChild(i),
|
|
285
|
+
i.setAttribute("data-popup-window-root", ""), e.body.appendChild(i), w = d(n, y, () => {
|
|
202
286
|
o.setState({ container: i }), t.onOpen?.(r);
|
|
203
287
|
});
|
|
204
288
|
};
|
|
@@ -206,42 +290,42 @@ function b(a = {}) {
|
|
|
206
290
|
popupWindow: r,
|
|
207
291
|
container: null,
|
|
208
292
|
blocked: !1
|
|
209
|
-
}), n ===
|
|
293
|
+
}), n === _ ? (b(i), T(i)) : h = window.setInterval(() => {
|
|
210
294
|
if (r.closed) {
|
|
211
295
|
a();
|
|
212
296
|
return;
|
|
213
297
|
}
|
|
214
298
|
let e;
|
|
215
299
|
try {
|
|
216
|
-
e =
|
|
300
|
+
e = x(r);
|
|
217
301
|
} catch {
|
|
218
|
-
|
|
302
|
+
p(!1), o.setState({ blocked: !0 }), t.onBlocked?.();
|
|
219
303
|
return;
|
|
220
304
|
}
|
|
221
|
-
e && (window.clearInterval(
|
|
222
|
-
},
|
|
223
|
-
}, [o,
|
|
224
|
-
o.getSnapshot().popupWindow ?
|
|
305
|
+
e && (window.clearInterval(h), T(e));
|
|
306
|
+
}, v), r;
|
|
307
|
+
}, [o, p]), C = e(() => p(!1), [p]), w = e(() => {
|
|
308
|
+
o.getSnapshot().popupWindow ? p(!1) : h();
|
|
225
309
|
}, [
|
|
226
310
|
o,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
]),
|
|
311
|
+
p,
|
|
312
|
+
h
|
|
313
|
+
]), T = e(() => {
|
|
230
314
|
let { popupWindow: e } = o.getSnapshot();
|
|
231
315
|
e && !e.closed && e.focus();
|
|
232
316
|
}, [o]);
|
|
233
|
-
return t(() => () =>
|
|
234
|
-
open:
|
|
235
|
-
close:
|
|
236
|
-
toggle:
|
|
237
|
-
focus:
|
|
238
|
-
isOpen:
|
|
239
|
-
isBlocked:
|
|
240
|
-
popupWindow:
|
|
317
|
+
return t(() => () => p(!1), [p]), {
|
|
318
|
+
open: h,
|
|
319
|
+
close: C,
|
|
320
|
+
toggle: w,
|
|
321
|
+
focus: T,
|
|
322
|
+
isOpen: f.popupWindow !== null,
|
|
323
|
+
isBlocked: f.blocked,
|
|
324
|
+
popupWindow: f.popupWindow,
|
|
241
325
|
Popup: s
|
|
242
326
|
};
|
|
243
327
|
}
|
|
244
328
|
//#endregion
|
|
245
|
-
export { o as copyStyles,
|
|
329
|
+
export { o as copyStyles, C as usePopupWindow };
|
|
246
330
|
|
|
247
331
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/copyStyles.ts","../src/whenStylesheetsLoad.ts","../src/usePopupWindow.ts"],"sourcesContent":["/**\n * Copy all stylesheets from `source` into `target` and (optionally) keep them\n * in sync while the popup is open:\n *\n * - `<link rel=\"stylesheet\">` and `<style>` elements are mirrored into the\n * target `<head>`. `<style>` contents are serialized from the CSSOM when\n * possible, so rules injected via `insertRule` (CSS-in-JS \"speedy\" mode)\n * are included.\n * - Additions/removals/edits of style nodes in the source `<head>` are\n * observed (covers Vite HMR, lazily loaded chunk CSS, styled-components).\n * - `class`/`style`/`data-*` attributes on `<html>` and `<body>` are mirrored\n * and kept in sync, so theme switching (e.g. a `dark` class) propagates.\n * - `document.adoptedStyleSheets` are re-constructed in the target document.\n *\n * Returns a function that stops observing.\n */\nexport function copyStyles(source: Document, target: Document, watch = true): () => void {\n return mirrorStyles(source, target, watch).stop\n}\n\n/** Attributes copied from a source `<link>` onto its mirror. */\nconst LINK_ATTRS = ['rel', 'media', 'title', 'crossorigin', 'referrerpolicy', 'integrity']\n\nexport interface StyleMirror {\n /** Stops observing the source document. */\n stop: () => void\n /**\n * The `<link>` elements created in `target` by the initial copy. The target\n * fetches each of them asynchronously.\n */\n links: HTMLLinkElement[]\n}\n\n/** {@link copyStyles}, plus the `<link>` elements the initial copy created. */\nexport function mirrorStyles(source: Document, target: Document, watch = true): StyleMirror {\n const mirrors = new Map<Element, Element>()\n\n const isStyleNode = (node: Node): node is HTMLStyleElement | HTMLLinkElement => {\n if (node.nodeType !== Node.ELEMENT_NODE) return false\n const el = node as Element\n return (\n el.tagName === 'STYLE' ||\n (el.tagName === 'LINK' && (el.getAttribute('rel') ?? '').toLowerCase().includes('stylesheet'))\n )\n }\n\n const serializeStyle = (el: HTMLStyleElement): string => {\n const sheet = el.sheet\n if (sheet) {\n try {\n return Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n')\n } catch {\n // Inaccessible cssRules — fall back to the raw text below.\n }\n }\n return el.textContent ?? ''\n }\n\n const mirror = (el: HTMLStyleElement | HTMLLinkElement): void => {\n if (mirrors.has(el)) return\n let clone: Element\n if (el.tagName === 'LINK') {\n const link = target.createElement('link')\n // `rel` is copied as is, so an alternate stylesheet stays disabled.\n // `crossorigin` and `referrerpolicy` make the popup send the same request\n // as the opener, so it can reuse the cached response.\n for (const name of LINK_ATTRS) {\n const value = el.getAttribute(name)\n if (value !== null) link.setAttribute(name, value)\n }\n // .href resolves to an absolute URL, so relative hrefs keep working\n // from the popup's `about:blank` document.\n link.href = (el as HTMLLinkElement).href\n clone = link\n } else {\n const style = target.createElement('style')\n style.textContent = serializeStyle(el as HTMLStyleElement)\n const media = el.getAttribute('media')\n if (media) style.setAttribute('media', media)\n clone = style\n }\n target.head.appendChild(clone)\n mirrors.set(el, clone)\n }\n\n const refresh = (el: HTMLStyleElement): void => {\n const clone = mirrors.get(el)\n if (clone) clone.textContent = serializeStyle(el)\n }\n\n const unmirrorSubtree = (removed: Node): void => {\n for (const [src, clone] of mirrors) {\n if (removed === src || (removed.nodeType === Node.ELEMENT_NODE && removed.contains(src))) {\n clone.remove()\n mirrors.delete(src)\n }\n }\n }\n\n const ROOT_ATTRS = ['class', 'style']\n const syncRootAttrs = (): void => {\n const pairs: Array<[Element | null, Element | null]> = [\n [source.documentElement, target.documentElement],\n [source.body, target.body],\n ]\n for (const [from, to] of pairs) {\n if (!from || !to) continue\n for (const attr of from.attributes) {\n if (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) {\n to.setAttribute(attr.name, attr.value)\n }\n }\n for (const attr of Array.from(to.attributes)) {\n if (\n (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) &&\n !from.hasAttribute(attr.name)\n ) {\n to.removeAttribute(attr.name)\n }\n }\n }\n }\n\n const syncAdoptedSheets = (): void => {\n const targetWindow = target.defaultView as (Window & { CSSStyleSheet?: typeof CSSStyleSheet }) | null\n if (!targetWindow?.CSSStyleSheet || !('adoptedStyleSheets' in source)) return\n try {\n target.adoptedStyleSheets = source.adoptedStyleSheets.map((sheet) => {\n const copy = new targetWindow.CSSStyleSheet!()\n copy.replaceSync(\n Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n'),\n )\n return copy\n })\n } catch {\n // Best effort — older browsers or cross-origin rules.\n }\n }\n\n for (const el of source.querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n const links = Array.from(mirrors.values()).filter(\n (clone): clone is HTMLLinkElement => clone.tagName === 'LINK',\n )\n syncRootAttrs()\n syncAdoptedSheets()\n\n if (!watch || typeof MutationObserver === 'undefined') {\n return { stop: () => {}, links }\n }\n\n const headObserver = new MutationObserver((records) => {\n for (const record of records) {\n if (record.type === 'characterData') {\n const parent = record.target.parentElement\n if (parent?.tagName === 'STYLE') refresh(parent as HTMLStyleElement)\n continue\n }\n // childList\n if (isStyleNode(record.target) && record.target.tagName === 'STYLE') {\n // textContent assignment replaces the text node → childList on <style>\n refresh(record.target as HTMLStyleElement)\n }\n for (const added of record.addedNodes) {\n if (isStyleNode(added)) {\n mirror(added)\n } else if (added.nodeType === Node.ELEMENT_NODE) {\n for (const el of (added as Element).querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n }\n }\n for (const removed of record.removedNodes) {\n unmirrorSubtree(removed)\n }\n }\n })\n headObserver.observe(source.head, { childList: true, subtree: true, characterData: true })\n\n const rootObserver = new MutationObserver(() => syncRootAttrs())\n rootObserver.observe(source.documentElement, { attributes: true })\n if (source.body) rootObserver.observe(source.body, { attributes: true })\n\n return {\n stop: () => {\n headObserver.disconnect()\n rootObserver.disconnect()\n },\n links,\n }\n}\n","/**\n * Calls `callback` once every stylesheet in `links` that applies has loaded\n * or failed, or after `timeoutMs`, whichever comes first.\n *\n * When none of them is still loading, `callback` runs synchronously, before\n * this function returns. Links that do not apply are not waited for: an\n * alternate stylesheet, or a `media` query that does not match, such as\n * `print`. Browsers fetch those at the lowest priority.\n *\n * Returns a function that stops waiting without calling `callback`.\n */\nexport function whenStylesheetsLoad(\n links: readonly HTMLLinkElement[],\n timeoutMs: number,\n callback: () => void,\n): () => void {\n const pending = links.filter((link) => link.sheet === null && applies(link))\n if (pending.length === 0) {\n callback()\n return () => {}\n }\n\n let remaining = pending.length\n const settle = () => {\n remaining--\n if (remaining === 0) finish()\n }\n const stop = () => {\n window.clearTimeout(timeout)\n for (const link of pending) {\n link.removeEventListener('load', settle)\n link.removeEventListener('error', settle)\n }\n }\n const finish = () => {\n stop()\n callback()\n }\n\n for (const link of pending) {\n link.addEventListener('load', settle)\n // A broken stylesheet must not keep the popup blank.\n link.addEventListener('error', settle)\n }\n const timeout = window.setTimeout(finish, timeoutMs)\n return stop\n}\n\nfunction applies(link: HTMLLinkElement): boolean {\n if (/\\balternate\\b/i.test(link.rel)) return false\n const view = link.ownerDocument.defaultView\n return !link.media || !view?.matchMedia || view.matchMedia(link.media).matches\n}\n","import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport type { FC } from 'react'\nimport { createPortal } from 'react-dom'\nimport { mirrorStyles } from './copyStyles'\nimport type {\n PopupProps,\n PopupWindowApi,\n PopupWindowFeatures,\n UsePopupWindowOptions,\n} from './types'\nimport { whenStylesheetsLoad } from './whenStylesheetsLoad'\n\ninterface PopupState {\n popupWindow: Window | null\n container: HTMLElement | null\n blocked: boolean\n}\n\nconst INITIAL_STATE: PopupState = { popupWindow: null, container: null, blocked: false }\n\nclass PopupStore {\n state: PopupState = INITIAL_STATE\n private listeners = new Set<() => void>()\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n getSnapshot = (): PopupState => this.state\n\n getServerSnapshot = (): PopupState => INITIAL_STATE\n\n setState(partial: Partial<PopupState>): void {\n this.state = { ...this.state, ...partial }\n for (const listener of this.listeners) listener()\n }\n}\n\nconst DEFAULT_FEATURES: PopupWindowFeatures = { popup: true, width: 640, height: 480 }\n\nfunction buildFeatures(options: UsePopupWindowOptions): string {\n const merged: PopupWindowFeatures = { ...DEFAULT_FEATURES, ...options.features }\n if (\n options.center !== false &&\n merged.left === undefined &&\n merged.top === undefined &&\n typeof merged.width === 'number' &&\n typeof merged.height === 'number'\n ) {\n merged.left = Math.max(0, Math.round(window.screenX + (window.outerWidth - merged.width) / 2))\n merged.top = Math.max(0, Math.round(window.screenY + (window.outerHeight - merged.height) / 2))\n }\n return Object.entries(merged)\n .filter(([, value]) => value !== undefined && value !== false)\n .map(([key, value]) => (value === true ? `${key}=yes` : `${key}=${value}`))\n .join(',')\n}\n\nconst ABOUT_BLANK = 'about:blank'\n/** Interval for checking whether the page given by `url` has loaded. */\nconst LOAD_POLL_MS = 20\n/** Longest time `Popup` waits for copied stylesheets before rendering anyway. */\nconst STYLES_TIMEOUT_MS = 3000\n\n/**\n * The popup's document once the page given by `url` has replaced the initial\n * `about:blank` document and finished parsing, otherwise `null`. Throws when\n * the document is not scriptable.\n */\nfunction loadedDocument(popupWindow: Window): Document | null {\n const doc = popupWindow.document\n if (doc.URL === ABOUT_BLANK || doc.readyState === 'loading') return null\n return doc\n}\n\nfunction createPopupComponent(store: PopupStore): FC<PopupProps> {\n function Popup({ children }: PopupProps) {\n const { container } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getServerSnapshot,\n )\n return container ? createPortal(children, container) : null\n }\n Popup.displayName = 'PopupWindow.Popup'\n return Popup\n}\n\n/**\n * Open part of your React tree in a separate browser window.\n *\n * The popup content is rendered with a portal into the popup's document, so\n * it stays part of your component tree: state, context and event handlers\n * all keep working across windows.\n *\n * ```tsx\n * const { open, close, isOpen, Popup } = usePopupWindow({ title: 'Panel' })\n *\n * return (\n * <>\n * <button onClick={open}>Open panel</button>\n * <Popup>\n * <MyPanel />\n * </Popup>\n * </>\n * )\n * ```\n */\nexport function usePopupWindow(options: UsePopupWindowOptions = {}): PopupWindowApi {\n const [store] = useState(() => new PopupStore())\n const [Popup] = useState(() => createPopupComponent(store))\n\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n const cleanupRef = useRef<(() => void) | null>(null)\n\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)\n\n // `userClosed` is true when the window was closed outside our control\n // (user hit the close button, opener unloaded) — then we must not touch it.\n const closePopup = useCallback(\n (userClosed: boolean) => {\n const { popupWindow } = store.getSnapshot()\n cleanupRef.current?.()\n cleanupRef.current = null\n if (!popupWindow) return\n if (!userClosed && !popupWindow.closed) popupWindow.close()\n store.setState({ popupWindow: null, container: null })\n optionsRef.current.onClose?.()\n },\n [store],\n )\n\n const open = useCallback((): Window | null => {\n if (typeof window === 'undefined') return null\n const current = store.getSnapshot()\n if (current.popupWindow && !current.popupWindow.closed) {\n current.popupWindow.focus()\n return current.popupWindow\n }\n\n const opts = optionsRef.current\n const url = opts.url ?? ABOUT_BLANK\n const popupWindow = window.open(url, opts.name ?? '_blank', buildFeatures(opts))\n if (!popupWindow) {\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n\n // Sandboxed embedders (VS Code's built-in browser, CodeSandbox/StackBlitz\n // previews, iframes sandboxed without popup-escape) open the popup with an\n // opaque origin, so reading its document throws a SecurityError. Treat\n // that as blocked instead of stranding a blank window the portal can\n // never reach.\n let initialDoc: Document\n try {\n initialDoc = popupWindow.document\n } catch {\n popupWindow.close()\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n\n const handleExternalClose = () => closePopup(true)\n const onPopupPagehide = () => {\n // pagehide also fires on navigation; only treat it as a close when the\n // window really is gone a tick later.\n setTimeout(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 0)\n }\n\n // Belt and braces: some browsers don't fire pagehide reliably for popups.\n const closePoll = window.setInterval(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 250)\n\n const onOpenerPagehide = () => popupWindow.close()\n window.addEventListener('pagehide', onOpenerPagehide)\n\n let loadPoll: number | undefined\n let prepared = false\n let stopStyleSync: (() => void) | undefined\n let stopStylesWait: (() => void) | undefined\n\n cleanupRef.current = () => {\n window.clearInterval(closePoll)\n window.clearInterval(loadPoll)\n window.removeEventListener('pagehide', onOpenerPagehide)\n if (prepared) popupWindow.removeEventListener('pagehide', onPopupPagehide)\n stopStyleSync?.()\n stopStylesWait?.()\n }\n\n // Title, styles and the portal container go into the document that\n // stays: `about:blank` itself, or the page `url` loads in its place.\n const prepare = (doc: Document) => {\n prepared = true\n popupWindow.addEventListener('pagehide', onPopupPagehide)\n doc.title = opts.title ?? document.title\n let links: HTMLLinkElement[] = []\n if (opts.copyStyles !== false) {\n const styles = mirrorStyles(document, doc)\n stopStyleSync = styles.stop\n links = styles.links\n }\n const container = doc.createElement('div')\n container.setAttribute('data-popup-window-root', '')\n doc.body.appendChild(container)\n // The popup fetches copied <link> stylesheets asynchronously. Render\n // only once they have loaded, so the first paint is not unstyled.\n stopStylesWait = whenStylesheetsLoad(links, STYLES_TIMEOUT_MS, () => {\n store.setState({ container })\n opts.onOpen?.(popupWindow)\n })\n }\n\n store.setState({ popupWindow, container: null, blocked: false })\n\n if (url === ABOUT_BLANK) {\n prepare(initialDoc)\n } else {\n // The window starts on an initial about:blank document and then\n // navigates to `url`. Wait until that page has replaced it and parsed.\n loadPoll = window.setInterval(() => {\n if (popupWindow.closed) {\n handleExternalClose()\n return\n }\n let doc: Document | null\n try {\n doc = loadedDocument(popupWindow)\n } catch {\n // The page went somewhere the opener cannot script, such as a\n // cross-origin redirect: nothing can ever render there.\n closePopup(false)\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return\n }\n if (!doc) return\n window.clearInterval(loadPoll)\n prepare(doc)\n }, LOAD_POLL_MS)\n }\n\n return popupWindow\n }, [store, closePopup])\n\n const close = useCallback(() => closePopup(false), [closePopup])\n\n const toggle = useCallback(() => {\n if (store.getSnapshot().popupWindow) {\n closePopup(false)\n } else {\n open()\n }\n }, [store, closePopup, open])\n\n const focus = useCallback(() => {\n const { popupWindow } = store.getSnapshot()\n if (popupWindow && !popupWindow.closed) popupWindow.focus()\n }, [store])\n\n // Close the popup when the owning component unmounts — its portal content\n // would unmount anyway, leaving an empty window behind.\n useEffect(() => {\n return () => closePopup(false)\n }, [closePopup])\n\n return {\n open,\n close,\n toggle,\n focus,\n isOpen: state.popupWindow !== null,\n isBlocked: state.blocked,\n popupWindow: state.popupWindow,\n Popup,\n }\n}\n"],"mappings":"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"}
|
|
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 * - A `<link>` added while watching is loaded by the target again. Until\n * that copy has loaded, the rules the source already loaded are applied\n * to the target through a constructed stylesheet.\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 // A <link> added while watching, such as lazily loaded chunk CSS: the\n // target loads its copy separately, and content that waits only for the\n // source's copy, as bundlers do, can render before the target's has\n // loaded. Until it has, apply the rules the source already parsed through a\n // constructed sheet. `baseURL` keeps relative url()s resolving against the\n // stylesheet. Adopted sheets come last in the cascade, which can change\n // precedence for that short time. Unreadable cross-origin sheets are left\n // to load normally.\n const unbridgeLinks = new Map<Element, () => void>()\n const bridgeLink = (el: HTMLLinkElement, clone: HTMLLinkElement): void => {\n const view = target.defaultView as (Window & typeof globalThis) | null\n if (!view || typeof view.CSSStyleSheet !== 'function' || !('adoptedStyleSheets' in target)) {\n return\n }\n if (/\\balternate\\b/i.test(el.rel)) return\n let bridge: CSSStyleSheet | null = null\n const add = (): void => {\n if (clone.sheet !== null || !el.sheet) return\n try {\n const text = Array.from(el.sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n')\n const sheet = new view.CSSStyleSheet({ baseURL: el.href, media: el.media || undefined })\n sheet.replaceSync(text)\n target.adoptedStyleSheets = [...target.adoptedStyleSheets, sheet]\n bridge = sheet\n } catch {\n // Cross-origin rules or no constructable stylesheets - wait for the copy.\n }\n }\n const remove = (): void => {\n el.removeEventListener('load', add)\n clone.removeEventListener('load', remove)\n clone.removeEventListener('error', remove)\n unbridgeLinks.delete(el)\n const sheet = bridge\n if (sheet) target.adoptedStyleSheets = target.adoptedStyleSheets.filter((s) => s !== sheet)\n bridge = null\n }\n clone.addEventListener('load', remove)\n clone.addEventListener('error', remove)\n unbridgeLinks.set(el, remove)\n if (el.sheet) add()\n else el.addEventListener('load', add)\n }\n\n // True during the initial copy. `usePopupWindow` waits for those links\n // itself before rendering.\n let copying = true\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 else if (!copying && observing) bridgeLink(el as HTMLLinkElement, clone as HTMLLinkElement)\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 unbridgeLinks.get(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 copying = false\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 for (const remove of [...unbridgeLinks.values()]) remove()\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 * Gives the initial `about:blank` document an HTML5 doctype, so the popup\n * lays out in standards mode like the opener instead of in quirks mode. Only\n * the parser can set the mode, so the document is written again. In Chromium\n * its `URL` then reports the opener's URL; a reload still loads `about:blank`.\n */\nfunction writeStandardsDocument(doc: Document): void {\n doc.open()\n doc.write('<!DOCTYPE html><html><head></head><body></body></html>')\n doc.close()\n}\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 // Before prepare(): writing the document removes event listeners on the\n // popup window.\n writeStandardsDocument(initialDoc)\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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePopupWindow.d.ts","sourceRoot":"","sources":["../src/usePopupWindow.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,cAAc,EAEd,qBAAqB,EACtB,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"usePopupWindow.d.ts","sourceRoot":"","sources":["../src/usePopupWindow.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,cAAc,EAEd,qBAAqB,EACtB,MAAM,SAAS,CAAA;AA4FhB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,cAAc,CAkLlF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jielga/react-popup-window",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "React hook to open part of your UI in a separate browser popup window — live portal rendering, style syncing and cross-window messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,7 +11,7 @@ description: >
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: core
|
|
13
13
|
library: '@jielga/react-popup-window'
|
|
14
|
-
library_version: '0.
|
|
14
|
+
library_version: '0.3.0'
|
|
15
15
|
sources:
|
|
16
16
|
- 'Jielga/react-popup-window:README.md'
|
|
17
17
|
- 'Jielga/react-popup-window:src/usePopupWindow.ts'
|
|
@@ -103,7 +103,8 @@ const { open, Popup } = usePopupWindow({ title: 'Panel', url: '/popup.html' })
|
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
`isOpen` turns `true` as soon as the window opens; `Popup` renders once the
|
|
106
|
-
page has loaded.
|
|
106
|
+
page has loaded. Start the page with `<!doctype html>`, or the popup lays out
|
|
107
|
+
in quirks mode. Important: do not point `url` at a route of the app, as that
|
|
107
108
|
starts a second copy of the app inside the popup. If the page redirects to
|
|
108
109
|
another origin, the popup closes and `onBlocked` is called.
|
|
109
110
|
|
|
@@ -12,7 +12,7 @@ description: >
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: sub-skill
|
|
14
14
|
library: '@jielga/react-popup-window'
|
|
15
|
-
library_version: '0.
|
|
15
|
+
library_version: '0.3.0'
|
|
16
16
|
requires:
|
|
17
17
|
- '@jielga/react-popup-window/getting-started'
|
|
18
18
|
sources:
|
|
@@ -32,7 +32,12 @@ 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`
|
|
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.
|
|
@@ -45,8 +50,10 @@ styling into it and keeps the mirror current:
|
|
|
45
50
|
The popup loads each copied `<link>` stylesheet again. `Popup` renders once
|
|
46
51
|
they have loaded or failed (at most 3 seconds), so the first frame shown is
|
|
47
52
|
styled. Alternate stylesheets and non-matching `media` are not waited for.
|
|
48
|
-
A `<link>` added to the opener while the popup is open
|
|
49
|
-
|
|
53
|
+
A `<link>` added to the opener while the popup is open, such as lazily loaded
|
|
54
|
+
chunk CSS, is loaded by the popup again; until then the popup applies the
|
|
55
|
+
rules the opener already loaded. A cross-origin stylesheet without CORS
|
|
56
|
+
cannot be read that way and is unstyled until the popup has loaded it.
|
|
50
57
|
|
|
51
58
|
`copyStyles: false` disables all of it. The mechanism is also exported
|
|
52
59
|
standalone as `copyStyles(source, target, watch?)`, returning a `stop`
|
|
@@ -401,28 +408,28 @@ message — require listeners on the `popupWindow` object.
|
|
|
401
408
|
|
|
402
409
|
Source: README.md (Communication)
|
|
403
410
|
|
|
404
|
-
### MEDIUM Expecting
|
|
411
|
+
### MEDIUM Expecting in-place CSSOM edits to sync after open
|
|
405
412
|
|
|
406
413
|
Wrong:
|
|
407
414
|
|
|
408
415
|
```tsx
|
|
409
416
|
// after the popup is open
|
|
410
|
-
|
|
417
|
+
rule.style.setProperty('color', 'red') // edits an existing rule; not mirrored
|
|
418
|
+
mediaRule.insertRule('.late { color: red }') // nested rule; not mirrored
|
|
411
419
|
```
|
|
412
420
|
|
|
413
421
|
Correct:
|
|
414
422
|
|
|
415
423
|
```tsx
|
|
416
|
-
//
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
document.head.appendChild(el)
|
|
424
|
+
// add a rule to the <style> sheet itself; insertRule on the sheet is mirrored
|
|
425
|
+
const sheet = styleElement.sheet!
|
|
426
|
+
sheet.insertRule('.late { color: red }', sheet.cssRules.length)
|
|
420
427
|
```
|
|
421
428
|
|
|
422
|
-
Style synchronization
|
|
423
|
-
|
|
424
|
-
sheet
|
|
425
|
-
|
|
426
|
-
are
|
|
429
|
+
Style synchronization copies each sheet when the popup opens, applies
|
|
430
|
+
`insertRule`/`deleteRule` calls on a `<style>` sheet as they happen, and
|
|
431
|
+
copies a sheet again when its node changes. Edits inside an existing rule,
|
|
432
|
+
rules inserted into a nested rule such as `@media`, and changes to
|
|
433
|
+
`document.adoptedStyleSheets` after the popup opened are not mirrored.
|
|
427
434
|
|
|
428
435
|
Source: src/copyStyles.ts
|