@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 +18 -16
- package/dist/index.d.mts +25 -14
- package/dist/index.d.ts +25 -14
- package/dist/index.js +201 -122
- package/dist/index.mjs +201 -121
- package/package.json +18 -2
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @pip-it-up/core
|
|
2
2
|
|
|
3
|
-
The
|
|
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'
|
|
24
|
+
fallback: 'new-tab' // 'new-tab' or 'none'
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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)
|
|
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
|
|
51
|
-
- `close()`: Closes the
|
|
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 |
|
|
59
|
-
Retrieves a created
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
19
|
+
|
|
20
|
+
type FallbackMode = "new-tab" | "none" | ((ctx: {
|
|
11
21
|
contentEl?: HTMLElement;
|
|
12
22
|
originEl?: HTMLElement;
|
|
13
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
19
|
+
|
|
20
|
+
type FallbackMode = "new-tab" | "none" | ((ctx: {
|
|
11
21
|
contentEl?: HTMLElement;
|
|
12
22
|
originEl?: HTMLElement;
|
|
13
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
229
|
+
let activeElement = null;
|
|
238
230
|
let selectionStart = null;
|
|
239
231
|
let selectionEnd = null;
|
|
240
232
|
let selectionDir = null;
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
257
|
-
walk(child);
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
walk(rootEl);
|
|
256
|
+
}
|
|
261
257
|
return {
|
|
262
258
|
restore: () => {
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
372
|
-
const
|
|
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 (
|
|
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)
|
|
388
|
+
if (shouldOpen === false) {
|
|
389
|
+
isOpening = false;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
387
392
|
}
|
|
388
393
|
let restoreFocusScroll = null;
|
|
389
|
-
if (options.restoreScroll !== false
|
|
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
|
-
|
|
394
|
-
|
|
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
|
|
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
|
|
404
|
-
pipWindow.addEventListener("pagehide",
|
|
405
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
418
|
-
|
|
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
|
-
|
|
468
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
198
|
+
let activeElement = null;
|
|
206
199
|
let selectionStart = null;
|
|
207
200
|
let selectionEnd = null;
|
|
208
201
|
let selectionDir = null;
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
225
|
-
walk(child);
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
walk(rootEl);
|
|
225
|
+
}
|
|
229
226
|
return {
|
|
230
227
|
restore: () => {
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
-
const
|
|
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 (
|
|
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)
|
|
357
|
+
if (shouldOpen === false) {
|
|
358
|
+
isOpening = false;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
355
361
|
}
|
|
356
362
|
let restoreFocusScroll = null;
|
|
357
|
-
if (options.restoreScroll !== false
|
|
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
|
-
|
|
362
|
-
|
|
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
|
|
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
|
|
372
|
-
pipWindow.addEventListener("pagehide",
|
|
373
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
386
|
-
|
|
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
|
-
|
|
436
|
-
|
|
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.
|
|
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":
|
|
41
|
+
"sideEffects": [
|
|
42
|
+
"src/global.d.ts"
|
|
43
|
+
],
|
|
28
44
|
"files": [
|
|
29
45
|
"dist"
|
|
30
46
|
],
|