|
|
|
@ -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 delegateOne($dropdown: any) {
|
|
|
|
|
function delegateDropdownModule($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 attachInitElements(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 (!item.classList.contains('filtered') && !item.classList.contains('tw-hidden')) {
|
|
|
|
|
if (!isHidden(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 (!visibleItems[i].matches('.divider')) continue;
|
|
|
|
|
if (visibleItems[i + 1].matches('.divider')) hideDivider(visibleItems[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|