PageSourceSearch

https://www.artres.com/Include/Matrix5/OnDemand/DamView/ResultItem…?v=MjAyNi0wNy0xNiAwNTozMTowNC4wMDA

js artres.com collected 2026-09-24 22:11:45 UTC 17,730 bytes, 520 lines download raw bytes

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.