diff --git a/web_src/js/features/group.ts b/web_src/js/features/group.ts new file mode 100644 index 0000000000..91989ffae4 --- /dev/null +++ b/web_src/js/features/group.ts @@ -0,0 +1,96 @@ +import {createSortable} from '../modules/sortable.ts'; +import Sortable, {type SortableEvent, type SortableOptions} from 'sortablejs'; +import {POST} from '../modules/fetch.ts'; +import {toggleElem} from '../utils/dom.ts'; +export function initCommonGroup() { + if (!document.querySelectorAll('.group').length) { + return; + } + + document.querySelector('.group.settings.options #group_name')?.addEventListener('input', function () { + const nameChanged = this.value.toLowerCase() !== this.getAttribute('data-group-name').toLowerCase(); + toggleElem('#group-name-change-prompt', nameChanged); + }); +} + +async function moveItem({item, from, to, oldIndex}: SortableEvent): Promise { + const closestUl = to.nodeName.toLowerCase() === 'ul' ? to : to.closest('ul'); + const sortable = Sortable.get(closestUl); + const isGroup = Boolean(item.getAttribute('data-is-group')); + const strs = sortable.toArray(); + const newIndex = Math.max(1, strs.filter((a) => isGroup ? + a.toLocaleLowerCase().startsWith('group') : + a.toLocaleLowerCase().startsWith('repo')).indexOf(item.getAttribute('data-sort-id')) + 1); + const data = { + newParent: parseInt(to.closest('ul').closest('li')?.getAttribute('data-id') || '0'), + id: parseInt(item.getAttribute('data-id')), + newPos: newIndex, + isGroup, + }; + try { + await POST(`${to.getAttribute('data-url')}/items/move`, { + data, + }); + } catch (error) { + console.error(error); + from.insertBefore(item, from.children[oldIndex]); + } +} + +function idSortFn(a: string, b: string): number { + return parseInt(a.split('-')[2]) - parseInt(b.split('-')[2]); +} + +function onEnd(ev: SortableEvent) { + const {to} = ev; + const closestUl = to.nodeName.toLowerCase() === 'ul' ? to : (to.closest('li')?.closest('ul') ?? to.closest('ul')); + const sortable = Sortable.get(closestUl); + const strs = sortable.toArray(); + const groups = strs.filter((a) => a.toLocaleLowerCase().startsWith('group')); + const repos = strs.filter((a) => a.toLocaleLowerCase().startsWith('repo')); + const newArr = [...groups.toSorted(idSortFn), ...repos.toSorted(idSortFn)]; + sortable.sort(newArr, true); + moveItem(ev); +} + +const baseSortableOptions: SortableOptions = { + group: { + name: 'group', + put(to, from, drag, ev) { + console.debug('to', to); + console.debug('from', from); + console.debug('drag', drag); + console.debug('ev', ev); + const closestUl = to.el.nodeName.toLowerCase() === 'ul' ? to.el : to.el.closest('ul'); + console.debug('put this'); + return Boolean(closestUl?.getAttribute('data-is-group') ?? true); + }, + pull: true, + }, + dataIdAttr: 'data-sort-id', + draggable: '.expandable-menu-item', + delayOnTouchOnly: true, + delay: 500, + // onMove: beforeMove, + onEnd, + emptyInsertThreshold: 25, +}; + +function initSubgroupSortable(list: Element) { + createSortable(list, {...baseSortableOptions}); + for (const el of list.querySelectorAll('.expandable-menu-item')) { + if (!el.getAttribute('data-is-group')) continue; + const sublist = el.querySelector('ul'); + initSubgroupSortable(sublist); + } +} + +async function initGroupSortable(parentEl: Element): Promise { + initSubgroupSortable(parentEl); +} + +export function initGroup(): void { + const mainContainer = document.querySelector('#group-navigation-menu > .sortable'); + if (!mainContainer) return; + initGroupSortable(mainContainer); +} diff --git a/web_src/js/index.ts b/web_src/js/index.ts index d4d5ea6cff..aa21ccdc06 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -67,6 +67,7 @@ import {callInitFunctions} from './modules/init.ts'; import {initRepoViewFileTree} from './features/repo-view-file-tree.ts'; import {initActionsPermissionsForm} from './features/common-actions-permissions.ts'; import {initGlobalShortcut} from './modules/shortcut.ts'; +import {initCommonGroup, initGroup} from "./features/group.ts"; const initStartTime = performance.now(); const initPerformanceTracer = callInitFunctions([ @@ -159,6 +160,9 @@ const initPerformanceTracer = callInitFunctions([ initOAuth2SettingsDisableCheckbox, initRepoFileView, + + initCommonGroup, + initGroup, initActionsPermissionsForm, ]); @@ -171,16 +175,4 @@ if (initDur > 500) { console.error(`slow init functions took ${initDur.toFixed(3)}ms`); } -// https://htmx.org/events/#htmx:sendError -type HtmxEvent = Event & {detail: HtmxResponseInfo}; -document.body.addEventListener('htmx:sendError', (event) => { - // TODO: add translations - showErrorToast(`Network error when calling ${(event as HtmxEvent).detail.requestConfig.path}`); -}); -// https://htmx.org/events/#htmx:responseError -document.body.addEventListener('htmx:responseError', (event) => { - // TODO: add translations - showErrorToast(`Error ${(event as HtmxEvent).detail.xhr.status} when calling ${(event as HtmxEvent).detail.requestConfig.path}`); -}); - document.dispatchEvent(new CustomEvent('gitea:index-ready'));