| 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 |
|