1/** 2 * ResultItemKeyboardNav - Manages keyboard navigation for DAM View result items 3 * 4 * Link: Custom keyboard navigation for asset grid 5 * 6 * Behavior: 7 * - When focused on DetailLink <a> tag and user presses Tab: move to next asset's DetailLink 8 * - When focused on DetailLink <a> tag and user presses Shift+Tab: move to previous asset's DetailLink 9 * - When focused on DetailLink <a> tag and user presses Enter: move focus to SelectCkb (without triggering selection) 10 * - SelectCkb elements are removed from tab order (tabindex=-1) 11 * 12 * @class 13 */ 14if (typeof window.ResultItemKeyboardNav === "undefined") 15{ 16 (function () 17 { 18 /** 19 * @typedef ResultItemKeyboardNavOptions 20 * @type {Object} 21 * @property {string} resultItemSelector - CSS selector for result item containers 22 * @property {string} selectPnlSelector - CSS selector for selection panel within result item 23 * @property {string} detailLinkSelector - CSS selector for detail link within result item 24 * @property {string} selectCkbSelector - CSS selector for selection checkbox within result item 25 */ 26 27 /** 28 * Constructor 29 * @param {ResultItemKeyboardNavOptions} options 30 */ 31 function ResultItemKeyboardNav(options) 32 { 33 this.options = options || {}; 34 this.resultItemSelector = this.options.resultItemSelector || '.VF[data-vu*="DamView_ResultsItem_SForm"]'; 35 this.selectPnlSelector = this.options.selectPnlSelector || '[id$="SelectPnl"]'; 36 this.detailLinkSelector = this.options.detailLinkSelector || '[id$="DetailLink"] a'; 37 this.selectCkbSelector = this.options.selectCkbSelector || '[id$="SelectCkb"]'; 38 39 this._boundKeyDownHandler = null; 40 this._initialized = false; 41 } 42 43 /** 44 * Singleton instance 45 * @type {ResultItemKeyboardNav} 46 */ 47 ResultItemKeyboardNav.Instance = null; 48 49 /** 50 * Get or create singleton instance 51 * @param {ResultItemKeyboardNavOptions} options 52 * @returns {ResultItemKeyboardNav} 53 */ 54 ResultItemKeyboardNav.GetInstance = function (options) 55 { 56 if (!ResultItemKeyboardNav.Instance) 57 { 58 ResultItemKeyboardNav.Instance = new ResultItemKeyboardNav(options); 59 } 60 return ResultItemKeyboardNav.Instance; 61 }; 62 63 /** 64 * Initialize keyboard navigation 65 */ 66 ResultItemKeyboardNav.prototype.Initialize = function () 67 { 68 if (this._initialized) 69 { 70 return; 71 } 72 73 var that = this; 74 75 // Create bound handler for proper removal later 76 this._boundKeyDownHandler = function (e) { that._handleKeyDown(e); }; 77 78 // Attach event listener at document level to catch all key events 79 document.addEventListener('keydown', this._boundKeyDownHandler, true); 80 81 // Set tabindex=-1 on all SelectCkb elements to remove them from tab order 82 this._updateSelectCkbTabIndex(); 83 84 // Watch for DOM changes to update new items 85 this._setupMutationObserver(); 86 87 this._initialized = true; 88 }; 89 90 /** 91 * Dispose and clean up event listeners 92 */ 93 ResultItemKeyboardNav.prototype.Dispose = function () 94 { 95 if (!this._initialized) 96 { 97 return; 98 } 99 100 if (this._boundKeyDownHandler) 101 { 102 document.removeEventListener('keydown', this._boundKeyDownHandler, true); 103 this._boundKeyDownHandler = null; 104 } 105 106 if (this._mutationObserver) 107 { 108 this._mutationObserver.disconnect(); 109 this._mutationObserver = null; 110 } 111 112 this._initialized = false; 113 }; 114 115 /** 116 * Update tabindex on all SelectCkb elements to remove from tab order 117 * @private 118 */ 119 ResultItemKeyboardNav.prototype._updateSelectCkbTabIndex = function () 120 { 121 var selectCkbElements = document.querySelectorAll(this.selectCkbSelector); 122 for (var i = 0; i < selectCkbElements.length; i++) 123 { 124 // Remove from tab order but keep focusable programmatically 125 selectCkbElements[i].setAttribute('tabindex', '-1'); 126 } 127 }; 128 129 /** 130 * Setup mutation observer to handle dynamically added items 131 * @private 132 */ 133 ResultItemKeyboardNav.prototype._setupMutationObserver = function () 134 { 135 var that = this; 136 137 // Watch for new result items being added to the DOM 138 this._mutationObserver = new MutationObserver(function (mutations) 139 { 140 var shouldUpdate = false;
141 142 for (var i = 0; i < mutations.length; i++) 143 { 144 var mutation = mutations[i]; 145 if (mutation.addedNodes && mutation.addedNodes.length > 0) 146 { 147 for (var j = 0; j < mutation.addedNodes.length; j++) 148 { 149 var node = mutation.addedNodes[j]; 150 if (node.nodeType === Node.ELEMENT_NODE) 151 { 152 // Check if added node is a result item or contains result items 153 if (node.matches && (node.matches(that.resultItemSelector) || 154 node.querySelector(that.resultItemSelector))) 155 { 156 shouldUpdate = true; 157 break; 158 } 159 } 160 } 161 } 162 if (shouldUpdate) break; 163 } 164 165 if (shouldUpdate) 166 { 167 that._updateSelectCkbTabIndex(); 168 } 169 }); 170 171 this._mutationObserver.observe(document.body, { 172 childList: true, 173 subtree: true 174 }); 175 }; 176 177 /** 178 * Handle keydown events 179 * @private 180 * @param {KeyboardEvent} e 181 */ 182 ResultItemKeyboardNav.prototype._handleKeyDown = function (e) 183 { 184 var activeElement = document.activeElement; 185 if (!activeElement) return; 186 187 // Check if the active element is a DetailLink 188 var detailLinkAnchor = this._findClosestDetailLink(activeElement); 189 if (!detailLinkAnchor) return; 190 191 var keyCode = e.keyCode || e.which; 192 193 // Handle Tab key (keyCode 9) 194 if (keyCode === 9 && !e.shiftKey) 195 { 196 // Tab: move to next DetailLink, skipping SelectCkb 197 // Only prevent default if we can actually move 198 if (this._canMoveToNext(detailLinkAnchor)) 199 { 200 e.preventDefault(); 201 e.stopPropagation(); 202 this._moveToNextDetailLink(detailLinkAnchor); 203 } 204 } 205 // Handle Shift+Tab key 206 else if (keyCode === 9 && e.shiftKey) 207 { 208 // Shift+Tab: move to previous DetailLink 209 // Only prevent default if we can actually move 210 if (this._canMoveToPrevious(detailLinkAnchor)) 211 { 212 e.preventDefault(); 213 e.stopPropagation(); 214 this._moveToPreviousDetailLink(detailLinkAnchor); 215 } 216 } 217 // Handle Enter key (keyCode 13) 218 else if (keyCode === 13) 219 { 220 // Enter: move focus to SelectCkb (but don't trigger selection) 221 e.preventDefault(); 222 e.stopPropagation(); 223 this._moveToSelectCkb(detailLinkAnchor); 224 } 225 }; 226 227 /** 228 * Find the closest DetailLink anchor element 229 * @private 230 * @param {HTMLElement} element 231 * @returns {HTMLElement|null} 232 */ 233 ResultItemKeyboardNav.prototype._findClosestDetailLink = function (element) 234 { 235 // Check if element is already the DetailLink anchor 236 if (element.tagName === 'A' && element.closest('[id$="DetailLink"]')) 237 { 238 return element; 239 } 240 241 // Check if element is within a DetailLink 242 var detailLink = element.closest('[id$="DetailLink"]'); 243 if (detailLink) 244 { 245 var anchor = detailLink.querySelector('a'); 246 if (anchor) return anchor; 247 } 248 249 return null; 250 }; 251 252 /** 253 * Get all result items in DOM order 254 * @private 255 * @returns {HTMLElement[]} 256 */ 257 ResultItemKeyboardNav.prototype._getAllResultItems = function () 258 { 259 return Array.prototype.slice.call(document.querySelectorAll(this.resultItemSelector)); 260 }; 261 262 /** 263 * Check if can move to next DetailLink 264 * @private 265 * @param {HTMLElement} currentAnchor 266 * @returns {boolean} 267 */ 268 ResultItemKeyboardNav.prototype._canMoveToNext = function (currentAnchor) 269 { 270 var currentItem = this._getResultItem(currentAnchor); 271 if (!currentItem) return false;
272 273 var allItems = this._getAllResultItems(); 274 var currentIndex = allItems.indexOf(currentItem); 275 276 if (currentIndex === -1) return false; 277 278 // Can move if not at the last item 279 return currentIndex < allItems.length - 1; 280 }; 281 282 /** 283 * Check if can move to previous DetailLink 284 * @private 285 * @param {HTMLElement} currentAnchor 286 * @returns {boolean} 287 */ 288 ResultItemKeyboardNav.prototype._canMoveToPrevious = function (currentAnchor) 289 { 290 var currentItem = this._getResultItem(currentAnchor); 291 if (!currentItem) return false; 292 293 var allItems = this._getAllResultItems(); 294 var currentIndex = allItems.indexOf(currentItem); 295 296 if (currentIndex === -1) return false; 297 298 // Can move if not at the first item 299 return currentIndex > 0; 300 }; 301 302 /** 303 * Get the result item containing the given element 304 * @private 305 * @param {HTMLElement} element 306 * @returns {HTMLElement|null} 307 */ 308 ResultItemKeyboardNav.prototype._getResultItem = function (element) 309 { 310 return element.closest(this.resultItemSelector); 311 }; 312 313 /** 314 * Hide all tooltips on result items 315 * @private 316 */ 317 ResultItemKeyboardNav.prototype._hideAllTooltips = function () 318 { 319 var allItems = this._getAllResultItems(); 320 for (var i = 0; i < allItems.length; i++) 321 { 322 var selectPnl = allItems[i].querySelector(this.selectPnlSelector); 323 if (selectPnl) 324 { 325 $(selectPnl).tipsy("hide"); 326 } 327 } 328 }; 329 330 /** 331 * Focus on an item's DetailLink, triggering DeferInject if needed 332 * @private 333 * @param {HTMLElement} item - The result item element 334 */ 335 ResultItemKeyboardNav.prototype._focusItemDetailLink = function (item) 336 { 337 var that = this; 338 339 // Hide all existing tooltips first 340 this._hideAllTooltips(); 341 342 // Check if DetailLink already exists 343 var detailLink = item.querySelector(this.detailLinkSelector); 344 var selectPnl = item.querySelector(this.selectPnlSelector); 345 346 if (detailLink) 347 { 348 // DetailLink exists, focus immediately 349 detailLink.focus(); 350 351 // Show tooltip for this item 352 if (selectPnl) 353 { 354 $(selectPnl).tipsy({ trigger: "manual", gravity: "n", fade: true }); 355 $(selectPnl).tipsy("show"); 356 } 357 return; 358 } 359 360 // DetailLink doesn't exist yet, need to trigger DeferInject 361 // Focus on HoverPanel to trigger DeferInject naturally 362 var hoverPanel = item.querySelector('[id$="HoverPnl"]'); 363 if (hoverPanel) 364 { 365 // Make HoverPanel focusable temporarily 366 var originalTabIndex = hoverPanel.getAttribute('tabindex'); 367 hoverPanel.setAttribute('tabindex', '-1'); 368 369 // Focus the HoverPanel to trigger DeferInject 370 hoverPanel.focus(); 371 372 // Restore original tabindex 373 if (originalTabIndex === null) 374 { 375 hoverPanel.removeAttribute('tabindex'); 376 } 377 else 378 { 379 hoverPanel.setAttribute('tabindex', originalTabIndex); 380 } 381 } 382 383 // Poll for DetailLink to appear (with timeout) 384 var attempts = 0; 385 var maxAttempts = 50; // 500ms total 386 var pollInterval = 10; 387 388 var pollForDetailLink = function() 389 { 390 attempts++; 391 detailLink = item.querySelector(that.detailLinkSelector); 392 393 if (detailLink) 394 { 395 // Found it, focus now 396 detailLink.focus(); 397 398 // Show tooltip for this item 399 if (selectPnl) 400 { 401 $(selectPnl).tipsy({ trigger: "manual", gravity: "n", fade: true }); 402 $(selectPnl).tipsy("show"); 403 } 404 } 405 else if (attempts < maxAttempts) 406 { 407 // Keep polling 408 setTimeout(pollForDetailLink, pollInterval); 409 }
410 // If we hit max attempts, give up silently 411 }; 412 413 // Start polling 414 setTimeout(pollForDetailLink, pollInterval); 415 }; 416 417 /** 418 * Move focus to the next DetailLink 419 * @private 420 * @param {HTMLElement} currentAnchor 421 */ 422 ResultItemKeyboardNav.prototype._moveToNextDetailLink = function (currentAnchor) 423 { 424 var currentItem = this._getResultItem(currentAnchor); 425 if (!currentItem) return; 426 427 var allItems = this._getAllResultItems(); 428 var currentIndex = allItems.indexOf(currentItem); 429 430 if (currentIndex === -1) return; 431 432 // Find next item 433 var nextIndex = currentIndex + 1; 434 if (nextIndex >= allItems.length) 435 { 436 // Don't wrap around - let natural tab order continue 437 return; 438 } 439 440 var nextItem = allItems[nextIndex]; 441 442 if (nextItem) 443 { 444 this._focusItemDetailLink(nextItem); 445 } 446 }; 447 448 /** 449 * Move focus to the previous DetailLink 450 * @private 451 * @param {HTMLElement} currentAnchor 452 */ 453 ResultItemKeyboardNav.prototype._moveToPreviousDetailLink = function (currentAnchor) 454 { 455 var currentItem = this._getResultItem(currentAnchor); 456 if (!currentItem) return; 457 458 var allItems = this._getAllResultItems(); 459 var currentIndex = allItems.indexOf(currentItem); 460 461 if (currentIndex === -1) return; 462 463 // Find previous item 464 var prevIndex = currentIndex - 1; 465 if (prevIndex < 0) 466 { 467 // Don't wrap around - let natural tab order continue 468 return; 469 } 470 471 var prevItem = allItems[prevIndex]; 472 473 if (prevItem) 474 { 475 this._focusItemDetailLink(prevItem); 476 } 477 }; 478 479 /** 480 * Move focus to the SelectCkb of the current item (without triggering selection) 481 * @private 482 * @param {HTMLElement} currentAnchor 483 */ 484 ResultItemKeyboardNav.prototype._moveToSelectCkb = function (currentAnchor) 485 { 486 var currentItem = this._getResultItem(currentAnchor); 487 if (!currentItem) return; 488 489 var selectCkb = currentItem.querySelector(this.selectCkbSelector); 490 491 if (selectCkb) 492 { 493 // Just focus the checkbox without clicking/triggering selection 494 selectCkb.focus(); 495 496 // Add visual feedback class 497 selectCkb.classList.add('keyboard-focused'); 498 499 // Setup handler to remove focus class when focus leaves 500 var removeHighlight = function () 501 { 502 selectCkb.classList.remove('keyboard-focused'); 503 selectCkb.removeEventListener('blur', removeHighlight); 504 }; 505 selectCkb.addEventListener('blur', removeHighlight); 506 } 507 }; 508 509 /** 510 * Public method to reinitialize after content changes 511 */ 512 ResultItemKeyboardNav.prototype.Refresh = function () 513 { 514 this._updateSelectCkbTabIndex(); 515 }; 516 517 // Expose to global scope 518 window.ResultItemKeyboardNav = ResultItemKeyboardNav; 519 })(); 520}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.