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

635 lines 17.8 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 hasScreenOptionsContent = () => {
496 const wrap = document.getElementById("screen-options-wrap");
497 return !!wrap && !!wrap.querySelector(
498 'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]), select, textarea'
499 );
500 };
501 const hasHelpContent = () => {
502 const wrap = document.getElementById("contextual-help-wrap");
503 if (!wrap) {
504 return false;
505 }
506 const panelEls = wrap.querySelectorAll(
507 ".help-tab-content, .contextual-help-sidebar"
508 );
509 for (let i = 0; i < panelEls.length; i++) {
510 if ((panelEls[i].textContent || "").trim() !== "") {
511 return true;
512 }
513 }
514 return false;
515 };
516 const start = () => {
517 const links = document.getElementById("screen-meta-links");
518 const screenOptionsBtn = links ? document.getElementById("show-settings-link") : null;
519 const helpBtn = links ? document.getElementById("contextual-help-link") : null;
520 const panels = [];
521 if (screenOptionsBtn && hasScreenOptionsContent()) {
522 panels.push("screen-options");
523 }
524 if (helpBtn && hasHelpContent()) {
525 panels.push("help");
526 }
527 try {
528 window.parent.postMessage(
529 { type: "desktop-mode-screen-meta", panels },
530 origin
531 );
532 } catch {
533 }
534 if (panels.length === 0) {
535 return;
536 }
537 const getOpenPanel = () => {
538 if (screenOptionsBtn && screenOptionsBtn.getAttribute("aria-expanded") === "true") {
539 return "screen-options";
540 }
541 if (helpBtn && helpBtn.getAttribute("aria-expanded") === "true") {
542 return "help";
543 }
544 return null;
545 };
546 const reportState = () => {
547 try {
548 window.parent.postMessage(
549 {
550 type: "desktop-mode-screen-meta-state",
551 open: getOpenPanel()
552 },
553 origin
554 );
555 } catch {
556 }
557 };
558 reportState();
559 const observer = new MutationObserver(reportState);
560 if (screenOptionsBtn) {
561 observer.observe(screenOptionsBtn, {
562 attributes: true,
563 attributeFilter: ["aria-expanded"]
564 });
565 }
566 if (helpBtn) {
567 observer.observe(helpBtn, {
568 attributes: true,
569 attributeFilter: ["aria-expanded"]
570 });
571 }
572 const forceClose = (button) => {
573 if (!button || button.getAttribute("aria-expanded") !== "true") {
574 return;
575 }
576 const panelId = button.getAttribute("aria-controls");
577 const panel = panelId ? document.getElementById(panelId) : null;
578 if (!panel) {
579 return;
580 }
581 const jq = window.jQuery;
582 if (jq) {
583 try {
584 jq(panel).stop(true, false);
585 } catch {
586 }
587 }
588 panel.style.display = "none";
589 panel.classList.add("hidden");
590 if (panel.parentElement) {
591 panel.parentElement.style.display = "none";
592 }
593 button.classList.remove("screen-meta-active");
594 button.setAttribute("aria-expanded", "false");
595 const toggles = document.querySelectorAll(
596 ".screen-meta-toggle"
597 );
598 toggles.forEach((t) => {
599 t.style.visibility = "";
600 });
601 };
602 window.addEventListener("message", (e) => {
603 if (e.origin !== origin) {
604 return;
605 }
606 const d = e.data;
607 if (!d || d.type !== "desktop-mode-toggle-panel") {
608 return;
609 }
610 let target = null;
611 if (d.panel === "screen-options" && screenOptionsBtn) {
612 target = screenOptionsBtn;
613 } else if (d.panel === "help" && helpBtn) {
614 target = helpBtn;
615 }
616 if (!target) {
617 return;
618 }
619 if (target.getAttribute("aria-expanded") !== "true") {
620 forceClose(
621 target === screenOptionsBtn ? helpBtn : screenOptionsBtn
622 );
623 }
624 target.click();
625 });
626 };
627 if (document.readyState === "loading") {
628 document.addEventListener("DOMContentLoaded", start, { once: true });
629 } else {
630 start();
631 }
632 }
633 })();
634 })();
635