@pip-it-up/core 0.1.1 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @pip-it-up/core
2
2
 
3
- The vanilla JavaScript engine for the Document Picture-in-Picture API.
3
+ The framework-agnostic JavaScript engine for the **Document Picture-in-Picture API**.
4
+
5
+ `@pip-it-up/core` provides a robust, framework-agnostic way to manage the lifecycle of **Picture-in-Picture** windows, including style synchronization, element positioning, and keyboard event bridging.
4
6
 
5
7
  ## Installation
6
8
 
@@ -17,15 +19,14 @@ const contentEl = document.getElementById('my-content');
17
19
  const originEl = document.getElementById('my-placeholder');
18
20
 
19
21
  const pip = createPip({
20
- contentEl,
21
- originEl,
22
22
  mode: 'move', // 'move', 'clone', or 'portal'
23
23
  copyStyles: 'sync', // 'sync', 'once', or false
24
- fallback: 'new-tab' // 'new-tab', 'modal', or 'none'
24
+ fallback: 'new-tab' // 'new-tab' or 'none'
25
25
  });
26
26
 
27
- pip.open().then(() => {
28
- console.log('PiP opened!');
27
+ // Elements are passed to the open call
28
+ pip.open({ contentEl, originEl }).then(() => {
29
+ console.log('Picture-in-Picture window opened!');
29
30
  });
30
31
  ```
31
32
 
@@ -33,27 +34,28 @@ pip.open().then(() => {
33
34
 
34
35
  ### `createPip(options: PipOptions): PipInstance`
35
36
 
36
- Creates a new PiP instance.
37
+ Creates a new **Picture-in-Picture** instance.
37
38
 
38
39
  #### `PipOptions`
39
- - `contentEl`: The DOM element to move/clone into the PiP window.
40
- - `originEl`: The placeholder element in the main window to return the content to when closed (required for `mode: 'move'`).
41
40
  - `mode`: `'move'` (default), `'clone'`, or `'portal'`.
42
41
  - `copyStyles`: `'sync'` (default), `'once'`, or `false`.
43
- - `fallback`: `'new-tab'` (default), `'modal'`, or `'none'`.
44
- - `width` / `height`: Initial dimensions.
42
+ - `fallback`: `'new-tab'` (default) or `'none'`.
43
+ - `width` / `height`: Initial dimensions. If not provided, they are inferred from the element passed to `open()`.
45
44
  - `lockAspectRatio`: Keep the window's aspect ratio fixed.
46
45
  - `fixedSize`: Prevent manual resizing.
46
+ - `reserveSpace`: Preserve the layout in the main window when `mode: 'move'` (default: `true`).
47
+ - `centerInPip`: Centering the content inside the window via flexbox (default: `false`).
48
+ - `pipBodyStyles`: Custom styles for the PiP window's `<body>`.
47
49
  - `onPipWindowReady`: Callback fired when the window is fully prepared.
48
50
 
49
51
  #### `PipInstance`
50
- - `open()`: Requests and opens the PiP window.
51
- - `close()`: Closes the PiP window.
52
- - `toggle()`: Toggles the window state.
52
+ - `open({ contentEl?, originEl? })`: Requests and opens the **Picture-in-Picture** window.
53
+ - `close()`: Closes the window.
54
+ - `toggle({ contentEl?, originEl? })`: Toggles the window state.
53
55
  - `isOpen()`: Returns boolean.
54
56
  - `getPipWindow()`: Returns the Window object or null.
55
57
  - `getState()`: Returns the current state.
56
58
  - `destroy()`: Cleans up listeners and DOM.
57
59
 
58
- ### `getPip(id: string): PipInstance | undefined`
59
- Retrieves a created PiP instance by ID.
60
+ ### `getPip(id: string): PipInstance | null`
61
+ Retrieves a created **Picture-in-Picture** instance by ID from the global registry.
package/dist/index.d.mts CHANGED
@@ -1,16 +1,26 @@
1
+ /**
2
+ * Global type augmentation for the Document Picture-in-Picture API.
3
+ */
1
4
  declare global {
2
- interface Window {
3
- documentPictureInPicture?: {
4
- requestWindow(options?: any): Promise<Window>;
5
- window: Window | null;
6
- onenter: ((this: Window, ev: Event) => any) | null;
7
- };
8
- }
5
+ interface Window {
6
+ documentPictureInPicture?: {
7
+ requestWindow(options?: {
8
+ width?: number;
9
+ height?: number;
10
+ disallowReturnToOpener?: boolean;
11
+ preferInitialWindowPlacement?: boolean;
12
+ lockAspectRatio?: boolean;
13
+ }): Promise<Window>;
14
+ window: Window | null;
15
+ onenter: ((this: Window, ev: Event) => void) | null;
16
+ };
17
+ }
9
18
  }
10
- type FallbackMode = "new-tab" | "modal" | "none" | ((ctx: {
19
+
20
+ type FallbackMode = "new-tab" | "none" | ((ctx: {
11
21
  contentEl?: HTMLElement;
12
22
  originEl?: HTMLElement;
13
- options: PipOptions;
23
+ resolvedOptions: PipOptions;
14
24
  }) => void);
15
25
  type DomMode = "move" | "clone" | "portal";
16
26
  type CopyStylesMode = "once" | "sync";
@@ -26,6 +36,10 @@ interface PipOptions {
26
36
  mode?: DomMode;
27
37
  fallback?: FallbackMode;
28
38
  fallbackUrl?: string;
39
+ forceFallback?: boolean;
40
+ reserveSpace?: boolean;
41
+ centerInPip?: boolean;
42
+ pipBodyStyles?: Partial<CSSStyleDeclaration> | false;
29
43
  forwardKeyboardEvents?: boolean;
30
44
  restoreScroll?: boolean;
31
45
  restoreFocus?: boolean;
@@ -34,8 +48,6 @@ interface PipOptions {
34
48
  onPipWindowReady?: (pipWindow: Window) => void;
35
49
  onClose?: () => void;
36
50
  onError?: (err: Error) => void;
37
- contentEl?: HTMLElement;
38
- originEl?: HTMLElement;
39
51
  }
40
52
  interface PipState {
41
53
  isOpen: boolean;
@@ -57,7 +69,7 @@ interface PipInstance {
57
69
  getPipWindow: () => Window | null;
58
70
  subscribe: (fn: () => void) => () => void;
59
71
  getState: () => PipState;
60
- updateElements: (elements: {
72
+ setDefaultElements: (elements: {
61
73
  contentEl?: HTMLElement;
62
74
  originEl?: HTMLElement;
63
75
  }) => void;
@@ -72,6 +84,5 @@ declare const registerPip: (id: string, instance: PipInstance) => void;
72
84
  declare const unregisterPip: (id: string) => void;
73
85
  declare const getPip: (id: string) => PipInstance | null;
74
86
  declare const subscribeRegistry: (id: string, fn: () => void) => (() => void);
75
- declare const clearRegistry: () => void;
76
87
 
77
- export { type CopyStylesMode, type DomMode, type FallbackMode, type PipInstance, type PipOptions, type PipState, clearRegistry, createPip, getPip, isSupported, registerPip, subscribeRegistry, unregisterPip };
88
+ export { type CopyStylesMode, type DomMode, type FallbackMode, type PipInstance, type PipOptions, type PipState, createPip, getPip, isSupported, registerPip, subscribeRegistry, unregisterPip };
package/dist/index.d.ts CHANGED
@@ -1,16 +1,26 @@
1
+ /**
2
+ * Global type augmentation for the Document Picture-in-Picture API.
3
+ */
1
4
  declare global {
2
- interface Window {
3
- documentPictureInPicture?: {
4
- requestWindow(options?: any): Promise<Window>;
5
- window: Window | null;
6
- onenter: ((this: Window, ev: Event) => any) | null;
7
- };
8
- }
5
+ interface Window {
6
+ documentPictureInPicture?: {
7
+ requestWindow(options?: {
8
+ width?: number;
9
+ height?: number;
10
+ disallowReturnToOpener?: boolean;
11
+ preferInitialWindowPlacement?: boolean;
12
+ lockAspectRatio?: boolean;
13
+ }): Promise<Window>;
14
+ window: Window | null;
15
+ onenter: ((this: Window, ev: Event) => void) | null;
16
+ };
17
+ }
9
18
  }
10
- type FallbackMode = "new-tab" | "modal" | "none" | ((ctx: {
19
+
20
+ type FallbackMode = "new-tab" | "none" | ((ctx: {
11
21
  contentEl?: HTMLElement;
12
22
  originEl?: HTMLElement;
13
- options: PipOptions;
23
+ resolvedOptions: PipOptions;
14
24
  }) => void);
15
25
  type DomMode = "move" | "clone" | "portal";
16
26
  type CopyStylesMode = "once" | "sync";
@@ -26,6 +36,10 @@ interface PipOptions {
26
36
  mode?: DomMode;
27
37
  fallback?: FallbackMode;
28
38
  fallbackUrl?: string;
39
+ forceFallback?: boolean;
40
+ reserveSpace?: boolean;
41
+ centerInPip?: boolean;
42
+ pipBodyStyles?: Partial<CSSStyleDeclaration> | false;
29
43
  forwardKeyboardEvents?: boolean;
30
44
  restoreScroll?: boolean;
31
45
  restoreFocus?: boolean;
@@ -34,8 +48,6 @@ interface PipOptions {
34
48
  onPipWindowReady?: (pipWindow: Window) => void;
35
49
  onClose?: () => void;
36
50
  onError?: (err: Error) => void;
37
- contentEl?: HTMLElement;
38
- originEl?: HTMLElement;
39
51
  }
40
52
  interface PipState {
41
53
  isOpen: boolean;
@@ -57,7 +69,7 @@ interface PipInstance {
57
69
  getPipWindow: () => Window | null;
58
70
  subscribe: (fn: () => void) => () => void;
59
71
  getState: () => PipState;
60
- updateElements: (elements: {
72
+ setDefaultElements: (elements: {
61
73
  contentEl?: HTMLElement;
62
74
  originEl?: HTMLElement;
63
75
  }) => void;
@@ -72,6 +84,5 @@ declare const registerPip: (id: string, instance: PipInstance) => void;
72
84
  declare const unregisterPip: (id: string) => void;
73
85
  declare const getPip: (id: string) => PipInstance | null;
74
86
  declare const subscribeRegistry: (id: string, fn: () => void) => (() => void);
75
- declare const clearRegistry: () => void;
76
87
 
77
- export { type CopyStylesMode, type DomMode, type FallbackMode, type PipInstance, type PipOptions, type PipState, clearRegistry, createPip, getPip, isSupported, registerPip, subscribeRegistry, unregisterPip };
88
+ export { type CopyStylesMode, type DomMode, type FallbackMode, type PipInstance, type PipOptions, type PipState, createPip, getPip, isSupported, registerPip, subscribeRegistry, unregisterPip };
package/dist/index.js CHANGED
@@ -20,7 +20,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- clearRegistry: () => clearRegistry,
24
23
  createPip: () => createPip,
25
24
  getPip: () => getPip,
26
25
  isSupported: () => isSupported,
@@ -32,51 +31,7 @@ module.exports = __toCommonJS(index_exports);
32
31
 
33
32
  // src/support.ts
34
33
  var isSupported = () => {
35
- if (typeof window === "undefined") {
36
- return false;
37
- }
38
- return "documentPictureInPicture" in window && window.documentPictureInPicture !== void 0;
39
- };
40
-
41
- // src/registry.ts
42
- var registry = /* @__PURE__ */ new Map();
43
- var listeners = /* @__PURE__ */ new Map();
44
- var registerPip = (id, instance) => {
45
- registry.set(id, instance);
46
- notifyListeners(id);
47
- };
48
- var unregisterPip = (id) => {
49
- registry.delete(id);
50
- notifyListeners(id);
51
- };
52
- var getPip = (id) => {
53
- return registry.get(id) || null;
54
- };
55
- var subscribeRegistry = (id, fn) => {
56
- let set = listeners.get(id);
57
- if (!set) {
58
- set = /* @__PURE__ */ new Set();
59
- listeners.set(id, set);
60
- }
61
- set.add(fn);
62
- return () => {
63
- set?.delete(fn);
64
- if (set?.size === 0) {
65
- listeners.delete(id);
66
- }
67
- };
68
- };
69
- var notifyListeners = (id) => {
70
- const set = listeners.get(id);
71
- if (set) {
72
- for (const fn of set) {
73
- fn();
74
- }
75
- }
76
- };
77
- var clearRegistry = () => {
78
- registry.clear();
79
- listeners.clear();
34
+ return typeof window !== "undefined" && "documentPictureInPicture" in window && typeof window.documentPictureInPicture?.requestWindow === "function";
80
35
  };
81
36
 
82
37
  // src/styles.ts
@@ -111,11 +66,28 @@ var startStylesSync = (pipWindow) => {
111
66
  }
112
67
  syncAttrs(openerDoc.documentElement, pipDoc.documentElement);
113
68
  syncAttrs(openerDoc.body, pipDoc.body);
69
+ const pendingTextUpdates = /* @__PURE__ */ new Map();
70
+ let pendingRafId = null;
71
+ const flushTextUpdates = () => {
72
+ pendingRafId = null;
73
+ for (const [source, clone] of pendingTextUpdates) {
74
+ clone.textContent = source.textContent;
75
+ }
76
+ pendingTextUpdates.clear();
77
+ };
78
+ const scheduleTextUpdate = (sourceStyle, clone) => {
79
+ pendingTextUpdates.set(sourceStyle, clone);
80
+ if (pendingRafId === null) {
81
+ pendingRafId = requestAnimationFrame(flushTextUpdates);
82
+ }
83
+ };
114
84
  const headObserver = new MutationObserver((mutations) => {
115
85
  for (const mutation of mutations) {
116
86
  if (mutation.type === "childList") {
117
87
  for (const node of Array.from(mutation.addedNodes)) {
118
88
  if (node.nodeName === "STYLE" || node.nodeName === "LINK" && node.rel === "stylesheet") {
89
+ const existingClone = nodeMap.get(node);
90
+ if (existingClone) continue;
119
91
  const clone = node.cloneNode(true);
120
92
  nodeMap.set(node, clone);
121
93
  pipDoc.head.appendChild(clone);
@@ -136,7 +108,7 @@ var startStylesSync = (pipWindow) => {
136
108
  if (current) {
137
109
  const clone = nodeMap.get(current);
138
110
  if (clone) {
139
- clone.textContent = current.textContent;
111
+ scheduleTextUpdate(current, clone);
140
112
  }
141
113
  }
142
114
  }
@@ -172,14 +144,36 @@ var startStylesSync = (pipWindow) => {
172
144
  return () => {
173
145
  headObserver.disconnect();
174
146
  attrObserver.disconnect();
147
+ if (pendingRafId !== null) {
148
+ cancelAnimationFrame(pendingRafId);
149
+ pendingRafId = null;
150
+ }
151
+ pendingTextUpdates.clear();
175
152
  };
176
153
  };
177
154
 
178
155
  // src/dom-modes.ts
179
- var applyMoveMode = (pipWindow, contentEl, originEl) => {
156
+ var applyMoveMode = (pipWindow, contentEl, originEl, reserveSpace = true) => {
157
+ if (reserveSpace) {
158
+ const rect = contentEl.getBoundingClientRect();
159
+ originEl.style.minWidth = `${rect.width}px`;
160
+ originEl.style.minHeight = `${rect.height}px`;
161
+ originEl.style.width = `${rect.width}px`;
162
+ originEl.style.height = `${rect.height}px`;
163
+ contentEl.style.width = `${rect.width}px`;
164
+ contentEl.style.height = `${rect.height}px`;
165
+ }
180
166
  pipWindow.document.body.appendChild(contentEl);
181
167
  return () => {
182
168
  if (originEl && contentEl) {
169
+ if (reserveSpace) {
170
+ originEl.style.minWidth = "";
171
+ originEl.style.minHeight = "";
172
+ originEl.style.width = "";
173
+ originEl.style.height = "";
174
+ contentEl.style.width = "";
175
+ contentEl.style.height = "";
176
+ }
183
177
  originEl.appendChild(contentEl);
184
178
  }
185
179
  };
@@ -188,10 +182,7 @@ var applyCloneMode = (pipWindow, contentEl) => {
188
182
  const clone = contentEl.cloneNode(true);
189
183
  pipWindow.document.body.appendChild(clone);
190
184
  return () => {
191
- };
192
- };
193
- var applyPortalAnchorMode = (pipWindow) => {
194
- return () => {
185
+ clone.parentNode?.removeChild(clone);
195
186
  };
196
187
  };
197
188
 
@@ -218,7 +209,7 @@ var startKeyboardBridge = (pipWindow, openerWindow = window) => {
218
209
  charCode: { get: () => e.charCode },
219
210
  which: { get: () => e.which }
220
211
  });
221
- const canceled = !openerWindow.document.dispatchEvent(cloneEvent);
212
+ const canceled = !openerWindow.dispatchEvent(cloneEvent);
222
213
  if (canceled) {
223
214
  e.preventDefault();
224
215
  }
@@ -232,48 +223,57 @@ var startKeyboardBridge = (pipWindow, openerWindow = window) => {
232
223
  };
233
224
 
234
225
  // src/focus-scroll.ts
235
- var snapshotScrollFocus = (rootEl) => {
226
+ var snapshotScrollFocus = (rootEl, opts = {}) => {
227
+ const { restoreScroll = true, restoreFocus = true } = opts;
236
228
  const openerDoc = window.document;
237
- const activeElement = openerDoc.activeElement;
229
+ let activeElement = null;
238
230
  let selectionStart = null;
239
231
  let selectionEnd = null;
240
232
  let selectionDir = null;
241
- if (activeElement && (activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement)) {
242
- try {
243
- selectionStart = activeElement.selectionStart;
244
- selectionEnd = activeElement.selectionEnd;
245
- selectionDir = activeElement.selectionDirection;
246
- } catch {
233
+ if (restoreFocus) {
234
+ activeElement = openerDoc.activeElement;
235
+ if (activeElement && (activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement)) {
236
+ try {
237
+ selectionStart = activeElement.selectionStart;
238
+ selectionEnd = activeElement.selectionEnd;
239
+ selectionDir = activeElement.selectionDirection;
240
+ } catch {
241
+ }
247
242
  }
248
243
  }
249
244
  const scrollMap = /* @__PURE__ */ new WeakMap();
250
- const walk = (node) => {
251
- if (node instanceof HTMLElement) {
252
- if (node.scrollTop > 0 || node.scrollLeft > 0) {
245
+ if (restoreScroll) {
246
+ const allElements = rootEl.querySelectorAll("*");
247
+ if (rootEl.scrollTop > 0 || rootEl.scrollLeft > 0) {
248
+ scrollMap.set(rootEl, { scrollTop: rootEl.scrollTop, scrollLeft: rootEl.scrollLeft });
249
+ }
250
+ for (let i = 0; i < allElements.length; i++) {
251
+ const node = allElements[i];
252
+ if (node instanceof HTMLElement && (node.scrollTop > 0 || node.scrollLeft > 0)) {
253
253
  scrollMap.set(node, { scrollTop: node.scrollTop, scrollLeft: node.scrollLeft });
254
254
  }
255
255
  }
256
- for (const child of Array.from(node.children)) {
257
- walk(child);
258
- }
259
- };
260
- walk(rootEl);
256
+ }
261
257
  return {
262
258
  restore: () => {
263
- const restoreWalk = (node) => {
264
- if (node instanceof HTMLElement) {
259
+ if (restoreScroll) {
260
+ const restoreState = (node) => {
265
261
  const state = scrollMap.get(node);
266
262
  if (state) {
267
263
  node.scrollTop = state.scrollTop;
268
264
  node.scrollLeft = state.scrollLeft;
269
265
  }
266
+ };
267
+ restoreState(rootEl);
268
+ const allElements = rootEl.querySelectorAll("*");
269
+ for (let i = 0; i < allElements.length; i++) {
270
+ const node = allElements[i];
271
+ if (node instanceof HTMLElement) {
272
+ restoreState(node);
273
+ }
270
274
  }
271
- for (const child of Array.from(node.children)) {
272
- restoreWalk(child);
273
- }
274
- };
275
- restoreWalk(rootEl);
276
- if (activeElement && openerDoc.body.contains(activeElement)) {
275
+ }
276
+ if (restoreFocus && activeElement && openerDoc.body.contains(activeElement)) {
277
277
  activeElement.focus({ preventScroll: true });
278
278
  if (selectionStart !== null && selectionEnd !== null) {
279
279
  try {
@@ -292,21 +292,20 @@ var snapshotScrollFocus = (rootEl) => {
292
292
 
293
293
  // src/fixed-size.ts
294
294
  var attachFixedSizeGuard = (pipWindow, width, height) => {
295
- let isResizing = false;
296
295
  const handleResize = () => {
297
- if (isResizing) return;
298
- if (pipWindow.innerWidth !== width || pipWindow.innerHeight !== height) {
299
- isResizing = true;
300
- try {
301
- pipWindow.resizeTo(width, height);
302
- } catch {
303
- }
304
- setTimeout(() => {
305
- isResizing = false;
306
- }, 50);
296
+ try {
297
+ pipWindow.resizeTo(width, height);
298
+ } catch {
307
299
  }
308
300
  };
309
301
  pipWindow.addEventListener("resize", handleResize);
302
+ pipWindow.document.documentElement.style.width = `${width}px`;
303
+ pipWindow.document.documentElement.style.height = `${height}px`;
304
+ pipWindow.document.documentElement.style.overflow = "hidden";
305
+ pipWindow.document.body.style.width = `${width}px`;
306
+ pipWindow.document.body.style.height = `${height}px`;
307
+ pipWindow.document.body.style.overflow = "hidden";
308
+ pipWindow.document.body.style.margin = "0";
310
309
  return () => {
311
310
  pipWindow.removeEventListener("resize", handleResize);
312
311
  };
@@ -315,8 +314,8 @@ var attachFixedSizeGuard = (pipWindow, width, height) => {
315
314
  // src/fallback.ts
316
315
  var executeFallback = (fallback, options, contentEl, originEl) => {
317
316
  if (typeof fallback === "function") {
318
- fallback({ contentEl, originEl, options });
319
- return;
317
+ const result = fallback({ contentEl, originEl, resolvedOptions: options });
318
+ return typeof result === "function" ? result : void 0;
320
319
  }
321
320
  switch (fallback) {
322
321
  case "new-tab":
@@ -325,12 +324,10 @@ var executeFallback = (fallback, options, contentEl, originEl) => {
325
324
  } else {
326
325
  console.warn('pip-it-up: fallback="new-tab" requires fallbackUrl option');
327
326
  }
328
- break;
329
- case "modal":
330
- break;
327
+ return;
331
328
  case "none":
332
329
  console.warn("pip-it-up: Document Picture-in-Picture is not supported in this browser.");
333
- break;
330
+ return;
334
331
  }
335
332
  };
336
333
 
@@ -345,6 +342,7 @@ var createPip = (options = {}) => {
345
342
  };
346
343
  const listeners2 = /* @__PURE__ */ new Set();
347
344
  const disposers = [];
345
+ let defaultElements = {};
348
346
  const updateState = (newState) => {
349
347
  state = { ...state, ...newState };
350
348
  listeners2.forEach((fn) => fn());
@@ -366,15 +364,19 @@ var createPip = (options = {}) => {
366
364
  options.onClose();
367
365
  }
368
366
  };
367
+ let isOpening = false;
369
368
  const open = async (elements) => {
370
- if (state.isOpen) return;
371
- const contentEl = elements?.contentEl || options.contentEl;
372
- const originEl = elements?.originEl || options.originEl;
369
+ if (state.isOpen || isOpening) return;
370
+ isOpening = true;
371
+ const contentEl = elements?.contentEl ?? defaultElements.contentEl;
372
+ const originEl = elements?.originEl ?? defaultElements.originEl;
373
373
  const mode = options.mode || "move";
374
- if (!state.isSupported) {
374
+ if (!state.isSupported || options.forceFallback) {
375
+ isOpening = false;
375
376
  const fallback = options.fallback || "none";
376
- executeFallback(fallback, options, contentEl, originEl);
377
- if (fallback === "modal") {
377
+ const fallbackCleanup = executeFallback(fallback, options, contentEl, originEl);
378
+ if (fallbackCleanup) disposers.push(fallbackCleanup);
379
+ if (fallback !== "none") {
378
380
  updateState({ isOpen: true, pipWindow: null });
379
381
  if (options.onOpen) options.onOpen(window);
380
382
  }
@@ -383,26 +385,52 @@ var createPip = (options = {}) => {
383
385
  try {
384
386
  if (options.onBeforeOpen) {
385
387
  const shouldOpen = await options.onBeforeOpen();
386
- if (shouldOpen === false) return;
388
+ if (shouldOpen === false) {
389
+ isOpening = false;
390
+ return;
391
+ }
387
392
  }
388
393
  let restoreFocusScroll = null;
389
- if (options.restoreScroll !== false && options.restoreFocus !== false && contentEl) {
390
- const snap = snapshotScrollFocus(contentEl);
394
+ if ((options.restoreScroll !== false || options.restoreFocus !== false) && contentEl) {
395
+ const snap = snapshotScrollFocus(contentEl, {
396
+ restoreScroll: options.restoreScroll !== false,
397
+ restoreFocus: options.restoreFocus !== false
398
+ });
391
399
  restoreFocusScroll = snap.restore;
392
400
  }
393
- const width = options.width || 900;
394
- const height = options.height || 600;
401
+ let reqWidth = options.width;
402
+ let reqHeight = options.height;
403
+ if ((!reqWidth || !reqHeight) && contentEl) {
404
+ const rect = contentEl.getBoundingClientRect();
405
+ if (rect.width > 0 && rect.height > 0) {
406
+ reqWidth = reqWidth || Math.max(300, Math.min(1600, Math.round(rect.width)));
407
+ reqHeight = reqHeight || Math.max(200, Math.min(1200, Math.round(rect.height)));
408
+ }
409
+ }
410
+ const width = reqWidth || 900;
411
+ const height = reqHeight || 600;
395
412
  const lockAspectRatio = options.lockAspectRatio || options.fixedSize || false;
396
- const pipReqOpts = {
413
+ const pipWindow = await window.documentPictureInPicture.requestWindow({
397
414
  width,
398
415
  height,
399
416
  disallowReturnToOpener: options.disallowReturnToOpener,
400
417
  preferInitialWindowPlacement: options.preferInitialWindowPlacement,
401
418
  ...lockAspectRatio ? { lockAspectRatio: true } : {}
402
- };
403
- const pipWindow = await window.documentPictureInPicture.requestWindow(pipReqOpts);
404
- pipWindow.addEventListener("pagehide", () => {
405
- close();
419
+ });
420
+ const onPipClose = () => close();
421
+ pipWindow.addEventListener("pagehide", onPipClose);
422
+ pipWindow.addEventListener("unload", onPipClose);
423
+ disposers.push(() => {
424
+ pipWindow.removeEventListener("pagehide", onPipClose);
425
+ pipWindow.removeEventListener("unload", onPipClose);
426
+ });
427
+ const closePollInterval = setInterval(() => {
428
+ if (pipWindow.closed) {
429
+ close();
430
+ }
431
+ }, 250);
432
+ disposers.push(() => {
433
+ clearInterval(closePollInterval);
406
434
  });
407
435
  const copyMode = options.copyStyles || "sync";
408
436
  if (copyMode === "sync") {
@@ -411,11 +439,28 @@ var createPip = (options = {}) => {
411
439
  copyStylesOnce(pipWindow);
412
440
  }
413
441
  if (contentEl && originEl && mode === "move") {
414
- disposers.push(applyMoveMode(pipWindow, contentEl, originEl));
442
+ const reserveSpace = options.reserveSpace !== false;
443
+ disposers.push(applyMoveMode(pipWindow, contentEl, originEl, reserveSpace));
415
444
  } else if (contentEl && mode === "clone") {
416
445
  disposers.push(applyCloneMode(pipWindow, contentEl));
417
- } else if (mode === "portal") {
418
- disposers.push(applyPortalAnchorMode(pipWindow));
446
+ }
447
+ if (options.pipBodyStyles !== false) {
448
+ const defaultStyles = {
449
+ margin: "0",
450
+ padding: "0",
451
+ boxSizing: "border-box",
452
+ width: options.fixedSize ? `${width}px` : "100%",
453
+ height: options.fixedSize ? `${height}px` : "auto",
454
+ overflow: options.fixedSize ? "hidden" : "auto",
455
+ ...options.centerInPip ? {
456
+ display: "flex",
457
+ alignItems: "center",
458
+ justifyContent: "center",
459
+ minHeight: "100vh"
460
+ } : {}
461
+ };
462
+ const stylesToApply = options.pipBodyStyles || defaultStyles;
463
+ Object.assign(pipWindow.document.body.style, stylesToApply);
419
464
  }
420
465
  if (options.forwardKeyboardEvents !== false) {
421
466
  disposers.push(startKeyboardBridge(pipWindow, window));
@@ -427,15 +472,18 @@ var createPip = (options = {}) => {
427
472
  disposers.push(restoreFocusScroll);
428
473
  }
429
474
  updateState({ isOpen: true, pipWindow });
475
+ isOpening = false;
430
476
  if (options.onOpen) {
431
477
  options.onOpen(pipWindow);
432
478
  }
433
- requestAnimationFrame(() => {
479
+ const rafId = requestAnimationFrame(() => {
434
480
  if (options.onPipWindowReady) {
435
481
  options.onPipWindowReady(pipWindow);
436
482
  }
437
483
  });
484
+ disposers.push(() => cancelAnimationFrame(rafId));
438
485
  } catch (err) {
486
+ isOpening = false;
439
487
  cleanup();
440
488
  updateState({ isOpen: false, pipWindow: null });
441
489
  if (options.onError) {
@@ -464,24 +512,55 @@ var createPip = (options = {}) => {
464
512
  return () => listeners2.delete(fn);
465
513
  },
466
514
  getState: () => state,
467
- updateElements: (elements) => {
468
- if (elements.contentEl !== void 0) options.contentEl = elements.contentEl;
469
- if (elements.originEl !== void 0) options.originEl = elements.originEl;
515
+ setDefaultElements: (elements) => {
516
+ defaultElements = elements;
470
517
  },
471
518
  destroy: () => {
472
519
  close();
473
- unregisterPip(id);
474
520
  listeners2.clear();
475
521
  }
476
522
  };
477
- if (options.id) {
478
- registerPip(id, instance);
479
- }
480
523
  return instance;
481
524
  };
525
+
526
+ // src/registry.ts
527
+ var registry = /* @__PURE__ */ new Map();
528
+ var listeners = /* @__PURE__ */ new Map();
529
+ var registerPip = (id, instance) => {
530
+ registry.set(id, instance);
531
+ notifyListeners(id);
532
+ };
533
+ var unregisterPip = (id) => {
534
+ registry.delete(id);
535
+ notifyListeners(id);
536
+ };
537
+ var getPip = (id) => {
538
+ return registry.get(id) || null;
539
+ };
540
+ var subscribeRegistry = (id, fn) => {
541
+ let set = listeners.get(id);
542
+ if (!set) {
543
+ set = /* @__PURE__ */ new Set();
544
+ listeners.set(id, set);
545
+ }
546
+ set.add(fn);
547
+ return () => {
548
+ set?.delete(fn);
549
+ if (set?.size === 0) {
550
+ listeners.delete(id);
551
+ }
552
+ };
553
+ };
554
+ var notifyListeners = (id) => {
555
+ const set = listeners.get(id);
556
+ if (set) {
557
+ for (const fn of set) {
558
+ fn();
559
+ }
560
+ }
561
+ };
482
562
  // Annotate the CommonJS export names for ESM import in node:
483
563
  0 && (module.exports = {
484
- clearRegistry,
485
564
  createPip,
486
565
  getPip,
487
566
  isSupported,
package/dist/index.mjs CHANGED
@@ -1,50 +1,6 @@
1
1
  // src/support.ts
2
2
  var isSupported = () => {
3
- if (typeof window === "undefined") {
4
- return false;
5
- }
6
- return "documentPictureInPicture" in window && window.documentPictureInPicture !== void 0;
7
- };
8
-
9
- // src/registry.ts
10
- var registry = /* @__PURE__ */ new Map();
11
- var listeners = /* @__PURE__ */ new Map();
12
- var registerPip = (id, instance) => {
13
- registry.set(id, instance);
14
- notifyListeners(id);
15
- };
16
- var unregisterPip = (id) => {
17
- registry.delete(id);
18
- notifyListeners(id);
19
- };
20
- var getPip = (id) => {
21
- return registry.get(id) || null;
22
- };
23
- var subscribeRegistry = (id, fn) => {
24
- let set = listeners.get(id);
25
- if (!set) {
26
- set = /* @__PURE__ */ new Set();
27
- listeners.set(id, set);
28
- }
29
- set.add(fn);
30
- return () => {
31
- set?.delete(fn);
32
- if (set?.size === 0) {
33
- listeners.delete(id);
34
- }
35
- };
36
- };
37
- var notifyListeners = (id) => {
38
- const set = listeners.get(id);
39
- if (set) {
40
- for (const fn of set) {
41
- fn();
42
- }
43
- }
44
- };
45
- var clearRegistry = () => {
46
- registry.clear();
47
- listeners.clear();
3
+ return typeof window !== "undefined" && "documentPictureInPicture" in window && typeof window.documentPictureInPicture?.requestWindow === "function";
48
4
  };
49
5
 
50
6
  // src/styles.ts
@@ -79,11 +35,28 @@ var startStylesSync = (pipWindow) => {
79
35
  }
80
36
  syncAttrs(openerDoc.documentElement, pipDoc.documentElement);
81
37
  syncAttrs(openerDoc.body, pipDoc.body);
38
+ const pendingTextUpdates = /* @__PURE__ */ new Map();
39
+ let pendingRafId = null;
40
+ const flushTextUpdates = () => {
41
+ pendingRafId = null;
42
+ for (const [source, clone] of pendingTextUpdates) {
43
+ clone.textContent = source.textContent;
44
+ }
45
+ pendingTextUpdates.clear();
46
+ };
47
+ const scheduleTextUpdate = (sourceStyle, clone) => {
48
+ pendingTextUpdates.set(sourceStyle, clone);
49
+ if (pendingRafId === null) {
50
+ pendingRafId = requestAnimationFrame(flushTextUpdates);
51
+ }
52
+ };
82
53
  const headObserver = new MutationObserver((mutations) => {
83
54
  for (const mutation of mutations) {
84
55
  if (mutation.type === "childList") {
85
56
  for (const node of Array.from(mutation.addedNodes)) {
86
57
  if (node.nodeName === "STYLE" || node.nodeName === "LINK" && node.rel === "stylesheet") {
58
+ const existingClone = nodeMap.get(node);
59
+ if (existingClone) continue;
87
60
  const clone = node.cloneNode(true);
88
61
  nodeMap.set(node, clone);
89
62
  pipDoc.head.appendChild(clone);
@@ -104,7 +77,7 @@ var startStylesSync = (pipWindow) => {
104
77
  if (current) {
105
78
  const clone = nodeMap.get(current);
106
79
  if (clone) {
107
- clone.textContent = current.textContent;
80
+ scheduleTextUpdate(current, clone);
108
81
  }
109
82
  }
110
83
  }
@@ -140,14 +113,36 @@ var startStylesSync = (pipWindow) => {
140
113
  return () => {
141
114
  headObserver.disconnect();
142
115
  attrObserver.disconnect();
116
+ if (pendingRafId !== null) {
117
+ cancelAnimationFrame(pendingRafId);
118
+ pendingRafId = null;
119
+ }
120
+ pendingTextUpdates.clear();
143
121
  };
144
122
  };
145
123
 
146
124
  // src/dom-modes.ts
147
- var applyMoveMode = (pipWindow, contentEl, originEl) => {
125
+ var applyMoveMode = (pipWindow, contentEl, originEl, reserveSpace = true) => {
126
+ if (reserveSpace) {
127
+ const rect = contentEl.getBoundingClientRect();
128
+ originEl.style.minWidth = `${rect.width}px`;
129
+ originEl.style.minHeight = `${rect.height}px`;
130
+ originEl.style.width = `${rect.width}px`;
131
+ originEl.style.height = `${rect.height}px`;
132
+ contentEl.style.width = `${rect.width}px`;
133
+ contentEl.style.height = `${rect.height}px`;
134
+ }
148
135
  pipWindow.document.body.appendChild(contentEl);
149
136
  return () => {
150
137
  if (originEl && contentEl) {
138
+ if (reserveSpace) {
139
+ originEl.style.minWidth = "";
140
+ originEl.style.minHeight = "";
141
+ originEl.style.width = "";
142
+ originEl.style.height = "";
143
+ contentEl.style.width = "";
144
+ contentEl.style.height = "";
145
+ }
151
146
  originEl.appendChild(contentEl);
152
147
  }
153
148
  };
@@ -156,10 +151,7 @@ var applyCloneMode = (pipWindow, contentEl) => {
156
151
  const clone = contentEl.cloneNode(true);
157
152
  pipWindow.document.body.appendChild(clone);
158
153
  return () => {
159
- };
160
- };
161
- var applyPortalAnchorMode = (pipWindow) => {
162
- return () => {
154
+ clone.parentNode?.removeChild(clone);
163
155
  };
164
156
  };
165
157
 
@@ -186,7 +178,7 @@ var startKeyboardBridge = (pipWindow, openerWindow = window) => {
186
178
  charCode: { get: () => e.charCode },
187
179
  which: { get: () => e.which }
188
180
  });
189
- const canceled = !openerWindow.document.dispatchEvent(cloneEvent);
181
+ const canceled = !openerWindow.dispatchEvent(cloneEvent);
190
182
  if (canceled) {
191
183
  e.preventDefault();
192
184
  }
@@ -200,48 +192,57 @@ var startKeyboardBridge = (pipWindow, openerWindow = window) => {
200
192
  };
201
193
 
202
194
  // src/focus-scroll.ts
203
- var snapshotScrollFocus = (rootEl) => {
195
+ var snapshotScrollFocus = (rootEl, opts = {}) => {
196
+ const { restoreScroll = true, restoreFocus = true } = opts;
204
197
  const openerDoc = window.document;
205
- const activeElement = openerDoc.activeElement;
198
+ let activeElement = null;
206
199
  let selectionStart = null;
207
200
  let selectionEnd = null;
208
201
  let selectionDir = null;
209
- if (activeElement && (activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement)) {
210
- try {
211
- selectionStart = activeElement.selectionStart;
212
- selectionEnd = activeElement.selectionEnd;
213
- selectionDir = activeElement.selectionDirection;
214
- } catch {
202
+ if (restoreFocus) {
203
+ activeElement = openerDoc.activeElement;
204
+ if (activeElement && (activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement)) {
205
+ try {
206
+ selectionStart = activeElement.selectionStart;
207
+ selectionEnd = activeElement.selectionEnd;
208
+ selectionDir = activeElement.selectionDirection;
209
+ } catch {
210
+ }
215
211
  }
216
212
  }
217
213
  const scrollMap = /* @__PURE__ */ new WeakMap();
218
- const walk = (node) => {
219
- if (node instanceof HTMLElement) {
220
- if (node.scrollTop > 0 || node.scrollLeft > 0) {
214
+ if (restoreScroll) {
215
+ const allElements = rootEl.querySelectorAll("*");
216
+ if (rootEl.scrollTop > 0 || rootEl.scrollLeft > 0) {
217
+ scrollMap.set(rootEl, { scrollTop: rootEl.scrollTop, scrollLeft: rootEl.scrollLeft });
218
+ }
219
+ for (let i = 0; i < allElements.length; i++) {
220
+ const node = allElements[i];
221
+ if (node instanceof HTMLElement && (node.scrollTop > 0 || node.scrollLeft > 0)) {
221
222
  scrollMap.set(node, { scrollTop: node.scrollTop, scrollLeft: node.scrollLeft });
222
223
  }
223
224
  }
224
- for (const child of Array.from(node.children)) {
225
- walk(child);
226
- }
227
- };
228
- walk(rootEl);
225
+ }
229
226
  return {
230
227
  restore: () => {
231
- const restoreWalk = (node) => {
232
- if (node instanceof HTMLElement) {
228
+ if (restoreScroll) {
229
+ const restoreState = (node) => {
233
230
  const state = scrollMap.get(node);
234
231
  if (state) {
235
232
  node.scrollTop = state.scrollTop;
236
233
  node.scrollLeft = state.scrollLeft;
237
234
  }
235
+ };
236
+ restoreState(rootEl);
237
+ const allElements = rootEl.querySelectorAll("*");
238
+ for (let i = 0; i < allElements.length; i++) {
239
+ const node = allElements[i];
240
+ if (node instanceof HTMLElement) {
241
+ restoreState(node);
242
+ }
238
243
  }
239
- for (const child of Array.from(node.children)) {
240
- restoreWalk(child);
241
- }
242
- };
243
- restoreWalk(rootEl);
244
- if (activeElement && openerDoc.body.contains(activeElement)) {
244
+ }
245
+ if (restoreFocus && activeElement && openerDoc.body.contains(activeElement)) {
245
246
  activeElement.focus({ preventScroll: true });
246
247
  if (selectionStart !== null && selectionEnd !== null) {
247
248
  try {
@@ -260,21 +261,20 @@ var snapshotScrollFocus = (rootEl) => {
260
261
 
261
262
  // src/fixed-size.ts
262
263
  var attachFixedSizeGuard = (pipWindow, width, height) => {
263
- let isResizing = false;
264
264
  const handleResize = () => {
265
- if (isResizing) return;
266
- if (pipWindow.innerWidth !== width || pipWindow.innerHeight !== height) {
267
- isResizing = true;
268
- try {
269
- pipWindow.resizeTo(width, height);
270
- } catch {
271
- }
272
- setTimeout(() => {
273
- isResizing = false;
274
- }, 50);
265
+ try {
266
+ pipWindow.resizeTo(width, height);
267
+ } catch {
275
268
  }
276
269
  };
277
270
  pipWindow.addEventListener("resize", handleResize);
271
+ pipWindow.document.documentElement.style.width = `${width}px`;
272
+ pipWindow.document.documentElement.style.height = `${height}px`;
273
+ pipWindow.document.documentElement.style.overflow = "hidden";
274
+ pipWindow.document.body.style.width = `${width}px`;
275
+ pipWindow.document.body.style.height = `${height}px`;
276
+ pipWindow.document.body.style.overflow = "hidden";
277
+ pipWindow.document.body.style.margin = "0";
278
278
  return () => {
279
279
  pipWindow.removeEventListener("resize", handleResize);
280
280
  };
@@ -283,8 +283,8 @@ var attachFixedSizeGuard = (pipWindow, width, height) => {
283
283
  // src/fallback.ts
284
284
  var executeFallback = (fallback, options, contentEl, originEl) => {
285
285
  if (typeof fallback === "function") {
286
- fallback({ contentEl, originEl, options });
287
- return;
286
+ const result = fallback({ contentEl, originEl, resolvedOptions: options });
287
+ return typeof result === "function" ? result : void 0;
288
288
  }
289
289
  switch (fallback) {
290
290
  case "new-tab":
@@ -293,12 +293,10 @@ var executeFallback = (fallback, options, contentEl, originEl) => {
293
293
  } else {
294
294
  console.warn('pip-it-up: fallback="new-tab" requires fallbackUrl option');
295
295
  }
296
- break;
297
- case "modal":
298
- break;
296
+ return;
299
297
  case "none":
300
298
  console.warn("pip-it-up: Document Picture-in-Picture is not supported in this browser.");
301
- break;
299
+ return;
302
300
  }
303
301
  };
304
302
 
@@ -313,6 +311,7 @@ var createPip = (options = {}) => {
313
311
  };
314
312
  const listeners2 = /* @__PURE__ */ new Set();
315
313
  const disposers = [];
314
+ let defaultElements = {};
316
315
  const updateState = (newState) => {
317
316
  state = { ...state, ...newState };
318
317
  listeners2.forEach((fn) => fn());
@@ -334,15 +333,19 @@ var createPip = (options = {}) => {
334
333
  options.onClose();
335
334
  }
336
335
  };
336
+ let isOpening = false;
337
337
  const open = async (elements) => {
338
- if (state.isOpen) return;
339
- const contentEl = elements?.contentEl || options.contentEl;
340
- const originEl = elements?.originEl || options.originEl;
338
+ if (state.isOpen || isOpening) return;
339
+ isOpening = true;
340
+ const contentEl = elements?.contentEl ?? defaultElements.contentEl;
341
+ const originEl = elements?.originEl ?? defaultElements.originEl;
341
342
  const mode = options.mode || "move";
342
- if (!state.isSupported) {
343
+ if (!state.isSupported || options.forceFallback) {
344
+ isOpening = false;
343
345
  const fallback = options.fallback || "none";
344
- executeFallback(fallback, options, contentEl, originEl);
345
- if (fallback === "modal") {
346
+ const fallbackCleanup = executeFallback(fallback, options, contentEl, originEl);
347
+ if (fallbackCleanup) disposers.push(fallbackCleanup);
348
+ if (fallback !== "none") {
346
349
  updateState({ isOpen: true, pipWindow: null });
347
350
  if (options.onOpen) options.onOpen(window);
348
351
  }
@@ -351,26 +354,52 @@ var createPip = (options = {}) => {
351
354
  try {
352
355
  if (options.onBeforeOpen) {
353
356
  const shouldOpen = await options.onBeforeOpen();
354
- if (shouldOpen === false) return;
357
+ if (shouldOpen === false) {
358
+ isOpening = false;
359
+ return;
360
+ }
355
361
  }
356
362
  let restoreFocusScroll = null;
357
- if (options.restoreScroll !== false && options.restoreFocus !== false && contentEl) {
358
- const snap = snapshotScrollFocus(contentEl);
363
+ if ((options.restoreScroll !== false || options.restoreFocus !== false) && contentEl) {
364
+ const snap = snapshotScrollFocus(contentEl, {
365
+ restoreScroll: options.restoreScroll !== false,
366
+ restoreFocus: options.restoreFocus !== false
367
+ });
359
368
  restoreFocusScroll = snap.restore;
360
369
  }
361
- const width = options.width || 900;
362
- const height = options.height || 600;
370
+ let reqWidth = options.width;
371
+ let reqHeight = options.height;
372
+ if ((!reqWidth || !reqHeight) && contentEl) {
373
+ const rect = contentEl.getBoundingClientRect();
374
+ if (rect.width > 0 && rect.height > 0) {
375
+ reqWidth = reqWidth || Math.max(300, Math.min(1600, Math.round(rect.width)));
376
+ reqHeight = reqHeight || Math.max(200, Math.min(1200, Math.round(rect.height)));
377
+ }
378
+ }
379
+ const width = reqWidth || 900;
380
+ const height = reqHeight || 600;
363
381
  const lockAspectRatio = options.lockAspectRatio || options.fixedSize || false;
364
- const pipReqOpts = {
382
+ const pipWindow = await window.documentPictureInPicture.requestWindow({
365
383
  width,
366
384
  height,
367
385
  disallowReturnToOpener: options.disallowReturnToOpener,
368
386
  preferInitialWindowPlacement: options.preferInitialWindowPlacement,
369
387
  ...lockAspectRatio ? { lockAspectRatio: true } : {}
370
- };
371
- const pipWindow = await window.documentPictureInPicture.requestWindow(pipReqOpts);
372
- pipWindow.addEventListener("pagehide", () => {
373
- close();
388
+ });
389
+ const onPipClose = () => close();
390
+ pipWindow.addEventListener("pagehide", onPipClose);
391
+ pipWindow.addEventListener("unload", onPipClose);
392
+ disposers.push(() => {
393
+ pipWindow.removeEventListener("pagehide", onPipClose);
394
+ pipWindow.removeEventListener("unload", onPipClose);
395
+ });
396
+ const closePollInterval = setInterval(() => {
397
+ if (pipWindow.closed) {
398
+ close();
399
+ }
400
+ }, 250);
401
+ disposers.push(() => {
402
+ clearInterval(closePollInterval);
374
403
  });
375
404
  const copyMode = options.copyStyles || "sync";
376
405
  if (copyMode === "sync") {
@@ -379,11 +408,28 @@ var createPip = (options = {}) => {
379
408
  copyStylesOnce(pipWindow);
380
409
  }
381
410
  if (contentEl && originEl && mode === "move") {
382
- disposers.push(applyMoveMode(pipWindow, contentEl, originEl));
411
+ const reserveSpace = options.reserveSpace !== false;
412
+ disposers.push(applyMoveMode(pipWindow, contentEl, originEl, reserveSpace));
383
413
  } else if (contentEl && mode === "clone") {
384
414
  disposers.push(applyCloneMode(pipWindow, contentEl));
385
- } else if (mode === "portal") {
386
- disposers.push(applyPortalAnchorMode(pipWindow));
415
+ }
416
+ if (options.pipBodyStyles !== false) {
417
+ const defaultStyles = {
418
+ margin: "0",
419
+ padding: "0",
420
+ boxSizing: "border-box",
421
+ width: options.fixedSize ? `${width}px` : "100%",
422
+ height: options.fixedSize ? `${height}px` : "auto",
423
+ overflow: options.fixedSize ? "hidden" : "auto",
424
+ ...options.centerInPip ? {
425
+ display: "flex",
426
+ alignItems: "center",
427
+ justifyContent: "center",
428
+ minHeight: "100vh"
429
+ } : {}
430
+ };
431
+ const stylesToApply = options.pipBodyStyles || defaultStyles;
432
+ Object.assign(pipWindow.document.body.style, stylesToApply);
387
433
  }
388
434
  if (options.forwardKeyboardEvents !== false) {
389
435
  disposers.push(startKeyboardBridge(pipWindow, window));
@@ -395,15 +441,18 @@ var createPip = (options = {}) => {
395
441
  disposers.push(restoreFocusScroll);
396
442
  }
397
443
  updateState({ isOpen: true, pipWindow });
444
+ isOpening = false;
398
445
  if (options.onOpen) {
399
446
  options.onOpen(pipWindow);
400
447
  }
401
- requestAnimationFrame(() => {
448
+ const rafId = requestAnimationFrame(() => {
402
449
  if (options.onPipWindowReady) {
403
450
  options.onPipWindowReady(pipWindow);
404
451
  }
405
452
  });
453
+ disposers.push(() => cancelAnimationFrame(rafId));
406
454
  } catch (err) {
455
+ isOpening = false;
407
456
  cleanup();
408
457
  updateState({ isOpen: false, pipWindow: null });
409
458
  if (options.onError) {
@@ -432,23 +481,54 @@ var createPip = (options = {}) => {
432
481
  return () => listeners2.delete(fn);
433
482
  },
434
483
  getState: () => state,
435
- updateElements: (elements) => {
436
- if (elements.contentEl !== void 0) options.contentEl = elements.contentEl;
437
- if (elements.originEl !== void 0) options.originEl = elements.originEl;
484
+ setDefaultElements: (elements) => {
485
+ defaultElements = elements;
438
486
  },
439
487
  destroy: () => {
440
488
  close();
441
- unregisterPip(id);
442
489
  listeners2.clear();
443
490
  }
444
491
  };
445
- if (options.id) {
446
- registerPip(id, instance);
447
- }
448
492
  return instance;
449
493
  };
494
+
495
+ // src/registry.ts
496
+ var registry = /* @__PURE__ */ new Map();
497
+ var listeners = /* @__PURE__ */ new Map();
498
+ var registerPip = (id, instance) => {
499
+ registry.set(id, instance);
500
+ notifyListeners(id);
501
+ };
502
+ var unregisterPip = (id) => {
503
+ registry.delete(id);
504
+ notifyListeners(id);
505
+ };
506
+ var getPip = (id) => {
507
+ return registry.get(id) || null;
508
+ };
509
+ var subscribeRegistry = (id, fn) => {
510
+ let set = listeners.get(id);
511
+ if (!set) {
512
+ set = /* @__PURE__ */ new Set();
513
+ listeners.set(id, set);
514
+ }
515
+ set.add(fn);
516
+ return () => {
517
+ set?.delete(fn);
518
+ if (set?.size === 0) {
519
+ listeners.delete(id);
520
+ }
521
+ };
522
+ };
523
+ var notifyListeners = (id) => {
524
+ const set = listeners.get(id);
525
+ if (set) {
526
+ for (const fn of set) {
527
+ fn();
528
+ }
529
+ }
530
+ };
450
531
  export {
451
- clearRegistry,
452
532
  createPip,
453
533
  getPip,
454
534
  isSupported,
package/package.json CHANGED
@@ -1,6 +1,20 @@
1
1
  {
2
2
  "name": "@pip-it-up/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.5",
4
+ "description": "Vanilla JS engine for the Document Picture-in-Picture API — move, clone, or portal any DOM element into a floating PiP window",
5
+ "keywords": [
6
+ "picture-in-picture",
7
+ "pip",
8
+ "document-picture-in-picture",
9
+ "floating-window",
10
+ "pip-window",
11
+ "document-pip",
12
+ "pip-api",
13
+ "web-api",
14
+ "browser-api",
15
+ "vanilla-js",
16
+ "framework-agnostic"
17
+ ],
4
18
  "repository": {
5
19
  "type": "git",
6
20
  "url": "git+https://github.com/Shakya47/pip-it-up.git",
@@ -24,7 +38,9 @@
24
38
  "access": "public",
25
39
  "provenance": true
26
40
  },
27
- "sideEffects": false,
41
+ "sideEffects": [
42
+ "src/global.d.ts"
43
+ ],
28
44
  "files": [
29
45
  "dist"
30
46
  ],