@@ -9,26 +9,33 @@ const fomanticDropdownFn = $.fn.dropdown;
99// use our own `$().dropdown` function to patch Fomantic's dropdown module
1010export function initAriaDropdownPatch ( ) {
1111 if ( $ . fn . dropdown === ariaDropdownFn ) throw new Error ( 'initAriaDropdownPatch could only be called once' ) ;
12+ $ . fn . dropdown . settings . onAfterFiltered = onAfterFiltered ;
1213 $ . fn . dropdown = ariaDropdownFn ;
1314 $ . fn . fomanticExt . onResponseKeepSelectedItem = onResponseKeepSelectedItem ;
1415 ( ariaDropdownFn as FomanticInitFunction ) . settings = fomanticDropdownFn . settings ;
1516}
1617
1718// the patched `$.fn.dropdown` function, it passes the arguments to Fomantic's `$.fn.dropdown` function, and:
18- // * it does the one-time attaching on the first call
19- // * it delegates the `onLabelCreate` to the patched `onLabelCreate` to add necessary aria attributes
19+ // * it does the one-time element event attaching on the first call
20+ // * it delegates the module internal functions like `onLabelCreate` to the patched functions to add more features.
2021function ariaDropdownFn ( this : any , ...args : Parameters < FomanticInitFunction > ) {
2122 const ret = fomanticDropdownFn . apply ( this , args ) ;
2223
23- // if the `$().dropdown()` call is without arguments, or it has non-string (object) argument,
24- // it means that this call will reset the dropdown internal settings, then we need to re-delegate the callbacks.
25- const needDelegate = ( ! args . length || typeof args [ 0 ] !== 'string' ) ;
2624 for ( const el of this ) {
2725 if ( ! el [ ariaPatchKey ] ) {
28- attachInit ( el ) ;
26+ // the elements don't belong to the dropdown "module" and won't be reset
27+ // so we only need to initialize them once.
28+ attachInitElements ( el ) ;
2929 }
30- if ( needDelegate ) {
31- delegateOne ( $ ( el ) ) ;
30+
31+ // if the `$().dropdown()` call is without arguments, or it has non-string (object) argument,
32+ // it means that such call will reset the dropdown "module" including internal settings,
33+ // then we need to re-delegate the callbacks.
34+ const $dropdown = $ ( el ) ;
35+ const dropdownModule = $dropdown . data ( 'module-dropdown' ) ;
36+ if ( ! dropdownModule . giteaDelegated ) {
37+ dropdownModule . giteaDelegated = true ;
38+ delegateDropdownModule ( $dropdown ) ;
3239 }
3340 }
3441 return ret ;
@@ -61,37 +68,17 @@ function updateSelectionLabel(label: HTMLElement) {
6168 }
6269}
6370
64- function processMenuItems ( $dropdown : any , dropdownCall : any ) {
65- const hideEmptyDividers = dropdownCall ( 'setting' , 'hideDividers' ) === 'empty' ;
71+ function onAfterFiltered ( this : any ) {
72+ const $dropdown = $ ( this ) ;
73+ const hideEmptyDividers = $dropdown . dropdown ( 'setting' , 'hideDividers' ) === 'empty' ;
6674 const itemsMenu = $dropdown [ 0 ] . querySelector ( '.scrolling.menu' ) || $dropdown [ 0 ] . querySelector ( '.menu' ) ;
6775 if ( hideEmptyDividers ) hideScopedEmptyDividers ( itemsMenu ) ;
6876}
6977
7078// delegate the dropdown's template functions and callback functions to add aria attributes.
71- function delegateOne ( $dropdown : any ) {
79+ function delegateDropdownModule ( $dropdown : any ) {
7280 const dropdownCall = fomanticDropdownFn . bind ( $dropdown ) ;
7381
74- // If there is a "search input" in the "menu", Fomantic will only "focus the input" but not "toggle the menu" when the "dropdown icon" is clicked.
75- // Actually, Fomantic UI doesn't support such layout/usage. It needs to patch the "focusSearch" / "blurSearch" functions to make sure it toggles the menu.
76- const oldFocusSearch = dropdownCall ( 'internal' , 'focusSearch' ) ;
77- const oldBlurSearch = dropdownCall ( 'internal' , 'blurSearch' ) ;
78- // * If the "dropdown icon" is clicked, Fomantic calls "focusSearch", so show the menu
79- dropdownCall ( 'internal' , 'focusSearch' , function ( this : any ) { dropdownCall ( 'show' ) ; oldFocusSearch . call ( this ) } ) ;
80- // * If the "dropdown icon" is clicked again when the menu is visible, Fomantic calls "blurSearch", so hide the menu
81- dropdownCall ( 'internal' , 'blurSearch' , function ( this : any ) { oldBlurSearch . call ( this ) ; dropdownCall ( 'hide' ) } ) ;
82-
83- const oldFilterItems = dropdownCall ( 'internal' , 'filterItems' ) ;
84- dropdownCall ( 'internal' , 'filterItems' , function ( this : any , ...args : any [ ] ) {
85- oldFilterItems . call ( this , ...args ) ;
86- processMenuItems ( $dropdown , dropdownCall ) ;
87- } ) ;
88-
89- const oldShow = dropdownCall ( 'internal' , 'show' ) ;
90- dropdownCall ( 'internal' , 'show' , function ( this : any , ...args : any [ ] ) {
91- oldShow . call ( this , ...args ) ;
92- processMenuItems ( $dropdown , dropdownCall ) ;
93- } ) ;
94-
9582 // the "template" functions are used for dynamic creation (eg: AJAX)
9683 const dropdownTemplates = { ...dropdownCall ( 'setting' , 'templates' ) , t : performance . now ( ) } ;
9784 const dropdownTemplatesMenuOld = dropdownTemplates . menu ;
@@ -163,9 +150,8 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men
163150 }
164151}
165152
166- function attachInit ( dropdown : HTMLElement ) {
153+ function attachInitElements ( dropdown : HTMLElement ) {
167154 ( dropdown as any ) [ ariaPatchKey ] = { } ;
168- if ( dropdown . classList . contains ( 'custom' ) ) return ;
169155
170156 // Dropdown has 2 different focusing behaviors
171157 // * with search input: the input is focused, and it works with aria-activedescendant pointing another sibling element.
@@ -305,9 +291,11 @@ export function hideScopedEmptyDividers(container: Element) {
305291 const visibleItems : Element [ ] = [ ] ;
306292 const curScopeVisibleItems : Element [ ] = [ ] ;
307293 let curScope : string = '' , lastVisibleScope : string = '' ;
308- const isScopedDivider = ( item : Element ) => item . matches ( '.divider' ) && item . hasAttribute ( 'data-scope' ) ;
294+ const isDivider = ( item : Element ) => item . classList . contains ( 'divider' ) ;
295+ const isScopedDivider = ( item : Element ) => isDivider ( item ) && item . hasAttribute ( 'data-scope' ) ;
309296 const hideDivider = ( item : Element ) => item . classList . add ( 'hidden' , 'transition' ) ; // dropdown has its own classes to hide items
310-
297+ const showDivider = ( item : Element ) => item . classList . remove ( 'hidden' , 'transition' ) ;
298+ const isHidden = ( item : Element ) => item . classList . contains ( 'hidden' ) || item . classList . contains ( 'filtered' ) || item . classList . contains ( 'tw-hidden' ) ;
311299 const handleScopeSwitch = ( itemScope : string ) => {
312300 if ( curScopeVisibleItems . length === 1 && isScopedDivider ( curScopeVisibleItems [ 0 ] ) ) {
313301 hideDivider ( curScopeVisibleItems [ 0 ] ) ;
@@ -323,34 +311,37 @@ export function hideScopedEmptyDividers(container: Element) {
323311 curScopeVisibleItems . length = 0 ;
324312 } ;
325313
314+ // reset hidden dividers
315+ queryElems ( container , '.divider' , showDivider ) ;
316+
326317 // hide the scope dividers if the scope items are empty
327318 for ( const item of container . children ) {
328319 const itemScope = item . getAttribute ( 'data-scope' ) || '' ;
329320 if ( itemScope !== curScope ) {
330321 handleScopeSwitch ( itemScope ) ;
331322 }
332- if ( ! item . classList . contains ( 'filtered' ) && ! item . classList . contains ( 'tw-hidden' ) ) {
323+ if ( ! isHidden ( item ) ) {
333324 curScopeVisibleItems . push ( item as HTMLElement ) ;
334325 }
335326 }
336327 handleScopeSwitch ( '' ) ;
337328
338329 // hide all leading and trailing dividers
339330 while ( visibleItems . length ) {
340- if ( ! visibleItems [ 0 ] . matches ( '.divider' ) ) break ;
331+ if ( ! isDivider ( visibleItems [ 0 ] ) ) break ;
341332 hideDivider ( visibleItems [ 0 ] ) ;
342333 visibleItems . shift ( ) ;
343334 }
344335 while ( visibleItems . length ) {
345- if ( ! visibleItems [ visibleItems . length - 1 ] . matches ( '.divider' ) ) break ;
336+ if ( ! isDivider ( visibleItems [ visibleItems . length - 1 ] ) ) break ;
346337 hideDivider ( visibleItems [ visibleItems . length - 1 ] ) ;
347338 visibleItems . pop ( ) ;
348339 }
349340 // hide all duplicate dividers, hide current divider if next sibling is still divider
350341 // no need to update "visibleItems" array since this is the last loop
351- for ( const item of visibleItems ) {
352- if ( ! item . matches ( '.divider' ) ) continue ;
353- if ( item . nextElementSibling ?. matches ( '.divider' ) ) hideDivider ( item ) ;
342+ for ( let i = 0 ; i < visibleItems . length - 1 ; i ++ ) {
343+ if ( ! visibleItems [ i ] . matches ( '.divider' ) ) continue ;
344+ if ( visibleItems [ i + 1 ] . matches ( '.divider' ) ) hideDivider ( visibleItems [ i ] ) ;
354345 }
355346}
356347
0 commit comments