This commit is contained in:
Linrador 2026-04-14 15:18:49 +02:00
parent 691c24e899
commit 779eeddf52

View File

@ -0,0 +1,290 @@
// frontend\src\components\ui\finishedSelectionStore.ts
'use client'
import { useSyncExternalStore } from 'react'
export type FinishedSelectionItem = {
key: string
file: string
}
type Listener = () => void
export type FinishedSelectionStore = {
subscribe: (listener: Listener) => () => void
subscribeKey: (key: string, listener: Listener) => () => void
getVersion: () => number
isSelected: (key: string) => boolean
hasAnySelection: () => boolean
getCount: () => number
getSelectedFiles: () => string[]
select: (item: FinishedSelectionItem) => void
selectMany: (items: FinishedSelectionItem[]) => void
deselect: (key: string) => void
deselectMany: (keys: string[]) => void
toggle: (item: FinishedSelectionItem) => void
clear: () => void
replaceFile: (oldFile: string, newFile: string) => void
}
export function createFinishedSelectionStore(): FinishedSelectionStore {
let items: Record<string, FinishedSelectionItem> = {}
let version = 0
const listeners = new Set<Listener>()
const keyListeners = new Map<string, Set<Listener>>()
const emit = (changedKeys?: Iterable<string>) => {
version += 1
for (const listener of listeners) {
listener()
}
if (!changedKeys) return
const notified = new Set<Listener>()
for (const key of changedKeys) {
const set = keyListeners.get(key)
if (!set) continue
for (const listener of set) {
if (notified.has(listener)) continue
notified.add(listener)
listener()
}
}
}
const subscribe = (listener: Listener) => {
listeners.add(listener)
return () => {
listeners.delete(listener)
}
}
const subscribeKey = (key: string, listener: Listener) => {
const cleanKey = String(key ?? '')
let set = keyListeners.get(cleanKey)
if (!set) {
set = new Set<Listener>()
keyListeners.set(cleanKey, set)
}
set.add(listener)
return () => {
const current = keyListeners.get(cleanKey)
if (!current) return
current.delete(listener)
if (current.size === 0) {
keyListeners.delete(cleanKey)
}
}
}
const isSelected = (key: string) => {
const cleanKey = String(key ?? '')
return Boolean(cleanKey && items[cleanKey])
}
const hasAnySelection = () => {
for (const _key in items) {
return true
}
return false
}
const getCount = () => Object.keys(items).length
const getSelectedFiles = () => {
return Object.values(items)
.map((item) => String(item.file || '').trim())
.filter(Boolean)
}
const select = (item: FinishedSelectionItem) => {
const key = String(item?.key ?? '').trim()
const file = String(item?.file ?? '').trim()
if (!key || !file) return
const prev = items[key]
if (prev && prev.file === file) return
items = {
...items,
[key]: { key, file },
}
emit([key])
}
const selectMany = (nextItems: FinishedSelectionItem[]) => {
if (!Array.isArray(nextItems) || nextItems.length === 0) return
let next = items
const changedKeys: string[] = []
for (const item of nextItems) {
const key = String(item?.key ?? '').trim()
const file = String(item?.file ?? '').trim()
if (!key || !file) continue
const prev = next[key]
if (prev && prev.file === file) continue
if (next === items) {
next = { ...items }
}
next[key] = { key, file }
changedKeys.push(key)
}
if (changedKeys.length === 0) return
items = next
emit(changedKeys)
}
const deselect = (key: string) => {
const cleanKey = String(key ?? '').trim()
if (!cleanKey || !items[cleanKey]) return
const next = { ...items }
delete next[cleanKey]
items = next
emit([cleanKey])
}
const deselectMany = (keys: string[]) => {
if (!Array.isArray(keys) || keys.length === 0) return
let next = items
const changedKeys: string[] = []
for (const rawKey of keys) {
const key = String(rawKey ?? '').trim()
if (!key || !next[key]) continue
if (next === items) {
next = { ...items }
}
delete next[key]
changedKeys.push(key)
}
if (changedKeys.length === 0) return
items = next
emit(changedKeys)
}
const toggle = (item: FinishedSelectionItem) => {
const key = String(item?.key ?? '').trim()
const file = String(item?.file ?? '').trim()
if (!key || !file) return
if (items[key]) {
const next = { ...items }
delete next[key]
items = next
emit([key])
return
}
items = {
...items,
[key]: { key, file },
}
emit([key])
}
const clear = () => {
const keys = Object.keys(items)
if (keys.length === 0) return
items = {}
emit(keys)
}
const replaceFile = (oldFile: string, newFile: string) => {
const from = String(oldFile ?? '').trim()
const to = String(newFile ?? '').trim()
if (!from || !to || from === to) return
let next = items
const changedKeys: string[] = []
for (const [key, item] of Object.entries(items)) {
if (item.file !== from) continue
if (next === items) {
next = { ...items }
}
next[key] = {
...item,
file: to,
}
changedKeys.push(key)
}
if (changedKeys.length === 0) return
items = next
emit(changedKeys)
}
return {
subscribe,
subscribeKey,
getVersion: () => version,
isSelected,
hasAnySelection,
getCount,
getSelectedFiles,
select,
selectMany,
deselect,
deselectMany,
toggle,
clear,
replaceFile,
}
}
export function useSelectionVersion(store: FinishedSelectionStore): number {
return useSyncExternalStore(
store.subscribe,
store.getVersion,
() => 0
)
}
export function useSelectionChecked(
store: FinishedSelectionStore,
key: string
): boolean {
const cleanKey = String(key ?? '')
return useSyncExternalStore(
(listener) => store.subscribeKey(cleanKey, listener),
() => store.isSelected(cleanKey),
() => false
)
}