updated
This commit is contained in:
parent
691c24e899
commit
779eeddf52
290
frontend/src/components/ui/finishedSelectionStore.ts
Normal file
290
frontend/src/components/ui/finishedSelectionStore.ts
Normal 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
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user