PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.4
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.4
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 / admin-bar.js

admin-bar.js in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.4, at assets/js/admin-bar.js

490 lines 17.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 ( function() {
2 var toggle = document.getElementById( 'wp-admin-bar-os-toggle' );
3 if ( ! toggle ) {
4 return;
5 }
6 var cfg = window.openStationAdminBar || {};
7 toggle.addEventListener( 'click', function( e ) {
8 e.preventDefault();
9 var isActive = !! cfg.active;
10 var newValue = isActive ? '' : '1';
11 // Fallback targets if the server response is missing a `redirect`
12 // field (shouldn't happen, but keep the click functional either
13 // way). Disabling -> classic admin (NOT the portal, which would
14 // auto-re-enable); enabling -> portal URL so the shell takes over.
15 var fallback = isActive ? cfg.classicUrl : cfg.portalUrl;
16 // The toggle lives in an admin bar that may be rendered either in
17 // the top window (classic) or — today it's suppressed in iframes,
18 // but a plugin could surface it — inside a chromeless iframe. In
19 // either case we want the ENTIRE browser tab to navigate, so we
20 // hit `window.top` and fall back to `window` if cross-origin
21 // security blocks access.
22 function navigate( url ) {
23 try {
24 window.top.location.href = url;
25 } catch ( err ) {
26 window.location.href = url;
27 }
28 }
29 var body = new URLSearchParams();
30 body.set( 'action', 'save-openstation' );
31 body.set( 'nonce', cfg.nonce );
32 body.set( 'enabled', newValue );
33 var xhr = new XMLHttpRequest();
34 xhr.open( 'POST', cfg.ajaxUrl, true );
35 xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
36 xhr.onload = function() {
37 if ( xhr.status !== 200 ) {
38 return;
39 }
40 var target = fallback;
41 try {
42 var resp = JSON.parse( xhr.responseText );
43 if ( resp && resp.success && resp.data && resp.data.redirect ) {
44 target = resp.data.redirect;
45 }
46 } catch ( parseErr ) {}
47 navigate( target );
48 };
49 xhr.send( body.toString() );
50 } );
51
52 // Layout menu — each child item calls a WindowManager method on
53 // the public shell API. We bind one delegated click listener on
54 // the parent submenu so adding more layouts in the future
55 // (split, full-width, etc.) is a matter of adding nodes in PHP,
56 // not new JS. `href=#` is set server-side; we preventDefault +
57 // intercept.
58 //
59 // The snap-to-grid checkbox is special: clicking it toggles the
60 // preference AND repaints the box without dismissing the menu
61 // (default WP behaviour would close the submenu on any click,
62 // breaking the "set it and forget it" feel of a checkbox).
63 var layoutMenu = document.getElementById( 'wp-admin-bar-desktop-layout-menu' );
64 if ( ! layoutMenu ) return;
65
66 function paintSnapCheckbox( enabled ) {
67 var node = document.querySelector(
68 '#wp-admin-bar-desktop-layout-snap .os-layout-checkbox'
69 );
70 if ( ! node ) return;
71 node.textContent = enabled ? '' : ''; // ☑ / ☐
72 var item = document.getElementById( 'wp-admin-bar-desktop-layout-snap' );
73 if ( item ) {
74 item.setAttribute( 'aria-checked', enabled ? 'true' : 'false' );
75 item.setAttribute( 'role', 'menuitemcheckbox' );
76 }
77 }
78
79 function getManager() {
80 return window.wp && window.wp.os && window.wp.os.windowManager;
81 }
82
83 // Initial paint — wait for the shell to publish the manager,
84 // then mirror the persisted snap preference. Polled rather than
85 // hooked because the inline script ships with the admin bar
86 // (loads early) and the shell's WindowManager arrives later.
87 //
88 // Bounded, because a poll with no exit is a timer that runs for
89 // the life of the page. The manager lands within a frame or two of
90 // the shell bundle executing; if it has not arrived in ten seconds
91 // it is not coming — the shell failed to boot, or a plugin
92 // surfaced this admin bar somewhere the shell never loads — and
93 // repainting one checkbox does not justify waking the event loop
94 // sixteen times a second until the tab closes. Giving up leaves
95 // the server-rendered box exactly as it is, which is what polling
96 // forever achieves anyway.
97 var SNAP_POLL_INTERVAL_MS = 60;
98 var SNAP_POLL_TIMEOUT_MS = 10000;
99 var snapPollWaited = 0;
100 function initFromManager() {
101 var wm = getManager();
102 if ( ! wm || typeof wm.isSnapEnabled !== 'function' ) {
103 snapPollWaited += SNAP_POLL_INTERVAL_MS;
104 if ( snapPollWaited < SNAP_POLL_TIMEOUT_MS ) {
105 window.setTimeout( initFromManager, SNAP_POLL_INTERVAL_MS );
106 }
107 return;
108 }
109 paintSnapCheckbox( wm.isSnapEnabled() );
110 }
111 initFromManager();
112
113 layoutMenu.addEventListener( 'click', function( e ) {
114 var t = e.target;
115 if ( ! t || ! t.closest ) return;
116
117 var snapItem = t.closest( '.os-layout-snap' );
118 if ( snapItem ) {
119 // Stop propagation so WP's own "click closes submenu"
120 // chain never fires. preventDefault keeps the `#` href
121 // from scrolling the page to top.
122 e.preventDefault();
123 e.stopPropagation();
124 var wm = getManager();
125 if ( ! wm || typeof wm.setSnapEnabled !== 'function' ) return;
126 var next = ! wm.isSnapEnabled();
127 wm.setSnapEnabled( next );
128 paintSnapCheckbox( next );
129 return;
130 }
131
132 var actionLink = t.closest( '.os-layout-action > .ab-item, .os-layout-action' );
133 if ( ! actionLink ) return;
134 e.preventDefault();
135 var id = actionLink.closest( '[id^="wp-admin-bar-desktop-layout-"]' );
136 if ( ! id ) return;
137 var manager = getManager();
138 if ( ! manager ) return;
139 if ( id.id === 'wp-admin-bar-desktop-layout-cascade' && typeof manager.cascade === 'function' ) {
140 manager.cascade();
141 } else if ( id.id === 'wp-admin-bar-desktop-layout-overview' && typeof manager.enterOverview === 'function' ) {
142 manager.enterOverview();
143 } else if ( id.id === 'wp-admin-bar-desktop-layout-tile' && typeof manager.tile === 'function' ) {
144 manager.tile();
145 } else if ( id.id.indexOf( 'wp-admin-bar-desktop-layout-custom-' ) === 0 ) {
146 // Plugin-registered custom item. Strip the shared prefix to
147 // recover the `id` the plugin supplied via the PHP filter,
148 // then dispatch the public JS action. Plugins subscribe
149 // via wp.hooks.addAction( 'os.arrange.custom-action', ... ).
150 var customId = id.id.replace( 'wp-admin-bar-desktop-layout-custom-', '' );
151 var hooks = window.wp && window.wp.hooks;
152 if ( hooks && typeof hooks.doAction === 'function' ) {
153 hooks.doAction( 'os.arrange.custom-action', { id: customId } );
154 }
155 }
156 // After running an action, dismiss the submenu so the user
157 // lands in the newly arranged desktop instead of the menu
158 // hanging open on top. WP's admin bar toggles visibility via
159 // a `.hover` class on the parent `li.menupop` — we remove it
160 // AND blur the active element so a re-hover is required for
161 // the next open. The snap checkbox stays open by design
162 // (it's handled by the earlier branch and never reaches
163 // this close path).
164 layoutMenu.classList.remove( 'hover' );
165 if ( document.activeElement && typeof document.activeElement.blur === 'function' ) {
166 document.activeElement.blur();
167 }
168 } );
169
170 // Fullscreen button — toggles browser fullscreen on the document
171 // element so the shell occupies the whole screen with no browser
172 // chrome. The click counts as a user gesture, which is required by
173 // the Fullscreen API. We listen for `fullscreenchange` instead of
174 // flipping state inside the click handler because the user can also
175 // exit via Esc or the OS, and we want the icon/label to stay in sync
176 // in those cases too.
177 var fsBtn = document.getElementById( 'wp-admin-bar-desktop-fullscreen' );
178 if ( fsBtn ) {
179 var fsLink = fsBtn.querySelector( '.ab-item' );
180 var fsLabel = fsBtn.querySelector( '.ab-label' );
181 var fsI18n = ( cfg && cfg.i18n ) || {};
182 // WP renders admin-bar items as `<a href="#">`, which HTML5
183 // marks implicitly draggable. In browser fullscreen the button
184 // sits at top:0 where cursor reveals (menu bar, exit
185 // affordance) produce enough pointer jitter between mousedown
186 // and mouseup that the browser commits to a native link-drag
187 // instead of a click — the handler never runs and the user
188 // sees a ghost flicker. Opt the element out of native drag so
189 // the click always lands.
190 if ( fsLink ) {
191 fsLink.setAttribute( 'draggable', 'false' );
192 }
193 function paintFullscreen( on ) {
194 // Body class drives the CSS that hides admin-bar items
195 // which sit too close to the screen-corner reveal hot zones
196 // in browser fullscreen — the OS / browser intercept clicks
197 // in those bands. Hiding them shifts the Fullscreen button
198 // further from the right edge so its own click lands.
199 document.body.classList.toggle( 'os-browser-fs', !! on );
200 if ( on ) {
201 fsBtn.classList.add( 'is-fullscreen' );
202 if ( fsLabel ) fsLabel.textContent = fsI18n.exitFullscreen || 'Exit fullscreen';
203 repaintLabel( fsLink, fsI18n.exitTitle || 'Exit fullscreen' );
204 } else {
205 fsBtn.classList.remove( 'is-fullscreen' );
206 if ( fsLabel ) fsLabel.textContent = fsI18n.enterFullscreen || 'Fullscreen';
207 repaintLabel( fsLink, fsI18n.enterTitle || 'Enter fullscreen' );
208 }
209 }
210 fsBtn.addEventListener( 'click', function( e ) {
211 e.preventDefault();
212 // `window.top` so the whole tab goes fullscreen even when the
213 // admin bar happens to be rendered inside an iframe (today
214 // it's suppressed there, but a plugin could surface it). Fall
215 // back to the current document if cross-origin blocks access.
216 var doc;
217 try {
218 doc = window.top.document;
219 } catch ( err ) {
220 doc = document;
221 }
222 if ( doc.fullscreenElement ) {
223 if ( doc.exitFullscreen ) {
224 doc.exitFullscreen();
225 }
226 } else if ( doc.documentElement && doc.documentElement.requestFullscreen ) {
227 doc.documentElement.requestFullscreen();
228 }
229 } );
230 // Listen on whichever document we're driving so OS/Esc exits
231 // repaint the button too. Bound on both to cover the iframe edge
232 // case without extra branching.
233 document.addEventListener( 'fullscreenchange', function() {
234 paintFullscreen( !! document.fullscreenElement );
235 } );
236 try {
237 if ( window.top.document !== document ) {
238 window.top.document.addEventListener( 'fullscreenchange', function() {
239 paintFullscreen( !! window.top.document.fullscreenElement );
240 } );
241 }
242 } catch ( err ) {}
243 }
244
245 // Bug Report button — same decoupling pattern as Ask AI. Dispatches
246 // `os-open-bug-report`; the shell answers by opening the
247 // Bug Report native window. Wired in `src/desktop.ts`.
248 var bugBtn = document.getElementById( 'wp-admin-bar-desktop-bug-report' );
249 if ( bugBtn ) {
250 bugBtn.addEventListener( 'click', function( e ) {
251 e.preventDefault();
252 document.dispatchEvent( new CustomEvent( 'os-open-bug-report' ) );
253 } );
254 }
255
256 // Keyboard Shortcuts button — toggles a floating popover anchored
257 // under the button, styled like the Arrange submenu. Content is
258 // passed in via `cfg.shortcuts` (translated server-side in PHP).
259 wireShortcutsPopover( document.getElementById( 'wp-admin-bar-desktop-help' ), cfg.shortcuts );
260
261 // Custom tooltips for the icon-only admin-bar buttons. The native
262 // `title` attribute renders a slow, OS-styled tooltip that conflicts
263 // with our dock-style custom tooltip; we swap each `title` to
264 // `data-desktop-tooltip` + `aria-label` so screen readers still see
265 // the label but the visual tooltip is fully ours.
266 wireTooltipsFor( [
267 'wp-admin-bar-desktop-fullscreen',
268 'wp-admin-bar-desktop-help',
269 'wp-admin-bar-desktop-bug-report',
270 'wp-admin-bar-desktop-layout-menu',
271 ] );
272
273 /**
274 * Re-anchors a native `title` attribute to a `data-desktop-tooltip`
275 * data attribute on the same node, plus mirrors it to `aria-label`
276 * so assistive tech keeps the description. Pure-CSS tooltip then
277 * renders from the data attribute via the
278 * `.ab-item[ data-desktop-tooltip ]::after` rule in admin-bar.php.
279 */
280 function wireTooltipsFor( ids ) {
281 for ( var i = 0; i < ids.length; i++ ) {
282 var node = document.getElementById( ids[ i ] );
283 if ( ! node ) continue;
284 var link = node.querySelector( '.ab-item' );
285 if ( ! link ) continue;
286 var label = link.getAttribute( 'title' );
287 if ( ! label ) continue;
288 link.removeAttribute( 'title' );
289 link.setAttribute( 'data-desktop-tooltip', label );
290 if ( ! link.hasAttribute( 'aria-label' ) ) {
291 link.setAttribute( 'aria-label', label );
292 }
293 }
294 }
295
296 /**
297 * Relabels a button whose action changed under it (today: Fullscreen,
298 * which flips between enter and exit). Writing only `title` is wrong
299 * once `wireTooltipsFor()` has run: the visible tooltip renders from
300 * `data-desktop-tooltip` and the accessible name comes from
301 * `aria-label`, so a stale pair describes the opposite action while
302 * the freshly re-added `title` brings the native OS tooltip back on
303 * top of ours (GH#493).
304 *
305 * `title` is only touched when the node is still wearing one, which
306 * makes this safe to call before wiring as well — the label lands on
307 * `title` and `wireTooltipsFor()` re-anchors it from there.
308 */
309 function repaintLabel( link, label ) {
310 if ( ! link ) return;
311 if ( link.hasAttribute( 'title' ) ) {
312 link.setAttribute( 'title', label );
313 }
314 if ( link.hasAttribute( 'data-desktop-tooltip' ) ) {
315 link.setAttribute( 'data-desktop-tooltip', label );
316 }
317 link.setAttribute( 'aria-label', label );
318 }
319
320 function wireShortcutsPopover( btn, data ) {
321 if ( ! btn || ! data ) {
322 return;
323 }
324 var anchor = btn.querySelector( '.ab-item' ) || btn;
325 var popover = buildShortcutsPopover( data );
326 btn.appendChild( popover );
327
328 var isOpen = false;
329 function open() {
330 if ( isOpen ) return;
331 isOpen = true;
332 popover.classList.add( 'is-open' );
333 btn.classList.add( 'is-open' );
334 document.addEventListener( 'mousedown', onDocMouseDown, true );
335 document.addEventListener( 'keydown', onKey, true );
336 }
337 function close() {
338 if ( ! isOpen ) return;
339 isOpen = false;
340 popover.classList.remove( 'is-open' );
341 btn.classList.remove( 'is-open' );
342 document.removeEventListener( 'mousedown', onDocMouseDown, true );
343 document.removeEventListener( 'keydown', onKey, true );
344 }
345 function onDocMouseDown( e ) {
346 if ( btn.contains( e.target ) ) return;
347 close();
348 }
349 function onKey( e ) {
350 if ( e.key === 'Escape' ) {
351 e.stopPropagation();
352 close();
353 }
354 }
355
356 anchor.addEventListener( 'click', function( e ) {
357 e.preventDefault();
358 e.stopPropagation();
359 isOpen ? close() : open();
360 } );
361 // Swallow stray clicks inside the popover so they don't bubble
362 // up to admin-bar handlers (and so the popover stays open while
363 // the user reads it).
364 popover.addEventListener( 'mousedown', function( e ) {
365 e.stopPropagation();
366 } );
367 popover.addEventListener( 'click', function( e ) {
368 e.stopPropagation();
369 } );
370 }
371
372 function buildShortcutsPopover( data ) {
373 var root = document.createElement( 'div' );
374 root.className = 'os-shortcuts-popover';
375 root.setAttribute( 'role', 'dialog' );
376 if ( data.title ) {
377 root.setAttribute( 'aria-label', data.title );
378 }
379
380 if ( data.contextual ) {
381 root.appendChild( buildShortcutsTable( data.contextual ) );
382 }
383 if ( data.general ) {
384 root.appendChild( buildShortcutsList( data.general ) );
385 }
386 return root;
387 }
388
389 function buildShortcutsTable( section ) {
390 var wrap = document.createElement( 'section' );
391 wrap.className = 'os-shortcuts-popover__section';
392
393 if ( section.heading ) {
394 var h = document.createElement( 'h3' );
395 h.className = 'os-shortcuts-popover__heading';
396 h.textContent = section.heading;
397 wrap.appendChild( h );
398 }
399
400 var table = document.createElement( 'table' );
401 table.className = 'os-shortcuts-popover__table';
402
403 var thead = document.createElement( 'thead' );
404 var headRow = document.createElement( 'tr' );
405 [ 'key', 'outside', 'inside', 'showDesktop' ].forEach( function( col ) {
406 var th = document.createElement( 'th' );
407 th.scope = 'col';
408 th.textContent = section.headers && section.headers[ col ] ? section.headers[ col ] : '';
409 headRow.appendChild( th );
410 } );
411 thead.appendChild( headRow );
412 table.appendChild( thead );
413
414 var tbody = document.createElement( 'tbody' );
415 ( section.rows || [] ).forEach( function( row ) {
416 var tr = document.createElement( 'tr' );
417
418 var keyCell = document.createElement( 'td' );
419 keyCell.className = 'os-shortcuts-popover__key-cell';
420 keyCell.appendChild( renderKeys( row.keys || [] ) );
421 if ( row.note ) {
422 var note = document.createElement( 'span' );
423 note.className = 'os-shortcuts-popover__note';
424 note.textContent = ' ' + row.note;
425 keyCell.appendChild( note );
426 }
427 tr.appendChild( keyCell );
428
429 [ 'outside', 'inside', 'showDesktop' ].forEach( function( col ) {
430 var td = document.createElement( 'td' );
431 td.textContent = row[ col ] || '';
432 tr.appendChild( td );
433 } );
434
435 tbody.appendChild( tr );
436 } );
437 table.appendChild( tbody );
438 wrap.appendChild( table );
439
440 return wrap;
441 }
442
443 function buildShortcutsList( section ) {
444 var wrap = document.createElement( 'section' );
445 wrap.className = 'os-shortcuts-popover__section';
446
447 if ( section.heading ) {
448 var h = document.createElement( 'h3' );
449 h.className = 'os-shortcuts-popover__heading';
450 h.textContent = section.heading;
451 wrap.appendChild( h );
452 }
453
454 var list = document.createElement( 'ul' );
455 list.className = 'os-shortcuts-popover__list';
456 ( section.items || [] ).forEach( function( item ) {
457 var li = document.createElement( 'li' );
458 li.className = 'os-shortcuts-popover__item';
459 li.appendChild( renderKeys( item.keys || [] ) );
460 var desc = document.createElement( 'span' );
461 desc.className = 'os-shortcuts-popover__description';
462 desc.textContent = item.description || '';
463 li.appendChild( desc );
464 list.appendChild( li );
465 } );
466 wrap.appendChild( list );
467
468 return wrap;
469 }
470
471 function renderKeys( keys ) {
472 var span = document.createElement( 'span' );
473 span.className = 'os-shortcuts-popover__keys';
474 keys.forEach( function( key, idx ) {
475 if ( idx > 0 ) {
476 var plus = document.createElement( 'span' );
477 plus.className = 'os-shortcuts-popover__plus';
478 plus.textContent = '+';
479 plus.setAttribute( 'aria-hidden', 'true' );
480 span.appendChild( plus );
481 }
482 var kbd = document.createElement( 'kbd' );
483 kbd.className = 'os-shortcuts-popover__kbd';
484 kbd.textContent = key;
485 span.appendChild( kbd );
486 } );
487 return span;
488 }
489 } )();
490