diff --git a/src/application/Context/State/Selection/IUserSelection.ts b/src/application/Context/State/Selection/IUserSelection.ts index 12463114..0e23e7b3 100644 --- a/src/application/Context/State/Selection/IUserSelection.ts +++ b/src/application/Context/State/Selection/IUserSelection.ts @@ -6,7 +6,6 @@ import { IEventSource } from '@/infrastructure/Events/IEventSource'; export interface IUserSelection { readonly changed: IEventSource>; readonly selectedScripts: ReadonlyArray; - readonly totalSelected: number; areAllSelected(category: ICategory): boolean; isAnySelected(category: ICategory): boolean; removeAllInCategory(categoryId: number): void; diff --git a/src/application/Context/State/Selection/UserSelection.ts b/src/application/Context/State/Selection/UserSelection.ts index 9f3f132c..1bf64717 100644 --- a/src/application/Context/State/Selection/UserSelection.ts +++ b/src/application/Context/State/Selection/UserSelection.ts @@ -101,10 +101,6 @@ export class UserSelection implements IUserSelection { return this.scripts.getItems(); } - public get totalSelected(): number { - return this.scripts.getItems().length; - } - public selectAll(): void { for (const script of this.collection.getAllScripts()) { if (!this.scripts.exists(script.id)) { diff --git a/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts b/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts new file mode 100644 index 00000000..2e630d08 --- /dev/null +++ b/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts @@ -0,0 +1,86 @@ +import { IScript } from '@/domain/IScript'; +import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; +import { RecommendationLevel } from '@/domain/RecommendationLevel'; +import { scrambledEqual } from '@/application/Common/Array'; +import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; + +export enum SelectionType { + Standard, + Strict, + All, + None, + Custom, +} + +export class SelectionTypeHandler { + constructor(private readonly state: ICategoryCollectionState) { + if (!state) { throw new Error('undefined state'); } + } + public selectType(type: SelectionType) { + if (type === SelectionType.Custom) { + throw new Error('cannot select custom type'); + } + const selector = selectors.get(type); + selector.select(this.state); + } + public getCurrentSelectionType(): SelectionType { + for (const [type, selector] of Array.from(selectors.entries())) { + if (selector.isSelected(this.state)) { + return type; + } + } + return SelectionType.Custom; + } +} + +interface ISingleTypeHandler { + isSelected: (state: ICategoryCollectionState) => boolean; + select: (state: ICategoryCollectionState) => void; +} + +const selectors = new Map([ + [SelectionType.None, { + select: (state) => + state.selection.deselectAll(), + isSelected: (state) => + state.selection.selectedScripts.length === 0, + }], + [SelectionType.Standard, getRecommendationLevelSelector(RecommendationLevel.Standard)], + [SelectionType.Strict, getRecommendationLevelSelector(RecommendationLevel.Strict)], + [SelectionType.All, { + select: (state) => + state.selection.selectAll(), + isSelected: (state) => + state.selection.selectedScripts.length === state.collection.totalScripts, + }], +]); + +function getRecommendationLevelSelector(level: RecommendationLevel): ISingleTypeHandler { + return { + select: (state) => selectOnly(level, state), + isSelected: (state) => hasAllSelectedLevelOf(level, state), + }; +} + +function hasAllSelectedLevelOf(level: RecommendationLevel, state: ICategoryCollectionState) { + const scripts = state.collection.getScriptsByLevel(level); + const selectedScripts = state.selection.selectedScripts; + return areAllSelected(scripts, selectedScripts); +} + +function selectOnly(level: RecommendationLevel, state: ICategoryCollectionState) { + const scripts = state.collection.getScriptsByLevel(level); + state.selection.selectOnly(scripts); +} + +function areAllSelected( + expectedScripts: ReadonlyArray, + selection: ReadonlyArray): boolean { + selection = selection.filter((selected) => !selected.revert); + if (expectedScripts.length < selection.length) { + return false; + } + const selectedScriptIds = selection.map((script) => script.id); + const expectedScriptIds = expectedScripts.map((script) => script.id); + return scrambledEqual(selectedScriptIds, expectedScriptIds); +} diff --git a/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue b/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue index 02edb76b..7aaee3e8 100644 --- a/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue +++ b/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue @@ -5,8 +5,8 @@
@@ -15,8 +15,8 @@
{ + public async selectType(type: SelectionType) { if (this.currentSelection === type) { return; } - const context = await this.getCurrentContextAsync(); - selectType(context.state, type); + this.selectionTypeHandler.selectType(type); } protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void { - this.updateSelections(newState); - newState.selection.changed.on(() => this.updateSelections(newState)); + this.selectionTypeHandler = new SelectionTypeHandler(newState); + this.updateSelections(); + newState.selection.changed.on(() => this.updateSelections()); if (oldState) { - oldState.selection.changed.on(() => this.updateSelections(oldState)); + oldState.selection.changed.on(() => this.updateSelections()); } } - private updateSelections(state: ICategoryCollectionState) { - this.currentSelection = getCurrentSelectionState(state); + private updateSelections() { + this.currentSelection = this.selectionTypeHandler.getCurrentSelectionType(); } } -interface ITypeSelector { - isSelected: (state: ICategoryCollectionState) => boolean; - select: (state: ICategoryCollectionState) => void; -} - -const selectors = new Map([ - [SelectionState.None, { - select: (state) => - state.selection.deselectAll(), - isSelected: (state) => - state.selection.totalSelected === 0, - }], - [SelectionState.Standard, { - select: (state) => - state.selection.selectOnly( - state.collection.getScriptsByLevel(RecommendationLevel.Standard)), - isSelected: (state) => - hasAllSelectedLevelOf(RecommendationLevel.Standard, state), - }], - [SelectionState.Strict, { - select: (state) => - state.selection.selectOnly(state.collection.getScriptsByLevel(RecommendationLevel.Strict)), - isSelected: (state) => - hasAllSelectedLevelOf(RecommendationLevel.Strict, state), - }], - [SelectionState.All, { - select: (state) => - state.selection.selectAll(), - isSelected: (state) => - state.selection.totalSelected === state.collection.totalScripts, - }], -]); - -function selectType(state: ICategoryCollectionState, type: SelectionState) { - const selector = selectors.get(type); - selector.select(state); -} - -function getCurrentSelectionState(state: ICategoryCollectionState): SelectionState { - for (const [type, selector] of Array.from(selectors.entries())) { - if (selector.isSelected(state)) { - return type; - } - } - return SelectionState.Custom; -} - -function hasAllSelectedLevelOf(level: RecommendationLevel, state: ICategoryCollectionState) { - const scripts = state.collection.getScriptsByLevel(level); - const selectedScripts = state.selection.selectedScripts; - return areAllSelected(scripts, selectedScripts); -} - -function areAllSelected( - expectedScripts: ReadonlyArray, - selection: ReadonlyArray): boolean { - selection = selection.filter((selected) => !selected.revert); - if (expectedScripts.length < selection.length) { - return false; - } - const selectedScriptIds = selection.map((script) => script.id).sort(); - const expectedScriptIds = expectedScripts.map((script) => script.id).sort(); - return selectedScriptIds.every((id, index) => id === expectedScriptIds[index]); -}