PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
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.10, at assets/js/admin-bar.js

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