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

723 lines 20.5 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-beforeunload-query") {
95 let prevent = false;
96 let msg = "";
97 const shimReturnValue = (e) => {
98 const self = e;
99 Object.defineProperty(e, "returnValue", {
100 get() {
101 return self._returnValue || "";
102 },
103 set(v) {
104 self._returnValue = v;
105 }
106 });
107 };
108 const checkPrevent = (event, result) => {
109 const retVal = event.returnValue;
110 const hasResult = typeof result === "string" && result !== "";
111 const hasRetVal = typeof retVal === "string" && retVal !== "";
112 if (event.defaultPrevented || hasResult || hasRetVal) {
113 prevent = true;
114 if (hasResult) {
115 msg = result;
116 } else if (hasRetVal) {
117 msg = retVal;
118 }
119 }
120 };
121 if (typeof window.onbeforeunload === "function") {
122 const unloadEvent = new Event("beforeunload", { cancelable: true });
123 shimReturnValue(unloadEvent);
124 const res = window.onbeforeunload(unloadEvent);
125 checkPrevent(unloadEvent, res);
126 }
127 if (!prevent) {
128 const dispatchEvent = new Event("beforeunload", { cancelable: true });
129 shimReturnValue(dispatchEvent);
130 window.dispatchEvent(dispatchEvent);
131 checkPrevent(dispatchEvent, null);
132 }
133 try {
134 window.parent.postMessage(
135 {
136 type: "desktop-mode-bridge-beforeunload-response",
137 prevent,
138 message: msg
139 },
140 parentOrigin
141 );
142 } catch {
143 }
144 return;
145 }
146 if (data.type === "desktop-mode-bridge-publish" && typeof data.topic === "string") {
147 const meta = {
148 topic: data.topic,
149 connectionId: data.connectionId
150 };
151 const bucket = subs[data.topic];
152 if (bucket) {
153 for (const cb of bucket) {
154 try {
155 cb(data.payload, meta);
156 } catch {
157 }
158 }
159 }
160 const wildcard = subs["*"];
161 if (wildcard) {
162 for (const cb of wildcard) {
163 try {
164 cb(data.payload, meta);
165 } catch {
166 }
167 }
168 }
169 return;
170 }
171 if (data.type === "desktop-mode-bridge-disconnect" && typeof data.connectionId === "string") {
172 delete connections[data.connectionId];
173 }
174 if (data.type === "desktop-mode-window-send" && typeof data.channel === "string") {
175 const d = data;
176 const meta = { channel: d.channel };
177 const bucket = channelSubs[d.channel];
178 if (bucket) {
179 for (const cb of bucket.slice()) {
180 try {
181 cb(d.payload, meta);
182 } catch {
183 }
184 }
185 }
186 const wildcard = channelSubs["*"];
187 if (wildcard) {
188 for (const cb of wildcard.slice()) {
189 try {
190 cb(d.payload, meta);
191 } catch {
192 }
193 }
194 }
195 }
196 });
197 const iframeApi = {
198 publish(topic, payload) {
199 if (typeof topic !== "string" || topic === "") {
200 return;
201 }
202 const ids = Object.keys(connections);
203 if (ids.length === 0) {
204 console.warn(
205 '[desktop-mode] wp.desktop.iframe.publish dropped: no open connection for topic "%s". The parent shell must call `wp.desktop.connect(windowId)` first.',
206 topic
207 );
208 return;
209 }
210 for (const id of ids) {
211 emitToParent(id, topic, payload);
212 }
213 },
214 subscribe(topic, cb) {
215 if (typeof topic !== "string" || topic === "" || typeof cb !== "function") {
216 return () => {
217 };
218 }
219 let bucket = subs[topic];
220 if (!bucket) {
221 bucket = [];
222 subs[topic] = bucket;
223 }
224 bucket.push(cb);
225 return () => {
226 const i = bucket.indexOf(cb);
227 if (i >= 0) {
228 bucket.splice(i, 1);
229 }
230 };
231 },
232 onConnection(cb) {
233 if (typeof cb !== "function") {
234 return () => {
235 };
236 }
237 connectionListeners.push(cb);
238 for (const id of Object.keys(connections)) {
239 try {
240 cb({
241 id: connections[id].id,
242 topics: connections[id].topics.slice()
243 });
244 } catch {
245 }
246 }
247 return () => {
248 const i = connectionListeners.indexOf(cb);
249 if (i >= 0) {
250 connectionListeners.splice(i, 1);
251 }
252 };
253 },
254 /**
255 * Iframe-initiated connection request. Asks the parent to open
256 * a connection back to this iframe. Returns a Promise that
257 * resolves with the new `{ id, topics }` once the parent acks
258 * (or rejects on timeout / refusal).
259 *
260 * Parent-side handler: see `src/connection/index.ts`
261 * `handleConnectionRequest` + the
262 * `desktop-mode.iframe.connection-request` filter.
263 */
264 chrome: {
265 setTheme(tokens) {
266 try {
267 window.parent.postMessage(
268 {
269 type: "desktop-mode-chrome-theme",
270 tokens: tokens ?? {}
271 },
272 parentOrigin
273 );
274 } catch {
275 }
276 },
277 setControls(config) {
278 try {
279 window.parent.postMessage(
280 {
281 type: "desktop-mode-chrome-controls",
282 config: config ?? null
283 },
284 parentOrigin
285 );
286 } catch {
287 }
288 },
289 setSlot(name, html) {
290 if (typeof name !== "string" || name === "") {
291 return;
292 }
293 try {
294 window.parent.postMessage(
295 {
296 type: "desktop-mode-chrome-slot",
297 slot: name,
298 html: typeof html === "string" ? html : ""
299 },
300 parentOrigin
301 );
302 } catch {
303 }
304 }
305 },
306 requestConnection(opts) {
307 const o = opts ?? {};
308 const topics = Array.isArray(o.topics) ? o.topics.slice() : [];
309 const requestId = "wpdir-" + Math.random().toString(36).slice(2, 10);
310 return new Promise((resolve, reject) => {
311 let settled = false;
312 const timeoutMs = typeof o.timeoutMs === "number" ? o.timeoutMs : 5e3;
313 const settle = (ok, value) => {
314 if (settled) {
315 return;
316 }
317 settled = true;
318 window.removeEventListener("message", onAck);
319 clearTimeout(timer);
320 if (ok) {
321 resolve(value);
322 } else {
323 reject(value);
324 }
325 };
326 const onAck = (ev) => {
327 if (ev.origin !== parentOrigin) {
328 return;
329 }
330 const d = ev?.data;
331 if (!d || typeof d !== "object" || d.type !== "desktop-mode-bridge-connection-ack" || d.requestId !== requestId) {
332 return;
333 }
334 if (d.accepted) {
335 const summary = {
336 id: typeof d.connectionId === "string" ? d.connectionId : "",
337 topics: topics.slice()
338 };
339 if (typeof o.onOpen === "function") {
340 try {
341 o.onOpen(summary);
342 } catch {
343 }
344 }
345 settle(true, summary);
346 } else {
347 settle(false, new Error(d.reason || "rejected"));
348 }
349 };
350 window.addEventListener("message", onAck);
351 const timer = setTimeout(() => {
352 settle(false, new Error("timeout"));
353 }, timeoutMs);
354 try {
355 window.parent.postMessage(
356 {
357 type: "desktop-mode-bridge-connection-request",
358 requestId,
359 topics
360 },
361 parentOrigin
362 );
363 } catch (err) {
364 settle(false, err);
365 }
366 });
367 },
368 get windowId() {
369 return _windowId;
370 },
371 whenWindowId() {
372 if (_windowId !== null) {
373 return Promise.resolve(_windowId);
374 }
375 return new Promise((resolve) => {
376 _windowIdWaiters.push(resolve);
377 });
378 },
379 isParentReachable() {
380 if (!window.parent || window.parent === window) {
381 return false;
382 }
383 try {
384 const parentOrig = window.parent.location.origin;
385 return parentOrig === parentOrigin;
386 } catch {
387 return false;
388 }
389 }
390 };
391 if (!w.wp) {
392 w.wp = {};
393 }
394 if (!w.wp.desktop) {
395 w.wp.desktop = {};
396 }
397 w.wp.desktop.iframe = iframeApi;
398 if (typeof w.wp.desktop.send !== "function") {
399 w.wp.desktop.send = (channel, payload) => {
400 if (typeof channel !== "string" || channel === "") {
401 return;
402 }
403 try {
404 window.parent.postMessage(
405 {
406 type: "desktop-mode-window-publish",
407 channel,
408 payload
409 },
410 parentOrigin
411 );
412 } catch {
413 }
414 };
415 }
416 if (typeof w.wp.desktop.on !== "function") {
417 w.wp.desktop.on = (channel, cb) => {
418 if (typeof channel !== "string" || channel === "" || typeof cb !== "function") {
419 return () => void 0;
420 }
421 let bucket = channelSubs[channel];
422 if (!bucket) {
423 bucket = [];
424 channelSubs[channel] = bucket;
425 }
426 bucket.push(cb);
427 return () => {
428 const i = bucket.indexOf(cb);
429 if (i >= 0) {
430 bucket.splice(i, 1);
431 }
432 };
433 };
434 }
435 const sentinelHost = window;
436 if (!sentinelHost.__desktopModeScreenMetaInstalled) {
437 sentinelHost.__desktopModeScreenMetaInstalled = true;
438 installScreenMetaHoist(parentOrigin);
439 }
440 if (!sentinelHost.__desktopModeOsFileDropForwarderInstalled) {
441 sentinelHost.__desktopModeOsFileDropForwarderInstalled = true;
442 const hasFiles = (ev) => {
443 const types = ev.dataTransfer?.types;
444 if (!types) {
445 return false;
446 }
447 const list = types;
448 if (typeof list.includes === "function") {
449 return list.includes("Files");
450 }
451 if (typeof list.contains === "function") {
452 return list.contains("Files");
453 }
454 for (let i = 0; i < list.length; i++) {
455 if (list[i] === "Files") {
456 return true;
457 }
458 }
459 return false;
460 };
461 const dropPassthroughSelectors = [
462 ".components-drop-zone",
463 "[data-drop-zone]",
464 ".uploader-window",
465 ".media-frame-content"
466 ];
467 const targetWantsFile = (target) => {
468 const el = target;
469 if (!el || !el.closest) {
470 return false;
471 }
472 for (const sel of dropPassthroughSelectors) {
473 if (el.closest(sel)) {
474 return true;
475 }
476 }
477 return false;
478 };
479 document.addEventListener(
480 "dragover",
481 (ev) => {
482 if (!hasFiles(ev)) {
483 return;
484 }
485 if (targetWantsFile(ev.target)) {
486 return;
487 }
488 if (ev.defaultPrevented) {
489 return;
490 }
491 ev.preventDefault();
492 if (ev.dataTransfer) {
493 ev.dataTransfer.dropEffect = "copy";
494 }
495 },
496 false
497 );
498 document.addEventListener(
499 "drop",
500 (ev) => {
501 if (!hasFiles(ev)) {
502 return;
503 }
504 if (targetWantsFile(ev.target)) {
505 return;
506 }
507 if (ev.defaultPrevented) {
508 return;
509 }
510 ev.preventDefault();
511 ev.stopPropagation();
512 const files = [];
513 if (ev.dataTransfer?.files) {
514 for (let i = 0; i < ev.dataTransfer.files.length; i++) {
515 files.push(ev.dataTransfer.files[i]);
516 }
517 }
518 if (files.length === 0) {
519 return;
520 }
521 try {
522 window.parent.postMessage(
523 {
524 type: "desktop-mode-os-file-drop",
525 files,
526 x: ev.clientX,
527 y: ev.clientY
528 },
529 parentOrigin
530 );
531 } catch {
532 }
533 },
534 false
535 );
536 }
537 if (!sentinelHost.__desktopModeDragHoverForwarderInstalled) {
538 sentinelHost.__desktopModeDragHoverForwarderInstalled = true;
539 const hoverHasFiles = (ev) => {
540 const types = ev.dataTransfer?.types;
541 if (!types) {
542 return false;
543 }
544 const list = types;
545 if (typeof list.includes === "function") {
546 return list.includes("Files");
547 }
548 return typeof list.contains === "function" && list.contains("Files");
549 };
550 let dragHoverLastSent = 0;
551 document.addEventListener(
552 "dragover",
553 (ev) => {
554 const now = Date.now();
555 if (now - dragHoverLastSent < 150) {
556 return;
557 }
558 dragHoverLastSent = now;
559 try {
560 window.parent.postMessage(
561 {
562 type: "desktop-mode-drag-hover",
563 payloadType: hoverHasFiles(ev) ? "os-file" : "external"
564 },
565 parentOrigin
566 );
567 } catch {
568 }
569 },
570 true
571 );
572 }
573 try {
574 if (window.parent && window.parent !== window) {
575 window.parent.postMessage(
576 { type: "desktop-mode-ready" },
577 parentOrigin
578 );
579 }
580 } catch {
581 }
582 function installScreenMetaHoist(origin) {
583 const hasScreenOptionsContent = () => {
584 const wrap = document.getElementById("screen-options-wrap");
585 return !!wrap && !!wrap.querySelector(
586 'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]), select, textarea'
587 );
588 };
589 const hasHelpContent = () => {
590 const wrap = document.getElementById("contextual-help-wrap");
591 if (!wrap) {
592 return false;
593 }
594 const panelEls = wrap.querySelectorAll(
595 ".help-tab-content, .contextual-help-sidebar"
596 );
597 for (let i = 0; i < panelEls.length; i++) {
598 if ((panelEls[i].textContent || "").trim() !== "") {
599 return true;
600 }
601 }
602 return false;
603 };
604 const start = () => {
605 const links = document.getElementById("screen-meta-links");
606 const screenOptionsBtn = links ? document.getElementById("show-settings-link") : null;
607 const helpBtn = links ? document.getElementById("contextual-help-link") : null;
608 const panels = [];
609 if (screenOptionsBtn && hasScreenOptionsContent()) {
610 panels.push("screen-options");
611 }
612 if (helpBtn && hasHelpContent()) {
613 panels.push("help");
614 }
615 try {
616 window.parent.postMessage(
617 { type: "desktop-mode-screen-meta", panels },
618 origin
619 );
620 } catch {
621 }
622 if (panels.length === 0) {
623 return;
624 }
625 const getOpenPanel = () => {
626 if (screenOptionsBtn && screenOptionsBtn.getAttribute("aria-expanded") === "true") {
627 return "screen-options";
628 }
629 if (helpBtn && helpBtn.getAttribute("aria-expanded") === "true") {
630 return "help";
631 }
632 return null;
633 };
634 const reportState = () => {
635 try {
636 window.parent.postMessage(
637 {
638 type: "desktop-mode-screen-meta-state",
639 open: getOpenPanel()
640 },
641 origin
642 );
643 } catch {
644 }
645 };
646 reportState();
647 const observer = new MutationObserver(reportState);
648 if (screenOptionsBtn) {
649 observer.observe(screenOptionsBtn, {
650 attributes: true,
651 attributeFilter: ["aria-expanded"]
652 });
653 }
654 if (helpBtn) {
655 observer.observe(helpBtn, {
656 attributes: true,
657 attributeFilter: ["aria-expanded"]
658 });
659 }
660 const forceClose = (button) => {
661 if (!button || button.getAttribute("aria-expanded") !== "true") {
662 return;
663 }
664 const panelId = button.getAttribute("aria-controls");
665 const panel = panelId ? document.getElementById(panelId) : null;
666 if (!panel) {
667 return;
668 }
669 const jq = window.jQuery;
670 if (jq) {
671 try {
672 jq(panel).stop(true, false);
673 } catch {
674 }
675 }
676 panel.style.display = "none";
677 panel.classList.add("hidden");
678 if (panel.parentElement) {
679 panel.parentElement.style.display = "none";
680 }
681 button.classList.remove("screen-meta-active");
682 button.setAttribute("aria-expanded", "false");
683 const toggles = document.querySelectorAll(
684 ".screen-meta-toggle"
685 );
686 toggles.forEach((t) => {
687 t.style.visibility = "";
688 });
689 };
690 window.addEventListener("message", (e) => {
691 if (e.origin !== origin) {
692 return;
693 }
694 const d = e.data;
695 if (!d || d.type !== "desktop-mode-toggle-panel") {
696 return;
697 }
698 let target = null;
699 if (d.panel === "screen-options" && screenOptionsBtn) {
700 target = screenOptionsBtn;
701 } else if (d.panel === "help" && helpBtn) {
702 target = helpBtn;
703 }
704 if (!target) {
705 return;
706 }
707 if (target.getAttribute("aria-expanded") !== "true") {
708 forceClose(
709 target === screenOptionsBtn ? helpBtn : screenOptionsBtn
710 );
711 }
712 target.click();
713 });
714 };
715 if (document.readyState === "loading") {
716 document.addEventListener("DOMContentLoaded", start, { once: true });
717 } else {
718 start();
719 }
720 }
721 })();
722 })();
723