@@ -9,26 +9,36 @@ 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' ) ;
26- for ( const el of this ) {
24+ for ( let el of this ) {
25+ // dropdown will replace '<select class="ui dropdown"/>' to '<div class="ui dropdown"><select (hidden)></select><div class="menu">...</div></div>'
26+ // so we need to correctly find the closest '.ui.dropdown' element, it is the real fomantic dropdown module.
27+ el = el . closest ( '.ui.dropdown' ) ;
2728 if ( ! el [ ariaPatchKey ] ) {
28- attachInit ( el ) ;
29+ // the elements don't belong to the dropdown "module" and won't be reset
30+ // so we only need to initialize them once.
31+ attachInitElements ( el ) ;
2932 }
30- if ( needDelegate ) {
31- delegateOne ( $ ( el ) ) ;
33+
34+ // if the `$().dropdown()` is called without arguments, or it has non-string (object) argument,
35+ // it means that such call will reset the dropdown "module" including internal settings,
36+ // then we need to re-delegate the callbacks.
37+ const $dropdown = $ ( el ) ;
38+ const dropdownModule = $dropdown . data ( 'module-dropdown' ) ;
39+ if ( ! dropdownModule . giteaDelegated ) {
40+ dropdownModule . giteaDelegated = true ;
41+ delegateDropdownModule ( $dropdown ) ;
3242 }
3343 }
3444 return ret ;
@@ -61,37 +71,17 @@ function updateSelectionLabel(label: HTMLElement) {
6171 }
6272}
6373
64- function processMenuItems ( $dropdown : any , dropdownCall : any ) {
65- const hideEmptyDividers = dropdownCall ( 'setting' , 'hideDividers' ) === 'empty' ;
74+ function onAfterFiltered ( this : any ) {
75+ const $dropdown = $ ( this ) ;
76+ const hideEmptyDividers = $dropdown . dropdown ( 'setting' , 'hideDividers' ) === 'empty' ;
6677 const itemsMenu = $dropdown [ 0 ] . querySelector ( '.scrolling.menu' ) || $dropdown [ 0 ] . querySelector ( '.menu' ) ;
6778 if ( hideEmptyDividers ) hideScopedEmptyDividers ( itemsMenu ) ;
6879}
6980
7081// delegate the dropdown's template functions and callback functions to add aria attributes.
71- function delegateOne ( $dropdown : any ) {
82+ function delegateDropdownModule ( $dropdown : any ) {
7283 const dropdownCall = fomanticDropdownFn . bind ( $dropdown ) ;
7384
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-
9585 // the "template" functions are used for dynamic creation (eg: AJAX)
9686 const dropdownTemplates = { ...dropdownCall ( 'setting' , 'templates' ) , t : performance . now ( ) } ;
9787 const dropdownTemplatesMenuOld = dropdownTemplates . menu ;
@@ -163,9 +153,8 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men
163153 }
164154}
165155
166- function attachInit ( dropdown : HTMLElement ) {
156+ function attachInitElements ( dropdown : HTMLElement ) {
167157 ( dropdown as any ) [ ariaPatchKey ] = { } ;
168- if ( dropdown . classList . contains ( 'custom' ) ) return ;
169158
170159 // Dropdown has 2 different focusing behaviors
171160 // * with search input: the input is focused, and it works with aria-activedescendant pointing another sibling element.
@@ -305,9 +294,11 @@ export function hideScopedEmptyDividers(container: Element) {
305294 const visibleItems : Element [ ] = [ ] ;
306295 const curScopeVisibleItems : Element [ ] = [ ] ;
307296 let curScope : string = '' , lastVisibleScope : string = '' ;
308- const isScopedDivider = ( item : Element ) => item . matches ( '.divider' ) && item . hasAttribute ( 'data-scope' ) ;
297+ const isDivider = ( item : Element ) => item . classList . contains ( 'divider' ) ;
298+ const isScopedDivider = ( item : Element ) => isDivider ( item ) && item . hasAttribute ( 'data-scope' ) ;
309299 const hideDivider = ( item : Element ) => item . classList . add ( 'hidden' , 'transition' ) ; // dropdown has its own classes to hide items
310-
300+ const showDivider = ( item : Element ) => item . classList . remove ( 'hidden' , 'transition' ) ;
301+ const isHidden = ( item : Element ) => item . classList . contains ( 'hidden' ) || item . classList . contains ( 'filtered' ) || item . classList . contains ( 'tw-hidden' ) ;
311302 const handleScopeSwitch = ( itemScope : string ) => {
312303 if ( curScopeVisibleItems . length === 1 && isScopedDivider ( curScopeVisibleItems [ 0 ] ) ) {
313304 hideDivider ( curScopeVisibleItems [ 0 ] ) ;
@@ -323,34 +314,37 @@ export function hideScopedEmptyDividers(container: Element) {
323314 curScopeVisibleItems . length = 0 ;
324315 } ;
325316
317+ // reset hidden dividers
318+ queryElems ( container , '.divider' , showDivider ) ;
319+
326320 // hide the scope dividers if the scope items are empty
327321 for ( const item of container . children ) {
328322 const itemScope = item . getAttribute ( 'data-scope' ) || '' ;
329323 if ( itemScope !== curScope ) {
330324 handleScopeSwitch ( itemScope ) ;
331325 }
332- if ( ! item . classList . contains ( 'filtered' ) && ! item . classList . contains ( 'tw-hidden' ) ) {
326+ if ( ! isHidden ( item ) ) {
333327 curScopeVisibleItems . push ( item as HTMLElement ) ;
334328 }
335329 }
336330 handleScopeSwitch ( '' ) ;
337331
338332 // hide all leading and trailing dividers
339333 while ( visibleItems . length ) {
340- if ( ! visibleItems [ 0 ] . matches ( '.divider' ) ) break ;
334+ if ( ! isDivider ( visibleItems [ 0 ] ) ) break ;
341335 hideDivider ( visibleItems [ 0 ] ) ;
342336 visibleItems . shift ( ) ;
343337 }
344338 while ( visibleItems . length ) {
345- if ( ! visibleItems [ visibleItems . length - 1 ] . matches ( '.divider' ) ) break ;
339+ if ( ! isDivider ( visibleItems [ visibleItems . length - 1 ] ) ) break ;
346340 hideDivider ( visibleItems [ visibleItems . length - 1 ] ) ;
347341 visibleItems . pop ( ) ;
348342 }
349343 // hide all duplicate dividers, hide current divider if next sibling is still divider
350344 // 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 ) ;
345+ for ( let i = 0 ; i < visibleItems . length - 1 ; i ++ ) {
346+ if ( ! visibleItems [ i ] . matches ( '.divider' ) ) continue ;
347+ if ( visibleItems [ i + 1 ] . matches ( '.divider' ) ) hideDivider ( visibleItems [ i ] ) ;
354348 }
355349}
356350
0 commit comments