@jielga/react-popup-window 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
1
  Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("react"),t=require("react-dom");function n(e,t,n=!0){let r=new Map,i=e=>{if(e.nodeType!==Node.ELEMENT_NODE)return!1;let t=e;return t.tagName===`STYLE`||t.tagName===`LINK`&&(t.getAttribute(`rel`)??``).toLowerCase().includes(`stylesheet`)},a=e=>{let t=e.sheet;if(t)try{return Array.from(t.cssRules).map(e=>e.cssText).join(`
2
2
  `)}catch{}return e.textContent??``},o=e=>{if(r.has(e))return;let n;if(e.tagName===`LINK`){let r=t.createElement(`link`);r.rel=`stylesheet`,r.href=e.href;let i=e.getAttribute(`media`);i&&(r.media=i),n=r}else{let r=t.createElement(`style`);r.textContent=a(e);let i=e.getAttribute(`media`);i&&r.setAttribute(`media`,i),n=r}t.head.appendChild(n),r.set(e,n)},s=e=>{let t=r.get(e);t&&(t.textContent=a(e))},c=e=>{for(let[t,n]of r)(e===t||e.nodeType===Node.ELEMENT_NODE&&e.contains(t))&&(n.remove(),r.delete(t))},l=[`class`,`style`],u=()=>{let n=[[e.documentElement,t.documentElement],[e.body,t.body]];for(let[e,t]of n)if(!(!e||!t)){for(let n of e.attributes)(l.includes(n.name)||n.name.startsWith(`data-`))&&t.setAttribute(n.name,n.value);for(let n of Array.from(t.attributes))(l.includes(n.name)||n.name.startsWith(`data-`))&&!e.hasAttribute(n.name)&&t.removeAttribute(n.name)}},d=()=>{let n=t.defaultView;if(!(!n?.CSSStyleSheet||!(`adoptedStyleSheets`in e)))try{t.adoptedStyleSheets=e.adoptedStyleSheets.map(e=>{let t=new n.CSSStyleSheet;return t.replaceSync(Array.from(e.cssRules).map(e=>e.cssText).join(`
3
- `)),t})}catch{}};for(let t of e.querySelectorAll(`style, link[rel~="stylesheet" i]`))o(t);if(u(),d(),!n||typeof MutationObserver>`u`)return()=>{};let f=new MutationObserver(e=>{for(let t of e){if(t.type===`characterData`){let e=t.target.parentElement;e?.tagName===`STYLE`&&s(e);continue}i(t.target)&&t.target.tagName===`STYLE`&&s(t.target);for(let e of t.addedNodes)if(i(e))o(e);else if(e.nodeType===Node.ELEMENT_NODE)for(let t of e.querySelectorAll(`style, link[rel~="stylesheet" i]`))o(t);for(let e of t.removedNodes)c(e)}});f.observe(e.head,{childList:!0,subtree:!0,characterData:!0});let p=new MutationObserver(()=>u());return p.observe(e.documentElement,{attributes:!0}),e.body&&p.observe(e.body,{attributes:!0}),()=>{f.disconnect(),p.disconnect()}}var r={popupWindow:null,container:null,blocked:!1},i=class{state=r;listeners=new Set;subscribe=e=>(this.listeners.add(e),()=>this.listeners.delete(e));getSnapshot=()=>this.state;getServerSnapshot=()=>r;setState(e){this.state={...this.state,...e};for(let e of this.listeners)e()}},a={popup:!0,width:640,height:480};function o(e){let t={...a,...e.features};return e.center!==!1&&t.left===void 0&&t.top===void 0&&typeof t.width==`number`&&typeof t.height==`number`&&(t.left=Math.max(0,Math.round(window.screenX+(window.outerWidth-t.width)/2)),t.top=Math.max(0,Math.round(window.screenY+(window.outerHeight-t.height)/2))),Object.entries(t).filter(([,e])=>e!==void 0&&e!==!1).map(([e,t])=>t===!0?`${e}=yes`:`${e}=${t}`).join(`,`)}function s(n){function r({children:r}){let{container:i}=(0,e.useSyncExternalStore)(n.subscribe,n.getSnapshot,n.getServerSnapshot);return i?(0,t.createPortal)(r,i):null}return r.displayName=`PopupWindow.Popup`,r}function c(t={}){let[r]=(0,e.useState)(()=>new i),[a]=(0,e.useState)(()=>s(r)),c=(0,e.useRef)(t);c.current=t;let l=(0,e.useRef)(null),u=(0,e.useSyncExternalStore)(r.subscribe,r.getSnapshot,r.getServerSnapshot),d=(0,e.useCallback)(e=>{let{popupWindow:t}=r.getSnapshot();l.current?.(),l.current=null,t&&(!e&&!t.closed&&t.close(),r.setState({popupWindow:null,container:null}),c.current.onClose?.())},[r]),f=(0,e.useCallback)(()=>{if(typeof window>`u`)return null;let e=r.getSnapshot();if(e.popupWindow&&!e.popupWindow.closed)return e.popupWindow.focus(),e.popupWindow;let t=c.current,i=window.open(`about:blank`,t.name??`_blank`,o(t));if(!i)return r.setState({blocked:!0}),t.onBlocked?.(),null;let a=i.document;a.title=t.title??document.title;let s;t.copyStyles!==!1&&(s=n(document,a));let u=a.createElement(`div`);u.setAttribute(`data-popup-window-root`,``),a.body.appendChild(u);let f=()=>d(!0),p=()=>{setTimeout(()=>{i.closed&&f()},0)};i.addEventListener(`pagehide`,p);let m=window.setInterval(()=>{i.closed&&f()},250),h=()=>i.close();return window.addEventListener(`pagehide`,h),l.current=()=>{window.clearInterval(m),window.removeEventListener(`pagehide`,h),i.removeEventListener(`pagehide`,p),s?.()},r.setState({popupWindow:i,container:u,blocked:!1}),t.onOpen?.(i),i},[r,d]),p=(0,e.useCallback)(()=>d(!1),[d]),m=(0,e.useCallback)(()=>{r.getSnapshot().popupWindow?d(!1):f()},[r,d,f]),h=(0,e.useCallback)(()=>{let{popupWindow:e}=r.getSnapshot();e&&!e.closed&&e.focus()},[r]);return(0,e.useEffect)(()=>()=>d(!1),[d]),{open:f,close:p,toggle:m,focus:h,isOpen:u.popupWindow!==null,isBlocked:u.blocked,popupWindow:u.popupWindow,Popup:a}}exports.copyStyles=n,exports.usePopupWindow=c;
3
+ `)),t})}catch{}};for(let t of e.querySelectorAll(`style, link[rel~="stylesheet" i]`))o(t);if(u(),d(),!n||typeof MutationObserver>`u`)return()=>{};let f=new MutationObserver(e=>{for(let t of e){if(t.type===`characterData`){let e=t.target.parentElement;e?.tagName===`STYLE`&&s(e);continue}i(t.target)&&t.target.tagName===`STYLE`&&s(t.target);for(let e of t.addedNodes)if(i(e))o(e);else if(e.nodeType===Node.ELEMENT_NODE)for(let t of e.querySelectorAll(`style, link[rel~="stylesheet" i]`))o(t);for(let e of t.removedNodes)c(e)}});f.observe(e.head,{childList:!0,subtree:!0,characterData:!0});let p=new MutationObserver(()=>u());return p.observe(e.documentElement,{attributes:!0}),e.body&&p.observe(e.body,{attributes:!0}),()=>{f.disconnect(),p.disconnect()}}var r={popupWindow:null,container:null,blocked:!1},i=class{state=r;listeners=new Set;subscribe=e=>(this.listeners.add(e),()=>this.listeners.delete(e));getSnapshot=()=>this.state;getServerSnapshot=()=>r;setState(e){this.state={...this.state,...e};for(let e of this.listeners)e()}},a={popup:!0,width:640,height:480};function o(e){let t={...a,...e.features};return e.center!==!1&&t.left===void 0&&t.top===void 0&&typeof t.width==`number`&&typeof t.height==`number`&&(t.left=Math.max(0,Math.round(window.screenX+(window.outerWidth-t.width)/2)),t.top=Math.max(0,Math.round(window.screenY+(window.outerHeight-t.height)/2))),Object.entries(t).filter(([,e])=>e!==void 0&&e!==!1).map(([e,t])=>t===!0?`${e}=yes`:`${e}=${t}`).join(`,`)}function s(n){function r({children:r}){let{container:i}=(0,e.useSyncExternalStore)(n.subscribe,n.getSnapshot,n.getServerSnapshot);return i?(0,t.createPortal)(r,i):null}return r.displayName=`PopupWindow.Popup`,r}function c(t={}){let[r]=(0,e.useState)(()=>new i),[a]=(0,e.useState)(()=>s(r)),c=(0,e.useRef)(t);c.current=t;let l=(0,e.useRef)(null),u=(0,e.useSyncExternalStore)(r.subscribe,r.getSnapshot,r.getServerSnapshot),d=(0,e.useCallback)(e=>{let{popupWindow:t}=r.getSnapshot();l.current?.(),l.current=null,t&&(!e&&!t.closed&&t.close(),r.setState({popupWindow:null,container:null}),c.current.onClose?.())},[r]),f=(0,e.useCallback)(()=>{if(typeof window>`u`)return null;let e=r.getSnapshot();if(e.popupWindow&&!e.popupWindow.closed)return e.popupWindow.focus(),e.popupWindow;let t=c.current,i=window.open(`about:blank`,t.name??`_blank`,o(t));if(!i)return r.setState({blocked:!0}),t.onBlocked?.(),null;let a;try{a=i.document}catch{return i.close(),r.setState({blocked:!0}),t.onBlocked?.(),null}a.title=t.title??document.title;let s;t.copyStyles!==!1&&(s=n(document,a));let u=a.createElement(`div`);u.setAttribute(`data-popup-window-root`,``),a.body.appendChild(u);let f=()=>d(!0),p=()=>{setTimeout(()=>{i.closed&&f()},0)};i.addEventListener(`pagehide`,p);let m=window.setInterval(()=>{i.closed&&f()},250),h=()=>i.close();return window.addEventListener(`pagehide`,h),l.current=()=>{window.clearInterval(m),window.removeEventListener(`pagehide`,h),i.removeEventListener(`pagehide`,p),s?.()},r.setState({popupWindow:i,container:u,blocked:!1}),t.onOpen?.(i),i},[r,d]),p=(0,e.useCallback)(()=>d(!1),[d]),m=(0,e.useCallback)(()=>{r.getSnapshot().popupWindow?d(!1):f()},[r,d,f]),h=(0,e.useCallback)(()=>{let{popupWindow:e}=r.getSnapshot();e&&!e.closed&&e.focus()},[r]);return(0,e.useEffect)(()=>()=>d(!1),[d]),{open:f,close:p,toggle:m,focus:h,isOpen:u.popupWindow!==null,isBlocked:u.blocked,popupWindow:u.popupWindow,Popup:a}}exports.copyStyles=n,exports.usePopupWindow=c;
4
4
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/copyStyles.ts","../src/usePopupWindow.ts"],"sourcesContent":["/**\r\n * Copy all stylesheets from `source` into `target` and (optionally) keep them\r\n * in sync while the popup is open:\r\n *\r\n * - `<link rel=\"stylesheet\">` and `<style>` elements are mirrored into the\r\n * target `<head>`. `<style>` contents are serialized from the CSSOM when\r\n * possible, so rules injected via `insertRule` (CSS-in-JS \"speedy\" mode)\r\n * are included.\r\n * - Additions/removals/edits of style nodes in the source `<head>` are\r\n * observed (covers Vite HMR, lazily loaded chunk CSS, styled-components).\r\n * - `class`/`style`/`data-*` attributes on `<html>` and `<body>` are mirrored\r\n * and kept in sync, so theme switching (e.g. a `dark` class) propagates.\r\n * - `document.adoptedStyleSheets` are re-constructed in the target document.\r\n *\r\n * Returns a function that stops observing.\r\n */\r\nexport function copyStyles(source: Document, target: Document, watch = true): () => void {\r\n const mirrors = new Map<Element, Element>()\r\n\r\n const isStyleNode = (node: Node): node is HTMLStyleElement | HTMLLinkElement => {\r\n if (node.nodeType !== Node.ELEMENT_NODE) return false\r\n const el = node as Element\r\n return (\r\n el.tagName === 'STYLE' ||\r\n (el.tagName === 'LINK' && (el.getAttribute('rel') ?? '').toLowerCase().includes('stylesheet'))\r\n )\r\n }\r\n\r\n const serializeStyle = (el: HTMLStyleElement): string => {\r\n const sheet = el.sheet\r\n if (sheet) {\r\n try {\r\n return Array.from(sheet.cssRules)\r\n .map((rule) => rule.cssText)\r\n .join('\\n')\r\n } catch {\r\n // Inaccessible cssRules — fall back to the raw text below.\r\n }\r\n }\r\n return el.textContent ?? ''\r\n }\r\n\r\n const mirror = (el: HTMLStyleElement | HTMLLinkElement): void => {\r\n if (mirrors.has(el)) return\r\n let clone: Element\r\n if (el.tagName === 'LINK') {\r\n const link = target.createElement('link')\r\n link.rel = 'stylesheet'\r\n // .href resolves to an absolute URL, so relative hrefs keep working\r\n // from the popup's `about:blank` document.\r\n link.href = (el as HTMLLinkElement).href\r\n const media = el.getAttribute('media')\r\n if (media) link.media = media\r\n clone = link\r\n } else {\r\n const style = target.createElement('style')\r\n style.textContent = serializeStyle(el as HTMLStyleElement)\r\n const media = el.getAttribute('media')\r\n if (media) style.setAttribute('media', media)\r\n clone = style\r\n }\r\n target.head.appendChild(clone)\r\n mirrors.set(el, clone)\r\n }\r\n\r\n const refresh = (el: HTMLStyleElement): void => {\r\n const clone = mirrors.get(el)\r\n if (clone) clone.textContent = serializeStyle(el)\r\n }\r\n\r\n const unmirrorSubtree = (removed: Node): void => {\r\n for (const [src, clone] of mirrors) {\r\n if (removed === src || (removed.nodeType === Node.ELEMENT_NODE && removed.contains(src))) {\r\n clone.remove()\r\n mirrors.delete(src)\r\n }\r\n }\r\n }\r\n\r\n const ROOT_ATTRS = ['class', 'style']\r\n const syncRootAttrs = (): void => {\r\n const pairs: Array<[Element | null, Element | null]> = [\r\n [source.documentElement, target.documentElement],\r\n [source.body, target.body],\r\n ]\r\n for (const [from, to] of pairs) {\r\n if (!from || !to) continue\r\n for (const attr of from.attributes) {\r\n if (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) {\r\n to.setAttribute(attr.name, attr.value)\r\n }\r\n }\r\n for (const attr of Array.from(to.attributes)) {\r\n if (\r\n (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) &&\r\n !from.hasAttribute(attr.name)\r\n ) {\r\n to.removeAttribute(attr.name)\r\n }\r\n }\r\n }\r\n }\r\n\r\n const syncAdoptedSheets = (): void => {\r\n const targetWindow = target.defaultView as (Window & { CSSStyleSheet?: typeof CSSStyleSheet }) | null\r\n if (!targetWindow?.CSSStyleSheet || !('adoptedStyleSheets' in source)) return\r\n try {\r\n target.adoptedStyleSheets = source.adoptedStyleSheets.map((sheet) => {\r\n const copy = new targetWindow.CSSStyleSheet!()\r\n copy.replaceSync(\r\n Array.from(sheet.cssRules)\r\n .map((rule) => rule.cssText)\r\n .join('\\n'),\r\n )\r\n return copy\r\n })\r\n } catch {\r\n // Best effort — older browsers or cross-origin rules.\r\n }\r\n }\r\n\r\n for (const el of source.querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\r\n mirror(el as HTMLStyleElement | HTMLLinkElement)\r\n }\r\n syncRootAttrs()\r\n syncAdoptedSheets()\r\n\r\n if (!watch || typeof MutationObserver === 'undefined') {\r\n return () => {}\r\n }\r\n\r\n const headObserver = new MutationObserver((records) => {\r\n for (const record of records) {\r\n if (record.type === 'characterData') {\r\n const parent = record.target.parentElement\r\n if (parent?.tagName === 'STYLE') refresh(parent as HTMLStyleElement)\r\n continue\r\n }\r\n // childList\r\n if (isStyleNode(record.target) && record.target.tagName === 'STYLE') {\r\n // textContent assignment replaces the text node → childList on <style>\r\n refresh(record.target as HTMLStyleElement)\r\n }\r\n for (const added of record.addedNodes) {\r\n if (isStyleNode(added)) {\r\n mirror(added)\r\n } else if (added.nodeType === Node.ELEMENT_NODE) {\r\n for (const el of (added as Element).querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\r\n mirror(el as HTMLStyleElement | HTMLLinkElement)\r\n }\r\n }\r\n }\r\n for (const removed of record.removedNodes) {\r\n unmirrorSubtree(removed)\r\n }\r\n }\r\n })\r\n headObserver.observe(source.head, { childList: true, subtree: true, characterData: true })\r\n\r\n const rootObserver = new MutationObserver(() => syncRootAttrs())\r\n rootObserver.observe(source.documentElement, { attributes: true })\r\n if (source.body) rootObserver.observe(source.body, { attributes: true })\r\n\r\n return () => {\r\n headObserver.disconnect()\r\n rootObserver.disconnect()\r\n }\r\n}\r\n","import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'\r\nimport type { FC } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport { copyStyles } from './copyStyles'\r\nimport type {\r\n PopupProps,\r\n PopupWindowApi,\r\n PopupWindowFeatures,\r\n UsePopupWindowOptions,\r\n} from './types'\r\n\r\ninterface PopupState {\r\n popupWindow: Window | null\r\n container: HTMLElement | null\r\n blocked: boolean\r\n}\r\n\r\nconst INITIAL_STATE: PopupState = { popupWindow: null, container: null, blocked: false }\r\n\r\nclass PopupStore {\r\n state: PopupState = INITIAL_STATE\r\n private listeners = new Set<() => void>()\r\n\r\n subscribe = (listener: () => void): (() => void) => {\r\n this.listeners.add(listener)\r\n return () => this.listeners.delete(listener)\r\n }\r\n\r\n getSnapshot = (): PopupState => this.state\r\n\r\n getServerSnapshot = (): PopupState => INITIAL_STATE\r\n\r\n setState(partial: Partial<PopupState>): void {\r\n this.state = { ...this.state, ...partial }\r\n for (const listener of this.listeners) listener()\r\n }\r\n}\r\n\r\nconst DEFAULT_FEATURES: PopupWindowFeatures = { popup: true, width: 640, height: 480 }\r\n\r\nfunction buildFeatures(options: UsePopupWindowOptions): string {\r\n const merged: PopupWindowFeatures = { ...DEFAULT_FEATURES, ...options.features }\r\n if (\r\n options.center !== false &&\r\n merged.left === undefined &&\r\n merged.top === undefined &&\r\n typeof merged.width === 'number' &&\r\n typeof merged.height === 'number'\r\n ) {\r\n merged.left = Math.max(0, Math.round(window.screenX + (window.outerWidth - merged.width) / 2))\r\n merged.top = Math.max(0, Math.round(window.screenY + (window.outerHeight - merged.height) / 2))\r\n }\r\n return Object.entries(merged)\r\n .filter(([, value]) => value !== undefined && value !== false)\r\n .map(([key, value]) => (value === true ? `${key}=yes` : `${key}=${value}`))\r\n .join(',')\r\n}\r\n\r\nfunction createPopupComponent(store: PopupStore): FC<PopupProps> {\r\n function Popup({ children }: PopupProps) {\r\n const { container } = useSyncExternalStore(\r\n store.subscribe,\r\n store.getSnapshot,\r\n store.getServerSnapshot,\r\n )\r\n return container ? createPortal(children, container) : null\r\n }\r\n Popup.displayName = 'PopupWindow.Popup'\r\n return Popup\r\n}\r\n\r\n/**\r\n * Open part of your React tree in a separate browser window.\r\n *\r\n * The popup content is rendered with a portal into the popup's document, so\r\n * it stays part of your component tree: state, context and event handlers\r\n * all keep working across windows.\r\n *\r\n * ```tsx\r\n * const { open, close, isOpen, Popup } = usePopupWindow({ title: 'Panel' })\r\n *\r\n * return (\r\n * <>\r\n * <button onClick={open}>Open panel</button>\r\n * <Popup>\r\n * <MyPanel />\r\n * </Popup>\r\n * </>\r\n * )\r\n * ```\r\n */\r\nexport function usePopupWindow(options: UsePopupWindowOptions = {}): PopupWindowApi {\r\n const [store] = useState(() => new PopupStore())\r\n const [Popup] = useState(() => createPopupComponent(store))\r\n\r\n const optionsRef = useRef(options)\r\n optionsRef.current = options\r\n\r\n const cleanupRef = useRef<(() => void) | null>(null)\r\n\r\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)\r\n\r\n // `userClosed` is true when the window was closed outside our control\r\n // (user hit the close button, opener unloaded) — then we must not touch it.\r\n const closePopup = useCallback(\r\n (userClosed: boolean) => {\r\n const { popupWindow } = store.getSnapshot()\r\n cleanupRef.current?.()\r\n cleanupRef.current = null\r\n if (!popupWindow) return\r\n if (!userClosed && !popupWindow.closed) popupWindow.close()\r\n store.setState({ popupWindow: null, container: null })\r\n optionsRef.current.onClose?.()\r\n },\r\n [store],\r\n )\r\n\r\n const open = useCallback((): Window | null => {\r\n if (typeof window === 'undefined') return null\r\n const current = store.getSnapshot()\r\n if (current.popupWindow && !current.popupWindow.closed) {\r\n current.popupWindow.focus()\r\n return current.popupWindow\r\n }\r\n\r\n const opts = optionsRef.current\r\n const popupWindow = window.open('about:blank', opts.name ?? '_blank', buildFeatures(opts))\r\n if (!popupWindow) {\r\n store.setState({ blocked: true })\r\n opts.onBlocked?.()\r\n return null\r\n }\r\n\r\n const doc = popupWindow.document\r\n doc.title = opts.title ?? document.title\r\n\r\n let stopStyleSync: (() => void) | undefined\r\n if (opts.copyStyles !== false) {\r\n stopStyleSync = copyStyles(document, doc)\r\n }\r\n\r\n const container = doc.createElement('div')\r\n container.setAttribute('data-popup-window-root', '')\r\n doc.body.appendChild(container)\r\n\r\n const handleExternalClose = () => closePopup(true)\r\n const onPopupPagehide = () => {\r\n // pagehide also fires on navigation; only treat it as a close when the\r\n // window really is gone a tick later.\r\n setTimeout(() => {\r\n if (popupWindow.closed) handleExternalClose()\r\n }, 0)\r\n }\r\n popupWindow.addEventListener('pagehide', onPopupPagehide)\r\n\r\n // Belt and braces: some browsers don't fire pagehide reliably for popups.\r\n const closePoll = window.setInterval(() => {\r\n if (popupWindow.closed) handleExternalClose()\r\n }, 250)\r\n\r\n const onOpenerPagehide = () => popupWindow.close()\r\n window.addEventListener('pagehide', onOpenerPagehide)\r\n\r\n cleanupRef.current = () => {\r\n window.clearInterval(closePoll)\r\n window.removeEventListener('pagehide', onOpenerPagehide)\r\n popupWindow.removeEventListener('pagehide', onPopupPagehide)\r\n stopStyleSync?.()\r\n }\r\n\r\n store.setState({ popupWindow, container, blocked: false })\r\n opts.onOpen?.(popupWindow)\r\n return popupWindow\r\n }, [store, closePopup])\r\n\r\n const close = useCallback(() => closePopup(false), [closePopup])\r\n\r\n const toggle = useCallback(() => {\r\n if (store.getSnapshot().popupWindow) {\r\n closePopup(false)\r\n } else {\r\n open()\r\n }\r\n }, [store, closePopup, open])\r\n\r\n const focus = useCallback(() => {\r\n const { popupWindow } = store.getSnapshot()\r\n if (popupWindow && !popupWindow.closed) popupWindow.focus()\r\n }, [store])\r\n\r\n // Close the popup when the owning component unmounts — its portal content\r\n // would unmount anyway, leaving an empty window behind.\r\n useEffect(() => {\r\n return () => closePopup(false)\r\n }, [closePopup])\r\n\r\n return {\r\n open,\r\n close,\r\n toggle,\r\n focus,\r\n isOpen: state.popupWindow !== null,\r\n isBlocked: state.blocked,\r\n popupWindow: state.popupWindow,\r\n Popup,\r\n }\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/copyStyles.ts","../src/usePopupWindow.ts"],"sourcesContent":["/**\n * Copy all stylesheets from `source` into `target` and (optionally) keep them\n * in sync while the popup is open:\n *\n * - `<link rel=\"stylesheet\">` and `<style>` elements are mirrored into the\n * target `<head>`. `<style>` contents are serialized from the CSSOM when\n * possible, so rules injected via `insertRule` (CSS-in-JS \"speedy\" mode)\n * are included.\n * - Additions/removals/edits of style nodes in the source `<head>` are\n * observed (covers Vite HMR, lazily loaded chunk CSS, styled-components).\n * - `class`/`style`/`data-*` attributes on `<html>` and `<body>` are mirrored\n * and kept in sync, so theme switching (e.g. a `dark` class) propagates.\n * - `document.adoptedStyleSheets` are re-constructed in the target document.\n *\n * Returns a function that stops observing.\n */\nexport function copyStyles(source: Document, target: Document, watch = true): () => void {\n const mirrors = new Map<Element, Element>()\n\n const isStyleNode = (node: Node): node is HTMLStyleElement | HTMLLinkElement => {\n if (node.nodeType !== Node.ELEMENT_NODE) return false\n const el = node as Element\n return (\n el.tagName === 'STYLE' ||\n (el.tagName === 'LINK' && (el.getAttribute('rel') ?? '').toLowerCase().includes('stylesheet'))\n )\n }\n\n const serializeStyle = (el: HTMLStyleElement): string => {\n const sheet = el.sheet\n if (sheet) {\n try {\n return Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n')\n } catch {\n // Inaccessible cssRules — fall back to the raw text below.\n }\n }\n return el.textContent ?? ''\n }\n\n const mirror = (el: HTMLStyleElement | HTMLLinkElement): void => {\n if (mirrors.has(el)) return\n let clone: Element\n if (el.tagName === 'LINK') {\n const link = target.createElement('link')\n link.rel = 'stylesheet'\n // .href resolves to an absolute URL, so relative hrefs keep working\n // from the popup's `about:blank` document.\n link.href = (el as HTMLLinkElement).href\n const media = el.getAttribute('media')\n if (media) link.media = media\n clone = link\n } else {\n const style = target.createElement('style')\n style.textContent = serializeStyle(el as HTMLStyleElement)\n const media = el.getAttribute('media')\n if (media) style.setAttribute('media', media)\n clone = style\n }\n target.head.appendChild(clone)\n mirrors.set(el, clone)\n }\n\n const refresh = (el: HTMLStyleElement): void => {\n const clone = mirrors.get(el)\n if (clone) clone.textContent = serializeStyle(el)\n }\n\n const unmirrorSubtree = (removed: Node): void => {\n for (const [src, clone] of mirrors) {\n if (removed === src || (removed.nodeType === Node.ELEMENT_NODE && removed.contains(src))) {\n clone.remove()\n mirrors.delete(src)\n }\n }\n }\n\n const ROOT_ATTRS = ['class', 'style']\n const syncRootAttrs = (): void => {\n const pairs: Array<[Element | null, Element | null]> = [\n [source.documentElement, target.documentElement],\n [source.body, target.body],\n ]\n for (const [from, to] of pairs) {\n if (!from || !to) continue\n for (const attr of from.attributes) {\n if (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) {\n to.setAttribute(attr.name, attr.value)\n }\n }\n for (const attr of Array.from(to.attributes)) {\n if (\n (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) &&\n !from.hasAttribute(attr.name)\n ) {\n to.removeAttribute(attr.name)\n }\n }\n }\n }\n\n const syncAdoptedSheets = (): void => {\n const targetWindow = target.defaultView as (Window & { CSSStyleSheet?: typeof CSSStyleSheet }) | null\n if (!targetWindow?.CSSStyleSheet || !('adoptedStyleSheets' in source)) return\n try {\n target.adoptedStyleSheets = source.adoptedStyleSheets.map((sheet) => {\n const copy = new targetWindow.CSSStyleSheet!()\n copy.replaceSync(\n Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n'),\n )\n return copy\n })\n } catch {\n // Best effort — older browsers or cross-origin rules.\n }\n }\n\n for (const el of source.querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n syncRootAttrs()\n syncAdoptedSheets()\n\n if (!watch || typeof MutationObserver === 'undefined') {\n return () => {}\n }\n\n const headObserver = new MutationObserver((records) => {\n for (const record of records) {\n if (record.type === 'characterData') {\n const parent = record.target.parentElement\n if (parent?.tagName === 'STYLE') refresh(parent as HTMLStyleElement)\n continue\n }\n // childList\n if (isStyleNode(record.target) && record.target.tagName === 'STYLE') {\n // textContent assignment replaces the text node → childList on <style>\n refresh(record.target as HTMLStyleElement)\n }\n for (const added of record.addedNodes) {\n if (isStyleNode(added)) {\n mirror(added)\n } else if (added.nodeType === Node.ELEMENT_NODE) {\n for (const el of (added as Element).querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n }\n }\n for (const removed of record.removedNodes) {\n unmirrorSubtree(removed)\n }\n }\n })\n headObserver.observe(source.head, { childList: true, subtree: true, characterData: true })\n\n const rootObserver = new MutationObserver(() => syncRootAttrs())\n rootObserver.observe(source.documentElement, { attributes: true })\n if (source.body) rootObserver.observe(source.body, { attributes: true })\n\n return () => {\n headObserver.disconnect()\n rootObserver.disconnect()\n }\n}\n","import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport type { FC } from 'react'\nimport { createPortal } from 'react-dom'\nimport { copyStyles } from './copyStyles'\nimport type {\n PopupProps,\n PopupWindowApi,\n PopupWindowFeatures,\n UsePopupWindowOptions,\n} from './types'\n\ninterface PopupState {\n popupWindow: Window | null\n container: HTMLElement | null\n blocked: boolean\n}\n\nconst INITIAL_STATE: PopupState = { popupWindow: null, container: null, blocked: false }\n\nclass PopupStore {\n state: PopupState = INITIAL_STATE\n private listeners = new Set<() => void>()\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n getSnapshot = (): PopupState => this.state\n\n getServerSnapshot = (): PopupState => INITIAL_STATE\n\n setState(partial: Partial<PopupState>): void {\n this.state = { ...this.state, ...partial }\n for (const listener of this.listeners) listener()\n }\n}\n\nconst DEFAULT_FEATURES: PopupWindowFeatures = { popup: true, width: 640, height: 480 }\n\nfunction buildFeatures(options: UsePopupWindowOptions): string {\n const merged: PopupWindowFeatures = { ...DEFAULT_FEATURES, ...options.features }\n if (\n options.center !== false &&\n merged.left === undefined &&\n merged.top === undefined &&\n typeof merged.width === 'number' &&\n typeof merged.height === 'number'\n ) {\n merged.left = Math.max(0, Math.round(window.screenX + (window.outerWidth - merged.width) / 2))\n merged.top = Math.max(0, Math.round(window.screenY + (window.outerHeight - merged.height) / 2))\n }\n return Object.entries(merged)\n .filter(([, value]) => value !== undefined && value !== false)\n .map(([key, value]) => (value === true ? `${key}=yes` : `${key}=${value}`))\n .join(',')\n}\n\nfunction createPopupComponent(store: PopupStore): FC<PopupProps> {\n function Popup({ children }: PopupProps) {\n const { container } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getServerSnapshot,\n )\n return container ? createPortal(children, container) : null\n }\n Popup.displayName = 'PopupWindow.Popup'\n return Popup\n}\n\n/**\n * Open part of your React tree in a separate browser window.\n *\n * The popup content is rendered with a portal into the popup's document, so\n * it stays part of your component tree: state, context and event handlers\n * all keep working across windows.\n *\n * ```tsx\n * const { open, close, isOpen, Popup } = usePopupWindow({ title: 'Panel' })\n *\n * return (\n * <>\n * <button onClick={open}>Open panel</button>\n * <Popup>\n * <MyPanel />\n * </Popup>\n * </>\n * )\n * ```\n */\nexport function usePopupWindow(options: UsePopupWindowOptions = {}): PopupWindowApi {\n const [store] = useState(() => new PopupStore())\n const [Popup] = useState(() => createPopupComponent(store))\n\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n const cleanupRef = useRef<(() => void) | null>(null)\n\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)\n\n // `userClosed` is true when the window was closed outside our control\n // (user hit the close button, opener unloaded) — then we must not touch it.\n const closePopup = useCallback(\n (userClosed: boolean) => {\n const { popupWindow } = store.getSnapshot()\n cleanupRef.current?.()\n cleanupRef.current = null\n if (!popupWindow) return\n if (!userClosed && !popupWindow.closed) popupWindow.close()\n store.setState({ popupWindow: null, container: null })\n optionsRef.current.onClose?.()\n },\n [store],\n )\n\n const open = useCallback((): Window | null => {\n if (typeof window === 'undefined') return null\n const current = store.getSnapshot()\n if (current.popupWindow && !current.popupWindow.closed) {\n current.popupWindow.focus()\n return current.popupWindow\n }\n\n const opts = optionsRef.current\n const popupWindow = window.open('about:blank', opts.name ?? '_blank', buildFeatures(opts))\n if (!popupWindow) {\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n\n // Sandboxed embedders (VS Code's built-in browser, CodeSandbox/StackBlitz\n // previews, iframes sandboxed without popup-escape) open the popup with an\n // opaque origin, so reading its document throws a SecurityError. Treat\n // that as blocked instead of stranding a blank window the portal can\n // never reach.\n let doc: Document\n try {\n doc = popupWindow.document\n } catch {\n popupWindow.close()\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n doc.title = opts.title ?? document.title\n\n let stopStyleSync: (() => void) | undefined\n if (opts.copyStyles !== false) {\n stopStyleSync = copyStyles(document, doc)\n }\n\n const container = doc.createElement('div')\n container.setAttribute('data-popup-window-root', '')\n doc.body.appendChild(container)\n\n const handleExternalClose = () => closePopup(true)\n const onPopupPagehide = () => {\n // pagehide also fires on navigation; only treat it as a close when the\n // window really is gone a tick later.\n setTimeout(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 0)\n }\n popupWindow.addEventListener('pagehide', onPopupPagehide)\n\n // Belt and braces: some browsers don't fire pagehide reliably for popups.\n const closePoll = window.setInterval(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 250)\n\n const onOpenerPagehide = () => popupWindow.close()\n window.addEventListener('pagehide', onOpenerPagehide)\n\n cleanupRef.current = () => {\n window.clearInterval(closePoll)\n window.removeEventListener('pagehide', onOpenerPagehide)\n popupWindow.removeEventListener('pagehide', onPopupPagehide)\n stopStyleSync?.()\n }\n\n store.setState({ popupWindow, container, blocked: false })\n opts.onOpen?.(popupWindow)\n return popupWindow\n }, [store, closePopup])\n\n const close = useCallback(() => closePopup(false), [closePopup])\n\n const toggle = useCallback(() => {\n if (store.getSnapshot().popupWindow) {\n closePopup(false)\n } else {\n open()\n }\n }, [store, closePopup, open])\n\n const focus = useCallback(() => {\n const { popupWindow } = store.getSnapshot()\n if (popupWindow && !popupWindow.closed) popupWindow.focus()\n }, [store])\n\n // Close the popup when the owning component unmounts — its portal content\n // would unmount anyway, leaving an empty window behind.\n useEffect(() => {\n return () => closePopup(false)\n }, [closePopup])\n\n return {\n open,\n close,\n toggle,\n focus,\n isOpen: state.popupWindow !== null,\n isBlocked: state.blocked,\n popupWindow: state.popupWindow,\n Popup,\n }\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -125,7 +125,12 @@ function f(a = {}) {
125
125
  if (e.popupWindow && !e.popupWindow.closed) return e.popupWindow.focus(), e.popupWindow;
126
126
  let t = f.current, n = window.open("about:blank", t.name ?? "_blank", u(t));
127
127
  if (!n) return s.setState({ blocked: !0 }), t.onBlocked?.(), null;
128
- let r = n.document;
128
+ let r;
129
+ try {
130
+ r = n.document;
131
+ } catch {
132
+ return n.close(), s.setState({ blocked: !0 }), t.onBlocked?.(), null;
133
+ }
129
134
  r.title = t.title ?? document.title;
130
135
  let i;
131
136
  t.copyStyles !== !1 && (i = o(document, r));
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/copyStyles.ts","../src/usePopupWindow.ts"],"sourcesContent":["/**\r\n * Copy all stylesheets from `source` into `target` and (optionally) keep them\r\n * in sync while the popup is open:\r\n *\r\n * - `<link rel=\"stylesheet\">` and `<style>` elements are mirrored into the\r\n * target `<head>`. `<style>` contents are serialized from the CSSOM when\r\n * possible, so rules injected via `insertRule` (CSS-in-JS \"speedy\" mode)\r\n * are included.\r\n * - Additions/removals/edits of style nodes in the source `<head>` are\r\n * observed (covers Vite HMR, lazily loaded chunk CSS, styled-components).\r\n * - `class`/`style`/`data-*` attributes on `<html>` and `<body>` are mirrored\r\n * and kept in sync, so theme switching (e.g. a `dark` class) propagates.\r\n * - `document.adoptedStyleSheets` are re-constructed in the target document.\r\n *\r\n * Returns a function that stops observing.\r\n */\r\nexport function copyStyles(source: Document, target: Document, watch = true): () => void {\r\n const mirrors = new Map<Element, Element>()\r\n\r\n const isStyleNode = (node: Node): node is HTMLStyleElement | HTMLLinkElement => {\r\n if (node.nodeType !== Node.ELEMENT_NODE) return false\r\n const el = node as Element\r\n return (\r\n el.tagName === 'STYLE' ||\r\n (el.tagName === 'LINK' && (el.getAttribute('rel') ?? '').toLowerCase().includes('stylesheet'))\r\n )\r\n }\r\n\r\n const serializeStyle = (el: HTMLStyleElement): string => {\r\n const sheet = el.sheet\r\n if (sheet) {\r\n try {\r\n return Array.from(sheet.cssRules)\r\n .map((rule) => rule.cssText)\r\n .join('\\n')\r\n } catch {\r\n // Inaccessible cssRules — fall back to the raw text below.\r\n }\r\n }\r\n return el.textContent ?? ''\r\n }\r\n\r\n const mirror = (el: HTMLStyleElement | HTMLLinkElement): void => {\r\n if (mirrors.has(el)) return\r\n let clone: Element\r\n if (el.tagName === 'LINK') {\r\n const link = target.createElement('link')\r\n link.rel = 'stylesheet'\r\n // .href resolves to an absolute URL, so relative hrefs keep working\r\n // from the popup's `about:blank` document.\r\n link.href = (el as HTMLLinkElement).href\r\n const media = el.getAttribute('media')\r\n if (media) link.media = media\r\n clone = link\r\n } else {\r\n const style = target.createElement('style')\r\n style.textContent = serializeStyle(el as HTMLStyleElement)\r\n const media = el.getAttribute('media')\r\n if (media) style.setAttribute('media', media)\r\n clone = style\r\n }\r\n target.head.appendChild(clone)\r\n mirrors.set(el, clone)\r\n }\r\n\r\n const refresh = (el: HTMLStyleElement): void => {\r\n const clone = mirrors.get(el)\r\n if (clone) clone.textContent = serializeStyle(el)\r\n }\r\n\r\n const unmirrorSubtree = (removed: Node): void => {\r\n for (const [src, clone] of mirrors) {\r\n if (removed === src || (removed.nodeType === Node.ELEMENT_NODE && removed.contains(src))) {\r\n clone.remove()\r\n mirrors.delete(src)\r\n }\r\n }\r\n }\r\n\r\n const ROOT_ATTRS = ['class', 'style']\r\n const syncRootAttrs = (): void => {\r\n const pairs: Array<[Element | null, Element | null]> = [\r\n [source.documentElement, target.documentElement],\r\n [source.body, target.body],\r\n ]\r\n for (const [from, to] of pairs) {\r\n if (!from || !to) continue\r\n for (const attr of from.attributes) {\r\n if (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) {\r\n to.setAttribute(attr.name, attr.value)\r\n }\r\n }\r\n for (const attr of Array.from(to.attributes)) {\r\n if (\r\n (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) &&\r\n !from.hasAttribute(attr.name)\r\n ) {\r\n to.removeAttribute(attr.name)\r\n }\r\n }\r\n }\r\n }\r\n\r\n const syncAdoptedSheets = (): void => {\r\n const targetWindow = target.defaultView as (Window & { CSSStyleSheet?: typeof CSSStyleSheet }) | null\r\n if (!targetWindow?.CSSStyleSheet || !('adoptedStyleSheets' in source)) return\r\n try {\r\n target.adoptedStyleSheets = source.adoptedStyleSheets.map((sheet) => {\r\n const copy = new targetWindow.CSSStyleSheet!()\r\n copy.replaceSync(\r\n Array.from(sheet.cssRules)\r\n .map((rule) => rule.cssText)\r\n .join('\\n'),\r\n )\r\n return copy\r\n })\r\n } catch {\r\n // Best effort — older browsers or cross-origin rules.\r\n }\r\n }\r\n\r\n for (const el of source.querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\r\n mirror(el as HTMLStyleElement | HTMLLinkElement)\r\n }\r\n syncRootAttrs()\r\n syncAdoptedSheets()\r\n\r\n if (!watch || typeof MutationObserver === 'undefined') {\r\n return () => {}\r\n }\r\n\r\n const headObserver = new MutationObserver((records) => {\r\n for (const record of records) {\r\n if (record.type === 'characterData') {\r\n const parent = record.target.parentElement\r\n if (parent?.tagName === 'STYLE') refresh(parent as HTMLStyleElement)\r\n continue\r\n }\r\n // childList\r\n if (isStyleNode(record.target) && record.target.tagName === 'STYLE') {\r\n // textContent assignment replaces the text node → childList on <style>\r\n refresh(record.target as HTMLStyleElement)\r\n }\r\n for (const added of record.addedNodes) {\r\n if (isStyleNode(added)) {\r\n mirror(added)\r\n } else if (added.nodeType === Node.ELEMENT_NODE) {\r\n for (const el of (added as Element).querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\r\n mirror(el as HTMLStyleElement | HTMLLinkElement)\r\n }\r\n }\r\n }\r\n for (const removed of record.removedNodes) {\r\n unmirrorSubtree(removed)\r\n }\r\n }\r\n })\r\n headObserver.observe(source.head, { childList: true, subtree: true, characterData: true })\r\n\r\n const rootObserver = new MutationObserver(() => syncRootAttrs())\r\n rootObserver.observe(source.documentElement, { attributes: true })\r\n if (source.body) rootObserver.observe(source.body, { attributes: true })\r\n\r\n return () => {\r\n headObserver.disconnect()\r\n rootObserver.disconnect()\r\n }\r\n}\r\n","import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'\r\nimport type { FC } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport { copyStyles } from './copyStyles'\r\nimport type {\r\n PopupProps,\r\n PopupWindowApi,\r\n PopupWindowFeatures,\r\n UsePopupWindowOptions,\r\n} from './types'\r\n\r\ninterface PopupState {\r\n popupWindow: Window | null\r\n container: HTMLElement | null\r\n blocked: boolean\r\n}\r\n\r\nconst INITIAL_STATE: PopupState = { popupWindow: null, container: null, blocked: false }\r\n\r\nclass PopupStore {\r\n state: PopupState = INITIAL_STATE\r\n private listeners = new Set<() => void>()\r\n\r\n subscribe = (listener: () => void): (() => void) => {\r\n this.listeners.add(listener)\r\n return () => this.listeners.delete(listener)\r\n }\r\n\r\n getSnapshot = (): PopupState => this.state\r\n\r\n getServerSnapshot = (): PopupState => INITIAL_STATE\r\n\r\n setState(partial: Partial<PopupState>): void {\r\n this.state = { ...this.state, ...partial }\r\n for (const listener of this.listeners) listener()\r\n }\r\n}\r\n\r\nconst DEFAULT_FEATURES: PopupWindowFeatures = { popup: true, width: 640, height: 480 }\r\n\r\nfunction buildFeatures(options: UsePopupWindowOptions): string {\r\n const merged: PopupWindowFeatures = { ...DEFAULT_FEATURES, ...options.features }\r\n if (\r\n options.center !== false &&\r\n merged.left === undefined &&\r\n merged.top === undefined &&\r\n typeof merged.width === 'number' &&\r\n typeof merged.height === 'number'\r\n ) {\r\n merged.left = Math.max(0, Math.round(window.screenX + (window.outerWidth - merged.width) / 2))\r\n merged.top = Math.max(0, Math.round(window.screenY + (window.outerHeight - merged.height) / 2))\r\n }\r\n return Object.entries(merged)\r\n .filter(([, value]) => value !== undefined && value !== false)\r\n .map(([key, value]) => (value === true ? `${key}=yes` : `${key}=${value}`))\r\n .join(',')\r\n}\r\n\r\nfunction createPopupComponent(store: PopupStore): FC<PopupProps> {\r\n function Popup({ children }: PopupProps) {\r\n const { container } = useSyncExternalStore(\r\n store.subscribe,\r\n store.getSnapshot,\r\n store.getServerSnapshot,\r\n )\r\n return container ? createPortal(children, container) : null\r\n }\r\n Popup.displayName = 'PopupWindow.Popup'\r\n return Popup\r\n}\r\n\r\n/**\r\n * Open part of your React tree in a separate browser window.\r\n *\r\n * The popup content is rendered with a portal into the popup's document, so\r\n * it stays part of your component tree: state, context and event handlers\r\n * all keep working across windows.\r\n *\r\n * ```tsx\r\n * const { open, close, isOpen, Popup } = usePopupWindow({ title: 'Panel' })\r\n *\r\n * return (\r\n * <>\r\n * <button onClick={open}>Open panel</button>\r\n * <Popup>\r\n * <MyPanel />\r\n * </Popup>\r\n * </>\r\n * )\r\n * ```\r\n */\r\nexport function usePopupWindow(options: UsePopupWindowOptions = {}): PopupWindowApi {\r\n const [store] = useState(() => new PopupStore())\r\n const [Popup] = useState(() => createPopupComponent(store))\r\n\r\n const optionsRef = useRef(options)\r\n optionsRef.current = options\r\n\r\n const cleanupRef = useRef<(() => void) | null>(null)\r\n\r\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)\r\n\r\n // `userClosed` is true when the window was closed outside our control\r\n // (user hit the close button, opener unloaded) — then we must not touch it.\r\n const closePopup = useCallback(\r\n (userClosed: boolean) => {\r\n const { popupWindow } = store.getSnapshot()\r\n cleanupRef.current?.()\r\n cleanupRef.current = null\r\n if (!popupWindow) return\r\n if (!userClosed && !popupWindow.closed) popupWindow.close()\r\n store.setState({ popupWindow: null, container: null })\r\n optionsRef.current.onClose?.()\r\n },\r\n [store],\r\n )\r\n\r\n const open = useCallback((): Window | null => {\r\n if (typeof window === 'undefined') return null\r\n const current = store.getSnapshot()\r\n if (current.popupWindow && !current.popupWindow.closed) {\r\n current.popupWindow.focus()\r\n return current.popupWindow\r\n }\r\n\r\n const opts = optionsRef.current\r\n const popupWindow = window.open('about:blank', opts.name ?? '_blank', buildFeatures(opts))\r\n if (!popupWindow) {\r\n store.setState({ blocked: true })\r\n opts.onBlocked?.()\r\n return null\r\n }\r\n\r\n const doc = popupWindow.document\r\n doc.title = opts.title ?? document.title\r\n\r\n let stopStyleSync: (() => void) | undefined\r\n if (opts.copyStyles !== false) {\r\n stopStyleSync = copyStyles(document, doc)\r\n }\r\n\r\n const container = doc.createElement('div')\r\n container.setAttribute('data-popup-window-root', '')\r\n doc.body.appendChild(container)\r\n\r\n const handleExternalClose = () => closePopup(true)\r\n const onPopupPagehide = () => {\r\n // pagehide also fires on navigation; only treat it as a close when the\r\n // window really is gone a tick later.\r\n setTimeout(() => {\r\n if (popupWindow.closed) handleExternalClose()\r\n }, 0)\r\n }\r\n popupWindow.addEventListener('pagehide', onPopupPagehide)\r\n\r\n // Belt and braces: some browsers don't fire pagehide reliably for popups.\r\n const closePoll = window.setInterval(() => {\r\n if (popupWindow.closed) handleExternalClose()\r\n }, 250)\r\n\r\n const onOpenerPagehide = () => popupWindow.close()\r\n window.addEventListener('pagehide', onOpenerPagehide)\r\n\r\n cleanupRef.current = () => {\r\n window.clearInterval(closePoll)\r\n window.removeEventListener('pagehide', onOpenerPagehide)\r\n popupWindow.removeEventListener('pagehide', onPopupPagehide)\r\n stopStyleSync?.()\r\n }\r\n\r\n store.setState({ popupWindow, container, blocked: false })\r\n opts.onOpen?.(popupWindow)\r\n return popupWindow\r\n }, [store, closePopup])\r\n\r\n const close = useCallback(() => closePopup(false), [closePopup])\r\n\r\n const toggle = useCallback(() => {\r\n if (store.getSnapshot().popupWindow) {\r\n closePopup(false)\r\n } else {\r\n open()\r\n }\r\n }, [store, closePopup, open])\r\n\r\n const focus = useCallback(() => {\r\n const { popupWindow } = store.getSnapshot()\r\n if (popupWindow && !popupWindow.closed) popupWindow.focus()\r\n }, [store])\r\n\r\n // Close the popup when the owning component unmounts — its portal content\r\n // would unmount anyway, leaving an empty window behind.\r\n useEffect(() => {\r\n return () => closePopup(false)\r\n }, [closePopup])\r\n\r\n return {\r\n open,\r\n close,\r\n toggle,\r\n focus,\r\n isOpen: state.popupWindow !== null,\r\n isBlocked: state.blocked,\r\n popupWindow: state.popupWindow,\r\n Popup,\r\n }\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/copyStyles.ts","../src/usePopupWindow.ts"],"sourcesContent":["/**\n * Copy all stylesheets from `source` into `target` and (optionally) keep them\n * in sync while the popup is open:\n *\n * - `<link rel=\"stylesheet\">` and `<style>` elements are mirrored into the\n * target `<head>`. `<style>` contents are serialized from the CSSOM when\n * possible, so rules injected via `insertRule` (CSS-in-JS \"speedy\" mode)\n * are included.\n * - Additions/removals/edits of style nodes in the source `<head>` are\n * observed (covers Vite HMR, lazily loaded chunk CSS, styled-components).\n * - `class`/`style`/`data-*` attributes on `<html>` and `<body>` are mirrored\n * and kept in sync, so theme switching (e.g. a `dark` class) propagates.\n * - `document.adoptedStyleSheets` are re-constructed in the target document.\n *\n * Returns a function that stops observing.\n */\nexport function copyStyles(source: Document, target: Document, watch = true): () => void {\n const mirrors = new Map<Element, Element>()\n\n const isStyleNode = (node: Node): node is HTMLStyleElement | HTMLLinkElement => {\n if (node.nodeType !== Node.ELEMENT_NODE) return false\n const el = node as Element\n return (\n el.tagName === 'STYLE' ||\n (el.tagName === 'LINK' && (el.getAttribute('rel') ?? '').toLowerCase().includes('stylesheet'))\n )\n }\n\n const serializeStyle = (el: HTMLStyleElement): string => {\n const sheet = el.sheet\n if (sheet) {\n try {\n return Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n')\n } catch {\n // Inaccessible cssRules — fall back to the raw text below.\n }\n }\n return el.textContent ?? ''\n }\n\n const mirror = (el: HTMLStyleElement | HTMLLinkElement): void => {\n if (mirrors.has(el)) return\n let clone: Element\n if (el.tagName === 'LINK') {\n const link = target.createElement('link')\n link.rel = 'stylesheet'\n // .href resolves to an absolute URL, so relative hrefs keep working\n // from the popup's `about:blank` document.\n link.href = (el as HTMLLinkElement).href\n const media = el.getAttribute('media')\n if (media) link.media = media\n clone = link\n } else {\n const style = target.createElement('style')\n style.textContent = serializeStyle(el as HTMLStyleElement)\n const media = el.getAttribute('media')\n if (media) style.setAttribute('media', media)\n clone = style\n }\n target.head.appendChild(clone)\n mirrors.set(el, clone)\n }\n\n const refresh = (el: HTMLStyleElement): void => {\n const clone = mirrors.get(el)\n if (clone) clone.textContent = serializeStyle(el)\n }\n\n const unmirrorSubtree = (removed: Node): void => {\n for (const [src, clone] of mirrors) {\n if (removed === src || (removed.nodeType === Node.ELEMENT_NODE && removed.contains(src))) {\n clone.remove()\n mirrors.delete(src)\n }\n }\n }\n\n const ROOT_ATTRS = ['class', 'style']\n const syncRootAttrs = (): void => {\n const pairs: Array<[Element | null, Element | null]> = [\n [source.documentElement, target.documentElement],\n [source.body, target.body],\n ]\n for (const [from, to] of pairs) {\n if (!from || !to) continue\n for (const attr of from.attributes) {\n if (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) {\n to.setAttribute(attr.name, attr.value)\n }\n }\n for (const attr of Array.from(to.attributes)) {\n if (\n (ROOT_ATTRS.includes(attr.name) || attr.name.startsWith('data-')) &&\n !from.hasAttribute(attr.name)\n ) {\n to.removeAttribute(attr.name)\n }\n }\n }\n }\n\n const syncAdoptedSheets = (): void => {\n const targetWindow = target.defaultView as (Window & { CSSStyleSheet?: typeof CSSStyleSheet }) | null\n if (!targetWindow?.CSSStyleSheet || !('adoptedStyleSheets' in source)) return\n try {\n target.adoptedStyleSheets = source.adoptedStyleSheets.map((sheet) => {\n const copy = new targetWindow.CSSStyleSheet!()\n copy.replaceSync(\n Array.from(sheet.cssRules)\n .map((rule) => rule.cssText)\n .join('\\n'),\n )\n return copy\n })\n } catch {\n // Best effort — older browsers or cross-origin rules.\n }\n }\n\n for (const el of source.querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n syncRootAttrs()\n syncAdoptedSheets()\n\n if (!watch || typeof MutationObserver === 'undefined') {\n return () => {}\n }\n\n const headObserver = new MutationObserver((records) => {\n for (const record of records) {\n if (record.type === 'characterData') {\n const parent = record.target.parentElement\n if (parent?.tagName === 'STYLE') refresh(parent as HTMLStyleElement)\n continue\n }\n // childList\n if (isStyleNode(record.target) && record.target.tagName === 'STYLE') {\n // textContent assignment replaces the text node → childList on <style>\n refresh(record.target as HTMLStyleElement)\n }\n for (const added of record.addedNodes) {\n if (isStyleNode(added)) {\n mirror(added)\n } else if (added.nodeType === Node.ELEMENT_NODE) {\n for (const el of (added as Element).querySelectorAll('style, link[rel~=\"stylesheet\" i]')) {\n mirror(el as HTMLStyleElement | HTMLLinkElement)\n }\n }\n }\n for (const removed of record.removedNodes) {\n unmirrorSubtree(removed)\n }\n }\n })\n headObserver.observe(source.head, { childList: true, subtree: true, characterData: true })\n\n const rootObserver = new MutationObserver(() => syncRootAttrs())\n rootObserver.observe(source.documentElement, { attributes: true })\n if (source.body) rootObserver.observe(source.body, { attributes: true })\n\n return () => {\n headObserver.disconnect()\n rootObserver.disconnect()\n }\n}\n","import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport type { FC } from 'react'\nimport { createPortal } from 'react-dom'\nimport { copyStyles } from './copyStyles'\nimport type {\n PopupProps,\n PopupWindowApi,\n PopupWindowFeatures,\n UsePopupWindowOptions,\n} from './types'\n\ninterface PopupState {\n popupWindow: Window | null\n container: HTMLElement | null\n blocked: boolean\n}\n\nconst INITIAL_STATE: PopupState = { popupWindow: null, container: null, blocked: false }\n\nclass PopupStore {\n state: PopupState = INITIAL_STATE\n private listeners = new Set<() => void>()\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n getSnapshot = (): PopupState => this.state\n\n getServerSnapshot = (): PopupState => INITIAL_STATE\n\n setState(partial: Partial<PopupState>): void {\n this.state = { ...this.state, ...partial }\n for (const listener of this.listeners) listener()\n }\n}\n\nconst DEFAULT_FEATURES: PopupWindowFeatures = { popup: true, width: 640, height: 480 }\n\nfunction buildFeatures(options: UsePopupWindowOptions): string {\n const merged: PopupWindowFeatures = { ...DEFAULT_FEATURES, ...options.features }\n if (\n options.center !== false &&\n merged.left === undefined &&\n merged.top === undefined &&\n typeof merged.width === 'number' &&\n typeof merged.height === 'number'\n ) {\n merged.left = Math.max(0, Math.round(window.screenX + (window.outerWidth - merged.width) / 2))\n merged.top = Math.max(0, Math.round(window.screenY + (window.outerHeight - merged.height) / 2))\n }\n return Object.entries(merged)\n .filter(([, value]) => value !== undefined && value !== false)\n .map(([key, value]) => (value === true ? `${key}=yes` : `${key}=${value}`))\n .join(',')\n}\n\nfunction createPopupComponent(store: PopupStore): FC<PopupProps> {\n function Popup({ children }: PopupProps) {\n const { container } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getServerSnapshot,\n )\n return container ? createPortal(children, container) : null\n }\n Popup.displayName = 'PopupWindow.Popup'\n return Popup\n}\n\n/**\n * Open part of your React tree in a separate browser window.\n *\n * The popup content is rendered with a portal into the popup's document, so\n * it stays part of your component tree: state, context and event handlers\n * all keep working across windows.\n *\n * ```tsx\n * const { open, close, isOpen, Popup } = usePopupWindow({ title: 'Panel' })\n *\n * return (\n * <>\n * <button onClick={open}>Open panel</button>\n * <Popup>\n * <MyPanel />\n * </Popup>\n * </>\n * )\n * ```\n */\nexport function usePopupWindow(options: UsePopupWindowOptions = {}): PopupWindowApi {\n const [store] = useState(() => new PopupStore())\n const [Popup] = useState(() => createPopupComponent(store))\n\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n const cleanupRef = useRef<(() => void) | null>(null)\n\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)\n\n // `userClosed` is true when the window was closed outside our control\n // (user hit the close button, opener unloaded) — then we must not touch it.\n const closePopup = useCallback(\n (userClosed: boolean) => {\n const { popupWindow } = store.getSnapshot()\n cleanupRef.current?.()\n cleanupRef.current = null\n if (!popupWindow) return\n if (!userClosed && !popupWindow.closed) popupWindow.close()\n store.setState({ popupWindow: null, container: null })\n optionsRef.current.onClose?.()\n },\n [store],\n )\n\n const open = useCallback((): Window | null => {\n if (typeof window === 'undefined') return null\n const current = store.getSnapshot()\n if (current.popupWindow && !current.popupWindow.closed) {\n current.popupWindow.focus()\n return current.popupWindow\n }\n\n const opts = optionsRef.current\n const popupWindow = window.open('about:blank', opts.name ?? '_blank', buildFeatures(opts))\n if (!popupWindow) {\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n\n // Sandboxed embedders (VS Code's built-in browser, CodeSandbox/StackBlitz\n // previews, iframes sandboxed without popup-escape) open the popup with an\n // opaque origin, so reading its document throws a SecurityError. Treat\n // that as blocked instead of stranding a blank window the portal can\n // never reach.\n let doc: Document\n try {\n doc = popupWindow.document\n } catch {\n popupWindow.close()\n store.setState({ blocked: true })\n opts.onBlocked?.()\n return null\n }\n doc.title = opts.title ?? document.title\n\n let stopStyleSync: (() => void) | undefined\n if (opts.copyStyles !== false) {\n stopStyleSync = copyStyles(document, doc)\n }\n\n const container = doc.createElement('div')\n container.setAttribute('data-popup-window-root', '')\n doc.body.appendChild(container)\n\n const handleExternalClose = () => closePopup(true)\n const onPopupPagehide = () => {\n // pagehide also fires on navigation; only treat it as a close when the\n // window really is gone a tick later.\n setTimeout(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 0)\n }\n popupWindow.addEventListener('pagehide', onPopupPagehide)\n\n // Belt and braces: some browsers don't fire pagehide reliably for popups.\n const closePoll = window.setInterval(() => {\n if (popupWindow.closed) handleExternalClose()\n }, 250)\n\n const onOpenerPagehide = () => popupWindow.close()\n window.addEventListener('pagehide', onOpenerPagehide)\n\n cleanupRef.current = () => {\n window.clearInterval(closePoll)\n window.removeEventListener('pagehide', onOpenerPagehide)\n popupWindow.removeEventListener('pagehide', onPopupPagehide)\n stopStyleSync?.()\n }\n\n store.setState({ popupWindow, container, blocked: false })\n opts.onOpen?.(popupWindow)\n return popupWindow\n }, [store, closePopup])\n\n const close = useCallback(() => closePopup(false), [closePopup])\n\n const toggle = useCallback(() => {\n if (store.getSnapshot().popupWindow) {\n closePopup(false)\n } else {\n open()\n }\n }, [store, closePopup, open])\n\n const focus = useCallback(() => {\n const { popupWindow } = store.getSnapshot()\n if (popupWindow && !popupWindow.closed) popupWindow.focus()\n }, [store])\n\n // Close the popup when the owning component unmounts — its portal content\n // would unmount anyway, leaving an empty window behind.\n useEffect(() => {\n return () => closePopup(false)\n }, [closePopup])\n\n return {\n open,\n close,\n toggle,\n focus,\n isOpen: state.popupWindow !== null,\n isBlocked: state.blocked,\n popupWindow: state.popupWindow,\n Popup,\n }\n}\n"],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -35,7 +35,11 @@ export interface UsePopupWindowOptions {
35
35
  onOpen?: (popupWindow: Window) => void;
36
36
  /** Called when the popup closes — via `close()`, the user, or the opener unloading. */
37
37
  onClose?: () => void;
38
- /** Called when `window.open` returns `null` (popup blocked by the browser). */
38
+ /**
39
+ * Called when the popup cannot be used: `window.open` returned `null`
40
+ * (blocked by the browser), or the popup's document is not scriptable
41
+ * because a sandboxed embedder gave it an opaque origin.
42
+ */
39
43
  onBlocked?: () => void;
40
44
  }
41
45
  export interface PopupProps {
@@ -56,7 +60,10 @@ export interface PopupWindowApi {
56
60
  focus: () => void;
57
61
  /** Whether the popup window is currently open. */
58
62
  isOpen: boolean;
59
- /** Whether the last `open()` attempt was blocked by the browser. */
63
+ /**
64
+ * Whether the last `open()` attempt was blocked — by the browser, or by a
65
+ * sandboxed embedder whose popups are not scriptable.
66
+ */
60
67
  isBlocked: boolean;
61
68
  /**
62
69
  * The popup `Window` while open, otherwise `null`. Escape hatch for
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAE1C;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;CACzD;AAED,MAAM,WAAW,qBAAqB;IACpC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sHAAsH;IACtH,QAAQ,CAAC,EAAE,mBAAmB,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,kEAAkE;IAClE,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,uFAAuF;IACvF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,IAAI,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IACzB,kDAAkD;IAClD,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,qCAAqC;IACrC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,yDAAyD;IACzD,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,kDAAkD;IAClD,MAAM,EAAE,OAAO,CAAA;IACf,oEAAoE;IACpE,SAAS,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B;;;OAGG;IACH,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,CAAA;CACtB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAE1C;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;CACzD;AAED,MAAM,WAAW,qBAAqB;IACpC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sHAAsH;IACtH,QAAQ,CAAC,EAAE,mBAAmB,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,kEAAkE;IAClE,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,uFAAuF;IACvF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,IAAI,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IACzB,kDAAkD;IAClD,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,qCAAqC;IACrC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,yDAAyD;IACzD,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,kDAAkD;IAClD,MAAM,EAAE,OAAO,CAAA;IACf;;;OAGG;IACH,SAAS,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B;;;OAGG;IACH,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,CAAA;CACtB"}
@@ -1 +1 @@
1
- {"version":3,"file":"usePopupWindow.d.ts","sourceRoot":"","sources":["../src/usePopupWindow.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,cAAc,EAEd,qBAAqB,EACtB,MAAM,SAAS,CAAA;AA8DhB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,cAAc,CAmHlF"}
1
+ {"version":3,"file":"usePopupWindow.d.ts","sourceRoot":"","sources":["../src/usePopupWindow.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,cAAc,EAEd,qBAAqB,EACtB,MAAM,SAAS,CAAA;AA8DhB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,cAAc,CAgIlF"}
package/package.json CHANGED
@@ -1,100 +1,101 @@
1
- {
2
- "name": "@jielga/react-popup-window",
3
- "version": "0.1.0",
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
- "type": "module",
6
- "license": "MIT",
7
- "author": "jielga",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/jielga/react-popup-window.git"
11
- },
12
- "homepage": "https://jielga.github.io/react-popup-window/",
13
- "bugs": {
14
- "url": "https://github.com/jielga/react-popup-window/issues"
15
- },
16
- "keywords": [
17
- "react",
18
- "hook",
19
- "popup",
20
- "window",
21
- "portal",
22
- "window.open",
23
- "multi-window",
24
- "detached",
25
- "panel",
26
- "tanstack-intent"
27
- ],
28
- "sideEffects": false,
29
- "files": [
30
- "dist",
31
- "skills",
32
- "!skills/_artifacts"
33
- ],
34
- "intent": {
35
- "docs": "README.md",
36
- "repo": "Jielga/react-popup-window"
37
- },
38
- "main": "./dist/index.cjs",
39
- "module": "./dist/index.js",
40
- "types": "./dist/index.d.ts",
41
- "exports": {
42
- ".": {
43
- "types": "./dist/index.d.ts",
44
- "import": "./dist/index.js",
45
- "require": "./dist/index.cjs"
46
- }
47
- },
48
- "scripts": {
49
- "dev": "vite --config docs/vite.config.ts",
50
- "build": "vite build && tsc -p tsconfig.build.json",
51
- "docs:build": "vite build --config docs/vite.config.ts",
52
- "docs:preview": "vite preview --config docs/vite.config.ts",
53
- "test": "vitest run",
54
- "test:watch": "vitest",
55
- "e2e": "playwright test",
56
- "typecheck": "tsc -p tsconfig.json --noEmit",
57
- "skills:validate": "intent validate",
58
- "changeset": "changeset",
59
- "version-packages": "changeset version && node scripts/sync-skill-version.mjs",
60
- "release": "changeset publish",
61
- "prepublishOnly": "npm run build"
62
- },
63
- "peerDependencies": {
64
- "react": ">=19.2.0",
65
- "react-dom": ">=19.2.0"
66
- },
67
- "publishConfig": {
68
- "access": "public",
69
- "provenance": true
70
- },
71
- "devDependencies": {
72
- "@changesets/changelog-github": "^1.0.0",
73
- "@changesets/cli": "^3.0.1",
74
- "@jielga/tmdatagrid": "^0.5.0",
75
- "@mantine/core": "^9.5.1",
76
- "@mantine/hooks": "^9.5.1",
77
- "@playwright/test": "^1.62.1",
78
- "@tabler/icons-react": "^3.46.0",
79
- "@tanstack/intent": "^0.3.6",
80
- "@tanstack/react-query": "^5.101.4",
81
- "@tanstack/react-store": "^0.11.1",
82
- "@tanstack/react-table": "9.0.0-beta.21",
83
- "@tanstack/react-virtual": "^3.14.9",
84
- "@tanstack/store": "^0.11.1",
85
- "@tanstack/table-core": "9.0.0-beta.21",
86
- "@testing-library/dom": "^10.4.1",
87
- "@testing-library/react": "^16.3.2",
88
- "@types/node": "^26.1.2",
89
- "@types/react": "^19.2.18",
90
- "@types/react-dom": "^19.2.4",
91
- "@vitejs/plugin-react": "^6.0.5",
92
- "jsdom": "^30.0.1",
93
- "react": "^19.2.8",
94
- "react-dom": "^19.2.8",
95
- "react-resizable-panels": "^4.12.2",
96
- "typescript": "^7.0.2",
97
- "vite": "^8.2.1",
98
- "vitest": "^4.1.10"
99
- }
100
- }
1
+ {
2
+ "name": "@jielga/react-popup-window",
3
+ "version": "0.1.1",
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
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "jielga",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Jielga/react-popup-window.git"
11
+ },
12
+ "homepage": "https://jielga.github.io/react-popup-window/",
13
+ "bugs": {
14
+ "url": "https://github.com/Jielga/react-popup-window/issues"
15
+ },
16
+ "keywords": [
17
+ "react",
18
+ "hook",
19
+ "popup",
20
+ "window",
21
+ "portal",
22
+ "window.open",
23
+ "multi-window",
24
+ "detached",
25
+ "panel",
26
+ "tanstack-intent"
27
+ ],
28
+ "sideEffects": false,
29
+ "files": [
30
+ "dist",
31
+ "skills",
32
+ "!skills/_artifacts"
33
+ ],
34
+ "intent": {
35
+ "docs": "README.md",
36
+ "repo": "Jielga/react-popup-window"
37
+ },
38
+ "main": "./dist/index.cjs",
39
+ "module": "./dist/index.js",
40
+ "types": "./dist/index.d.ts",
41
+ "exports": {
42
+ ".": {
43
+ "types": "./dist/index.d.ts",
44
+ "import": "./dist/index.js",
45
+ "require": "./dist/index.cjs"
46
+ }
47
+ },
48
+ "scripts": {
49
+ "dev": "vite --config docs/vite.config.ts",
50
+ "build": "vite build && tsc -p tsconfig.build.json",
51
+ "docs:build": "vite build --config docs/vite.config.ts",
52
+ "docs:preview": "vite preview --config docs/vite.config.ts",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "e2e": "playwright test",
56
+ "typecheck": "tsc -p tsconfig.json --noEmit",
57
+ "skills:validate": "node node_modules/@tanstack/intent/dist/cli.mjs validate",
58
+ "changeset": "changeset",
59
+ "version-packages": "changeset version && node scripts/sync-skill-version.mjs",
60
+ "release": "changeset publish",
61
+ "prepublishOnly": "npm run build"
62
+ },
63
+ "peerDependencies": {
64
+ "react": ">=19.2.0",
65
+ "react-dom": ">=19.2.0"
66
+ },
67
+ "publishConfig": {
68
+ "access": "public",
69
+ "provenance": true
70
+ },
71
+ "devDependencies": {
72
+ "@changesets/changelog-github": "^1.0.0",
73
+ "@changesets/cli": "^3.0.1",
74
+ "@jielga/tmdatagrid": "^2.0.0-beta.15",
75
+ "@mantine/core": "^9.5.1",
76
+ "@mantine/hooks": "^9.5.1",
77
+ "@playwright/test": "^1.62.1",
78
+ "@tabler/icons-react": "^3.46.0",
79
+ "@tanstack/intent": "^0.3.6",
80
+ "@tanstack/react-form": "^1.33.5",
81
+ "@tanstack/react-query": "^5.101.4",
82
+ "@tanstack/react-store": "^0.11.1",
83
+ "@tanstack/react-table": "^9.2.4",
84
+ "@tanstack/react-virtual": "^3.14.9",
85
+ "@tanstack/store": "^0.11.1",
86
+ "@tanstack/table-core": "^9.2.4",
87
+ "@testing-library/dom": "^10.4.1",
88
+ "@testing-library/react": "^16.3.2",
89
+ "@types/node": "^26.1.2",
90
+ "@types/react": "^19.2.18",
91
+ "@types/react-dom": "^19.2.4",
92
+ "@vitejs/plugin-react": "^6.0.5",
93
+ "jsdom": "^30.0.1",
94
+ "react": "^19.2.8",
95
+ "react-dom": "^19.2.8",
96
+ "react-resizable-panels": "^4.12.2",
97
+ "typescript": "^7.0.2",
98
+ "vite": "^8.2.1",
99
+ "vitest": "^4.1.10"
100
+ }
101
+ }