PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.9
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.9
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.9, at assets/js/iframe-bridge.js

619 lines 17.0 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 if (ev.defaultPrevented) {
437 return;
438 }
439 ev.preventDefault();
440 if (ev.dataTransfer) {
441 ev.dataTransfer.dropEffect = "copy";
442 }
443 },
444 false
445 );
446 document.addEventListener(
447 "drop",
448 (ev) => {
449 if (!hasFiles(ev)) {
450 return;
451 }
452 if (targetWantsFile(ev.target)) {
453 return;
454 }
455 if (ev.defaultPrevented) {
456 return;
457 }
458 ev.preventDefault();
459 ev.stopPropagation();
460 const files = [];
461 if (ev.dataTransfer?.files) {
462 for (let i = 0; i < ev.dataTransfer.files.length; i++) {
463 files.push(ev.dataTransfer.files[i]);
464 }
465 }
466 if (files.length === 0) {
467 return;
468 }
469 try {
470 window.parent.postMessage(
471 {
472 type: "desktop-mode-os-file-drop",
473 files,
474 x: ev.clientX,
475 y: ev.clientY
476 },
477 parentOrigin
478 );
479 } catch {
480 }
481 },
482 false
483 );
484 }
485 try {
486 if (window.parent && window.parent !== window) {
487 window.parent.postMessage(
488 { type: "desktop-mode-ready" },
489 parentOrigin
490 );
491 }
492 } catch {
493 }
494 function installScreenMetaHoist(origin) {
495 const start = () => {
496 const links = document.getElementById("screen-meta-links");
497 if (!links) {
498 return;
499 }
500 const screenOptionsBtn = document.getElementById(
501 "show-settings-link"
502 );
503 const helpBtn = document.getElementById("contextual-help-link");
504 const panels = [];
505 if (screenOptionsBtn) {
506 panels.push("screen-options");
507 }
508 if (helpBtn) {
509 panels.push("help");
510 }
511 if (panels.length === 0) {
512 return;
513 }
514 try {
515 window.parent.postMessage(
516 { type: "desktop-mode-screen-meta", panels },
517 origin
518 );
519 } catch {
520 }
521 const getOpenPanel = () => {
522 if (screenOptionsBtn && screenOptionsBtn.getAttribute("aria-expanded") === "true") {
523 return "screen-options";
524 }
525 if (helpBtn && helpBtn.getAttribute("aria-expanded") === "true") {
526 return "help";
527 }
528 return null;
529 };
530 const reportState = () => {
531 try {
532 window.parent.postMessage(
533 {
534 type: "desktop-mode-screen-meta-state",
535 open: getOpenPanel()
536 },
537 origin
538 );
539 } catch {
540 }
541 };
542 reportState();
543 const observer = new MutationObserver(reportState);
544 if (screenOptionsBtn) {
545 observer.observe(screenOptionsBtn, {
546 attributes: true,
547 attributeFilter: ["aria-expanded"]
548 });
549 }
550 if (helpBtn) {
551 observer.observe(helpBtn, {
552 attributes: true,
553 attributeFilter: ["aria-expanded"]
554 });
555 }
556 const forceClose = (button) => {
557 if (!button || button.getAttribute("aria-expanded") !== "true") {
558 return;
559 }
560 const panelId = button.getAttribute("aria-controls");
561 const panel = panelId ? document.getElementById(panelId) : null;
562 if (!panel) {
563 return;
564 }
565 const jq = window.jQuery;
566 if (jq) {
567 try {
568 jq(panel).stop(true, false);
569 } catch {
570 }
571 }
572 panel.style.display = "none";
573 panel.classList.add("hidden");
574 if (panel.parentElement) {
575 panel.parentElement.style.display = "none";
576 }
577 button.classList.remove("screen-meta-active");
578 button.setAttribute("aria-expanded", "false");
579 const toggles = document.querySelectorAll(
580 ".screen-meta-toggle"
581 );
582 toggles.forEach((t) => {
583 t.style.visibility = "";
584 });
585 };
586 window.addEventListener("message", (e) => {
587 if (e.origin !== origin) {
588 return;
589 }
590 const d = e.data;
591 if (!d || d.type !== "desktop-mode-toggle-panel") {
592 return;
593 }
594 let target = null;
595 if (d.panel === "screen-options" && screenOptionsBtn) {
596 target = screenOptionsBtn;
597 } else if (d.panel === "help" && helpBtn) {
598 target = helpBtn;
599 }
600 if (!target) {
601 return;
602 }
603 if (target.getAttribute("aria-expanded") !== "true") {
604 forceClose(
605 target === screenOptionsBtn ? helpBtn : screenOptionsBtn
606 );
607 }
608 target.click();
609 });
610 };
611 if (document.readyState === "loading") {
612 document.addEventListener("DOMContentLoaded", start, { once: true });
613 } else {
614 start();
615 }
616 }
617 })();
618 })();
619