PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.1
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.1, at assets/js/admin-bar.js

462 lines 16.8 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 // AI Assistant button — dispatches the `desktop-mode-open-ai` event that
155 // the AiAssistant class listens for. Using an event instead of a direct
156 // call decouples the admin-bar inline script (which runs early, before
157 // the desktop shell has initialised) from the AiAssistant instance.
158 var aiBtn = document.getElementById( 'wp-admin-bar-desktop-ai-assistant' );
159 if ( aiBtn ) {
160 aiBtn.addEventListener( 'click', function( e ) {
161 e.preventDefault();
162 document.dispatchEvent( new CustomEvent( 'desktop-mode-open-ai' ) );
163 } );
164 }
165
166 // Fullscreen button — toggles browser fullscreen on the document
167 // element so the shell occupies the whole screen with no browser
168 // chrome. The click counts as a user gesture, which is required by
169 // the Fullscreen API. We listen for `fullscreenchange` instead of
170 // flipping state inside the click handler because the user can also
171 // exit via Esc or the OS, and we want the icon/label to stay in sync
172 // in those cases too.
173 var fsBtn = document.getElementById( 'wp-admin-bar-desktop-fullscreen' );
174 if ( fsBtn ) {
175 var fsLink = fsBtn.querySelector( '.ab-item' );
176 var fsLabel = fsBtn.querySelector( '.ab-label' );
177 var fsI18n = ( cfg && cfg.i18n ) || {};
178 // WP renders admin-bar items as `<a href="#">`, which HTML5
179 // marks implicitly draggable. In browser fullscreen the button
180 // sits at top:0 where cursor reveals (menu bar, exit
181 // affordance) produce enough pointer jitter between mousedown
182 // and mouseup that the browser commits to a native link-drag
183 // instead of a click — the handler never runs and the user
184 // sees a ghost flicker. Opt the element out of native drag so
185 // the click always lands.
186 if ( fsLink ) {
187 fsLink.setAttribute( 'draggable', 'false' );
188 }
189 function paintFullscreen( on ) {
190 // Body class drives the CSS that hides admin-bar items
191 // which sit too close to the screen-corner reveal hot zones
192 // in browser fullscreen — the OS / browser intercept clicks
193 // in those bands. Hiding them shifts the Fullscreen button
194 // further from the right edge so its own click lands.
195 document.body.classList.toggle( 'desktop-mode-browser-fs', !! on );
196 if ( on ) {
197 fsBtn.classList.add( 'is-fullscreen' );
198 if ( fsLabel ) fsLabel.textContent = fsI18n.exitFullscreen || 'Exit fullscreen';
199 if ( fsLink ) fsLink.setAttribute( 'title', fsI18n.exitTitle || 'Exit fullscreen' );
200 } else {
201 fsBtn.classList.remove( 'is-fullscreen' );
202 if ( fsLabel ) fsLabel.textContent = fsI18n.enterFullscreen || 'Fullscreen';
203 if ( fsLink ) fsLink.setAttribute( 'title', fsI18n.enterTitle || 'Enter fullscreen' );
204 }
205 }
206 fsBtn.addEventListener( 'click', function( e ) {
207 e.preventDefault();
208 // `window.top` so the whole tab goes fullscreen even when the
209 // admin bar happens to be rendered inside an iframe (today
210 // it's suppressed there, but a plugin could surface it). Fall
211 // back to the current document if cross-origin blocks access.
212 var doc;
213 try {
214 doc = window.top.document;
215 } catch ( err ) {
216 doc = document;
217 }
218 if ( doc.fullscreenElement ) {
219 if ( doc.exitFullscreen ) {
220 doc.exitFullscreen();
221 }
222 } else if ( doc.documentElement && doc.documentElement.requestFullscreen ) {
223 doc.documentElement.requestFullscreen();
224 }
225 } );
226 // Listen on whichever document we're driving so OS/Esc exits
227 // repaint the button too. Bound on both to cover the iframe edge
228 // case without extra branching.
229 document.addEventListener( 'fullscreenchange', function() {
230 paintFullscreen( !! document.fullscreenElement );
231 } );
232 try {
233 if ( window.top.document !== document ) {
234 window.top.document.addEventListener( 'fullscreenchange', function() {
235 paintFullscreen( !! window.top.document.fullscreenElement );
236 } );
237 }
238 } catch ( err ) {}
239 }
240
241 // Bug Report button — same decoupling pattern as Ask AI. Dispatches
242 // `desktop-mode-open-bug-report`; the shell answers by opening the
243 // Bug Report native window. Wired in `src/desktop.ts`.
244 var bugBtn = document.getElementById( 'wp-admin-bar-desktop-bug-report' );
245 if ( bugBtn ) {
246 bugBtn.addEventListener( 'click', function( e ) {
247 e.preventDefault();
248 document.dispatchEvent( new CustomEvent( 'desktop-mode-open-bug-report' ) );
249 } );
250 }
251
252 // Keyboard Shortcuts button — toggles a floating popover anchored
253 // under the button, styled like the Arrange submenu. Content is
254 // passed in via `cfg.shortcuts` (translated server-side in PHP).
255 wireShortcutsPopover( document.getElementById( 'wp-admin-bar-desktop-help' ), cfg.shortcuts );
256
257 // Custom tooltips for the icon-only admin-bar buttons. The native
258 // `title` attribute renders a slow, OS-styled tooltip that conflicts
259 // with our dock-style custom tooltip; we swap each `title` to
260 // `data-desktop-tooltip` + `aria-label` so screen readers still see
261 // the label but the visual tooltip is fully ours.
262 wireTooltipsFor( [
263 'wp-admin-bar-desktop-fullscreen',
264 'wp-admin-bar-desktop-help',
265 'wp-admin-bar-desktop-bug-report',
266 'wp-admin-bar-desktop-layout-menu',
267 ] );
268
269 /**
270 * Re-anchors a native `title` attribute to a `data-desktop-tooltip`
271 * data attribute on the same node, plus mirrors it to `aria-label`
272 * so assistive tech keeps the description. Pure-CSS tooltip then
273 * renders from the data attribute via the `[data-desktop-tooltip]
274 * .ab-item::after` rule in admin-bar.php.
275 */
276 function wireTooltipsFor( ids ) {
277 for ( var i = 0; i < ids.length; i++ ) {
278 var node = document.getElementById( ids[ i ] );
279 if ( ! node ) continue;
280 var link = node.querySelector( '.ab-item' );
281 if ( ! link ) continue;
282 var label = link.getAttribute( 'title' );
283 if ( ! label ) continue;
284 link.removeAttribute( 'title' );
285 link.setAttribute( 'data-desktop-tooltip', label );
286 if ( ! link.hasAttribute( 'aria-label' ) ) {
287 link.setAttribute( 'aria-label', label );
288 }
289 }
290 }
291
292 function wireShortcutsPopover( btn, data ) {
293 if ( ! btn || ! data ) {
294 return;
295 }
296 var anchor = btn.querySelector( '.ab-item' ) || btn;
297 var popover = buildShortcutsPopover( data );
298 btn.appendChild( popover );
299
300 var isOpen = false;
301 function open() {
302 if ( isOpen ) return;
303 isOpen = true;
304 popover.classList.add( 'is-open' );
305 btn.classList.add( 'is-open' );
306 document.addEventListener( 'mousedown', onDocMouseDown, true );
307 document.addEventListener( 'keydown', onKey, true );
308 }
309 function close() {
310 if ( ! isOpen ) return;
311 isOpen = false;
312 popover.classList.remove( 'is-open' );
313 btn.classList.remove( 'is-open' );
314 document.removeEventListener( 'mousedown', onDocMouseDown, true );
315 document.removeEventListener( 'keydown', onKey, true );
316 }
317 function onDocMouseDown( e ) {
318 if ( btn.contains( e.target ) ) return;
319 close();
320 }
321 function onKey( e ) {
322 if ( e.key === 'Escape' ) {
323 e.stopPropagation();
324 close();
325 }
326 }
327
328 anchor.addEventListener( 'click', function( e ) {
329 e.preventDefault();
330 e.stopPropagation();
331 isOpen ? close() : open();
332 } );
333 // Swallow stray clicks inside the popover so they don't bubble
334 // up to admin-bar handlers (and so the popover stays open while
335 // the user reads it).
336 popover.addEventListener( 'mousedown', function( e ) {
337 e.stopPropagation();
338 } );
339 popover.addEventListener( 'click', function( e ) {
340 e.stopPropagation();
341 } );
342 }
343
344 function buildShortcutsPopover( data ) {
345 var root = document.createElement( 'div' );
346 root.className = 'desktop-mode-shortcuts-popover';
347 root.setAttribute( 'role', 'dialog' );
348 if ( data.title ) {
349 root.setAttribute( 'aria-label', data.title );
350 }
351
352 if ( data.contextual ) {
353 root.appendChild( buildShortcutsTable( data.contextual ) );
354 }
355 if ( data.general ) {
356 root.appendChild( buildShortcutsList( data.general ) );
357 }
358 return root;
359 }
360
361 function buildShortcutsTable( section ) {
362 var wrap = document.createElement( 'section' );
363 wrap.className = 'desktop-mode-shortcuts-popover__section';
364
365 if ( section.heading ) {
366 var h = document.createElement( 'h3' );
367 h.className = 'desktop-mode-shortcuts-popover__heading';
368 h.textContent = section.heading;
369 wrap.appendChild( h );
370 }
371
372 var table = document.createElement( 'table' );
373 table.className = 'desktop-mode-shortcuts-popover__table';
374
375 var thead = document.createElement( 'thead' );
376 var headRow = document.createElement( 'tr' );
377 [ 'key', 'outside', 'inside', 'showDesktop' ].forEach( function( col ) {
378 var th = document.createElement( 'th' );
379 th.scope = 'col';
380 th.textContent = section.headers && section.headers[ col ] ? section.headers[ col ] : '';
381 headRow.appendChild( th );
382 } );
383 thead.appendChild( headRow );
384 table.appendChild( thead );
385
386 var tbody = document.createElement( 'tbody' );
387 ( section.rows || [] ).forEach( function( row ) {
388 var tr = document.createElement( 'tr' );
389
390 var keyCell = document.createElement( 'td' );
391 keyCell.className = 'desktop-mode-shortcuts-popover__key-cell';
392 keyCell.appendChild( renderKeys( row.keys || [] ) );
393 if ( row.note ) {
394 var note = document.createElement( 'span' );
395 note.className = 'desktop-mode-shortcuts-popover__note';
396 note.textContent = ' ' + row.note;
397 keyCell.appendChild( note );
398 }
399 tr.appendChild( keyCell );
400
401 [ 'outside', 'inside', 'showDesktop' ].forEach( function( col ) {
402 var td = document.createElement( 'td' );
403 td.textContent = row[ col ] || '';
404 tr.appendChild( td );
405 } );
406
407 tbody.appendChild( tr );
408 } );
409 table.appendChild( tbody );
410 wrap.appendChild( table );
411
412 return wrap;
413 }
414
415 function buildShortcutsList( section ) {
416 var wrap = document.createElement( 'section' );
417 wrap.className = 'desktop-mode-shortcuts-popover__section';
418
419 if ( section.heading ) {
420 var h = document.createElement( 'h3' );
421 h.className = 'desktop-mode-shortcuts-popover__heading';
422 h.textContent = section.heading;
423 wrap.appendChild( h );
424 }
425
426 var list = document.createElement( 'ul' );
427 list.className = 'desktop-mode-shortcuts-popover__list';
428 ( section.items || [] ).forEach( function( item ) {
429 var li = document.createElement( 'li' );
430 li.className = 'desktop-mode-shortcuts-popover__item';
431 li.appendChild( renderKeys( item.keys || [] ) );
432 var desc = document.createElement( 'span' );
433 desc.className = 'desktop-mode-shortcuts-popover__description';
434 desc.textContent = item.description || '';
435 li.appendChild( desc );
436 list.appendChild( li );
437 } );
438 wrap.appendChild( list );
439
440 return wrap;
441 }
442
443 function renderKeys( keys ) {
444 var span = document.createElement( 'span' );
445 span.className = 'desktop-mode-shortcuts-popover__keys';
446 keys.forEach( function( key, idx ) {
447 if ( idx > 0 ) {
448 var plus = document.createElement( 'span' );
449 plus.className = 'desktop-mode-shortcuts-popover__plus';
450 plus.textContent = '+';
451 plus.setAttribute( 'aria-hidden', 'true' );
452 span.appendChild( plus );
453 }
454 var kbd = document.createElement( 'kbd' );
455 kbd.className = 'desktop-mode-shortcuts-popover__kbd';
456 kbd.textContent = key;
457 span.appendChild( kbd );
458 } );
459 return span;
460 }
461 } )();
462