@ -9,26 +9,33 @@ const fomanticDropdownFn = $.fn.dropdown;
// use our own `$().dropdown` function to patch Fomantic's dropdown module
export function initAriaDropdownPatch() {
if ( $ . fn . dropdown === ariaDropdownFn ) throw new Error ( 'initAriaDropdownPatch could only be called once' ) ;
$ . fn . dropdown . settings . onAfterFiltered = onAfterFiltered ;
$ . fn . dropdown = ariaDropdownFn ;
$ . fn . fomanticExt . onResponseKeepSelectedItem = onResponseKeepSelectedItem ;
( ariaDropdownFn as FomanticInitFunction ) . settings = fomanticDropdownFn . settings ;
}
// the patched `$.fn.dropdown` function, it passes the arguments to Fomantic's `$.fn.dropdown` function, and:
// * it does the one-time attaching on the first call
// * it delegates the `onLabelCreate` to the patched `onLabelCreate` to add necessary aria attributes
// * it does the one-time element event attaching on the first call
// * it delegates the module internal functions like `onLabelCreate` to the patched functions to add more features.
function ariaDropdownFn ( this : any , . . . args : Parameters < FomanticInitFunction > ) {
const ret = fomanticDropdownFn . apply ( this , args ) ;
// if the `$().dropdown()` call is without arguments, or it has non-string (object) argument,
// it means that this call will reset the dropdown internal settings, then we need to re-delegate the callbacks.
const needDelegate = ( ! args . length || typeof args [ 0 ] !== 'string' ) ;
for ( const el of this ) {
if ( ! el [ ariaPatchKey ] ) {
attachInit ( el ) ;
// the elements don't belong to the dropdown "module" and won't be reset
// so we only need to initialize them once.
attachInitElements ( el ) ;
}
if ( needDelegate ) {
delegateOne ( $ ( el ) ) ;
// if the `$().dropdown()` call is without arguments, or it has non-string (object) argument,
// it means that such call will reset the dropdown "module" including internal settings,
// then we need to re-delegate the callbacks.
const $dropdown = $ ( el ) ;
const dropdownModule = $dropdown . data ( 'module-dropdown' ) ;
if ( ! dropdownModule . giteaDelegated ) {
dropdownModule . giteaDelegated = true ;
delegateDropdownModule ( $dropdown ) ;
}
}
return ret ;
@ -61,37 +68,17 @@ function updateSelectionLabel(label: HTMLElement) {
}
}
function processMenuItems ( $dropdown : any , dropdownCall : any ) {
const hideEmptyDividers = dropdownCall ( 'setting' , 'hideDividers' ) === 'empty' ;
function onAfterFiltered ( this : any ) {
const $dropdown = $ ( this ) ;
const hideEmptyDividers = $dropdown . dropdown ( 'setting' , 'hideDividers' ) === 'empty' ;
const itemsMenu = $dropdown [ 0 ] . querySelector ( '.scrolling.menu' ) || $dropdown [ 0 ] . querySelector ( '.menu' ) ;
if ( hideEmptyDividers ) hideScopedEmptyDividers ( itemsMenu ) ;
}
// delegate the dropdown's template functions and callback functions to add aria attributes.
function delegate On e( $dropdown : any ) {
function delegate DropdownModul e( $dropdown : any ) {
const dropdownCall = fomanticDropdownFn . bind ( $dropdown ) ;
// 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.
// Actually, Fomantic UI doesn't support such layout/usage. It needs to patch the "focusSearch" / "blurSearch" functions to make sure it toggles the menu.
const oldFocusSearch = dropdownCall ( 'internal' , 'focusSearch' ) ;
const oldBlurSearch = dropdownCall ( 'internal' , 'blurSearch' ) ;
// * If the "dropdown icon" is clicked, Fomantic calls "focusSearch", so show the menu
dropdownCall ( 'internal' , 'focusSearch' , function ( this : any ) { dropdownCall ( 'show' ) ; oldFocusSearch . call ( this ) } ) ;
// * If the "dropdown icon" is clicked again when the menu is visible, Fomantic calls "blurSearch", so hide the menu
dropdownCall ( 'internal' , 'blurSearch' , function ( this : any ) { oldBlurSearch . call ( this ) ; dropdownCall ( 'hide' ) } ) ;
const oldFilterItems = dropdownCall ( 'internal' , 'filterItems' ) ;
dropdownCall ( 'internal' , 'filterItems' , function ( this : any , . . . args : any [ ] ) {
oldFilterItems . call ( this , . . . args ) ;
processMenuItems ( $dropdown , dropdownCall ) ;
} ) ;
const oldShow = dropdownCall ( 'internal' , 'show' ) ;
dropdownCall ( 'internal' , 'show' , function ( this : any , . . . args : any [ ] ) {
oldShow . call ( this , . . . args ) ;
processMenuItems ( $dropdown , dropdownCall ) ;
} ) ;
// the "template" functions are used for dynamic creation (eg: AJAX)
const dropdownTemplates = { . . . dropdownCall ( 'setting' , 'templates' ) , t : performance.now ( ) } ;
const dropdownTemplatesMenuOld = dropdownTemplates . menu ;
@ -163,9 +150,8 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men
}
}
function attachInit ( dropdown : HTMLElement ) {
function attachInit Elements ( dropdown : HTMLElement ) {
( dropdown as any ) [ ariaPatchKey ] = { } ;
if ( dropdown . classList . contains ( 'custom' ) ) return ;
// Dropdown has 2 different focusing behaviors
// * 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) {
const visibleItems : Element [ ] = [ ] ;
const curScopeVisibleItems : Element [ ] = [ ] ;
let curScope : string = '' , lastVisibleScope : string = '' ;
const isScopedDivider = ( item : Element ) = > item . matches ( '.divider' ) && item . hasAttribute ( 'data-scope' ) ;
const isDivider = ( item : Element ) = > item . classList . contains ( 'divider' ) ;
const isScopedDivider = ( item : Element ) = > isDivider ( item ) && item . hasAttribute ( 'data-scope' ) ;
const hideDivider = ( item : Element ) = > item . classList . add ( 'hidden' , 'transition' ) ; // dropdown has its own classes to hide items
const showDivider = ( item : Element ) = > item . classList . remove ( 'hidden' , 'transition' ) ;
const isHidden = ( item : Element ) = > item . classList . contains ( 'hidden' ) || item . classList . contains ( 'filtered' ) || item . classList . contains ( 'tw-hidden' ) ;
const handleScopeSwitch = ( itemScope : string ) = > {
if ( curScopeVisibleItems . length === 1 && isScopedDivider ( curScopeVisibleItems [ 0 ] ) ) {
hideDivider ( curScopeVisibleItems [ 0 ] ) ;
@ -323,13 +311,16 @@ export function hideScopedEmptyDividers(container: Element) {
curScopeVisibleItems . length = 0 ;
} ;
// reset hidden dividers
queryElems ( container , '.divider' , showDivider ) ;
// hide the scope dividers if the scope items are empty
for ( const item of container . children ) {
const itemScope = item . getAttribute ( 'data-scope' ) || '' ;
if ( itemScope !== curScope ) {
handleScopeSwitch ( itemScope ) ;
}
if ( ! i tem. classList . contains ( 'filtered' ) && ! item . classList . contains ( 'tw-hidden' ) ) {
if ( ! i sHidden( item ) ) {
curScopeVisibleItems . push ( item as HTMLElement ) ;
}
}
@ -337,20 +328,20 @@ export function hideScopedEmptyDividers(container: Element) {
// hide all leading and trailing dividers
while ( visibleItems . length ) {
if ( ! visibleItems[ 0 ] . matches ( '.divider' ) ) break ;
if ( ! isDivider( visibleItems[ 0 ] ) ) break ;
hideDivider ( visibleItems [ 0 ] ) ;
visibleItems . shift ( ) ;
}
while ( visibleItems . length ) {
if ( ! visibleItems[ visibleItems . length - 1 ] . matches ( '.divider' ) ) break ;
if ( ! isDivider( visibleItems[ visibleItems . length - 1 ] ) ) break ;
hideDivider ( visibleItems [ visibleItems . length - 1 ] ) ;
visibleItems . pop ( ) ;
}
// hide all duplicate dividers, hide current divider if next sibling is still divider
// no need to update "visibleItems" array since this is the last loop
for ( const item of visibleItems ) {
if ( ! item. matches ( '.divider' ) ) continue ;
if ( item. nextElementSibling ? . matches ( '.divider' ) ) hideDivider ( item) ;
for ( let i = 0 ; i < visibleItems . length - 1 ; i ++ ) {
if ( ! v isibleI tems[ i ] . matches ( '.divider' ) ) continue ;
if ( visibleItems[ i + 1 ] . matches ( '.divider' ) ) hideDivider ( v isibleI tems[ i ] ) ;
}
}