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