Skip to content

Commit 0114f5d

Browse files
committed
refactor: extract catalog interaction handlers into composable
Signed-off-by: Vitor Mattos <[email protected]>
1 parent 1a2713d commit 0114f5d

2 files changed

Lines changed: 173 additions & 120 deletions

File tree

src/views/Settings/PolicyWorkbench/Catalog/Catalog.vue

Lines changed: 13 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@ import type { RealPolicySettingCategory } from '../settings/realTypes'
552552
import PolicyRuleEditorPanel from '../PolicyRuleEditorPanel.vue'
553553
import { createRealPolicyWorkbenchState } from '../useRealPolicyWorkbench'
554554
import { useCatalogState } from './composables/useCatalogState'
555+
import { useCatalogInteractions } from './composables/useCatalogInteractions'
555556
import { useNavigation } from './composables/useNavigation'
556557
557558
defineOptions({
@@ -571,9 +572,6 @@ const selectedCreateScope = ref<'system' | 'group' | 'user' | null>(null)
571572
const isRemovingRule = ref(false)
572573
const removalFeedback = ref<string | null>(null)
573574
const removalFeedbackTimeout = ref<number | null>(null)
574-
const lastPress = ref<{ surface: 'cards' | 'list', key: string, x: number, y: number } | null>(null)
575-
const recentSelectionGesture = ref<{ surface: 'cards' | 'list', key: string, at: number } | null>(null)
576-
const clearCatalogFocusOnClose = ref(false)
577575
const openRuleActionsKey = ref<string | null>(null)
578576
const crudSearch = ref('')
579577
const crudScopeFilter = ref<'all' | 'system' | 'group' | 'user'>('all')
@@ -583,10 +581,20 @@ const isRtl = ref(false)
583581
584582
// Initialize catalog state composable
585583
const catalogState = useCatalogState()
584+
const {
585+
clearCatalogFocusOnClose,
586+
markSelectionGesture,
587+
trackPress,
588+
openSettingFromPointer,
589+
openSettingFromAction,
590+
openSettingFromKeyboard,
591+
highlightText,
592+
} = useCatalogInteractions({
593+
getFilter: () => catalogState.settingsFilter.value,
594+
onOpenSetting: (key) => state.openSetting(key as never),
595+
})
586596
587597
const CRUD_PAGE_SIZE = 20
588-
const DRAG_OPEN_THRESHOLD_PX = 6
589-
const SELECTION_GUARD_WINDOW_MS = 400
590598
const REMOVAL_FEEDBACK_DURATION_MS = 6000
591599
592600
const CATEGORY_ORDER: RealPolicySettingCategory[] = [
@@ -1217,121 +1225,6 @@ function requestBackToCreateScope() {
12171225
selectedCreateScope.value = null
12181226
}
12191227
1220-
function markSelectionGesture(surface: 'cards' | 'list', key: string) {
1221-
if (!hasActiveTextSelection()) {
1222-
return
1223-
}
1224-
1225-
recentSelectionGesture.value = {
1226-
surface,
1227-
key,
1228-
at: Date.now(),
1229-
}
1230-
}
1231-
1232-
function shouldIgnoreDueToRecentSelection(surface: 'cards' | 'list', key: string) {
1233-
const gesture = recentSelectionGesture.value
1234-
if (!gesture) {
1235-
return false
1236-
}
1237-
1238-
const expired = (Date.now() - gesture.at) > SELECTION_GUARD_WINDOW_MS
1239-
const matchesTarget = gesture.surface === surface && gesture.key === key
1240-
if (expired || !matchesTarget) {
1241-
return false
1242-
}
1243-
1244-
recentSelectionGesture.value = null
1245-
return true
1246-
}
1247-
1248-
function isPlainPrimaryClick(event: MouseEvent) {
1249-
const button = typeof event.button === 'number' ? event.button : 0
1250-
const hasModifier = Boolean(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
1251-
return button === 0 && !hasModifier
1252-
}
1253-
1254-
function trackPress(surface: 'cards' | 'list', key: string, event: PointerEvent) {
1255-
if (event.button !== 0) {
1256-
lastPress.value = null
1257-
return
1258-
}
1259-
1260-
lastPress.value = {
1261-
surface,
1262-
key,
1263-
x: event.clientX,
1264-
y: event.clientY,
1265-
}
1266-
}
1267-
1268-
function movedBeyondThreshold(event: MouseEvent, press: { x: number, y: number }) {
1269-
const deltaX = Math.abs(event.clientX - press.x)
1270-
const deltaY = Math.abs(event.clientY - press.y)
1271-
return deltaX > DRAG_OPEN_THRESHOLD_PX || deltaY > DRAG_OPEN_THRESHOLD_PX
1272-
}
1273-
1274-
function openSettingFromPointer(surface: 'cards' | 'list', key: string, event: MouseEvent) {
1275-
if (!isPlainPrimaryClick(event)) {
1276-
return
1277-
}
1278-
1279-
if (shouldIgnoreDueToRecentSelection(surface, key)) {
1280-
return
1281-
}
1282-
1283-
if (hasActiveTextSelection()) {
1284-
return
1285-
}
1286-
1287-
const press = lastPress.value
1288-
if (press && press.surface === surface && press.key === key && movedBeyondThreshold(event, press)) {
1289-
return
1290-
}
1291-
1292-
clearCatalogFocusOnClose.value = true
1293-
state.openSetting(key as never)
1294-
}
1295-
1296-
function openSettingFromAction(key: string, event: MouseEvent) {
1297-
clearCatalogFocusOnClose.value = event.detail > 0
1298-
state.openSetting(key as never)
1299-
}
1300-
1301-
function openSettingFromKeyboard(key: string) {
1302-
clearCatalogFocusOnClose.value = false
1303-
state.openSetting(key as never)
1304-
}
1305-
1306-
function hasActiveTextSelection() {
1307-
const selection = window.getSelection()
1308-
return !!selection && selection.type === 'Range' && selection.toString().trim().length > 0
1309-
}
1310-
1311-
function escapeRegExp(value: string) {
1312-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
1313-
}
1314-
1315-
function escapeHtml(value: string) {
1316-
return value
1317-
.replaceAll('&', '&amp;')
1318-
.replaceAll('<', '&lt;')
1319-
.replaceAll('>', '&gt;')
1320-
.replaceAll('"', '&quot;')
1321-
.replaceAll("'", '&#39;')
1322-
}
1323-
1324-
function highlightText(value: string) {
1325-
const query = catalogState.settingsFilter.value.trim()
1326-
const safeValue = escapeHtml(value)
1327-
if (!query) {
1328-
return safeValue
1329-
}
1330-
1331-
const matcher = new RegExp(`(${escapeRegExp(query)})`, 'ig')
1332-
return safeValue.replace(matcher, '<mark>$1</mark>')
1333-
}
1334-
13351228
function hasActiveOverrides(groupCount: number, userCount: number) {
13361229
return groupCount > 0 || userCount > 0
13371230
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { ref } from 'vue'
7+
8+
type Surface = 'cards' | 'list'
9+
10+
type LastPress = {
11+
surface: Surface,
12+
key: string,
13+
x: number,
14+
y: number,
15+
}
16+
17+
type RecentSelectionGesture = {
18+
surface: Surface,
19+
key: string,
20+
at: number,
21+
}
22+
23+
const DRAG_OPEN_THRESHOLD_PX = 6
24+
const SELECTION_GUARD_WINDOW_MS = 400
25+
26+
type UseCatalogInteractionsOptions = {
27+
getFilter: () => string,
28+
onOpenSetting: (key: string) => void,
29+
}
30+
31+
export function useCatalogInteractions(options: UseCatalogInteractionsOptions) {
32+
const lastPress = ref<LastPress | null>(null)
33+
const recentSelectionGesture = ref<RecentSelectionGesture | null>(null)
34+
const clearCatalogFocusOnClose = ref(false)
35+
36+
function hasActiveTextSelection() {
37+
const selection = window.getSelection()
38+
return !!selection && selection.type === 'Range' && selection.toString().trim().length > 0
39+
}
40+
41+
function markSelectionGesture(surface: Surface, key: string) {
42+
if (!hasActiveTextSelection()) {
43+
return
44+
}
45+
46+
recentSelectionGesture.value = {
47+
surface,
48+
key,
49+
at: Date.now(),
50+
}
51+
}
52+
53+
function shouldIgnoreDueToRecentSelection(surface: Surface, key: string) {
54+
const gesture = recentSelectionGesture.value
55+
if (!gesture) {
56+
return false
57+
}
58+
59+
const expired = (Date.now() - gesture.at) > SELECTION_GUARD_WINDOW_MS
60+
const matchesTarget = gesture.surface === surface && gesture.key === key
61+
if (expired || !matchesTarget) {
62+
return false
63+
}
64+
65+
recentSelectionGesture.value = null
66+
return true
67+
}
68+
69+
function isPlainPrimaryClick(event: MouseEvent) {
70+
const button = typeof event.button === 'number' ? event.button : 0
71+
const hasModifier = Boolean(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
72+
return button === 0 && !hasModifier
73+
}
74+
75+
function trackPress(surface: Surface, key: string, event: PointerEvent) {
76+
if (event.button !== 0) {
77+
lastPress.value = null
78+
return
79+
}
80+
81+
lastPress.value = {
82+
surface,
83+
key,
84+
x: event.clientX,
85+
y: event.clientY,
86+
}
87+
}
88+
89+
function movedBeyondThreshold(event: MouseEvent, press: { x: number, y: number }) {
90+
const deltaX = Math.abs(event.clientX - press.x)
91+
const deltaY = Math.abs(event.clientY - press.y)
92+
return deltaX > DRAG_OPEN_THRESHOLD_PX || deltaY > DRAG_OPEN_THRESHOLD_PX
93+
}
94+
95+
function openSettingFromPointer(surface: Surface, key: string, event: MouseEvent) {
96+
if (!isPlainPrimaryClick(event)) {
97+
return
98+
}
99+
100+
if (shouldIgnoreDueToRecentSelection(surface, key)) {
101+
return
102+
}
103+
104+
if (hasActiveTextSelection()) {
105+
return
106+
}
107+
108+
const press = lastPress.value
109+
if (press && press.surface === surface && press.key === key && movedBeyondThreshold(event, press)) {
110+
return
111+
}
112+
113+
clearCatalogFocusOnClose.value = true
114+
options.onOpenSetting(key)
115+
}
116+
117+
function openSettingFromAction(key: string, event: MouseEvent) {
118+
clearCatalogFocusOnClose.value = event.detail > 0
119+
options.onOpenSetting(key)
120+
}
121+
122+
function openSettingFromKeyboard(key: string) {
123+
clearCatalogFocusOnClose.value = false
124+
options.onOpenSetting(key)
125+
}
126+
127+
function escapeRegExp(value: string) {
128+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
129+
}
130+
131+
function escapeHtml(value: string) {
132+
return value
133+
.replaceAll('&', '&amp;')
134+
.replaceAll('<', '&lt;')
135+
.replaceAll('>', '&gt;')
136+
.replaceAll('"', '&quot;')
137+
.replaceAll("'", '&#39;')
138+
}
139+
140+
function highlightText(value: string) {
141+
const query = options.getFilter().trim()
142+
const safeValue = escapeHtml(value)
143+
if (!query) {
144+
return safeValue
145+
}
146+
147+
const matcher = new RegExp(`(${escapeRegExp(query)})`, 'ig')
148+
return safeValue.replace(matcher, '<mark>$1</mark>')
149+
}
150+
151+
return {
152+
clearCatalogFocusOnClose,
153+
markSelectionGesture,
154+
trackPress,
155+
openSettingFromPointer,
156+
openSettingFromAction,
157+
openSettingFromKeyboard,
158+
highlightText,
159+
}
160+
}

0 commit comments

Comments
 (0)