@nexxtmove/ui 1.47.0 → 1.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/organisms/IconPicker/IconPicker.vue.d.ts +142 -0
- package/dist/components/organisms/IconPicker/_IconPickerGrid.vue.d.ts +40 -0
- package/dist/components/organisms/IconPicker/_IconPickerRail.vue.d.ts +22 -0
- package/dist/components/organisms/IconPicker/iconPickerHelpers.d.ts +39 -0
- package/dist/components/organisms/IconPicker/useIconPickerCatalogue.d.ts +72 -0
- package/dist/components/organisms/IconPicker/useIconPickerNavigation.d.ts +60 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2097 -1365
- package/dist/nuxt.js +1 -0
- package/package.json +1 -1
- package/src/components/organisms/IconPicker/IconPicker.vue +621 -0
- package/src/components/organisms/IconPicker/_IconPickerGrid.vue +282 -0
- package/src/components/organisms/IconPicker/_IconPickerRail.vue +112 -0
- package/src/components/organisms/IconPicker/iconPickerHelpers.ts +111 -0
- package/src/components/organisms/IconPicker/useIconPickerCatalogue.ts +238 -0
- package/src/components/organisms/IconPicker/useIconPickerNavigation.ts +206 -0
- package/src/components.json +1 -0
- package/src/index.ts +18 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { computed, toValue, type MaybeRefOrGetter, type Ref } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
NexxtIconPickerCategory,
|
|
4
|
+
NexxtIconPickerIcon,
|
|
5
|
+
NexxtIconPickerPinned,
|
|
6
|
+
NexxtIconPickerStyle,
|
|
7
|
+
NexxtIconPickerValue,
|
|
8
|
+
} from './IconPicker.vue'
|
|
9
|
+
import {
|
|
10
|
+
iconLabelFromName,
|
|
11
|
+
iconSearchIndex,
|
|
12
|
+
iconStyleFor,
|
|
13
|
+
searchIcons,
|
|
14
|
+
sortIconsByLabel,
|
|
15
|
+
} from './iconPickerHelpers'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Six columns with a 12px gap (`gap-3`) scan more easily than eight close together. `grid-cols-6`
|
|
19
|
+
* in `_IconPickerGrid.vue` is the same number: change both together.
|
|
20
|
+
*/
|
|
21
|
+
export const COLUMNS = 6
|
|
22
|
+
/** Two rows. */
|
|
23
|
+
const PINNED_MAX = 2 * COLUMNS
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* On every glyph `<i>`. Font Awesome centres its `::before` with `text-align` in a fixed 1.25em
|
|
27
|
+
* box, which stops working once the box is a flex container (Font Awesome's display variable set
|
|
28
|
+
* to `inline-flex`, as in Storybook); centring it as a flex box as well holds in both, as
|
|
29
|
+
* NexxtIcon does. `leading-none` keeps the line box at the glyph's own height.
|
|
30
|
+
*/
|
|
31
|
+
const GLYPH_CLASSES = 'items-center justify-center leading-none'
|
|
32
|
+
|
|
33
|
+
/** A line of the grid: a section heading, or up to `COLUMNS` icons. */
|
|
34
|
+
export type GridRow =
|
|
35
|
+
| { kind: 'heading'; key: string; label: string }
|
|
36
|
+
| { kind: 'icons'; key: string; icons: NexxtIconPickerIcon[] }
|
|
37
|
+
|
|
38
|
+
/** A cell, by its index in the rows and its column. */
|
|
39
|
+
export interface GridPosition {
|
|
40
|
+
row: number
|
|
41
|
+
col: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A rail item; `null` is "all icons". */
|
|
45
|
+
export interface RailTab {
|
|
46
|
+
id: string | null
|
|
47
|
+
label: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const toRows = (list: NexxtIconPickerIcon[], section: string): GridRow[] =>
|
|
51
|
+
Array.from({ length: Math.ceil(list.length / COLUMNS) }, (_, index) => ({
|
|
52
|
+
kind: 'icons' as const,
|
|
53
|
+
key: `${section}-${index}`,
|
|
54
|
+
icons: list.slice(index * COLUMNS, (index + 1) * COLUMNS),
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
const isKnown = (icon: NexxtIconPickerIcon | undefined): icon is NexxtIconPickerIcon => !!icon
|
|
58
|
+
|
|
59
|
+
interface IconPickerCatalogueOptions {
|
|
60
|
+
icons: MaybeRefOrGetter<NexxtIconPickerIcon[]>
|
|
61
|
+
categories: MaybeRefOrGetter<NexxtIconPickerCategory[]>
|
|
62
|
+
pinned: MaybeRefOrGetter<NexxtIconPickerPinned | undefined>
|
|
63
|
+
styles: MaybeRefOrGetter<NexxtIconPickerStyle[]>
|
|
64
|
+
/** A getter, never the function itself: `toValue` would call that without a name. */
|
|
65
|
+
iconClass: () => (name: string) => string
|
|
66
|
+
sortLocale: MaybeRefOrGetter<string>
|
|
67
|
+
/** Heading of the full list under a pinned section. */
|
|
68
|
+
allIconsLabel: MaybeRefOrGetter<string>
|
|
69
|
+
model: Ref<NexxtIconPickerValue | null>
|
|
70
|
+
/** The trimmed search. */
|
|
71
|
+
term: Ref<string>
|
|
72
|
+
/** The chosen category; `null` is "all icons". */
|
|
73
|
+
category: Ref<string | null>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* What the picker shows, derived from the catalogue, the model, the search and the category: the
|
|
78
|
+
* rail's categories, the grid's rows and where the current icon is in them, and the classes that
|
|
79
|
+
* draw a glyph. No DOM, no focus: that is `useIconPickerNavigation` and the parts.
|
|
80
|
+
*/
|
|
81
|
+
export const useIconPickerCatalogue = (options: IconPickerCatalogueOptions) => {
|
|
82
|
+
const { model, term, category } = options
|
|
83
|
+
|
|
84
|
+
const iconsByName = computed(
|
|
85
|
+
() => new Map(toValue(options.icons).map((icon) => [icon.name, icon])),
|
|
86
|
+
)
|
|
87
|
+
const searchIndex = computed(() =>
|
|
88
|
+
iconSearchIndex(toValue(options.icons), toValue(options.sortLocale)),
|
|
89
|
+
)
|
|
90
|
+
const sortedIcons = computed(() => searchIndex.value.map((entry) => entry.icon))
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The categories the rail shows, sorted, each with the icons of it the catalogue knows. One
|
|
94
|
+
* without any is left out: while the catalogue loads that is all of them, and the skeletons
|
|
95
|
+
* stand in.
|
|
96
|
+
*/
|
|
97
|
+
const shownCategories = computed(() => {
|
|
98
|
+
const { compare } = new Intl.Collator(toValue(options.sortLocale))
|
|
99
|
+
return toValue(options.categories)
|
|
100
|
+
.map(({ id, label, icons: names }) => ({
|
|
101
|
+
id,
|
|
102
|
+
label,
|
|
103
|
+
icons: names.map((name) => iconsByName.value.get(name)).filter(isKnown),
|
|
104
|
+
}))
|
|
105
|
+
.filter((entry) => entry.icons.length > 0)
|
|
106
|
+
.sort((a, b) => compare(a.label, b.label))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const styleOrder = computed(() => toValue(options.styles).map((style) => style.id))
|
|
110
|
+
const stylesById = computed(
|
|
111
|
+
() => new Map(toValue(options.styles).map((style) => [style.id, style])),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const catalogueEmpty = computed(() => toValue(options.icons).length === 0)
|
|
115
|
+
|
|
116
|
+
/** The current icon, also while the catalogue that would describe it is not in. */
|
|
117
|
+
const currentIcon = computed<NexxtIconPickerIcon | null>(() => {
|
|
118
|
+
if (!model.value) return null
|
|
119
|
+
const { icon, style } = model.value
|
|
120
|
+
return (
|
|
121
|
+
iconsByName.value.get(icon) ?? {
|
|
122
|
+
name: icon,
|
|
123
|
+
label: iconLabelFromName(icon),
|
|
124
|
+
terms: [],
|
|
125
|
+
styles: [style],
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
/** The style of the trigger: the stored one. */
|
|
131
|
+
const currentStyle = computed(() =>
|
|
132
|
+
model.value ? (stylesById.value.get(model.value.style) ?? null) : null,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
/** The category on show: the chosen one while the rail still has it, "all icons" otherwise. */
|
|
136
|
+
const activeCategory = computed(
|
|
137
|
+
() => shownCategories.value.find(({ id }) => id === category.value) ?? null,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
/** The current icon first, then the pinned ones the catalogue knows, two rows at most. */
|
|
141
|
+
const pinnedIcons = computed(() => {
|
|
142
|
+
const pinned = toValue(options.pinned)
|
|
143
|
+
if (!pinned) return []
|
|
144
|
+
const current = currentIcon.value
|
|
145
|
+
const others = pinned.icons
|
|
146
|
+
.filter((name) => name !== current?.name)
|
|
147
|
+
.map((name) => iconsByName.value.get(name))
|
|
148
|
+
.filter(isKnown)
|
|
149
|
+
return [...(current ? [current] : []), ...others].slice(0, PINNED_MAX)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const rows = computed<GridRow[]>(() => {
|
|
153
|
+
// No catalogue (it failed, or is on its way behind the skeletons): the current icon stays
|
|
154
|
+
// choosable
|
|
155
|
+
if (catalogueEmpty.value) return toRows(currentIcon.value ? [currentIcon.value] : [], 'current')
|
|
156
|
+
|
|
157
|
+
if (term.value) return toRows(searchIcons(searchIndex.value, term.value), 'results')
|
|
158
|
+
|
|
159
|
+
if (activeCategory.value) {
|
|
160
|
+
const { id, icons: list } = activeCategory.value
|
|
161
|
+
return toRows(sortIconsByLabel(list, toValue(options.sortLocale)), `category-${id}`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const pinned = toValue(options.pinned)
|
|
165
|
+
if (pinned && pinnedIcons.value.length > 0) {
|
|
166
|
+
return [
|
|
167
|
+
{ kind: 'heading', key: 'pinned-heading', label: pinned.label },
|
|
168
|
+
...toRows(pinnedIcons.value, 'pinned'),
|
|
169
|
+
{ kind: 'heading', key: 'all-heading', label: toValue(options.allIconsLabel) },
|
|
170
|
+
...toRows(sortedIcons.value, 'all'),
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return toRows(sortedIcons.value, 'all')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
/** The indexes of the rows that hold icons, in order. */
|
|
178
|
+
const iconRows = computed(() =>
|
|
179
|
+
rows.value.flatMap((row, index) => (row.kind === 'icons' ? [index] : [])),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
/** The icons in the list on show; the pinned section repeats icons, so it does not count. */
|
|
183
|
+
const iconCount = computed(
|
|
184
|
+
() =>
|
|
185
|
+
rows.value.reduce((count, row) => count + (row.kind === 'icons' ? row.icons.length : 0), 0) -
|
|
186
|
+
(rows.value[0]?.kind === 'heading' ? pinnedIcons.value.length : 0),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
const rowLength = (row: number | undefined) => {
|
|
190
|
+
const entry = row === undefined ? undefined : rows.value[row]
|
|
191
|
+
return entry?.kind === 'icons' ? entry.icons.length : 0
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const iconAt = (position: GridPosition | null) => {
|
|
195
|
+
if (!position) return null
|
|
196
|
+
const row = rows.value[position.row]
|
|
197
|
+
return row?.kind === 'icons' ? (row.icons[position.col] ?? null) : null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Where the current icon first appears in the grid. */
|
|
201
|
+
const currentPosition = computed<GridPosition | null>(() => {
|
|
202
|
+
const name = model.value?.icon
|
|
203
|
+
if (!name) return null
|
|
204
|
+
for (const row of iconRows.value) {
|
|
205
|
+
const entry = rows.value[row]
|
|
206
|
+
const col = entry?.kind === 'icons' ? entry.icons.findIndex((icon) => icon.name === name) : -1
|
|
207
|
+
if (col !== -1) return { row, col }
|
|
208
|
+
}
|
|
209
|
+
return null
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
/** What a choice of this icon writes, and so what its cell shows: the current style, or its first. */
|
|
213
|
+
const styleOf = (icon: NexxtIconPickerIcon) =>
|
|
214
|
+
iconStyleFor(icon.styles, model.value?.style, styleOrder.value)
|
|
215
|
+
|
|
216
|
+
/** The classes of a glyph: those of its style, the one from `iconClass`, and the centring. */
|
|
217
|
+
const glyphClass = (icon: NexxtIconPickerIcon, style: string | null = styleOf(icon)) => [
|
|
218
|
+
...(style ? (stylesById.value.get(style)?.classes ?? []) : []),
|
|
219
|
+
options.iconClass()(icon.name),
|
|
220
|
+
GLYPH_CLASSES,
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
shownCategories,
|
|
225
|
+
activeCategory,
|
|
226
|
+
catalogueEmpty,
|
|
227
|
+
currentIcon,
|
|
228
|
+
currentStyle,
|
|
229
|
+
rows,
|
|
230
|
+
iconRows,
|
|
231
|
+
iconCount,
|
|
232
|
+
currentPosition,
|
|
233
|
+
rowLength,
|
|
234
|
+
iconAt,
|
|
235
|
+
styleOf,
|
|
236
|
+
glyphClass,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { computed, ref, watch, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
import { useDebounceFn } from '@vueuse/core'
|
|
3
|
+
import type { NexxtIconPickerIcon } from './IconPicker.vue'
|
|
4
|
+
import type { GridPosition } from './useIconPickerCatalogue'
|
|
5
|
+
|
|
6
|
+
const RESULTS_DELAY = 500
|
|
7
|
+
|
|
8
|
+
/** What the grid part exposes: the parts of navigating that need its scroll box. */
|
|
9
|
+
export interface IconPickerGridHandle {
|
|
10
|
+
/** Focus the grid itself; the highlight is its active descendant. */
|
|
11
|
+
focus: () => void
|
|
12
|
+
/** Scroll the least needed to show the row, and render the rows there at once. */
|
|
13
|
+
reveal: (row: number) => void
|
|
14
|
+
/** Whether the row is fully in view. */
|
|
15
|
+
inView: (row: number) => boolean
|
|
16
|
+
/** The rows that fit in view, less one, at least one. */
|
|
17
|
+
pageRows: () => number
|
|
18
|
+
scrollToTop: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface IconPickerNavigationOptions {
|
|
22
|
+
/** The indexes of the rows that hold icons, in order. */
|
|
23
|
+
iconRows: ComputedRef<number[]>
|
|
24
|
+
currentPosition: ComputedRef<GridPosition | null>
|
|
25
|
+
iconCount: ComputedRef<number>
|
|
26
|
+
catalogueEmpty: ComputedRef<boolean>
|
|
27
|
+
iconAt: (position: GridPosition | null) => NexxtIconPickerIcon | null
|
|
28
|
+
rowLength: (row: number | undefined) => number
|
|
29
|
+
/** The raw search: a letter typed in the grid is appended to it. */
|
|
30
|
+
search: Ref<string>
|
|
31
|
+
/** The trimmed search. */
|
|
32
|
+
term: ComputedRef<string>
|
|
33
|
+
category: Ref<string | null>
|
|
34
|
+
isOpen: Ref<boolean>
|
|
35
|
+
grid: () => IconPickerGridHandle | null | undefined
|
|
36
|
+
focusSearch: () => void
|
|
37
|
+
choose: (icon: NexxtIconPickerIcon) => void
|
|
38
|
+
/** The copy of the announcement. */
|
|
39
|
+
results: () => (count: number) => string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isPrintable = (event: KeyboardEvent) =>
|
|
43
|
+
event.key.length === 1 && event.key !== ' ' && !event.ctrlKey && !event.metaKey && !event.altKey
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The keyboard, highlight and focus state of the picker: the highlighted cell and whether the grid
|
|
47
|
+
* has focus, the keys of the search field and the grid, the move from one to the other, and the
|
|
48
|
+
* results count for the live region. Only a choice writes the model; moving the highlight never does.
|
|
49
|
+
*/
|
|
50
|
+
export const useIconPickerNavigation = (options: IconPickerNavigationOptions) => {
|
|
51
|
+
const { iconRows, currentPosition, iconAt, rowLength, search, term, category, isOpen } = options
|
|
52
|
+
|
|
53
|
+
const highlight = ref<GridPosition | null>(null)
|
|
54
|
+
const gridFocused = ref(false)
|
|
55
|
+
const announcement = ref('')
|
|
56
|
+
/** Opening resets search and category; that reset is no change to announce. */
|
|
57
|
+
let skipAnnouncement = false
|
|
58
|
+
|
|
59
|
+
const highlightedIcon = computed(() => iconAt(highlight.value))
|
|
60
|
+
|
|
61
|
+
const moveTo = (row: number | undefined, col: number) => {
|
|
62
|
+
if (row === undefined || rowLength(row) === 0) return
|
|
63
|
+
highlight.value = { row, col: Math.max(0, Math.min(col, rowLength(row) - 1)) }
|
|
64
|
+
options.grid()?.reveal(row)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* On opening: the reset (search, category) runs in between, so the highlight lands on the
|
|
69
|
+
* current icon of the full list.
|
|
70
|
+
*/
|
|
71
|
+
const open = (reset: () => void) => {
|
|
72
|
+
// On the state the watcher sees: a search of only spaces is no term, and resetting it fires nothing
|
|
73
|
+
skipAnnouncement = term.value !== '' || category.value !== null
|
|
74
|
+
reset()
|
|
75
|
+
highlight.value = currentPosition.value
|
|
76
|
+
announcement.value = ''
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const close = () => {
|
|
80
|
+
gridFocused.value = false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Into the grid from the search field: the current icon when it is in view, the first cell otherwise. */
|
|
84
|
+
const enterGrid = () => {
|
|
85
|
+
const current = currentPosition.value
|
|
86
|
+
if (current && options.grid()?.inView(current.row)) highlight.value = current
|
|
87
|
+
else moveTo(iconRows.value[0], 0)
|
|
88
|
+
options.grid()?.focus()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const onGridFocus = () => {
|
|
92
|
+
gridFocused.value = true
|
|
93
|
+
if (!highlightedIcon.value) moveTo(iconRows.value[0], 0)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const onGridBlur = () => {
|
|
97
|
+
gridFocused.value = false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const onCellHover = (position: GridPosition) => {
|
|
101
|
+
highlight.value = position
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const onGridKeydown = (event: KeyboardEvent) => {
|
|
105
|
+
// A letter is a search: the field takes it, and the focus
|
|
106
|
+
if (isPrintable(event)) {
|
|
107
|
+
event.preventDefault()
|
|
108
|
+
search.value += event.key
|
|
109
|
+
options.focusSearch()
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const position = highlight.value
|
|
114
|
+
const icon = iconAt(position)
|
|
115
|
+
if (!position || !icon) return
|
|
116
|
+
|
|
117
|
+
const order = iconRows.value
|
|
118
|
+
const at = order.indexOf(position.row)
|
|
119
|
+
const page = options.grid()?.pageRows() ?? 1
|
|
120
|
+
const toEdge = event.ctrlKey || event.metaKey
|
|
121
|
+
const last = order.at(-1)
|
|
122
|
+
|
|
123
|
+
switch (event.key) {
|
|
124
|
+
case 'ArrowRight':
|
|
125
|
+
if (position.col + 1 < rowLength(position.row)) moveTo(position.row, position.col + 1)
|
|
126
|
+
else moveTo(order[at + 1], 0)
|
|
127
|
+
break
|
|
128
|
+
case 'ArrowLeft':
|
|
129
|
+
if (position.col > 0) moveTo(position.row, position.col - 1)
|
|
130
|
+
else moveTo(order[at - 1], rowLength(order[at - 1]) - 1)
|
|
131
|
+
break
|
|
132
|
+
case 'ArrowDown':
|
|
133
|
+
moveTo(order[at + 1], position.col)
|
|
134
|
+
break
|
|
135
|
+
case 'ArrowUp':
|
|
136
|
+
if (at === 0) options.focusSearch()
|
|
137
|
+
else moveTo(order[at - 1], position.col)
|
|
138
|
+
break
|
|
139
|
+
case 'Home':
|
|
140
|
+
moveTo(toEdge ? order[0] : position.row, 0)
|
|
141
|
+
break
|
|
142
|
+
case 'End':
|
|
143
|
+
moveTo(toEdge ? last : position.row, rowLength(toEdge ? last : position.row) - 1)
|
|
144
|
+
break
|
|
145
|
+
case 'PageDown':
|
|
146
|
+
moveTo(order[Math.min(at + page, order.length - 1)], position.col)
|
|
147
|
+
break
|
|
148
|
+
case 'PageUp':
|
|
149
|
+
moveTo(order[Math.max(at - page, 0)], position.col)
|
|
150
|
+
break
|
|
151
|
+
case 'Enter':
|
|
152
|
+
case ' ':
|
|
153
|
+
options.choose(icon)
|
|
154
|
+
break
|
|
155
|
+
default:
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
event.preventDefault()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const onSearchKeydown = (event: KeyboardEvent) => {
|
|
163
|
+
if (event.key === 'ArrowDown' && iconRows.value.length > 0) {
|
|
164
|
+
event.preventDefault()
|
|
165
|
+
enterGrid()
|
|
166
|
+
} else if (event.key === 'Enter' && term.value) {
|
|
167
|
+
event.preventDefault()
|
|
168
|
+
const first = iconAt({ row: iconRows.value[0] ?? -1, col: 0 })
|
|
169
|
+
if (first) options.choose(first)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const announceResults = useDebounceFn(() => {
|
|
174
|
+
announcement.value = options.results()(options.iconCount.value)
|
|
175
|
+
}, RESULTS_DELAY)
|
|
176
|
+
|
|
177
|
+
// A new list starts at the top with nothing highlighted; back on the full list (also on opening,
|
|
178
|
+
// which resets both) the current icon is highlighted again
|
|
179
|
+
watch([term, category], () => {
|
|
180
|
+
highlight.value = !term.value && category.value === null ? currentPosition.value : null
|
|
181
|
+
options.grid()?.scrollToTop()
|
|
182
|
+
if (skipAnnouncement) skipAnnouncement = false
|
|
183
|
+
else if (isOpen.value) announceResults()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// The catalogue came in while the panel was open: highlight the current icon, as on opening
|
|
187
|
+
watch(options.catalogueEmpty, (empty) => {
|
|
188
|
+
if (!empty && isOpen.value && !term.value && category.value === null) {
|
|
189
|
+
highlight.value = currentPosition.value
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
highlight,
|
|
195
|
+
highlightedIcon,
|
|
196
|
+
gridFocused,
|
|
197
|
+
announcement,
|
|
198
|
+
open,
|
|
199
|
+
close,
|
|
200
|
+
onGridFocus,
|
|
201
|
+
onGridBlur,
|
|
202
|
+
onCellHover,
|
|
203
|
+
onGridKeydown,
|
|
204
|
+
onSearchKeydown,
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/components.json
CHANGED
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"NexxtDropdownButton": "components/organisms/DropdownButton/DropdownButton.vue",
|
|
62
62
|
"NexxtFontPicker": "components/organisms/FontPicker/FontPicker.vue",
|
|
63
63
|
"NexxtHeader": "components/organisms/Header/Header.vue",
|
|
64
|
+
"NexxtIconPicker": "components/organisms/IconPicker/IconPicker.vue",
|
|
64
65
|
"NexxtModal": "components/organisms/Modal/Modal.vue",
|
|
65
66
|
"NexxtPickerPanel": "components/organisms/PickerPanel/PickerPanel.vue",
|
|
66
67
|
"NexxtProgressBar": "components/organisms/ProgressBar/ProgressBar.vue",
|
package/src/index.ts
CHANGED
|
@@ -126,6 +126,23 @@ export type {
|
|
|
126
126
|
NexxtFontPickerMessages,
|
|
127
127
|
NexxtFontPickerProps,
|
|
128
128
|
} from './components/organisms/FontPicker/FontPicker.vue'
|
|
129
|
+
export type {
|
|
130
|
+
NexxtIconPickerCategory,
|
|
131
|
+
NexxtIconPickerIcon,
|
|
132
|
+
NexxtIconPickerMessages,
|
|
133
|
+
NexxtIconPickerPinned,
|
|
134
|
+
NexxtIconPickerProps,
|
|
135
|
+
NexxtIconPickerStyle,
|
|
136
|
+
NexxtIconPickerValue,
|
|
137
|
+
} from './components/organisms/IconPicker/IconPicker.vue'
|
|
138
|
+
export {
|
|
139
|
+
iconLabelFromName,
|
|
140
|
+
iconSearchIndex,
|
|
141
|
+
iconStyleFor,
|
|
142
|
+
searchIcons,
|
|
143
|
+
sortIconsByLabel,
|
|
144
|
+
} from './components/organisms/IconPicker/iconPickerHelpers'
|
|
145
|
+
export type { NexxtIconPickerSearchEntry } from './components/organisms/IconPicker/iconPickerHelpers'
|
|
129
146
|
export { EDITOR_PADDING_STEPS } from './components/editor/molecules/EditorPadding/EditorPadding.vue'
|
|
130
147
|
export type {
|
|
131
148
|
EditorPaddingMessages,
|
|
@@ -214,6 +231,7 @@ export { default as NexxtFloatingPanel } from './components/atoms/FloatingPanel/
|
|
|
214
231
|
export { default as NexxtFontPicker } from './components/organisms/FontPicker/FontPicker.vue'
|
|
215
232
|
export { default as NexxtHeader } from './components/organisms/Header/Header.vue'
|
|
216
233
|
export { default as NexxtIcon } from './components/atoms/Icon/Icon.vue'
|
|
234
|
+
export { default as NexxtIconPicker } from './components/organisms/IconPicker/IconPicker.vue'
|
|
217
235
|
export { default as NexxtInfoBlock } from './components/molecules/InfoBlock/InfoBlock.vue'
|
|
218
236
|
export { default as NexxtInputField } from './components/molecules/InputField/InputField.vue'
|
|
219
237
|
export { default as NexxtInputSelect } from './components/molecules/InputSelect/InputSelect.vue'
|