PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.8
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / assets / js / iframe-bridge.js

iframe-bridge.js in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.8.8, at assets/js/iframe-bridge.js

613 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function() {
2 "use strict";
3 (function() {
4 if (!window.parent || window.parent === window) {
5 return;
6 }
7 const w = window;
8 if (w.wp?.desktop?.iframe) {
9 return;
10 }
11 const parentOrigin = window.location.origin;
12 const connections = {};
13 const connectionListeners = [];
14 const subs = {};
15 let _windowId = null;
16 const _windowIdWaiters = [];
17 const _setWindowId = (id) => {
18 if (!id || _windowId === id) {
19 return;
20 }
21 _windowId = id;
22 const waiters = _windowIdWaiters.splice(0);
23 for (const waiter of waiters) {
24 try {
25 waiter(id);
26 } catch {
27 }
28 }
29 };
30 const channelSubs = {};
31 const emitToParent = (connectionId, topic, payload) => {
32 try {
33 window.parent.postMessage(
34 {
35 type: "desktop-mode-bridge-publish",
36 connectionId,
37 topic,
38 payload
39 },
40 parentOrigin
41 );
42 } catch {
43 }
44 };
45 window.addEventListener("message", (ev) => {
46 if (ev.origin !== parentOrigin) {
47 return;
48 }
49 const data = ev?.data;
50 if (!data || typeof data !== "object" || typeof data.type !== "string") {
51 return;
52 }
53 if (data.type === "desktop-mode-bridge-handshake" && typeof data.connectionId === "string") {
54 const tw = data.targetWindowId;
55 if (typeof tw === "string" && tw !== "") {
56 _setWindowId(tw);
57 }
58 if (connections[data.connectionId]) {
59 try {
60 window.parent.postMessage(
61 {
62 type: "desktop-mode-bridge-handshake-ack",
63 connectionId: data.connectionId
64 },
65 parentOrigin
66 );
67 } catch {
68 }
69 return;
70 }
71 const conn = {
72 id: data.connectionId,
73 topics: Array.isArray(data.topics) ? data.topics.slice() : []
74 };
75 connections[conn.id] = conn;
76 try {
77 window.parent.postMessage(
78 {
79 type: "desktop-mode-bridge-handshake-ack",
80 connectionId: conn.id
81 },
82 parentOrigin
83 );
84 } catch {
85 }
86 for (const listener of connectionListeners) {
87 try {
88 listener({ id: conn.id, topics: conn.topics.slice() });
89 } catch {
90 }
91 }
92 return;
93 }
94 if (data.type === "desktop-mode-bridge-publish" && typeof data.topic === "string") {
95 const meta = {
96 topic: data.topic,
97 connectionId: data.connectionId
98 };
99 const bucket = subs[data.topic];
100 if (bucket) {
101 for (const cb of bucket) {
102 try {
103 cb(data.payload, meta);
104 } catch {
105 }
106 }
107 }
108 const wildcard = subs["*"];
109 if (wildcard) {
110 for (const cb of wildcard) {
111 try {
112 cb(data.payload, meta);
113 } catch {
114 }
115 }
116 }
117 return;
118 }
119 if (data.type === "desktop-mode-bridge-disconnect" && typeof data.connectionId === "string") {
120 delete connections[data.connectionId];
121 }
122 if (data.type === "desktop-mode-window-send" && typeof data.channel === "string") {
123 const d = data;
124 const meta = { channel: d.channel };
125 const bucket = channelSubs[d.channel];
126 if (bucket) {
127 for (const cb of bucket.slice()) {
128 try {
129 cb(d.payload, meta);
130 } catch {
131 }
132 }
133 }
134 const wildcard = channelSubs["*"];
135 if (wildcard) {
136 for (const cb of wildcard.slice()) {
137 try {
138 cb(d.payload, meta);
139 } catch {
140 }
141 }
142 }
143 }
144 });
145 const iframeApi = {
146 publish(topic, payload) {
147 if (typeof topic !== "string" || topic === "") {
148 return;
149 }
150 const ids = Object.keys(connections);
151 if (ids.length === 0) {
152 console.warn(
153 '[desktop-mode] wp.desktop.iframe.publish dropped: no open connection for topic "%s". The parent shell must call `wp.desktop.connect(windowId)` first.',
154 topic
155 );
156 return;
157 }
158 for (const id of ids) {
159 emitToParent(id, topic, payload);
160 }
161 },
162 subscribe(topic, cb) {
163 if (typeof topic !== "string" || topic === "" || typeof cb !== "function") {
164 return () => {
165 };
166 }
167 let bucket = subs[topic];
168 if (!bucket) {
169 bucket = [];
170 subs[topic] = bucket;
171 }
172 bucket.push(cb);
173 return () => {
174 const i = bucket.indexOf(cb);
175 if (i >= 0) {
176 bucket.splice(i, 1);
177 }
178 };
179 },
180 onConnection(cb) {
181 if (typeof cb !== "function") {
182 return () => {
183 };
184 }
185 connectionListeners.push(cb);
186 for (const id of Object.keys(connections)) {
187 try {
188 cb({
189 id: connections[id].id,
190 topics: connections[id].topics.slice()
191 });
192 } catch {
193 }
194 }
195 return () => {
196 const i = connectionListeners.indexOf(cb);
197 if (i >= 0) {
198 connectionListeners.splice(i, 1);
199 }
200 };
201 },
202 /**
203 * Iframe-initiated connection request. Asks the parent to open
204 * a connection back to this iframe. Returns a Promise that
205 * resolves with the new `{ id, topics }` once the parent acks
206 * (or rejects on timeout / refusal).
207 *
208 * Parent-side handler: see `src/connection/index.ts`
209 * `handleConnectionRequest` + the
210 * `desktop-mode.iframe.connection-request` filter.
211 */
212 chrome: {
213 setTheme(tokens) {
214 try {
215 window.parent.postMessage(
216 {
217 type: "desktop-mode-chrome-theme",
218 tokens: tokens ?? {}
219 },
220 parentOrigin
221 );
222 } catch {
223 }
224 },
225 setControls(config) {
226 try {
227 window.parent.postMessage(
228 {
229 type: "desktop-mode-chrome-controls",
230 config: config ?? null
231 },
232 parentOrigin
233 );
234 } catch {
235 }
236 },
237 setSlot(name, html) {
238 if (typeof name !== "string" || name === "") {
239 return;
240 }
241 try {
242 window.parent.postMessage(
243 {
244 type: "desktop-mode-chrome-slot",
245 slot: name,
246 html: typeof html === "string" ? html : ""
247 },
248 parentOrigin
249 );
250 } catch {
251 }
252 }
253 },
254 requestConnection(opts) {
255 const o = opts ?? {};
256 const topics = Array.isArray(o.topics) ? o.topics.slice() : [];
257 const requestId = "wpdir-" + Math.random().toString(36).slice(2, 10);
258 return new Promise((resolve, reject) => {
259 let settled = false;
260 const timeoutMs = typeof o.timeoutMs === "number" ? o.timeoutMs : 5e3;
261 const settle = (ok, value) => {
262 if (settled) {
263 return;
264 }
265 settled = true;
266 window.removeEventListener("message", onAck);
267 clearTimeout(timer);
268 if (ok) {
269 resolve(value);
270 } else {
271 reject(value);
272 }
273 };
274 const onAck = (ev) => {
275 if (ev.origin !== parentOrigin) {
276 return;
277 }
278 const d = ev?.data;
279 if (!d || typeof d !== "object" || d.type !== "desktop-mode-bridge-connection-ack" || d.requestId !== requestId) {
280 return;
281 }
282 if (d.accepted) {
283 const summary = {
284 id: typeof d.connectionId === "string" ? d.connectionId : "",
285 topics: topics.slice()
286 };
287 if (typeof o.onOpen === "function") {
288 try {
289 o.onOpen(summary);
290 } catch {
291 }
292 }
293 settle(true, summary);
294 } else {
295 settle(false, new Error(d.reason || "rejected"));
296 }
297 };
298 window.addEventListener("message", onAck);
299 const timer = setTimeout(() => {
300 settle(false, new Error("timeout"));
301 }, timeoutMs);
302 try {
303 window.parent.postMessage(
304 {
305 type: "desktop-mode-bridge-connection-request",
306 requestId,
307 topics
308 },
309 parentOrigin
310 );
311 } catch (err) {
312 settle(false, err);
313 }
314 });
315 },
316 get windowId() {
317 return _windowId;
318 },
319 whenWindowId() {
320 if (_windowId !== null) {
321 return Promise.resolve(_windowId);
322 }
323 return new Promise((resolve) => {
324 _windowIdWaiters.push(resolve);
325 });
326 },
327 isParentReachable() {
328 if (!window.parent || window.parent === window) {
329 return false;
330 }
331 try {
332 const parentOrig = window.parent.location.origin;
333 return parentOrig === parentOrigin;
334 } catch {
335 return false;
336 }
337 }
338 };
339 if (!w.wp) {
340 w.wp = {};
341 }
342 if (!w.wp.desktop) {
343 w.wp.desktop = {};
344 }
345 w.wp.desktop.iframe = iframeApi;
346 if (typeof w.wp.desktop.send !== "function") {
347 w.wp.desktop.send = (channel, payload) => {
348 if (typeof channel !== "string" || channel === "") {
349 return;
350 }
351 try {
352 window.parent.postMessage(
353 {
354 type: "desktop-mode-window-publish",
355 channel,
356 payload
357 },
358 parentOrigin
359 );
360 } catch {
361 }
362 };
363 }
364 if (typeof w.wp.desktop.on !== "function") {
365 w.wp.desktop.on = (channel, cb) => {
366 if (typeof channel !== "string" || channel === "" || typeof cb !== "function") {
367 return () => void 0;
368 }
369 let bucket = channelSubs[channel];
370 if (!bucket) {
371 bucket = [];
372 channelSubs[channel] = bucket;
373 }
374 bucket.push(cb);
375 return () => {
376 const i = bucket.indexOf(cb);
377 if (i >= 0) {
378 bucket.splice(i, 1);
379 }
380 };
381 };
382 }
383 const sentinelHost = window;
384 if (!sentinelHost.__desktopModeScreenMetaInstalled) {
385 sentinelHost.__desktopModeScreenMetaInstalled = true;
386 installScreenMetaHoist(parentOrigin);
387 }
388 if (!sentinelHost.__desktopModeOsFileDropForwarderInstalled) {
389 sentinelHost.__desktopModeOsFileDropForwarderInstalled = true;
390 const hasFiles = (ev) => {
391 const types = ev.dataTransfer?.types;
392 if (!types) {
393 return false;
394 }
395 const list = types;
396 if (typeof list.includes === "function") {
397 return list.includes("Files");
398 }
399 if (typeof list.contains === "function") {
400 return list.contains("Files");
401 }
402 for (let i = 0; i < list.length; i++) {
403 if (list[i] === "Files") {
404 return true;
405 }
406 }
407 return false;
408 };
409 const dropPassthroughSelectors = [
410 ".components-drop-zone",
411 "[data-drop-zone]",
412 ".uploader-window",
413 ".media-frame-content"
414 ];
415 const targetWantsFile = (target) => {
416 const el = target;
417 if (!el || !el.closest) {
418 return false;
419 }
420 for (const sel of dropPassthroughSelectors) {
421 if (el.closest(sel)) {
422 return true;
423 }
424 }
425 return false;
426 };
427 document.addEventListener(
428 "dragover",
429 (ev) => {
430 if (!hasFiles(ev)) {
431 return;
432 }
433 if (targetWantsFile(ev.target)) {
434 return;
435 }
436 ev.preventDefault();
437 if (ev.dataTransfer) {
438 ev.dataTransfer.dropEffect = "copy";
439 }
440 },
441 true
442 );
443 document.addEventListener(
444 "drop",
445 (ev) => {
446 if (!hasFiles(ev)) {
447 return;
448 }
449 if (targetWantsFile(ev.target)) {
450 return;
451 }
452 ev.preventDefault();
453 ev.stopPropagation();
454 const files = [];
455 if (ev.dataTransfer?.files) {
456 for (let i = 0; i < ev.dataTransfer.files.length; i++) {
457 files.push(ev.dataTransfer.files[i]);
458 }
459 }
460 if (files.length === 0) {
461 return;
462 }
463 try {
464 window.parent.postMessage(
465 {
466 type: "desktop-mode-os-file-drop",
467 files,
468 x: ev.clientX,
469 y: ev.clientY
470 },
471 parentOrigin
472 );
473 } catch {
474 }
475 },
476 true
477 );
478 }
479 try {
480 if (window.parent && window.parent !== window) {
481 window.parent.postMessage(
482 { type: "desktop-mode-ready" },
483 parentOrigin
484 );
485 }
486 } catch {
487 }
488 function installScreenMetaHoist(origin) {
489 const start = () => {
490 const links = document.getElementById("screen-meta-links");
491 if (!links) {
492 return;
493 }
494 const screenOptionsBtn = document.getElementById(
495 "show-settings-link"
496 );
497 const helpBtn = document.getElementById("contextual-help-link");
498 const panels = [];
499 if (screenOptionsBtn) {
500 panels.push("screen-options");
501 }
502 if (helpBtn) {
503 panels.push("help");
504 }
505 if (panels.length === 0) {
506 return;
507 }
508 try {
509 window.parent.postMessage(
510 { type: "desktop-mode-screen-meta", panels },
511 origin
512 );
513 } catch {
514 }
515 const getOpenPanel = () => {
516 if (screenOptionsBtn && screenOptionsBtn.getAttribute("aria-expanded") === "true") {
517 return "screen-options";
518 }
519 if (helpBtn && helpBtn.getAttribute("aria-expanded") === "true") {
520 return "help";
521 }
522 return null;
523 };
524 const reportState = () => {
525 try {
526 window.parent.postMessage(
527 {
528 type: "desktop-mode-screen-meta-state",
529 open: getOpenPanel()
530 },
531 origin
532 );
533 } catch {
534 }
535 };
536 reportState();
537 const observer = new MutationObserver(reportState);
538 if (screenOptionsBtn) {
539 observer.observe(screenOptionsBtn, {
540 attributes: true,
541 attributeFilter: ["aria-expanded"]
542 });
543 }
544 if (helpBtn) {
545 observer.observe(helpBtn, {
546 attributes: true,
547 attributeFilter: ["aria-expanded"]
548 });
549 }
550 const forceClose = (button) => {
551 if (!button || button.getAttribute("aria-expanded") !== "true") {
552 return;
553 }
554 const panelId = button.getAttribute("aria-controls");
555 const panel = panelId ? document.getElementById(panelId) : null;
556 if (!panel) {
557 return;
558 }
559 const jq = window.jQuery;
560 if (jq) {
561 try {
562 jq(panel).stop(true, false);
563 } catch {
564 }
565 }
566 panel.style.display = "none";
567 panel.classList.add("hidden");
568 if (panel.parentElement) {
569 panel.parentElement.style.display = "none";
570 }
571 button.classList.remove("screen-meta-active");
572 button.setAttribute("aria-expanded", "false");
573 const toggles = document.querySelectorAll(
574 ".screen-meta-toggle"
575 );
576 toggles.forEach((t) => {
577 t.style.visibility = "";
578 });
579 };
580 window.addEventListener("message", (e) => {
581 if (e.origin !== origin) {
582 return;
583 }
584 const d = e.data;
585 if (!d || d.type !== "desktop-mode-toggle-panel") {
586 return;
587 }
588 let target = null;
589 if (d.panel === "screen-options" && screenOptionsBtn) {
590 target = screenOptionsBtn;
591 } else if (d.panel === "help" && helpBtn) {
592 target = helpBtn;
593 }
594 if (!target) {
595 return;
596 }
597 if (target.getAttribute("aria-expanded") !== "true") {
598 forceClose(
599 target === screenOptionsBtn ? helpBtn : screenOptionsBtn
600 );
601 }
602 target.click();
603 });
604 };
605 if (document.readyState === "loading") {
606 document.addEventListener("DOMContentLoaded", start, { once: true });
607 } else {
608 start();
609 }
610 }
611 })();
612 })();
613