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 / admin-bar.js

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

450 lines 16.3 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-desktop-mode-toggle' );
3 if ( ! toggle ) {
4 return;
5 }
6 var cfg = window.desktopModeAdminBar || {};
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-desktop-mode' );
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 .desktop-mode-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.desktop && window.wp.desktop.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 function initFromManager() {
88 var wm = getManager();
89 if ( ! wm || typeof wm.isSnapEnabled !== 'function' ) {
90 window.setTimeout( initFromManager, 60 );
91 return;
92 }
93 paintSnapCheckbox( wm.isSnapEnabled() );
94 }
95 initFromManager();
96
97 layoutMenu.addEventListener( 'click', function( e ) {
98 var t = e.target;
99 if ( ! t || ! t.closest ) return;
100
101 var snapItem = t.closest( '.desktop-mode-layout-snap' );
102 if ( snapItem ) {
103 // Stop propagation so WP's own "click closes submenu"
104 // chain never fires. preventDefault keeps the `#` href
105 // from scrolling the page to top.
106 e.preventDefault();
107 e.stopPropagation();
108 var wm = getManager();
109 if ( ! wm || typeof wm.setSnapEnabled !== 'function' ) return;
110 var next = ! wm.isSnapEnabled();
111 wm.setSnapEnabled( next );
112 paintSnapCheckbox( next );
113 return;
114 }
115
116 var actionLink = t.closest( '.desktop-mode-layout-action > .ab-item, .desktop-mode-layout-action' );
117 if ( ! actionLink ) return;
118 e.preventDefault();
119 var id = actionLink.closest( '[id^="wp-admin-bar-desktop-layout-"]' );
120 if ( ! id ) return;
121 var manager = getManager();
122 if ( ! manager ) return;
123 if ( id.id === 'wp-admin-bar-desktop-layout-cascade' && typeof manager.cascade === 'function' ) {
124 manager.cascade();
125 } else if ( id.id === 'wp-admin-bar-desktop-layout-overview' && typeof manager.enterOverview === 'function' ) {
126 manager.enterOverview();
127 } else if ( id.id === 'wp-admin-bar-desktop-layout-tile' && typeof manager.tile === 'function' ) {
128 manager.tile();
129 } else if ( id.id.indexOf( 'wp-admin-bar-desktop-layout-custom-' ) === 0 ) {
130 // Plugin-registered custom item. Strip the shared prefix to
131 // recover the `id` the plugin supplied via the PHP filter,
132 // then dispatch the public JS action. Plugins subscribe
133 // via wp.hooks.addAction( 'desktop-mode.arrange.custom-action', ... ).
134 var customId = id.id.replace( 'wp-admin-bar-desktop-layout-custom-', '' );
135 var hooks = window.wp && window.wp.hooks;
136 if ( hooks && typeof hooks.doAction === 'function' ) {
137 hooks.doAction( 'desktop-mode.arrange.custom-action', { id: customId } );
138 }
139 }
140 // After running an action, dismiss the submenu so the user
141 // lands in the newly arranged desktop instead of the menu
142 // hanging open on top. WP's admin bar toggles visibility via
143 // a `.hover` class on the parent `li.menupop` — we remove it
144 // AND blur the active element so a re-hover is required for
145 // the next open. The snap checkbox stays open by design
146 // (it's handled by the earlier branch and never reaches
147 // this close path).
148 layoutMenu.classList.remove( 'hover' );
149 if ( document.activeElement && typeof document.activeElement.blur === 'function' ) {
150 document.activeElement.blur();
151 }
152 } );
153
154 // Fullscreen button — toggles browser fullscreen on the document
155 // element so the shell occupies the whole screen with no browser
156 // chrome. The click counts as a user gesture, which is required by
157 // the Fullscreen API. We listen for `fullscreenchange` instead of
158 // flipping state inside the click handler because the user can also
159 // exit via Esc or the OS, and we want the icon/label to stay in sync
160 // in those cases too.
161 var fsBtn = document.getElementById( 'wp-admin-bar-desktop-fullscreen' );
162 if ( fsBtn ) {
163 var fsLink = fsBtn.querySelector( '.ab-item' );
164 var fsLabel = fsBtn.querySelector( '.ab-label' );
165 var fsI18n = ( cfg && cfg.i18n ) || {};
166 // WP renders admin-bar items as `<a href="#">`, which HTML5
167 // marks implicitly draggable. In browser fullscreen the button
168 // sits at top:0 where cursor reveals (menu bar, exit
169 // affordance) produce enough pointer jitter between mousedown
170 // and mouseup that the browser commits to a native link-drag
171 // instead of a click — the handler never runs and the user
172 // sees a ghost flicker. Opt the element out of native drag so
173 // the click always lands.
174 if ( fsLink ) {
175 fsLink.setAttribute( 'draggable', 'false' );
176 }
177 function paintFullscreen( on ) {
178 // Body class drives the CSS that hides admin-bar items
179 // which sit too close to the screen-corner reveal hot zones
180 // in browser fullscreen — the OS / browser intercept clicks
181 // in those bands. Hiding them shifts the Fullscreen button
182 // further from the right edge so its own click lands.
183 document.body.classList.toggle( 'desktop-mode-browser-fs', !! on );
184 if ( on ) {
185 fsBtn.classList.add( 'is-fullscreen' );
186 if ( fsLabel ) fsLabel.textContent = fsI18n.exitFullscreen || 'Exit fullscreen';
187 if ( fsLink ) fsLink.setAttribute( 'title', fsI18n.exitTitle || 'Exit fullscreen' );
188 } else {
189 fsBtn.classList.remove( 'is-fullscreen' );
190 if ( fsLabel ) fsLabel.textContent = fsI18n.enterFullscreen || 'Fullscreen';
191 if ( fsLink ) fsLink.setAttribute( 'title', fsI18n.enterTitle || 'Enter fullscreen' );
192 }
193 }
194 fsBtn.addEventListener( 'click', function( e ) {
195 e.preventDefault();
196 // `window.top` so the whole tab goes fullscreen even when the
197 // admin bar happens to be rendered inside an iframe (today
198 // it's suppressed there, but a plugin could surface it). Fall
199 // back to the current document if cross-origin blocks access.
200 var doc;
201 try {
202 doc = window.top.document;
203 } catch ( err ) {
204 doc = document;
205 }
206 if ( doc.fullscreenElement ) {
207 if ( doc.exitFullscreen ) {
208 doc.exitFullscreen();
209 }
210 } else if ( doc.documentElement && doc.documentElement.requestFullscreen ) {
211 doc.documentElement.requestFullscreen();
212 }
213 } );
214 // Listen on whichever document we're driving so OS/Esc exits
215 // repaint the button too. Bound on both to cover the iframe edge
216 // case without extra branching.
217 document.addEventListener( 'fullscreenchange', function() {
218 paintFullscreen( !! document.fullscreenElement );
219 } );
220 try {
221 if ( window.top.document !== document ) {
222 window.top.document.addEventListener( 'fullscreenchange', function() {
223 paintFullscreen( !! window.top.document.fullscreenElement );
224 } );
225 }
226 } catch ( err ) {}
227 }
228
229 // Bug Report button — same decoupling pattern as Ask AI. Dispatches
230 // `desktop-mode-open-bug-report`; the shell answers by opening the
231 // Bug Report native window. Wired in `src/desktop.ts`.
232 var bugBtn = document.getElementById( 'wp-admin-bar-desktop-bug-report' );
233 if ( bugBtn ) {
234 bugBtn.addEventListener( 'click', function( e ) {
235 e.preventDefault();
236 document.dispatchEvent( new CustomEvent( 'desktop-mode-open-bug-report' ) );
237 } );
238 }
239
240 // Keyboard Shortcuts button — toggles a floating popover anchored
241 // under the button, styled like the Arrange submenu. Content is
242 // passed in via `cfg.shortcuts` (translated server-side in PHP).
243 wireShortcutsPopover( document.getElementById( 'wp-admin-bar-desktop-help' ), cfg.shortcuts );
244
245 // Custom tooltips for the icon-only admin-bar buttons. The native
246 // `title` attribute renders a slow, OS-styled tooltip that conflicts
247 // with our dock-style custom tooltip; we swap each `title` to
248 // `data-desktop-tooltip` + `aria-label` so screen readers still see
249 // the label but the visual tooltip is fully ours.
250 wireTooltipsFor( [
251 'wp-admin-bar-desktop-fullscreen',
252 'wp-admin-bar-desktop-help',
253 'wp-admin-bar-desktop-bug-report',
254 'wp-admin-bar-desktop-layout-menu',
255 ] );
256
257 /**
258 * Re-anchors a native `title` attribute to a `data-desktop-tooltip`
259 * data attribute on the same node, plus mirrors it to `aria-label`
260 * so assistive tech keeps the description. Pure-CSS tooltip then
261 * renders from the data attribute via the `[data-desktop-tooltip]
262 * .ab-item::after` rule in admin-bar.php.
263 */
264 function wireTooltipsFor( ids ) {
265 for ( var i = 0; i < ids.length; i++ ) {
266 var node = document.getElementById( ids[ i ] );
267 if ( ! node ) continue;
268 var link = node.querySelector( '.ab-item' );
269 if ( ! link ) continue;
270 var label = link.getAttribute( 'title' );
271 if ( ! label ) continue;
272 link.removeAttribute( 'title' );
273 link.setAttribute( 'data-desktop-tooltip', label );
274 if ( ! link.hasAttribute( 'aria-label' ) ) {
275 link.setAttribute( 'aria-label', label );
276 }
277 }
278 }
279
280 function wireShortcutsPopover( btn, data ) {
281 if ( ! btn || ! data ) {
282 return;
283 }
284 var anchor = btn.querySelector( '.ab-item' ) || btn;
285 var popover = buildShortcutsPopover( data );
286 btn.appendChild( popover );
287
288 var isOpen = false;
289 function open() {
290 if ( isOpen ) return;
291 isOpen = true;
292 popover.classList.add( 'is-open' );
293 btn.classList.add( 'is-open' );
294 document.addEventListener( 'mousedown', onDocMouseDown, true );
295 document.addEventListener( 'keydown', onKey, true );
296 }
297 function close() {
298 if ( ! isOpen ) return;
299 isOpen = false;
300 popover.classList.remove( 'is-open' );
301 btn.classList.remove( 'is-open' );
302 document.removeEventListener( 'mousedown', onDocMouseDown, true );
303 document.removeEventListener( 'keydown', onKey, true );
304 }
305 function onDocMouseDown( e ) {
306 if ( btn.contains( e.target ) ) return;
307 close();
308 }
309 function onKey( e ) {
310 if ( e.key === 'Escape' ) {
311 e.stopPropagation();
312 close();
313 }
314 }
315
316 anchor.addEventListener( 'click', function( e ) {
317 e.preventDefault();
318 e.stopPropagation();
319 isOpen ? close() : open();
320 } );
321 // Swallow stray clicks inside the popover so they don't bubble
322 // up to admin-bar handlers (and so the popover stays open while
323 // the user reads it).
324 popover.addEventListener( 'mousedown', function( e ) {
325 e.stopPropagation();
326 } );
327 popover.addEventListener( 'click', function( e ) {
328 e.stopPropagation();
329 } );
330 }
331
332 function buildShortcutsPopover( data ) {
333 var root = document.createElement( 'div' );
334 root.className = 'desktop-mode-shortcuts-popover';
335 root.setAttribute( 'role', 'dialog' );
336 if ( data.title ) {
337 root.setAttribute( 'aria-label', data.title );
338 }
339
340 if ( data.contextual ) {
341 root.appendChild( buildShortcutsTable( data.contextual ) );
342 }
343 if ( data.general ) {
344 root.appendChild( buildShortcutsList( data.general ) );
345 }
346 return root;
347 }
348
349 function buildShortcutsTable( section ) {
350 var wrap = document.createElement( 'section' );
351 wrap.className = 'desktop-mode-shortcuts-popover__section';
352
353 if ( section.heading ) {
354 var h = document.createElement( 'h3' );
355 h.className = 'desktop-mode-shortcuts-popover__heading';
356 h.textContent = section.heading;
357 wrap.appendChild( h );
358 }
359
360 var table = document.createElement( 'table' );
361 table.className = 'desktop-mode-shortcuts-popover__table';
362
363 var thead = document.createElement( 'thead' );
364 var headRow = document.createElement( 'tr' );
365 [ 'key', 'outside', 'inside', 'showDesktop' ].forEach( function( col ) {
366 var th = document.createElement( 'th' );
367 th.scope = 'col';
368 th.textContent = section.headers && section.headers[ col ] ? section.headers[ col ] : '';
369 headRow.appendChild( th );
370 } );
371 thead.appendChild( headRow );
372 table.appendChild( thead );
373
374 var tbody = document.createElement( 'tbody' );
375 ( section.rows || [] ).forEach( function( row ) {
376 var tr = document.createElement( 'tr' );
377
378 var keyCell = document.createElement( 'td' );
379 keyCell.className = 'desktop-mode-shortcuts-popover__key-cell';
380 keyCell.appendChild( renderKeys( row.keys || [] ) );
381 if ( row.note ) {
382 var note = document.createElement( 'span' );
383 note.className = 'desktop-mode-shortcuts-popover__note';
384 note.textContent = ' ' + row.note;
385 keyCell.appendChild( note );
386 }
387 tr.appendChild( keyCell );
388
389 [ 'outside', 'inside', 'showDesktop' ].forEach( function( col ) {
390 var td = document.createElement( 'td' );
391 td.textContent = row[ col ] || '';
392 tr.appendChild( td );
393 } );
394
395 tbody.appendChild( tr );
396 } );
397 table.appendChild( tbody );
398 wrap.appendChild( table );
399
400 return wrap;
401 }
402
403 function buildShortcutsList( section ) {
404 var wrap = document.createElement( 'section' );
405 wrap.className = 'desktop-mode-shortcuts-popover__section';
406
407 if ( section.heading ) {
408 var h = document.createElement( 'h3' );
409 h.className = 'desktop-mode-shortcuts-popover__heading';
410 h.textContent = section.heading;
411 wrap.appendChild( h );
412 }
413
414 var list = document.createElement( 'ul' );
415 list.className = 'desktop-mode-shortcuts-popover__list';
416 ( section.items || [] ).forEach( function( item ) {
417 var li = document.createElement( 'li' );
418 li.className = 'desktop-mode-shortcuts-popover__item';
419 li.appendChild( renderKeys( item.keys || [] ) );
420 var desc = document.createElement( 'span' );
421 desc.className = 'desktop-mode-shortcuts-popover__description';
422 desc.textContent = item.description || '';
423 li.appendChild( desc );
424 list.appendChild( li );
425 } );
426 wrap.appendChild( list );
427
428 return wrap;
429 }
430
431 function renderKeys( keys ) {
432 var span = document.createElement( 'span' );
433 span.className = 'desktop-mode-shortcuts-popover__keys';
434 keys.forEach( function( key, idx ) {
435 if ( idx > 0 ) {
436 var plus = document.createElement( 'span' );
437 plus.className = 'desktop-mode-shortcuts-popover__plus';
438 plus.textContent = '+';
439 plus.setAttribute( 'aria-hidden', 'true' );
440 span.appendChild( plus );
441 }
442 var kbd = document.createElement( 'kbd' );
443 kbd.className = 'desktop-mode-shortcuts-popover__kbd';
444 kbd.textContent = key;
445 span.appendChild( kbd );
446 } );
447 return span;
448 }
449 } )();
450