PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
1.1.11 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 All 35 releases
← All changes | assets/js/admin-bar.js +8 -445 1.1.8 → 1.1.11 View file →
@@ -1,5 +1,9 @@
1 1 ( function() {
2 + // The "Switch to OpenStation" node only renders in classic admin —
3 + // PHP skips it once the user is viewing the shell, where the
4 + // "Exit OpenStation" dock tile is the way back out. So this handler
5 + // only ever enables.
2 6 var toggle = document.getElementById( 'wp-admin-bar-os-toggle' );
3 7 if ( ! toggle ) {
4 8 return;
5 9 }
@@ -5,15 +9,12 @@
5 9 }
6 10 var cfg = window.openStationAdminBar || {};
7 11 toggle.addEventListener( 'click', function( e ) {
8 12 e.preventDefault();
9 - var isActive = !! cfg.active;
10 - var newValue = isActive ? '' : '1';
11 - // Fallback targets if the server response is missing a `redirect`
13 + // Fallback target if the server response is missing a `redirect`
12 14 // 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;
15 + // way): the portal URL, so the shell takes over.
16 + var fallback = cfg.portalUrl;
16 17 // The toggle lives in an admin bar that may be rendered either in
17 18 // the top window (classic) or — today it's suppressed in iframes,
18 19 // but a plugin could surface it — inside a chromeless iframe. In
19 20 // either case we want the ENTIRE browser tab to navigate, so we
@@ -28,9 +29,9 @@
28 29 }
29 30 var body = new URLSearchParams();
30 31 body.set( 'action', 'save-openstation' );
31 32 body.set( 'nonce', cfg.nonce );
32 - body.set( 'enabled', newValue );
33 + body.set( 'enabled', '1' );
33 34 // Tells the handler which admin this click came from; see the
34 35 // note in `includes/ajax.php`.
35 36 if ( cfg.network ) {
36 37 body.set( 'network', '1' );
@@ -52,443 +53,5 @@
52 53 navigate( target );
53 54 };
54 55 xhr.send( body.toString() );
55 56 } );
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 57 } )();