From e14ded9ac465080520787fc8f849cc0440b98364 Mon Sep 17 00:00:00 2001 From: Hendrik Oenings Date: Mon, 6 Jul 2026 10:31:00 +0200 Subject: [PATCH 1/8] feat(iconMenu): `menus` should be mutable --- .../components/NineRegionsButton.ce.vue | 9 +-- .../components/StandardMenuList.ce.vue | 8 +-- src/plugins/iconMenu/store.ts | 66 ++++++++++++------- 3 files changed, 44 insertions(+), 39 deletions(-) diff --git a/src/plugins/iconMenu/components/NineRegionsButton.ce.vue b/src/plugins/iconMenu/components/NineRegionsButton.ce.vue index 8bb25a50d5..e7ec75ee16 100644 --- a/src/plugins/iconMenu/components/NineRegionsButton.ce.vue +++ b/src/plugins/iconMenu/components/NineRegionsButton.ce.vue @@ -13,7 +13,6 @@ import { storeToRefs } from 'pinia' import { computed, inject } from 'vue' import PolarIconButton from '@/components/PolarIconButton.ce.vue' -import { useCoreStore } from '@/core/stores' import { useIconMenuStore } from '../store' @@ -30,13 +29,7 @@ const active = computed(() => open.value === props.id) const updateMaxWidth = inject('updateMaxWidth') as () => void function toggle() { - if (open.value === props.id) { - open.value = null - useCoreStore().setMoveHandle(null) - } else { - open.value = props.id - iconMenuStore.openInMoveHandle(props.id) - } + open.value = open.value === props.id ? null : props.id updateMaxWidth() } diff --git a/src/plugins/iconMenu/components/StandardMenuList.ce.vue b/src/plugins/iconMenu/components/StandardMenuList.ce.vue index 1b0df4448f..3485ee63f9 100644 --- a/src/plugins/iconMenu/components/StandardMenuList.ce.vue +++ b/src/plugins/iconMenu/components/StandardMenuList.ce.vue @@ -122,13 +122,7 @@ function updateMaxWidth() { } function toggle(id: string) { - if (open.value === id) { - open.value = null - coreStore.setMoveHandle(null) - } else { - open.value = id - iconMenuStore.openInMoveHandle(id) - } + open.value = open.value === id ? null : id updateMaxWidth() } diff --git a/src/plugins/iconMenu/store.ts b/src/plugins/iconMenu/store.ts index 30a638d72c..64335a3e77 100644 --- a/src/plugins/iconMenu/store.ts +++ b/src/plugins/iconMenu/store.ts @@ -4,14 +4,13 @@ */ /* eslint-enable tsdoc/syntax */ -import type { Component } from 'vue' import type { Icon } from '@/core' import type { Menu } from './types' import { toMerged } from 'es-toolkit' import { t } from 'i18next' import { acceptHMRUpdate, defineStore } from 'pinia' -import { computed, markRaw, ref } from 'vue' +import { computed, markRaw, ref, watch } from 'vue' import { useCoreStore } from '@/core/stores' @@ -42,6 +41,33 @@ export const useIconMenuStore = defineStore('plugins/iconMenu', () => { () => coreStore.configuration.iconMenu?.layoutTag ?? '' ) + const flatMenuItems = computed(() => + menus.value.concat(focusMenus.value).flat() + ) + watch( + flatMenuItems, + (newItems, oldItems) => { + const getDiffItems = (to, from) => + to.filter( + (item) => + !from.some((oldItem) => oldItem.plugin.id === item.plugin.id) + ) + getDiffItems(newItems, oldItems).forEach((newItem) => { + coreStore.addPlugin(toMerged(newItem.plugin, { independent: false })) + }) + getDiffItems(oldItems, newItems).forEach((oldItem) => { + if (open.value === oldItem.plugin.id) { + open.value = null + } + if (focusOpen.value === oldItem.plugin.id) { + focusOpen.value = null + } + coreStore.removePlugin(oldItem.plugin.id) + }) + }, + { deep: true } + ) + function setupPlugin() { menus.value = (coreStore.configuration.iconMenu?.menus || []).map( (menuGroup) => @@ -64,24 +90,6 @@ export const useIconMenuStore = defineStore('plugins/iconMenu', () => { coreStore.addPlugin(toMerged(plugin, { independent: false })) }) - // Otherwise, the component itself is made reactive - menus.value.map((menuGroup) => - menuGroup.map((menuItem) => - toMerged(menuItem, { - plugin: { - component: markRaw(menuItem.plugin.component as Component), - }, - }) - ) - ) - focusMenus.value.map((menuItem) => - toMerged(menuItem, { - plugin: { - component: markRaw(menuItem.plugin.component as Component), - }, - }) - ) - const initiallyOpen = coreStore.configuration.iconMenu?.initiallyOpen if ( !coreStore.hasSmallHeight && @@ -104,21 +112,31 @@ export const useIconMenuStore = defineStore('plugins/iconMenu', () => { function openMenuById(openId: string) { const entry = menus.value.flat().find(({ plugin: { id } }) => id === openId) - if (entry) { open.value = openId - openInMoveHandle(openId) } } + watch(open, (open) => { + if (open) { + openInMoveHandle(open) + } else { + coreStore.setMoveHandle(null) + } + }) function openFocusMenuById(openId: string) { const entry = focusMenus.value.find(({ plugin: { id } }) => id === openId) - if (entry) { focusOpen.value = openId - openInMoveHandle(openId, true) } } + watch(focusOpen, (focusOpen) => { + if (focusOpen) { + openInMoveHandle(focusOpen, true) + } else { + coreStore.setMoveHandle(null) + } + }) function openInMoveHandle(openId: string, focusMenu = false) { const menu = (focusMenu ? focusMenus.value : menus.value.flat()).find( From 8f36876df4f2ce72c247d9da45284c47a2e34e0e Mon Sep 17 00:00:00 2001 From: Hendrik Oenings Date: Mon, 6 Jul 2026 10:31:34 +0200 Subject: [PATCH 2/8] test(snowbox): allow removal and live insertion of iconMenu plugins --- examples/snowbox/index.html | 1 + examples/snowbox/index.js | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/examples/snowbox/index.html b/examples/snowbox/index.html index 79a0317df1..14ee71a948 100644 --- a/examples/snowbox/index.html +++ b/examples/snowbox/index.html @@ -58,6 +58,7 @@

POLAR map client

+
Coordinates of currently selected feature:
diff --git a/examples/snowbox/index.js b/examples/snowbox/index.js index ea0bc33808..44dc6c16ed 100644 --- a/examples/snowbox/index.js +++ b/examples/snowbox/index.js @@ -572,3 +572,16 @@ document colorScheme = colorScheme === 'light' ? 'dark' : 'light' updateState(map, 'core', 'colorScheme', colorScheme) }) + +let filterItem = null +document + .getElementById('toggle-filter') + .addEventListener('click', ({ target }) => { + const store = getStore(map, 'iconMenu') + if (!filterItem) { + ;[filterItem] = store.menus.splice(1, 1) + } else { + store.menus.splice(1, 0, filterItem) + filterItem = null + } + }) From 4888d290dd5e0b313c87778321ec2b047e48385a Mon Sep 17 00:00:00 2001 From: Hendrik Oenings Date: Mon, 6 Jul 2026 10:32:16 +0200 Subject: [PATCH 3/8] fix(filter): reset filter state on plugin removal --- src/plugins/filter/store.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/filter/store.ts b/src/plugins/filter/store.ts index 340db586c6..23eb01dd62 100644 --- a/src/plugins/filter/store.ts +++ b/src/plugins/filter/store.ts @@ -52,10 +52,12 @@ export const useFilterStore = defineStore('plugins/filter', () => { { deep: true, immediate: true } ) ) + teardownCallbacks.push(callback) }) } function teardownPlugin() { + filterMainStore.state = {} teardownCallbacks.forEach((callback) => { callback() }) From d7e23aebc335cd54d82a5671608901bf0208cb58 Mon Sep 17 00:00:00 2001 From: Hendrik Oenings Date: Wed, 29 Jul 2026 17:17:01 +0200 Subject: [PATCH 4/8] refactor(iconMenu): extract lib function watchArray --- src/lib/watchArray.ts | 69 +++++++++++++++++++++++++++++++++++ src/plugins/iconMenu/store.ts | 32 +++++++--------- 2 files changed, 82 insertions(+), 19 deletions(-) create mode 100644 src/lib/watchArray.ts diff --git a/src/lib/watchArray.ts b/src/lib/watchArray.ts new file mode 100644 index 0000000000..29d8ed7c66 --- /dev/null +++ b/src/lib/watchArray.ts @@ -0,0 +1,69 @@ +import type { WatchOptions, WatchSource } from 'vue' + +import { watch } from 'vue' + +function getDifference(items: T[], otherItems: T[]) { + const unmatchedItems = [...otherItems] + return items.filter((item) => { + const matchIndex = unmatchedItems.findIndex((otherItem) => + Object.is(item, otherItem) + ) + if (matchIndex === -1) { + return true + } + unmatchedItems.splice(matchIndex, 1) + return false + }) +} + +/** + * Watches an array and invokes callbacks for every added and removed item. + * + * @param source - Reactive array source to watch + * @param onAdded - Callback invoked for every added item + * @param onRemoved - Callback invoked for every removed item + * @param options - Vue watch options + */ +export function watchArray( + source: WatchSource, + onAdded: (item: T) => void, + onRemoved: (item: T) => void, + options?: WatchOptions +) { + return watch( + source, + (newItems, oldItems) => { + const previousItems = oldItems ?? [] + getDifference(newItems, previousItems).forEach((item) => { + onAdded(item) + }) + getDifference(previousItems, newItems).forEach((item) => { + onRemoved(item) + }) + }, + options + ) +} + +if (import.meta.vitest) { + const { expect, test, vi } = import.meta.vitest + const { ref } = await import('vue') + + test('calls the respective callback for each added and removed item', () => { + const items = ref([{ id: 1 }, { id: 2 }]) + const onAdded = vi.fn() + const onRemoved = vi.fn() + + watchArray(items, onAdded, onRemoved, { flush: 'sync' }) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const retainedItem = items.value[1]! + const addedItem = { id: 3 } + items.value = [retainedItem, addedItem] + + expect(onAdded).toHaveBeenCalledOnce() + expect(onAdded).toHaveBeenCalledWith(addedItem) + expect(onRemoved).toHaveBeenCalledOnce() + expect(onRemoved).toHaveBeenCalledWith({ id: 1 }) + }) +} diff --git a/src/plugins/iconMenu/store.ts b/src/plugins/iconMenu/store.ts index 03dcc06d28..85459a03ef 100644 --- a/src/plugins/iconMenu/store.ts +++ b/src/plugins/iconMenu/store.ts @@ -14,6 +14,7 @@ import { acceptHMRUpdate, defineStore } from 'pinia' import { computed, markRaw, ref, toRaw, watch } from 'vue' import { useCoreStore } from '@/core/stores' +import { watchArray } from '@/lib/watchArray' import { PluginId } from './types' @@ -45,26 +46,19 @@ export const useIconMenuStore = defineStore('plugins/iconMenu', () => { const flatMenuItems = computed(() => menus.value.concat(focusMenus.value).flat() ) - watch( + watchArray( flatMenuItems, - (newItems, oldItems) => { - const getDiffItems = (to, from) => - to.filter( - (item) => - !from.some((oldItem) => oldItem.plugin.id === item.plugin.id) - ) - getDiffItems(newItems, oldItems).forEach((newItem) => { - coreStore.addPlugin(toMerged(newItem.plugin, { independent: false })) - }) - getDiffItems(oldItems, newItems).forEach((oldItem) => { - if (open.value === oldItem.plugin.id) { - open.value = null - } - if (focusOpen.value === oldItem.plugin.id) { - focusOpen.value = null - } - coreStore.removePlugin(oldItem.plugin.id) - }) + (newItem) => { + coreStore.addPlugin(toMerged(newItem.plugin, { independent: false })) + }, + (oldItem) => { + if (open.value === oldItem.plugin.id) { + open.value = null + } + if (focusOpen.value === oldItem.plugin.id) { + focusOpen.value = null + } + coreStore.removePlugin(oldItem.plugin.id) }, { deep: true } ) From b47d6cfd5a6f9e62101a0370679bd70d30588c23 Mon Sep 17 00:00:00 2001 From: Hendrik Oenings Date: Wed, 29 Jul 2026 17:34:40 +0200 Subject: [PATCH 5/8] feat(iconMenu): allow adding and removal of plugins --- src/plugins/iconMenu/store.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/plugins/iconMenu/store.ts b/src/plugins/iconMenu/store.ts index 85459a03ef..50e1fabc5f 100644 --- a/src/plugins/iconMenu/store.ts +++ b/src/plugins/iconMenu/store.ts @@ -62,6 +62,19 @@ export const useIconMenuStore = defineStore('plugins/iconMenu', () => { }, { deep: true } ) + function addPlugin(menu: Menu) { + menus.value.push([menu]) + } + function removePlugin(pluginId: string) { + menus.value = menus.value + .map((menuGroup) => + menuGroup.filter(({ plugin: { id } }) => id !== pluginId) + ) + .filter((menuGroup) => menuGroup.length > 0) + focusMenus.value = focusMenus.value.filter( + ({ plugin: { id } }) => id !== pluginId + ) + } const visibleMenus = computed(() => menus.value.map((menuGroup) => @@ -210,6 +223,26 @@ export const useIconMenuStore = defineStore('plugins/iconMenu', () => { openMenuById, openFocusMenuById, + /** @alpha */ + menus, + + /** @alpha */ + focusMenus, + + /** + * Appends a plugin to the icon menu. + * + * @param menu - The menu item to add. + */ + addPlugin, + + /** + * Removes a plugin from the icon menu. + * + * @param pluginId - The ID of the plugin to remove. + */ + removePlugin, + /** @alpha */ layoutTag, From 28a8ef6b8ffc192e47e07cf245a28607b3f6db4c Mon Sep 17 00:00:00 2001 From: Hendrik Oenings Date: Wed, 29 Jul 2026 19:31:27 +0200 Subject: [PATCH 6/8] refactor(iconMenu): replace watcher with explicit mutations As decided in a not-yet-published architectural decision, we usually do not want to use watchers, but explicit mutations instead :-( --- src/plugins/iconMenu/store.ts | 118 ++++++++++++++-------------------- src/plugins/iconMenu/types.ts | 4 ++ 2 files changed, 53 insertions(+), 69 deletions(-) diff --git a/src/plugins/iconMenu/store.ts b/src/plugins/iconMenu/store.ts index 50e1fabc5f..4def458279 100644 --- a/src/plugins/iconMenu/store.ts +++ b/src/plugins/iconMenu/store.ts @@ -4,17 +4,14 @@ */ /* eslint-enable tsdoc/syntax */ -import type { Component } from 'vue' -import type { Icon } from '@/core' -import type { Menu } from './types' +import type { FocusMenu, Menu } from './types' import { toMerged } from 'es-toolkit' import { t } from 'i18next' import { acceptHMRUpdate, defineStore } from 'pinia' -import { computed, markRaw, ref, toRaw, watch } from 'vue' +import { computed, markRaw, ref, watch } from 'vue' import { useCoreStore } from '@/core/stores' -import { watchArray } from '@/lib/watchArray' import { PluginId } from './types' @@ -29,7 +26,7 @@ export const useIconMenuStore = defineStore('plugins/iconMenu', () => { const coreStore = useCoreStore() const menus = ref>([]) - const focusMenus = ref<(Menu & { icon: Icon })[]>([]) + const focusMenus = ref([]) const open = ref(null) const focusOpen = ref(null) @@ -43,37 +40,48 @@ export const useIconMenuStore = defineStore('plugins/iconMenu', () => { () => coreStore.configuration.iconMenu?.layoutTag ?? '' ) - const flatMenuItems = computed(() => - menus.value.concat(focusMenus.value).flat() - ) - watchArray( - flatMenuItems, - (newItem) => { - coreStore.addPlugin(toMerged(newItem.plugin, { independent: false })) - }, - (oldItem) => { - if (open.value === oldItem.plugin.id) { - open.value = null - } - if (focusOpen.value === oldItem.plugin.id) { - focusOpen.value = null + function isPluginInIconMenu(pluginId: string) { + const display = coreStore.configuration[pluginId]?.displayComponent + return typeof display === 'boolean' ? display : true + } + function addPlugin(menuGroup: Menu[]) { + const filteredMenuGroup = menuGroup.filter(({ plugin: { id } }) => + isPluginInIconMenu(id) + ) + filteredMenuGroup.forEach(({ plugin }) => { + if (plugin.component) { + markRaw(plugin.component) } - coreStore.removePlugin(oldItem.plugin.id) - }, - { deep: true } - ) - function addPlugin(menu: Menu) { - menus.value.push([menu]) + coreStore.addPlugin(toMerged(plugin, { independent: false })) + }) + menus.value.push(filteredMenuGroup) + } + function addFocusPlugin(menu: FocusMenu) { + if (!isPluginInIconMenu(menu.plugin.id)) { + return + } + if (menu.plugin.component) { + markRaw(menu.plugin.component) + } + coreStore.addPlugin(toMerged(menu.plugin, { independent: false })) + focusMenus.value.push(menu) } function removePlugin(pluginId: string) { + if (open.value === pluginId) { + open.value = null + } menus.value = menus.value .map((menuGroup) => menuGroup.filter(({ plugin: { id } }) => id !== pluginId) ) .filter((menuGroup) => menuGroup.length > 0) + if (focusOpen.value === pluginId) { + focusOpen.value = null + } focusMenus.value = focusMenus.value.filter( ({ plugin: { id } }) => id !== pluginId ) + coreStore.removePlugin(pluginId) } const visibleMenus = computed(() => @@ -90,41 +98,12 @@ export const useIconMenuStore = defineStore('plugins/iconMenu', () => { ) function setupPlugin() { - // Components are marked raw so they themselves are not made reactive - menus.value = (coreStore.configuration.iconMenu?.menus || []).map( - (menuGroup) => - menuGroup - .filter(({ plugin: { id } }) => { - const display = coreStore.configuration[id]?.displayComponent - return typeof display === 'boolean' ? display : true - }) - .map((menuItem) => ({ - ...menuItem, - plugin: { - ...menuItem.plugin, - component: markRaw(toRaw(menuItem.plugin.component as Component)), - }, - })) - ) - focusMenus.value = (coreStore.configuration.iconMenu?.focusMenus || []) - .filter(({ plugin: { id } }) => { - const display = coreStore.configuration[id]?.displayComponent - return typeof display === 'boolean' ? display : true - }) - .map((menuItem) => ({ - ...menuItem, - plugin: { - ...menuItem.plugin, - component: markRaw(toRaw(menuItem.plugin.component as Component)), - }, - })) - - menus.value - .concat(focusMenus.value) - .flat() - .forEach(({ plugin }) => { - coreStore.addPlugin(toMerged(plugin, { independent: false })) - }) + ;(coreStore.configuration.iconMenu?.menus || []).forEach((menuGroup) => { + addPlugin(menuGroup) + }) + ;(coreStore.configuration.iconMenu?.focusMenus || []).forEach((menu) => { + addFocusPlugin(menu) + }) const initiallyOpen = coreStore.configuration.iconMenu?.initiallyOpen if ( @@ -223,19 +202,20 @@ export const useIconMenuStore = defineStore('plugins/iconMenu', () => { openMenuById, openFocusMenuById, - /** @alpha */ - menus, - - /** @alpha */ - focusMenus, - /** - * Appends a plugin to the icon menu. + * Appends a group of plugins to the icon menu. * - * @param menu - The menu item to add. + * @param menu - The menu item group to add. */ addPlugin, + /** + * Appends a plugin to the icon menu as a focus menu. + * + * @param menu - The focus menu item to add. + */ + addFocusPlugin, + /** * Removes a plugin from the icon menu. * diff --git a/src/plugins/iconMenu/types.ts b/src/plugins/iconMenu/types.ts index d8de83629e..ed64916d05 100644 --- a/src/plugins/iconMenu/types.ts +++ b/src/plugins/iconMenu/types.ts @@ -26,6 +26,10 @@ export interface Menu { icon?: Icon } +export interface FocusMenu extends Menu { + icon: Icon +} + /** * Plugin options for iconMenu plugin. */ From bb128d22c3eb2af387b301e62a324d4da01596b3 Mon Sep 17 00:00:00 2001 From: Hendrik Oenings Date: Wed, 29 Jul 2026 19:40:45 +0200 Subject: [PATCH 7/8] test(snowbox): adapt new iconMenu modification --- examples/snowbox/index.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/snowbox/index.js b/examples/snowbox/index.js index f71065a6fe..7d4014b7ff 100644 --- a/examples/snowbox/index.js +++ b/examples/snowbox/index.js @@ -394,6 +394,7 @@ addPlugin( layoutTag: 'BOTTOM_LEFT', }) ) +let filterItem addPlugin( map, pluginIconMenu({ @@ -409,7 +410,7 @@ addPlugin( plugin: pluginLayerChooser({}), }, ], - [ + (filterItem = [ { plugin: pluginFilter({ layers: { @@ -473,7 +474,7 @@ addPlugin( }, }), }, - ], + ]), [ { plugin: pluginGeoLocation({ @@ -584,15 +585,16 @@ document updateState(map, 'core', 'colorScheme', colorScheme) }) -let filterItem = null +let hasFilter = true document .getElementById('toggle-filter') .addEventListener('click', ({ target }) => { const store = getStore(map, 'iconMenu') - if (!filterItem) { - ;[filterItem] = store.menus.splice(1, 1) + if (hasFilter) { + store.removePlugin('filter') + hasFilter = false } else { - store.menus.splice(1, 0, filterItem) - filterItem = null + store.addPlugin(filterItem) + hasFilter = true } }) From 1d136e78b7b6c20aef1ec7786487406219ee1dd2 Mon Sep 17 00:00:00 2001 From: Hendrik Oenings Date: Wed, 29 Jul 2026 20:01:26 +0200 Subject: [PATCH 8/8] refactor(iconMenu): replace open/focusOpen watcher with mutations As decided in a not-yet-published architectural decision, we usually do not want to use watchers, but explicit mutations instead :-( --- .../components/NineRegionsButton.ce.vue | 6 ++- .../components/StandardFocusMenu.ce.vue | 6 +-- .../components/StandardMenuList.ce.vue | 6 ++- src/plugins/iconMenu/store.ts | 50 +++++++++++-------- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/plugins/iconMenu/components/NineRegionsButton.ce.vue b/src/plugins/iconMenu/components/NineRegionsButton.ce.vue index e7ec75ee16..e9382d81c5 100644 --- a/src/plugins/iconMenu/components/NineRegionsButton.ce.vue +++ b/src/plugins/iconMenu/components/NineRegionsButton.ce.vue @@ -29,7 +29,11 @@ const active = computed(() => open.value === props.id) const updateMaxWidth = inject('updateMaxWidth') as () => void function toggle() { - open.value = open.value === props.id ? null : props.id + if (open.value === props.id) { + iconMenuStore.openMenuById(null) + } else { + iconMenuStore.openMenuById(props.id) + } updateMaxWidth() } diff --git a/src/plugins/iconMenu/components/StandardFocusMenu.ce.vue b/src/plugins/iconMenu/components/StandardFocusMenu.ce.vue index 9080f88540..03d03c0aa2 100644 --- a/src/plugins/iconMenu/components/StandardFocusMenu.ce.vue +++ b/src/plugins/iconMenu/components/StandardFocusMenu.ce.vue @@ -75,16 +75,14 @@ const maxHeight = computed(() => function toggle(id: string) { if (iconMenuStore.focusOpen === id) { - iconMenuStore.focusOpen = null + iconMenuStore.openFocusMenuById(null) pluginComponent.value = null - coreStore.setMoveHandle(null) } else { - iconMenuStore.focusOpen = id + iconMenuStore.openFocusMenuById(id) pluginComponent.value = markRaw( (props.menus.find(({ plugin }) => plugin.id === id) as Menu).plugin .component as Component ) - iconMenuStore.openInMoveHandle(id, true) } } diff --git a/src/plugins/iconMenu/components/StandardMenuList.ce.vue b/src/plugins/iconMenu/components/StandardMenuList.ce.vue index 3485ee63f9..fde1c760a9 100644 --- a/src/plugins/iconMenu/components/StandardMenuList.ce.vue +++ b/src/plugins/iconMenu/components/StandardMenuList.ce.vue @@ -122,7 +122,11 @@ function updateMaxWidth() { } function toggle(id: string) { - open.value = open.value === id ? null : id + if (open.value === id) { + iconMenuStore.openMenuById(null) + } else { + iconMenuStore.openMenuById(id) + } updateMaxWidth() } diff --git a/src/plugins/iconMenu/store.ts b/src/plugins/iconMenu/store.ts index 4def458279..fedb1e4d34 100644 --- a/src/plugins/iconMenu/store.ts +++ b/src/plugins/iconMenu/store.ts @@ -9,7 +9,7 @@ import type { FocusMenu, Menu } from './types' import { toMerged } from 'es-toolkit' import { t } from 'i18next' import { acceptHMRUpdate, defineStore } from 'pinia' -import { computed, markRaw, ref, watch } from 'vue' +import { computed, markRaw, readonly, ref } from 'vue' import { useCoreStore } from '@/core/stores' @@ -125,33 +125,27 @@ export const useIconMenuStore = defineStore('plugins/iconMenu', () => { } function teardownPlugin() {} - function openMenuById(openId: string) { + function openMenuById(openId: string | null) { const entry = menus.value.flat().find(({ plugin: { id } }) => id === openId) - if (entry) { + if (openId && entry) { open.value = openId - } - } - watch(open, (open) => { - if (open) { - openInMoveHandle(open) + openInMoveHandle(openId) } else { + open.value = null coreStore.setMoveHandle(null) } - }) + } - function openFocusMenuById(openId: string) { + function openFocusMenuById(openId: string | null) { const entry = focusMenus.value.find(({ plugin: { id } }) => id === openId) - if (entry) { + if (openId && entry) { focusOpen.value = openId - } - } - watch(focusOpen, (focusOpen) => { - if (focusOpen) { - openInMoveHandle(focusOpen, true) + openInMoveHandle(openId, true) } else { + focusOpen.value = null coreStore.setMoveHandle(null) } - }) + } function openInMoveHandle(openId: string, focusMenu = false) { const menu = (focusMenu ? focusMenus.value : menus.value.flat()).find( @@ -195,10 +189,26 @@ export const useIconMenuStore = defineStore('plugins/iconMenu', () => { return { visibleMenus, visibleFocusMenus, - open, - focusOpen, + + /** + * Determines which menu is currently open. + * + * To change the open menu, use {@link openMenuById}. + * + * @readonly + */ + open: readonly(open), + + /** + * Determines which focus menu is currently open. + * + * To change the open focus menu, use {@link openFocusMenuById}. + * + * @readonly + */ + focusOpen: readonly(focusOpen), + buttonComponent, - openInMoveHandle, openMenuById, openFocusMenuById,